diff --git a/skills/rig/samples/280-commit-churn-classifier.md b/skills/rig/samples/280-commit-churn-classifier.md new file mode 100644 index 0000000..96b3ea2 --- /dev/null +++ b/skills/rig/samples/280-commit-churn-classifier.md @@ -0,0 +1,31 @@ +# 280 - Commit Churn Classifier + +```rig +import { agent, p, s, steering } from "rig"; + +// Agent role: classify repository files by commit churn frequency and assign a risk level. +const commitChurnClassifier = agent({ + model: "small", + addons: steering({ message: "Be precise: assign riskLevel based on churnCount: >20=critical, >10=volatile, >5=active, else stable." }), + instructions: p`Classify repository files by how frequently they are committed (churn). + +File churn counts from last 200 commits: +${p.bash("git log --name-only --format='' HEAD~200..HEAD 2>/dev/null | grep -v '^$' | sort | uniq -c | sort -rn | head -40")} + +For each file, parse the churn count and assign a riskLevel: +- critical: churnCount > 20 +- volatile: churnCount > 10 +- active: churnCount > 5 +- stable: churnCount <= 5 + +Return a record keyed by file path with churnCount (integer) and riskLevel.`, + output: s.record( + s.object({ + churnCount: s.int, + riskLevel: s.enum("stable", "active", "volatile", "critical"), + }) + ), +}); + +export default commitChurnClassifier; +``` diff --git a/skills/rig/samples/281-npm-package-size-estimator.md b/skills/rig/samples/281-npm-package-size-estimator.md new file mode 100644 index 0000000..c38c0f8 --- /dev/null +++ b/skills/rig/samples/281-npm-package-size-estimator.md @@ -0,0 +1,41 @@ +# 281 - NPM Package Size Estimator + +```rig +import { agent, defineTool, p, s } from "rig"; + +const classifySize = defineTool("classifySize", { + description: "Classify a package size in KB into a tier rating.", + parameters: s.object({ sizeKb: s.number }), + handler({ sizeKb }: { sizeKb: number }) { + if (sizeKb < 10) return "tiny" as const; + if (sizeKb < 100) return "small" as const; + if (sizeKb < 500) return "medium" as const; + if (sizeKb < 2000) return "large" as const; + return "xlarge" as const; + }, +}); + +// Agent role: estimate the NPM package size and rate it by tier. +const npmPackageSizeEstimator = agent({ + model: "small", + instructions: p`Estimate the NPM package size for this project. + +Pack dry-run output: +${p.bash("npm pack --dry-run 2>&1 | tail -20")} + +Directory size: +${p.bash("du -sh . 2>/dev/null | head -5")} + +Use classifySize tool with the estimated total size in KB to get the sizeRating. +List the top files by size and provide a brief recommendation.`, + tools: [classifySize], + output: s.object({ + estimatedSizeKb: s.number, + topFiles: s.array(s.object({ name: s.string, sizeKb: s.number })), + sizeRating: s.enum("tiny", "small", "medium", "large", "xlarge"), + recommendation: s.string, + }), +}); + +export default npmPackageSizeEstimator; +``` diff --git a/skills/rig/samples/282-ts-branch-coverage-analyzer.md b/skills/rig/samples/282-ts-branch-coverage-analyzer.md new file mode 100644 index 0000000..ff609c1 --- /dev/null +++ b/skills/rig/samples/282-ts-branch-coverage-analyzer.md @@ -0,0 +1,44 @@ +# 282 - TS Branch Coverage Analyzer + +```rig +import { agent, p, s } from "rig"; + +const branchItem = s.object({ + functionName: s.string, + line: s.int, + branchType: s.enum("if", "ternary", "switch", "nullish"), + covered: s.boolean, +}); + +// Agent role: extract all branch points from the provided TypeScript source code. +const branchAnalyzer = agent({ + model: "small", + instructions: p`Analyze the TypeScript source code provided in the input and list every branch point (if/ternary/switch/nullish coalescing). For each branch, identify the enclosing function name, line number, branch type, and whether it appears covered (has a test or usage path). Return an array of branch objects.`, + input: s.object({ filePath: s.path }), + output: s.array(branchItem), +}); + +// Agent role: coordinate branch coverage analysis for a TypeScript file. +const tsBranchCoverageAnalyzer = agent({ + model: "small", + input: s.object({ filePath: s.path }), + instructions: p`Perform branch coverage analysis on a TypeScript file. + +File content: +${p.readInput("filePath")} + +Delegate the analysis to the branchAnalyzer subagent, passing the filePath. +Collect its output (an array of branch objects), then compute a summary with totalBranches, coveredBranches, and uncoveredBranches counts.`, + output: s.object({ + branches: s.array(branchItem), + summary: s.object({ + totalBranches: s.int, + coveredBranches: s.int, + uncoveredBranches: s.int, + }), + }), + agents: { branchAnalyzer }, +}); + +export default tsBranchCoverageAnalyzer; +``` diff --git a/skills/rig/samples/283-env-variable-type-inferrer.md b/skills/rig/samples/283-env-variable-type-inferrer.md new file mode 100644 index 0000000..ccf1dd5 --- /dev/null +++ b/skills/rig/samples/283-env-variable-type-inferrer.md @@ -0,0 +1,41 @@ +# 283 - Env Variable Type Inferrer + +```rig +import { agent, defineTool, p, s } from "rig"; + +const inferVarType = defineTool("inferVarType", { + description: "Infer the type of an environment variable value using regex heuristics.", + parameters: s.object({ value: s.string }), + handler({ value }: { value: string }) { + if (/^https?:\/\//i.test(value)) return "url" as const; + if (/^(true|false|yes|no|1|0)$/i.test(value)) return "boolean" as const; + if (/^\d+(\.\d+)?$/.test(value)) return "number" as const; + if (/^(\/|\.\/|~\/)/.test(value) || /\.(txt|json|yaml|yml|pem|key|crt)$/.test(value)) return "path" as const; + if (value === "") return "unknown" as const; + return "string" as const; + }, +}); + +// Agent role: infer types for all environment variables defined in .env.example. +const envVariableTypeInferrer = agent({ + model: "small", + instructions: p`Infer the type of each environment variable defined in the .env.example file. + +.env.example contents: +${p.readOptional(".env.example", "(no .env.example found)")} + +For each KEY=VALUE line, call inferVarType with the value to get its type. Generate a short description of what each variable likely controls. Set allDocumented to true only if every variable has a non-empty value or inline comment hint.`, + tools: [inferVarType], + output: s.object({ + vars: s.record( + s.object({ + type: s.enum("string", "number", "boolean", "url", "path", "unknown"), + description: s.string, + }) + ), + allDocumented: s.boolean, + }), +}); + +export default envVariableTypeInferrer; +``` diff --git a/skills/rig/samples/284-makefile-target-extractor.md b/skills/rig/samples/284-makefile-target-extractor.md new file mode 100644 index 0000000..6ecc203 --- /dev/null +++ b/skills/rig/samples/284-makefile-target-extractor.md @@ -0,0 +1,62 @@ +# 284 - Makefile Target Extractor + +```rig +import { agent, defineTool, p, s, repair } from "rig"; + +const parseTargets = defineTool("parseTargets", { + description: "Parse Makefile content to extract phony and real targets with descriptions.", + parameters: s.object({ content: s.string }), + handler({ content }: { content: string }) { + const lines = content.split("\n"); + const phonySet = new Set(); + const targets: Array<{ name: string; isPhony: boolean; hasHelp: boolean; description?: string }> = []; + + for (const line of lines) { + const phonyMatch = line.match(/^\.PHONY\s*:\s*(.+)/); + if (phonyMatch) { + for (const t of phonyMatch[1].split(/\s+/)) phonySet.add(t.trim()); + } + } + + const targetRe = /^([a-zA-Z0-9_\-./]+)\s*:/; + for (let i = 0; i < lines.length; i++) { + const m = lines[i].match(targetRe); + if (m && !m[1].startsWith(".")) { + const name = m[1]; + const helpLine = i > 0 ? lines[i - 1] : ""; + const hasHelp = /##/.test(helpLine) || /##/.test(lines[i]); + const description = helpLine.match(/##\s*(.+)/)?.[1]?.trim(); + targets.push({ name, isPhony: phonySet.has(name), hasHelp, ...(description ? { description } : {}) }); + } + } + return targets; + }, +}); + +// Agent role: extract and classify Makefile targets. +const makefileTargetExtractor = agent({ + model: "small", + addons: repair(), + instructions: p`Extract and classify targets from the project Makefile. + +Makefile contents: +${p.readOptional("Makefile", "(no Makefile found)")} + +Call parseTargets with the full Makefile content. Return the array of targets and include totalCount (all targets) and phonyCount (targets where isPhony is true).`, + tools: [parseTargets], + output: s.object({ + targets: s.array( + s.object({ + name: s.string, + isPhony: s.boolean, + hasHelp: s.boolean, + description: s.optional(s.string), + }) + ), + totalCount: s.int, + phonyCount: s.int, + }), +}); + +export default makefileTargetExtractor; +``` diff --git a/skills/rig/samples/285-test-fixture-generator.md b/skills/rig/samples/285-test-fixture-generator.md new file mode 100644 index 0000000..df20c76 --- /dev/null +++ b/skills/rig/samples/285-test-fixture-generator.md @@ -0,0 +1,41 @@ +# 285 - Test Fixture Generator + +```rig +import { agent, p, s } from "rig"; + +// Agent role: analyze a function signature and return its parameters and return type. +const signatureAnalyzer = agent({ + model: "small", + instructions: p`Analyze the TypeScript source provided in the input. Extract the signature details for the specified function: its parameters (name and type), return type, and any JSDoc description. Return structured signature details.`, + input: s.object({ sourceFile: s.path, functionName: s.string }), + output: s.object({ + functionName: s.string, + parameters: s.array(s.object({ name: s.string, type: s.string })), + returnType: s.string, + description: s.optional(s.string), + }), +}); + +// Agent role: generate a test fixture for a specific function in a TypeScript source file. +const testFixtureGenerator = agent({ + model: "small", + input: s.object({ sourceFile: s.path, functionName: s.string }), + instructions: p`Generate a test fixture file for the specified function. + +Source file content: +${p.readInput("sourceFile")} + +1. Delegate to signatureAnalyzer to get the function signature for the function named in the input. +2. Using the signature, generate fixture code: example inputs, mock helpers, and a vitest describe block. +3. Write the fixture code to fixture.ts: ${p.writeOutput("fixtureCode", "fixture.ts")} +4. Return fixtureCode (the full fixture source), the required imports, and suggestedFileName.`, + output: s.object({ + fixtureCode: s.string, + imports: s.array(s.string), + suggestedFileName: s.path, + }), + agents: { signatureAnalyzer }, +}); + +export default testFixtureGenerator; +``` diff --git a/skills/rig/samples/286-toml-config-analyzer.md b/skills/rig/samples/286-toml-config-analyzer.md new file mode 100644 index 0000000..124ad16 --- /dev/null +++ b/skills/rig/samples/286-toml-config-analyzer.md @@ -0,0 +1,44 @@ +# 286 - TOML Config Analyzer + +```rig +import { agent, defineTool, p, s, repair } from "rig"; + +const analyzeTomlFile = defineTool("analyzeTomlFile", { + description: "Read a TOML file and extract its top-level section headers and key count.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + const { readFile } = await import("node:fs/promises"); + try { + const content = await readFile(filePath, "utf8"); + const sections = [...content.matchAll(/^\[([^\]]+)\]/gm)].map((m: RegExpMatchArray) => m[1] as string); + const keyCount = (content.match(/^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*=/gm) ?? []).length; + const hasRequired = sections.includes("package") || sections.includes("tool") || keyCount > 0; + return { sections, keyCount, hasRequired }; + } catch { + return { sections: [], keyCount: 0, hasRequired: false }; + } + }, +}); + +// Agent role: analyze all TOML config files in the workspace and summarize their sections. +const tomlConfigAnalyzer = agent({ + model: "small", + addons: repair(), + instructions: p`Analyze all TOML config files found in the workspace. + +TOML files found: +${p.glob("**/*.toml")} + +For each file path listed above, call analyzeTomlFile to extract its sections, keyCount, and hasRequired flag. Return a record keyed by file path.`, + tools: [analyzeTomlFile], + output: s.record( + s.object({ + sections: s.array(s.string), + keyCount: s.int, + hasRequired: s.boolean, + }) + ), +}); + +export default tomlConfigAnalyzer; +``` diff --git a/skills/rig/samples/287-git-checkpoint-summarizer.md b/skills/rig/samples/287-git-checkpoint-summarizer.md new file mode 100644 index 0000000..b538ea6 --- /dev/null +++ b/skills/rig/samples/287-git-checkpoint-summarizer.md @@ -0,0 +1,44 @@ +# 287 - Git Checkpoint Summarizer + +```rig +import { agent, defineTool, p, s } from "rig"; + +const classifyCheckpoint = defineTool("classifyCheckpoint", { + description: "Classify a git checkpoint line as stash, commit, tag, or branch.", + parameters: s.object({ line: s.string }), + handler({ line }: { line: string }) { + if (line.startsWith("stash@{")) return "stash" as const; + if (/^refs\/tags\//.test(line)) return "tag" as const; + if (/^refs\/heads\//.test(line)) return "branch" as const; + return "commit" as const; + }, +}); + +// Agent role: summarize git checkpoints (stashes and recent commits) in the repository. +const gitCheckpointSummarizer = agent({ + model: "small", + instructions: p`Summarize the git checkpoints (stashes and recent commits) for this repository. + +Stash list: +${p.bash("git stash list 2>/dev/null || echo '(no stashes)'")} + +Recent commits: +${p.bash("git log --oneline -20 2>/dev/null || echo '(no commits)'")} + +For each stash entry and each commit line, call classifyCheckpoint to determine its type. Build the checkpoints array with ref, type, and message fields. Set latestCheckpoint to the most recent commit hash or stash ref, and totalCount to the combined count.`, + tools: [classifyCheckpoint], + output: s.object({ + checkpoints: s.array( + s.object({ + ref: s.string, + type: s.enum("stash", "commit", "tag", "branch"), + message: s.string, + }) + ), + latestCheckpoint: s.optional(s.string), + totalCount: s.int, + }), +}); + +export default gitCheckpointSummarizer; +``` diff --git a/skills/rig/samples/288-ts-literal-union-extractor.md b/skills/rig/samples/288-ts-literal-union-extractor.md new file mode 100644 index 0000000..2cce2ed --- /dev/null +++ b/skills/rig/samples/288-ts-literal-union-extractor.md @@ -0,0 +1,48 @@ +# 288 - TS Literal Union Extractor + +```rig +import { agent, defineTool, p, s, steering } from "rig"; + +const extractLiteralUnions = defineTool("extractLiteralUnions", { + description: "Extract string literal union type declarations from a TypeScript file.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + const { readFile } = await import("node:fs/promises"); + try { + const content = await readFile(filePath, "utf8"); + const re = /type\s+(\w+)\s*=\s*((?:'[^']*'|"[^"]*")(?:\s*\|\s*(?:'[^']*'|"[^"]*"))+)/g; + const results: Record = {}; + for (const m of content.matchAll(re)) { + const name = m[1] as string; + const members = (m[2] as string).split("|").map((v: string) => v.trim().replace(/^['"]|['"]$/g, "")); + results[name] = { members, memberCount: members.length, isStringLiteral: true }; + } + return results; + } catch { + return {}; + } + }, +}); + +// Agent role: extract all string literal union type declarations from TypeScript files. +const tsLiteralUnionExtractor = agent({ + model: "small", + addons: steering({ message: "Call extractLiteralUnions for each file path; merge results across all files." }), + instructions: p`Extract string literal union type declarations from all TypeScript source files. + +TypeScript files: +${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | head -40")} + +For each file path above, call extractLiteralUnions to get its literal union types. Merge all results into a single record keyed by type name with members, memberCount, and isStringLiteral.`, + tools: [extractLiteralUnions], + output: s.record( + s.object({ + members: s.array(s.string), + memberCount: s.int, + isStringLiteral: s.boolean, + }) + ), +}); + +export default tsLiteralUnionExtractor; +``` diff --git a/skills/rig/samples/289-markdown-heading-validator.md b/skills/rig/samples/289-markdown-heading-validator.md new file mode 100644 index 0000000..af70384 --- /dev/null +++ b/skills/rig/samples/289-markdown-heading-validator.md @@ -0,0 +1,56 @@ +# 289 - Markdown Heading Validator + +```rig +import { agent, defineTool, p, s, repair } from "rig"; + +const validateHeadings = defineTool("validateHeadings", { + description: "Parse heading structure from markdown content and validate level ordering.", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }: { filePath: string }) { + const { readFile } = await import("node:fs/promises"); + try { + const content = await readFile(filePath, "utf8"); + const headings: Array<{ level: number; text: string }> = []; + for (const m of content.matchAll(/^(#{1,6})\s+(.+)/gm)) { + headings.push({ level: (m[1] as string).length, text: (m[2] as string).trim() }); + } + const issues: string[] = []; + const h1Count = headings.filter((h: { level: number }) => h.level === 1).length; + if (h1Count > 1) issues.push(`Multiple H1 headings found (${h1Count})`); + if (h1Count === 0 && headings.length > 0) issues.push("No H1 heading found"); + for (let i = 1; i < headings.length; i++) { + if (headings[i].level - headings[i - 1].level > 1) { + issues.push(`Skipped heading level at "${headings[i].text}" (level ${headings[i].level} after level ${headings[i - 1].level})`); + } + } + const maxDepth = headings.reduce((max: number, h: { level: number }) => Math.max(max, h.level), 0); + return { headings, maxDepth, isValid: issues.length === 0, issues }; + } catch { + return { headings: [], maxDepth: 0, isValid: false, issues: ["Could not read file"] }; + } + }, +}); + +// Agent role: validate the heading structure of all markdown files in the workspace. +const markdownHeadingValidator = agent({ + model: "small", + addons: repair(), + instructions: p`Validate the heading structure of all markdown files in the workspace. + +Markdown files: +${p.glob("**/*.md")} + +For each file path above, call validateHeadings to check its heading structure. Return a record keyed by file path with headings array, maxDepth, isValid, and any issues found.`, + tools: [validateHeadings], + output: s.record( + s.object({ + headings: s.array(s.object({ level: s.int, text: s.string })), + maxDepth: s.int, + isValid: s.boolean, + issues: s.array(s.string), + }) + ), +}); + +export default markdownHeadingValidator; +```