diff --git a/skills/rig/samples/290-commit-churn-classifier.md b/skills/rig/samples/290-commit-churn-classifier.md new file mode 100644 index 0000000..7f01550 --- /dev/null +++ b/skills/rig/samples/290-commit-churn-classifier.md @@ -0,0 +1,30 @@ +# 290-commit-churn-classifier - Commit Churn Classifier + +```rig +import { agent, p, s, defineTool, steering, repair } from "rig"; + +const classifyRisk = defineTool("classifyRisk", { + description: "Classify churn risk level based on commit count", + parameters: s.object({ churnCount: s.int }), + handler: ({ churnCount }) => { + if (churnCount <= 2) return "stable" as const; + if (churnCount <= 5) return "active" as const; + if (churnCount <= 10) return "volatile" as const; + return "critical" as const; + }, +}); + +// Agent role: analyze git commit history to classify file churn and risk levels +const commitChurnClassifier = agent({ + model: "small", + instructions: p`Analyze commit history from ${p.bash("git log --name-only --format='' | grep -v '^$' | sort | uniq -c | sort -rn | head -30")} and classify each file's churn risk using the classifyRisk tool. Return a record keyed by file path.`, + output: s.record(s.object({ + churnCount: s.int, + riskLevel: s.enum("stable", "active", "volatile", "critical"), + })), + tools: [classifyRisk], + addons: [steering(), repair()], +}); + +export default commitChurnClassifier; +``` diff --git a/skills/rig/samples/291-npm-package-size-estimator.md b/skills/rig/samples/291-npm-package-size-estimator.md new file mode 100644 index 0000000..da14bec --- /dev/null +++ b/skills/rig/samples/291-npm-package-size-estimator.md @@ -0,0 +1,37 @@ +# 291-npm-package-size-estimator - Npm Package Size Estimator + +```rig +import { agent, p, s, defineTool, repair } 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 }) => { + if (sizeKb < 50) return "tiny" as const; + if (sizeKb < 200) 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 classify it into a tier rating +const npmPackageSizeEstimator = agent({ + model: "small", + instructions: p`Estimate this package's size using: +- Pack dry-run: ${p.bash("npm pack --dry-run 2>&1 | head -40")} +- Directory size: ${p.bash("du -sh . 2>/dev/null | head -5")} + +Use the classifySize tool to determine the tier rating. Return estimatedSizeKb, topFiles (up to 5 files with their sizes), sizeRating, and a recommendation.`, + 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, + }), + tools: [classifySize], + addons: [repair()], +}); + +export default npmPackageSizeEstimator; +``` diff --git a/skills/rig/samples/292-ts-branch-coverage-analyzer.md b/skills/rig/samples/292-ts-branch-coverage-analyzer.md new file mode 100644 index 0000000..2bcf8d7 --- /dev/null +++ b/skills/rig/samples/292-ts-branch-coverage-analyzer.md @@ -0,0 +1,42 @@ +# 292-ts-branch-coverage-analyzer - Ts Branch Coverage Analyzer + +```rig +import { agent, p, s } from "rig"; + +// Agent role: analyze TypeScript source for branch coverage patterns +const branchAnalyzer = agent({ + name: "branchAnalyzer", + model: "small", + instructions: p`Analyze the TypeScript source provided in the input and identify all branches (if/ternary/switch/logical). For each branch, determine the function name, line number, branch type, and whether it is likely covered based on test presence. Return a detailed array of branch objects.`, + input: s.object({ source: s.string }), + output: s.array(s.object({ + functionName: s.string, + line: s.int, + branchType: s.enum("if", "ternary", "switch", "logical"), + covered: s.boolean, + })), +}); + +// Agent role: coordinate branch coverage analysis for a TypeScript file +const tsBranchCoverageAnalyzer = agent({ + model: "small", + instructions: p`Read the TypeScript file at the path in the input using the readInput intent and delegate branch analysis to the branchAnalyzer subagent. Compute totalBranches, coveredBranches, and uncoveredBranches from the results.`, + input: s.object({ filePath: s.path }), + output: s.object({ + branches: s.array(s.object({ + functionName: s.string, + line: s.int, + branchType: s.enum("if", "ternary", "switch", "logical"), + covered: s.boolean, + })), + summary: s.object({ + totalBranches: s.int, + coveredBranches: s.int, + uncoveredBranches: s.int, + }), + }), + agents: { branchAnalyzer }, +}); + +export default tsBranchCoverageAnalyzer; +``` diff --git a/skills/rig/samples/293-env-variable-type-inferrer.md b/skills/rig/samples/293-env-variable-type-inferrer.md new file mode 100644 index 0000000..b4fb84e --- /dev/null +++ b/skills/rig/samples/293-env-variable-type-inferrer.md @@ -0,0 +1,35 @@ +# 293-env-variable-type-inferrer - Env Variable Type Inferrer + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const inferType = defineTool("inferType", { + description: "Infer the type of an environment variable value", + parameters: s.object({ key: s.string, value: s.string }), + handler: ({ value }) => { + if (value === "true" || value === "false") return "boolean" as const; + if (/^\d+(\.\d+)?$/.test(value)) return "number" as const; + if (/^https?:\/\//.test(value)) return "url" as const; + if (/^[./~]|^\$HOME/.test(value)) return "path" as const; + if (value.length === 0 || value.startsWith("<") || value.startsWith("#")) return "unknown" as const; + return "string" as const; + }, +}); + +// Agent role: infer types of environment variables from .env.example +const envVariableTypeInferrer = agent({ + model: "small", + instructions: p`Analyze the environment variable definitions from ${p.readOptional(".env.example", "# no .env.example found")} and use the inferType tool for each variable. Produce a record of variable names to their inferred type and description, plus allDocumented indicating whether all variables have non-empty descriptions.`, + output: s.object({ + variables: s.record(s.object({ + type: s.enum("string", "number", "boolean", "url", "path", "unknown"), + description: s.string, + })), + allDocumented: s.boolean, + }), + tools: [inferType], + addons: [repair()], +}); + +export default envVariableTypeInferrer; +``` diff --git a/skills/rig/samples/294-makefile-target-extractor.md b/skills/rig/samples/294-makefile-target-extractor.md new file mode 100644 index 0000000..5ce5df6 --- /dev/null +++ b/skills/rig/samples/294-makefile-target-extractor.md @@ -0,0 +1,41 @@ +# 294-makefile-target-extractor - Makefile Target Extractor + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const parseTarget = defineTool("parseTarget", { + description: "Parse a Makefile target line and return its metadata", + parameters: s.object({ line: s.string, phonyTargets: s.string }), + handler: ({ line, phonyTargets }) => { + const match = line.match(/^([a-zA-Z0-9_.-]+)\s*:/); + if (!match) return null; + const name = match[1]; + const isPhony = phonyTargets.includes(name); + const hasHelp = line.includes("##"); + const descMatch = line.match(/##\s*(.+)/); + return { name, isPhony, hasHelp, description: descMatch ? descMatch[1].trim() : undefined }; + }, +}); + +// Agent role: extract and classify targets from a Makefile +const makefileTargetExtractor = agent({ + model: "small", + instructions: p`Parse the Makefile content: ${p.readOptional("Makefile", "# no Makefile found")} + +Use the parseTarget tool for each target line. Collect all targets and their metadata, count phony vs real targets, and return the structured result.`, + 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, + }), + tools: [parseTarget], + addons: [repair()], +}); + +export default makefileTargetExtractor; +``` diff --git a/skills/rig/samples/295-test-fixture-generator.md b/skills/rig/samples/295-test-fixture-generator.md new file mode 100644 index 0000000..8442d21 --- /dev/null +++ b/skills/rig/samples/295-test-fixture-generator.md @@ -0,0 +1,37 @@ +# 295-test-fixture-generator - Test Fixture Generator + +```rig +import { agent, p, s } from "rig"; + +// Agent role: extract function signature from a TypeScript source file +const signatureAnalyzer = agent({ + name: "signatureAnalyzer", + model: "small", + instructions: p`Given the TypeScript source in the input, extract the signature for the function named in functionName. Return the parameter types, return type, and any imports needed to use this function in a test.`, + input: s.object({ source: s.string, functionName: s.string }), + output: s.object({ + params: s.array(s.object({ name: s.string, type: s.string })), + returnType: s.string, + imports: s.array(s.string), + }), +}); + +// Agent role: generate test fixtures for a TypeScript function +const testFixtureGenerator = agent({ + model: "small", + instructions: p`Generate a test fixture for the function specified in the input. + +Read the source file: ${p.readInput("sourceFile")} + +Delegate signature extraction to the signatureAnalyzer subagent for the function named in the input's functionName field. Then generate a complete test fixture file with sample inputs, expected outputs, and required imports. Write the fixture to the path in suggestedFileName.`, + input: s.object({ sourceFile: s.path, functionName: s.string }), + 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/296-git-file-ownership-mapper.md b/skills/rig/samples/296-git-file-ownership-mapper.md new file mode 100644 index 0000000..fdab9c7 --- /dev/null +++ b/skills/rig/samples/296-git-file-ownership-mapper.md @@ -0,0 +1,48 @@ +# 296-git-file-ownership-mapper - Git File Ownership Mapper + +```rig +import { agent, p, s, defineTool, steering } from "rig"; +import { execSync } from "node:child_process"; + +const getFileOwner = defineTool("getFileOwner", { + description: "Get git ownership info for a file path", + parameters: s.object({ filePath: s.path }), + handler: async ({ filePath }) => { + try { + const output = execSync(`git log --format="%ae" -- "${filePath}" 2>/dev/null`, { encoding: "utf8" }); + const emails = output.trim().split("\n").filter((e: string) => e.length > 0); + const counts: Record = {}; + for (const email of emails) { + counts[email] = (counts[email] ?? 0) + 1; + } + const sorted = Object.entries(counts).sort((a: [string, number], b: [string, number]) => b[1] - a[1]); + const primaryOwner = sorted[0]?.[0] ?? "unknown"; + const commitCount = emails.length; + const contributors = sorted.map((e: [string, number]) => e[0]); + return { primaryOwner, commitCount, contributors }; + } catch { + return { primaryOwner: "unknown", commitCount: 0, contributors: [] }; + } + }, +}); + +// Agent role: map git file ownership by analyzing commit history for TypeScript files +const gitFileOwnershipMapper = agent({ + model: "small", + instructions: p`Discover TypeScript source files: ${p.glob("src/**/*.ts")} + +Use the getFileOwner tool for each discovered file path to collect ownership data. Build a complete ownership record and identify the mostActiveContributor across all files.`, + output: s.object({ + ownership: s.record(s.object({ + primaryOwner: s.string, + commitCount: s.int, + contributors: s.array(s.string), + })), + mostActiveContributor: s.string, + }), + tools: [getFileOwner], + addons: [steering()], +}); + +export default gitFileOwnershipMapper; +``` diff --git a/skills/rig/samples/297-workflow-input-validator.md b/skills/rig/samples/297-workflow-input-validator.md new file mode 100644 index 0000000..f88fbb0 --- /dev/null +++ b/skills/rig/samples/297-workflow-input-validator.md @@ -0,0 +1,52 @@ +# 297-workflow-input-validator - Workflow Input Validator + +```rig +import { agent, p, s, defineTool, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const validateWorkflowInputs = defineTool("validateWorkflowInputs", { + description: "Parse workflow_dispatch inputs from a GitHub Actions workflow YAML file", + parameters: s.object({ filePath: s.path }), + handler: async ({ filePath }) => { + const content = await readFile(filePath, "utf8"); + const inputSection = content.match(/workflow_dispatch:[\s\S]*?inputs:([\s\S]*?)(?=\n\S|\n\w+:|\n [a-z]+:(?!\s*\n\s+type:)|\Z)/)?.[1] ?? ""; + const inputMatches = [...inputSection.matchAll(/^\s{6}(\w[\w-]*):/gm)]; + const inputs = inputMatches.map((m: RegExpMatchArray) => { + const name = m[1]; + const block = inputSection.slice(m.index ?? 0, (m.index ?? 0) + 300); + const type = block.match(/type:\s*(\w+)/)?.[1] ?? "string"; + const required = /required:\s*true/.test(block); + const hasDefault = /default:/.test(block); + return { name, type, required, hasDefault }; + }); + return { + inputs, + inputCount: inputs.length, + hasRequiredWithoutDefault: inputs.some((i: { required: boolean; hasDefault: boolean }) => i.required && !i.hasDefault), + }; + }, +}); + +// Agent role: validate GitHub Actions workflow_dispatch inputs across all workflow files +const workflowInputValidator = agent({ + model: "small", + instructions: p`Validate workflow_dispatch inputs in all workflow files discovered at ${p.glob(".github/workflows/*.yml")}. Use the validateWorkflowInputs tool for each file path. Return a record keyed by filename and the total workflow count.`, + output: s.object({ + workflows: s.record(s.object({ + inputs: s.array(s.object({ + name: s.string, + type: s.string, + required: s.boolean, + hasDefault: s.boolean, + })), + inputCount: s.int, + hasRequiredWithoutDefault: s.boolean, + })), + totalWorkflows: s.int, + }), + tools: [validateWorkflowInputs], + addons: [repair()], +}); + +export default workflowInputValidator; +``` diff --git a/skills/rig/samples/298-ts-dead-export-finder.md b/skills/rig/samples/298-ts-dead-export-finder.md new file mode 100644 index 0000000..0085f72 --- /dev/null +++ b/skills/rig/samples/298-ts-dead-export-finder.md @@ -0,0 +1,48 @@ +# 298-ts-dead-export-finder - Ts Dead Export Finder + +```rig +import { agent, p, s, defineTool, steering, repair } from "rig"; +import { readFile } from "node:fs/promises"; + +const findUnusedExports = defineTool("findUnusedExports", { + description: "Find exported symbols in a TypeScript file that are not imported elsewhere", + parameters: s.object({ filePath: s.path, allFiles: s.array(s.string) }), + handler: async ({ filePath, allFiles }) => { + const content = await readFile(filePath, "utf8"); + const exportMatches = [...content.matchAll(/export\s+(?:const|function|class|type|interface|enum)\s+(\w+)/g)]; + const exportedNames = exportMatches.map((m: RegExpMatchArray) => m[1]); + const unusedNames: string[] = []; + for (const name of exportedNames) { + let isImported = false; + for (const other of allFiles) { + if (other === filePath) continue; + try { + const otherContent = await readFile(other, "utf8"); + if (otherContent.includes(name)) { isImported = true; break; } + } catch { /* skip */ } + } + if (!isImported) unusedNames.push(name); + } + return unusedNames; + }, +}); + +// Agent role: find unused TypeScript exports across the src directory +const tsDeadExportFinder = agent({ + model: "small", + instructions: p`Find potentially dead (unused) TypeScript exports. +Files with exports: ${p.bash("grep -rl 'export ' src/ --include='*.ts' 2>/dev/null | head -20 || echo 'none'")} +All TS files: ${p.glob("src/**/*.ts")} + +Use the findUnusedExports tool for each file that has exports, passing all discovered files. Build a record of unused exports keyed by file path, the total count, and whether dead code was found.`, + output: s.object({ + unusedExports: s.record(s.array(s.string)), + totalUnused: s.int, + hasDeadCode: s.boolean, + }), + tools: [findUnusedExports], + addons: [steering(), repair()], +}); + +export default tsDeadExportFinder; +``` diff --git a/skills/rig/samples/299-package-json-scripts-documenter.md b/skills/rig/samples/299-package-json-scripts-documenter.md new file mode 100644 index 0000000..51d1191 --- /dev/null +++ b/skills/rig/samples/299-package-json-scripts-documenter.md @@ -0,0 +1,48 @@ +# 299-package-json-scripts-documenter - Package Json Scripts Documenter + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const inferScriptPurpose = defineTool("inferScriptPurpose", { + description: "Classify an npm script and suggest a one-line description of its purpose", + parameters: s.object({ name: s.string, command: s.string }), + handler: ({ name, command }: { name: string; command: string }) => { + const lname = name.toLowerCase(); + const lcmd = command.toLowerCase(); + let category: "build" | "test" | "lint" | "release" | "dev" | "other"; + if (lname.includes("build") || lcmd.includes("tsc") || lcmd.includes("webpack") || lcmd.includes("rollup") || lcmd.includes("vite build")) { + category = "build"; + } else if (lname.includes("test") || lcmd.includes("vitest") || lcmd.includes("jest") || lcmd.includes("mocha")) { + category = "test"; + } else if (lname.includes("lint") || lcmd.includes("eslint") || lcmd.includes("prettier")) { + category = "lint"; + } else if (lname.includes("release") || lname.includes("publish") || lname.includes("version")) { + category = "release"; + } else if (lname.includes("dev") || lname.includes("start") || lname.includes("watch")) { + category = "dev"; + } else { + category = "other"; + } + return { category }; + }, +}); + +// Agent role: document npm scripts from package.json and write a SCRIPTS.md file +const packageJsonScriptsDocumenter = agent({ + model: "small", + instructions: p`Read the npm scripts from ${p.read("package.json")} and use the inferScriptPurpose tool for each script to classify it. Then write ${p.write("SCRIPTS.md", "scripts")} a well-structured markdown documentation file listing each script with its purpose, category, and command. Return the structured record of scripts, the count, and the output file path.`, + output: s.object({ + scripts: s.record(s.object({ + purpose: s.string, + category: s.enum("build", "test", "lint", "release", "dev", "other"), + command: s.string, + })), + documentedCount: s.int, + outputFile: s.path, + }), + tools: [inferScriptPurpose], + addons: [repair()], +}); + +export default packageJsonScriptsDocumenter; +```