diff --git a/skills/rig/samples/230-git-rename-tracker-v3.md b/skills/rig/samples/230-git-rename-tracker-v3.md new file mode 100644 index 0000000..8727c1e --- /dev/null +++ b/skills/rig/samples/230-git-rename-tracker-v3.md @@ -0,0 +1,29 @@ +# 230 - Git Rename Tracker V3 + +```rig +import { agent, p, s, repair } from "rig"; + +// Agent role: track file renames in recent git history and return a structured report. +const gitRenameTracker = agent({ + model: "small", + instructions: p`Analyze the following git log output for file renames: + +${p.bash("git log --diff-filter=R --summary --oneline -50")} + +Parse each rename line (format: "rename old/path => new/path (similarity%)"). +Extract the commit hash from the preceding commit line, and the old and new paths. +Return the structured list of renames with totals.`, + output: s.object({ + renames: s.array(s.object({ + hash: s.string, + oldPath: s.path, + newPath: s.path, + })), + totalRenames: s.int, + mostRenamed: s.optional(s.path), + }), + addons: [repair()], +}); + +export default gitRenameTracker; +``` diff --git a/skills/rig/samples/231-yaml-workflow-linter-v2.md b/skills/rig/samples/231-yaml-workflow-linter-v2.md new file mode 100644 index 0000000..0990641 --- /dev/null +++ b/skills/rig/samples/231-yaml-workflow-linter-v2.md @@ -0,0 +1,45 @@ +# 231 - Yaml Workflow Linter V2 + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const validateWorkflow = defineTool("validateWorkflow", { + description: "Validate a GitHub Actions workflow YAML file for required top-level keys.", + parameters: { filePath: s.path }, + handler: async ({ filePath }) => { + const content = await readFile(filePath, "utf8"); + const hasName = /^name:/m.test(content); + const hasOn = /^on:/m.test(content); + const hasJobs = /^jobs:/m.test(content); + const issues: string[] = []; + if (!hasName) issues.push("Missing 'name' key"); + if (!hasOn) issues.push("Missing 'on' key"); + if (!hasJobs) issues.push("Missing 'jobs' key"); + const status = issues.length === 0 ? "pass" : issues.length < 2 ? "warn" : "fail"; + return { hasName, hasOn, hasJobs, status, issues }; + }, +}); + +// Agent role: lint GitHub Actions workflow YAML files for required top-level keys. +const yamlWorkflowLinter = agent({ + model: "small", + instructions: p`Validate each GitHub Actions workflow file found in the workspace. + +Workflow files: ${p.glob(".github/workflows/**/*.yml")} + +For each file path listed above, call the validateWorkflow tool to check for required keys. +Return a record keyed by filename with validation results.`, + output: s.record(s.object({ + hasName: s.boolean, + hasOn: s.boolean, + hasJobs: s.boolean, + status: s.enum("pass", "warn", "fail"), + issues: s.array(s.string), + })), + tools: [validateWorkflow], + addons: [repair()], +}); + +export default yamlWorkflowLinter; +``` diff --git a/skills/rig/samples/232-two-phase-complexity-review-v3.md b/skills/rig/samples/232-two-phase-complexity-review-v3.md new file mode 100644 index 0000000..c086cf8 --- /dev/null +++ b/skills/rig/samples/232-two-phase-complexity-review-v3.md @@ -0,0 +1,73 @@ +# 232 - Two Phase Complexity Review V3 + +```rig +import { agent, p, s } from "rig"; + +// Agent role: extract TypeScript files with line counts from the workspace. +const extractor = agent({ + name: "extractor", + model: "small", + instructions: p`List TypeScript source files and their line counts. + +Files found: ${p.glob("**/*.ts")} +Line counts: ${p.bash("wc -l $(find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' | head -20) 2>/dev/null | tail -21")} + +Return a list of files with their line counts. Exclude totals and node_modules.`, + output: s.array(s.object({ + file: s.path, + lines: s.int, + })), +}); + +// Agent role: classify TypeScript files by complexity tier based on line counts. +const reviewer = agent({ + name: "reviewer", + model: "small", + instructions: p`You will receive a JSON list of TypeScript files with line counts. +Classify each file's complexity: +- trivial: 0-20 lines +- small: 21-100 lines +- medium: 101-300 lines +- large: 301-600 lines +- huge: 600+ lines + +Return the classified list plus a summary.`, + input: s.array(s.object({ file: s.path, lines: s.int })), + output: s.object({ + files: s.array(s.object({ + file: s.path, + lines: s.int, + complexity: s.enum("trivial", "small", "medium", "large", "huge"), + })), + summary: s.object({ + totalFiles: s.int, + largestFile: s.optional(s.path), + mostComplex: s.optional(s.path), + }), + }), +}); + +// Agent role: coordinate two-phase TypeScript complexity analysis. +const coordinator = agent({ + model: "small", + instructions: p`Perform a two-phase TypeScript file complexity analysis: +1. Use the extractor subagent to get a list of TypeScript files with line counts. +2. Pass the extractor's output as input to the reviewer subagent for complexity classification. +3. Return the reviewer's output as your final result.`, + output: s.object({ + files: s.array(s.object({ + file: s.path, + lines: s.int, + complexity: s.enum("trivial", "small", "medium", "large", "huge"), + })), + summary: s.object({ + totalFiles: s.int, + largestFile: s.optional(s.path), + mostComplex: s.optional(s.path), + }), + }), + agents: { extractor, reviewer }, +}); + +export default coordinator; +``` diff --git a/skills/rig/samples/233-barrel-file-generator-v3.md b/skills/rig/samples/233-barrel-file-generator-v3.md new file mode 100644 index 0000000..0f8c564 --- /dev/null +++ b/skills/rig/samples/233-barrel-file-generator-v3.md @@ -0,0 +1,43 @@ +# 233 - Barrel File Generator V3 + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const detectExports = defineTool("detectExports", { + description: "Detect exported symbols from a TypeScript source file.", + parameters: { filePath: s.path }, + handler: async ({ filePath }) => { + const content = await readFile(filePath, "utf8"); + const exportMatches = content.match(/^export\s+(?:default\s+)?(?:const|function|class|type|interface|enum)\s+(\w+)/gm) ?? []; + const symbols = exportMatches.map(m => { + const match = m.match(/(\w+)\s*$/); + return match ? match[1] : ""; + }).filter(Boolean); + return { filePath, symbols, count: symbols.length }; + }, +}); + +// Agent role: scan TypeScript source files and generate a barrel index.ts with all exports. +const barrelFileGenerator = agent({ + model: "small", + instructions: p`Generate a TypeScript barrel file (index.ts) that re-exports all symbols from source files. + +Source files: ${p.glob("src/**/*.ts")} + +For each file path above, call the detectExports tool to find exported symbols. +Then generate the barrel file content as a series of export statements. +Write it to ${p.writeOutput("barrelContent", "src/index.ts")}. +Return the file count, export count, barrel content, and output path.`, + output: s.object({ + filesScanned: s.int, + exportCount: s.int, + barrelContent: s.string, + outputPath: s.path, + }), + tools: [detectExports], + addons: [repair()], +}); + +export default barrelFileGenerator; +``` diff --git a/skills/rig/samples/234-git-hook-inventory-v3.md b/skills/rig/samples/234-git-hook-inventory-v3.md new file mode 100644 index 0000000..9ef8657 --- /dev/null +++ b/skills/rig/samples/234-git-hook-inventory-v3.md @@ -0,0 +1,48 @@ +# 234 - Git Hook Inventory V3 + +```rig +import { agent, p, s, defineTool } from "rig"; +import { readFile, access, constants } from "node:fs/promises"; + +const classifyHook = defineTool("classifyHook", { + description: "Classify a git hook as active, stub, or missing based on its content.", + parameters: { hookName: s.string }, + handler: async ({ hookName }) => { + const hookPath = `.git/hooks/${hookName}`; + try { + await access(hookPath, constants.F_OK); + const content = await readFile(hookPath, "utf8"); + const isSample = content.includes("sample") || hookPath.endsWith(".sample"); + const isAsync = content.includes("&") || content.includes("nohup"); + const summary = isSample + ? `Sample/stub hook at ${hookPath}` + : `Active hook: ${content.split("\n")[1]?.trim() ?? ""}`; + return { status: isSample ? "stub" : "active", isAsync, summary }; + } catch { + return { status: "missing", isAsync: false, summary: `No hook at ${hookPath}` }; + } + }, +}); + +// Agent role: inventory and classify all standard git hooks in this repository. +const gitHookInventory = agent({ + model: "small", + instructions: p`Inspect all standard git hooks and classify each one. + +Hook directory listing: ${p.bash("ls -la .git/hooks/ 2>/dev/null || echo 'No .git/hooks directory'")} + +Standard hooks to check: pre-commit, pre-push, commit-msg, post-commit, post-merge, +pre-rebase, post-checkout, prepare-commit-msg, pre-receive, post-receive. + +For each hook name, call the classifyHook tool to determine its status. +Return a record keyed by hook name with status, isAsync, and summary.`, + output: s.record(s.object({ + status: s.enum("active", "stub", "missing"), + isAsync: s.boolean, + summary: s.string, + })), + tools: [classifyHook], +}); + +export default gitHookInventory; +``` diff --git a/skills/rig/samples/235-pr-review-checklist-v3.md b/skills/rig/samples/235-pr-review-checklist-v3.md new file mode 100644 index 0000000..b0e5ffb --- /dev/null +++ b/skills/rig/samples/235-pr-review-checklist-v3.md @@ -0,0 +1,34 @@ +# 235 - Pr Review Checklist V3 + +```rig +import { agent, p, s, repair } from "rig"; + +// Agent role: generate a structured PR review checklist from the current git diff. +const prReviewChecklist = agent({ + model: "small", + instructions: p`Generate a structured PR review checklist based on the git diff below. + +Changed files summary: +${p.bash("git diff HEAD~1 --stat 2>/dev/null || git diff --stat 2>/dev/null || echo 'No diff available'")} + +Diff content (first 200 lines): +${p.bash("git diff HEAD~1 -- . 2>/dev/null | head -200 || git diff -- . | head -200 || echo 'No diff available'")} + +Analyze the diff and produce: +1. A checklist of specific review items, each categorized and prioritized. +2. The list of changed file paths. +3. Whether the PR appears ready to merge (ready: true if no high-priority issues).`, + output: s.object({ + checklist: s.array(s.object({ + item: s.string, + category: s.enum("security", "performance", "correctness", "style", "docs", "tests"), + priority: s.enum("high", "medium", "low"), + })), + changedFiles: s.array(s.path), + ready: s.boolean, + }), + addons: [repair()], +}); + +export default prReviewChecklist; +``` diff --git a/skills/rig/samples/236-vitest-coverage-parser.md b/skills/rig/samples/236-vitest-coverage-parser.md new file mode 100644 index 0000000..8a57f67 --- /dev/null +++ b/skills/rig/samples/236-vitest-coverage-parser.md @@ -0,0 +1,65 @@ +# 236 - Vitest Coverage Parser + +```rig +import { agent, p, s, defineTool } from "rig"; +import { readFile } from "node:fs/promises"; + +const parseCoverageSummary = defineTool("parseCoverageSummary", { + description: "Parse a vitest coverage-summary.json file to extract per-file coverage metrics.", + parameters: { filePath: s.path }, + handler: async ({ filePath }) => { + const raw = await readFile(filePath, "utf8"); + const json = JSON.parse(raw) as Record; + const files: Record = {}; + let totalLines = 0, totalBranches = 0, totalFunctions = 0, count = 0; + for (const [file, data] of Object.entries(json)) { + if (file === "total") continue; + const lines = data.lines?.pct ?? 0; + const branches = data.branches?.pct ?? 0; + const functions = data.functions?.pct ?? 0; + const avg = (lines + branches + functions) / 3; + const status = avg >= 80 ? "covered" : avg >= 40 ? "partial" : "uncovered"; + files[file] = { lines, branches, functions, status }; + totalLines += lines; totalBranches += branches; totalFunctions += functions; count++; + } + const n = count || 1; + return { + files, + overall: { lines: totalLines / n, branches: totalBranches / n, functions: totalFunctions / n }, + totalFiles: count, + }; + }, +}); + +// Agent role: parse vitest coverage output and return a per-file coverage report. +const vitestCoverageParser = agent({ + model: "small", + instructions: p`Parse the vitest coverage report for this project. + +Coverage summary file: ${p.readOptional("coverage/coverage-summary.json")} + +If the coverage summary file is available, call parseCoverageSummary with path "coverage/coverage-summary.json". +Otherwise, report all counts as 0 and status as "uncovered". + +Count files with lines coverage >= 80% as "well covered". +Return the per-file metrics, overall averages, total file count, and well-covered count.`, + output: s.object({ + files: s.record(s.object({ + lines: s.number, + branches: s.number, + functions: s.number, + status: s.enum("covered", "partial", "uncovered"), + })), + overall: s.object({ + lines: s.number, + branches: s.number, + functions: s.number, + }), + totalFiles: s.int, + wellCoveredCount: s.int, + }), + tools: [parseCoverageSummary], +}); + +export default vitestCoverageParser; +``` diff --git a/skills/rig/samples/237-git-log-stats-aggregator.md b/skills/rig/samples/237-git-log-stats-aggregator.md new file mode 100644 index 0000000..54c8398 --- /dev/null +++ b/skills/rig/samples/237-git-log-stats-aggregator.md @@ -0,0 +1,52 @@ +# 237 - Git Log Stats Aggregator + +```rig +import { agent, p, s, defineTool } from "rig"; + +const aggregateStats = defineTool("aggregateStats", { + description: "Aggregate git log lines into per-author and per-day-of-week commit counts.", + parameters: { logLines: s.string }, + handler: ({ logLines }) => { + const perAuthor: Record = {}; + const perDayOfWeek: Record = {}; + const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + let totalCommits = 0; + for (const line of logLines.split("\n")) { + if (!line.trim()) continue; + const parts = line.split("|"); + if (parts.length < 4) continue; + const author = parts[1].trim(); + const dateStr = parts[3].trim(); + perAuthor[author] = (perAuthor[author] ?? 0) + 1; + const day = days[new Date(dateStr).getDay()]; + if (day) perDayOfWeek[day] = (perDayOfWeek[day] ?? 0) + 1; + totalCommits++; + } + const mostActiveAuthor = Object.entries(perAuthor).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null; + const mostActiveDay = Object.entries(perDayOfWeek).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null; + return { perAuthor, perDayOfWeek, totalCommits, mostActiveAuthor, mostActiveDay }; + }, +}); + +// Agent role: aggregate git log statistics into per-author and per-day-of-week commit counts. +const gitLogStatsAggregator = agent({ + model: "small", + instructions: p`Aggregate git commit statistics from the following log: + +${p.bash("git log --format='%H|%an|%ae|%ad|%s' --date=short -100 2>/dev/null || echo ''")} + +Call aggregateStats with the full log text above as logLines. +Return the result including perAuthor counts, perDayOfWeek counts, totalCommits, +mostActiveAuthor, and mostActiveDay.`, + output: s.object({ + perAuthor: s.record(s.int), + perDayOfWeek: s.record(s.int), + totalCommits: s.int, + mostActiveAuthor: s.optional(s.string), + mostActiveDay: s.optional(s.string), + }), + tools: [aggregateStats], +}); + +export default gitLogStatsAggregator; +``` diff --git a/skills/rig/samples/238-ts-import-depth-analyzer.md b/skills/rig/samples/238-ts-import-depth-analyzer.md new file mode 100644 index 0000000..a37097f --- /dev/null +++ b/skills/rig/samples/238-ts-import-depth-analyzer.md @@ -0,0 +1,52 @@ +# 238 - Ts Import Depth Analyzer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const analyzeImportDepth = defineTool("analyzeImportDepth", { + description: "Analyze relative import depth in a TypeScript file by counting '../' occurrences.", + parameters: { filePath: s.path }, + handler: async ({ filePath }) => { + const content = await readFile(filePath, "utf8"); + const importRegex = /from\s+['"]([^'"]+)['"]/g; + const imports: string[] = []; + let match: RegExpExecArray | null; + while ((match = importRegex.exec(content)) !== null) { + imports.push(match[1]); + } + const relativeImports = imports.filter(i => i.startsWith(".")); + const depths = relativeImports.map(i => (i.match(/\.\.\//g) ?? []).length); + const maxDepth = depths.length > 0 ? Math.max(...depths) : 0; + const deepImports = relativeImports.filter(i => (i.match(/\.\.\//g) ?? []).length >= 2); + return { maxDepth, deepImports, importCount: imports.length }; + }, +}); + +// Agent role: analyze relative import depth across TypeScript files to find overly deep imports. +const tsImportDepthAnalyzer = agent({ + model: "small", + instructions: p`Analyze TypeScript import depth across all source files. + +Source files to analyze: ${p.glob("**/*.ts")} + +For each .ts file (excluding node_modules), call analyzeImportDepth to get maxDepth, deepImports, and importCount. +Calculate the average depth across all files. +Identify the file with the highest maxDepth as deepestFile. +Return the full per-file record plus summary stats.`, + output: s.object({ + files: s.record(s.object({ + maxDepth: s.int, + deepImports: s.array(s.string), + importCount: s.int, + })), + deepestFile: s.optional(s.path), + averageDepth: s.number, + totalFiles: s.int, + }), + tools: [analyzeImportDepth], + addons: [repair()], +}); + +export default tsImportDepthAnalyzer; +``` diff --git a/skills/rig/samples/239-env-file-completeness-checker.md b/skills/rig/samples/239-env-file-completeness-checker.md new file mode 100644 index 0000000..49dab11 --- /dev/null +++ b/skills/rig/samples/239-env-file-completeness-checker.md @@ -0,0 +1,51 @@ +# 239 - Env File Completeness Checker + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const parseEnvKeys = defineTool("parseEnvKeys", { + description: "Parse an env file string and return the list of key names.", + parameters: { content: s.string }, + handler: ({ content }) => { + return content + .split("\n") + .map((line: string) => line.trim()) + .filter((line: string) => line && !line.startsWith("#")) + .map((line: string) => line.split("=")[0].trim()) + .filter(Boolean); + }, +}); + +// Agent role: compare .env.example with .env to check completeness of environment configuration. +const envFileCompletenessChecker = agent({ + model: "small", + instructions: p`Compare .env.example with .env to check that all required keys are set. + +.env.example content: ${p.readOptional(".env.example")} + +.env content: ${p.readOptional(".env")} + +Steps: +1. Call parseEnvKeys with the .env.example content to get exampleKeys. +2. Call parseEnvKeys with the .env content to get presentKeys. +3. Compute missingKeys (in exampleKeys but not presentKeys) and extraKeys (in presentKeys but not exampleKeys). +4. Compute completeness as (exampleKeys.length - missingKeys.length) / exampleKeys.length (or 1.0 if example is empty). +5. Set status: + - "missing-example" if .env.example is empty/missing + - "missing-env" if .env is empty/missing + - "complete" if missingKeys is empty + - "partial" otherwise`, + output: s.object({ + exampleKeys: s.array(s.string), + presentKeys: s.array(s.string), + missingKeys: s.array(s.string), + extraKeys: s.array(s.string), + completeness: s.number, + status: s.enum("complete", "partial", "missing-example", "missing-env"), + }), + tools: [parseEnvKeys], + addons: [repair()], +}); + +export default envFileCompletenessChecker; +```