-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-08-30 #513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # 491 - Dotenv Process Drift Detector | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, repair } from "rig"; | ||
|
|
||
| const classifyEnvKey = defineTool("classifyEnvKey", { | ||
| description: "Classify an env key based on whether it's in .env.example and used in code", | ||
| parameters: s.object({ key: s.string, inExample: s.boolean, usedInCode: s.boolean }), | ||
| handler({ inExample, usedInCode }): "declared_and_used" | "declared_not_used" | "used_not_declared" { | ||
| if (inExample && usedInCode) return "declared_and_used" as const; | ||
| if (inExample && !usedInCode) return "declared_not_used" as const; | ||
| return "used_not_declared" as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: detect drift between .env.example declarations and actual process.env usages in source code. | ||
| const dotenvDriftDetector = agent({ | ||
| model: "small", | ||
| instructions: p`Compare declared env keys in ${p.read(".env.example")} with actual usages from ${p.bash("grep -rn 'process\\.env\\.' src/ 2>/dev/null || true")}. Use classifyEnvKey for each key found. Return the full drift report.`, | ||
| output: s.object({ | ||
| keys: s.record(s.object({ inExample: s.boolean, usedInCode: s.boolean, status: s.string })), | ||
| totalKeys: s.int, | ||
| missingFromExample: s.array(s.string), | ||
| unusedDeclarations: s.array(s.string), | ||
| }), | ||
| tools: [classifyEnvKey], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default dotenvDriftDetector; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # 492 - Vitest Snapshot Count Workflow | ||
|
|
||
| ```rig | ||
| import { agent, workflow, p, s } from "rig"; | ||
|
|
||
| // Agent role: scan for Vitest snapshot files and count lines. | ||
| const snapshotScanner = agent({ | ||
| model: "small", | ||
| instructions: p`Find snapshot files with ${p.bash("find . -path '*/__snapshots__/*.snap' -not -path '*/node_modules/*' 2>/dev/null | head -20 || true")} and count lines in each with ${p.bash("find . -path '*/__snapshots__/*.snap' -not -path '*/node_modules/*' 2>/dev/null | xargs wc -l 2>/dev/null || echo '0 total'")}. Return the file list and total count.`, | ||
| output: s.object({ | ||
| files: s.array(s.object({ path: s.path, lineCount: s.int })), | ||
| totalFiles: s.int, | ||
| }), | ||
| }); | ||
|
|
||
| // Agent role: count snapshot export entries across snapshot files. | ||
| const snapshotParser = agent({ | ||
| model: "small", | ||
| instructions: p`Find snapshot files containing exports using ${p.bash("grep -rl 'exports\\[' . --include='*.snap' 2>/dev/null | head -20 || true")}. Count total export entries with ${p.bash("grep -r 'exports\\[' . --include='*.snap' 2>/dev/null | wc -l || echo 0")}. Return file list and count.`, | ||
| output: s.object({ | ||
| snapshotFiles: s.array(s.path), | ||
| exportCount: s.int, | ||
| }), | ||
| }); | ||
|
|
||
| // Workflow role: run snapshot scanner and parser in parallel and combine results. | ||
| const vitestSnapshotWorkflow = workflow({ | ||
| meta: { name: "vitest-snapshot-count-workflow", description: "Count Vitest snapshots in parallel" }, | ||
| body: async ({ call }) => { | ||
| const [scanResult, parseResult] = await Promise.all([ | ||
| call(snapshotScanner, ""), | ||
| call(snapshotParser, ""), | ||
| ]); | ||
| return { | ||
| totalSnapshotFiles: scanResult?.totalFiles ?? 0, | ||
| totalLines: (scanResult?.files ?? []).reduce((sum: number, f: { lineCount: number }) => sum + f.lineCount, 0), | ||
| snapshotFiles: parseResult?.snapshotFiles ?? [], | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/codebase-design] 💡 Include exportCount in the return valuereturn {
totalSnapshotFiles: scanResult?.totalFiles ?? 0,
totalLines: (scanResult?.files ?? []).reduce((sum: number, f: { lineCount: number }) => sum + f.lineCount, 0),
snapshotFiles: parseResult?.snapshotFiles ?? [],
exportCount: parseResult?.exportCount ?? 0, // add this
}; |
||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| export default vitestSnapshotWorkflow; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # 493 - Git Stale Branch Reporter | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
|
|
||
| const classifyBranchAge = defineTool("classifyBranchAge", { | ||
| description: "Classify a branch by age based on its last activity description", | ||
| parameters: s.object({ branch: s.string, lastActivity: s.string }), | ||
| handler({ lastActivity }): "fresh" | "aging" | "stale" | "ancient" { | ||
| const l = lastActivity.toLowerCase(); | ||
| if (l.includes("second") || l.includes("minute") || l.includes("hour")) return "fresh" as const; | ||
| if (l.includes("day") && !l.includes("week")) return "aging" as const; | ||
| if (l.includes("week") || l.includes("month")) return "stale" as const; | ||
| return "ancient" as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: report on git remote branches sorted by last commit date and classify their age. | ||
| const gitStaleBranchReporter = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze git branches from ${p.bash("git branch -r --sort=-committerdate --format='%(refname:short) %(committerdate:relative)' 2>/dev/null | head -30 || true")}. Use classifyBranchAge for each branch to determine its freshness status. Return the full report.`, | ||
| output: s.object({ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/grill-with-docs] Same pattern: |
||
| branches: s.array(s.object({ name: s.string, lastActivity: s.string, status: s.string })), | ||
| staleBranches: s.array(s.string), | ||
| freshCount: s.int, | ||
| staleCount: s.int, | ||
| }), | ||
| tools: [classifyBranchAge], | ||
| }); | ||
|
|
||
| export default gitStaleBranchReporter; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # 494 - Ts Abstract Class Finder | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
|
|
||
| const extractAbstractClass = defineTool("extractAbstractClass", { | ||
| description: "Extract abstract class name and export status from a declaration line", | ||
| parameters: s.object({ filePath: s.path, line: s.int, declaration: s.string }), | ||
| handler({ declaration }) { | ||
| const exported = declaration.trim().startsWith("export"); | ||
| const match = declaration.match(/abstract\s+class\s+(\w+)/); | ||
| return { className: match?.[1] ?? "Unknown", isExported: exported }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: find all TypeScript abstract classes in the source tree and report their export status. | ||
| const tsAbstractClassFinder = agent({ | ||
| model: "small", | ||
| instructions: p`Search for TypeScript abstract classes using ${p.bash("grep -rn 'abstract class' src/ 2>/dev/null || true")}. Also list source files with ${p.glob("src/**/*.ts")}. Use extractAbstractClass for each match to get the class name and export status. Return the full list.`, | ||
| output: s.object({ | ||
| abstractClasses: s.array(s.object({ className: s.string, filePath: s.path, line: s.int, isExported: s.boolean })), | ||
| totalCount: s.int, | ||
| exportedCount: s.int, | ||
| }), | ||
| tools: [extractAbstractClass], | ||
| }); | ||
|
|
||
| export default tsAbstractClassFinder; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # 495 - Dockerfile Env Inspector | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
|
|
||
| const parseEnvInstruction = defineTool("parseEnvInstruction", { | ||
| description: "Parse a Dockerfile ENV instruction line and extract key, value, and whether a default is set", | ||
| parameters: s.object({ file: s.path, line: s.int, raw: s.string }), | ||
| handler({ raw }) { | ||
| const parts = raw.replace(/^ENV\s+/, "").split(/\s+|=/, 2); | ||
| const key = parts[0] ?? ""; | ||
| const value = parts[1] ?? ""; | ||
| return { key, value, hasDefault: value.length > 0 }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: locate Dockerfiles and extract all ENV instructions with their key-value details. | ||
| const dockerfileEnvInspector = agent({ | ||
| model: "small", | ||
| instructions: p`Find Dockerfiles using ${p.bash("find . -name 'Dockerfile*' -not -path '*/node_modules/*' 2>/dev/null | head -20 || true")}. Extract ENV instructions with ${p.bash("grep -rn '^ENV ' $(find . -name 'Dockerfile*' -not -path '*/node_modules/*' 2>/dev/null | head -10 | tr '\\n' ' ') 2>/dev/null || true")}. Use parseEnvInstruction for each ENV line found. Return all env vars.`, | ||
| output: s.object({ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/grill-with-docs] The nested shell substitution in the 💡 Safer alternative: use xargsp.bash("find . -name 'Dockerfile*' -not -path '*/node_modules/*' 2>/dev/null | head -10 | xargs -I{} grep -n '^ENV ' {} 2>/dev/null || true")Using |
||
| envVars: s.array(s.object({ key: s.string, value: s.string, hasDefault: s.boolean, file: s.path, line: s.int })), | ||
| totalCount: s.int, | ||
| dockerfileCount: s.int, | ||
| }), | ||
| tools: [parseEnvInstruction], | ||
| }); | ||
|
|
||
| export default dockerfileEnvInspector; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # 496 - Npm Script Prefix Analyzer | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
|
|
||
| const classifyScriptPrefix = defineTool("classifyScriptPrefix", { | ||
| description: "Classify an npm script by its command prefix", | ||
| parameters: s.object({ scriptName: s.string, command: s.string }), | ||
| handler({ command }): "node" | "npm" | "npx" | "ts-node" | "sh" | "other" { | ||
| const c = command.trim().split(" ")[0] ?? ""; | ||
| if (c === "node") return "node" as const; | ||
| if (c === "npm") return "npm" as const; | ||
| if (c === "npx") return "npx" as const; | ||
| if (c === "ts-node" || c === "tsx") return "ts-node" as const; | ||
| if (c === "sh" || c === "bash") return "sh" as const; | ||
| return "other" as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: read package.json scripts and classify each by command prefix, returning counts per prefix. | ||
| const npmScriptPrefixAnalyzer = agent({ | ||
| model: "small", | ||
| instructions: p`Read scripts from ${p.read("package.json")}. Use classifyScriptPrefix for each script entry to determine its command prefix. Return the per-script details and prefix counts.`, | ||
| output: s.object({ | ||
| scripts: s.record(s.object({ command: s.string, prefix: s.string })), | ||
| prefixCounts: s.record(s.int), | ||
| totalScripts: s.int, | ||
| }), | ||
| tools: [classifyScriptPrefix], | ||
| }); | ||
|
|
||
| export default npmScriptPrefixAnalyzer; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # 497 - Ts Default Param Extractor | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, repair } from "rig"; | ||
|
|
||
| const extractDefaultParam = defineTool("extractDefaultParam", { | ||
| description: "Extract function name, parameter name, and default value from a TypeScript line", | ||
| parameters: s.object({ file: s.path, line: s.int, raw: s.string }), | ||
| handler({ raw }) { | ||
| const fnMatch = raw.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=)/); | ||
| const paramMatch = raw.match(/(\w+)\s*=\s*([^,)]+)/); | ||
| return { | ||
| functionName: fnMatch?.[1] ?? fnMatch?.[2] ?? "anonymous", | ||
| paramName: paramMatch?.[1] ?? "unknown", | ||
| defaultValue: paramMatch?.[2]?.trim() ?? "unknown", | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: find TypeScript functions with default parameter values and report them. | ||
| const tsDefaultParamExtractor = agent({ | ||
| model: "small", | ||
| instructions: p`Find TypeScript files with ${p.glob("src/**/*.ts")}. Search for function parameters with defaults using ${p.bash("grep -rn 'function\\|const.*=>' src/ 2>/dev/null | grep '=' | grep -v '=>' | head -50 || true")}. Use extractDefaultParam for each match. Return the full list of default parameters.`, | ||
| output: s.object({ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/grill-with-docs] The bash grep filter is ambiguous and will produce many false positives. The pattern 💡 More precise grep for default parametersgrep -rn '([^)]*\w\+\s*=\s*[^=][^)]*)' src/ --include='*.ts' 2>/dev/null | head -50 || trueOr target |
||
| defaults: s.array(s.object({ functionName: s.string, paramName: s.string, defaultValue: s.string, file: s.path, line: s.int })), | ||
| totalDefaults: s.int, | ||
| filesWithDefaults: s.array(s.path), | ||
| }), | ||
| tools: [extractDefaultParam], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default tsDefaultParamExtractor; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # 498 - Git Branch Risk Classifier | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, steering, repair } from "rig"; | ||
|
|
||
| const classifyBranchRisk = defineTool("classifyBranchRisk", { | ||
| description: "Classify a branch merge risk based on ahead/behind commit counts", | ||
| parameters: s.object({ branch: s.string, ahead: s.int, behind: s.int }), | ||
| handler({ ahead, behind }): "safe" | "caution" | "risky" | "critical" { | ||
| const total = ahead + behind; | ||
| if (total === 0) return "safe" as const; | ||
| if (total <= 5) return "caution" as const; | ||
| if (total <= 20) return "risky" as const; | ||
| return "critical" as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: list local git branches with ahead/behind counts and classify each by merge risk. | ||
| const gitBranchRiskClassifier = agent({ | ||
| model: "small", | ||
| instructions: p`List branches with ${p.bash("git branch --format='%(refname:short) %(ahead-behind:HEAD)' 2>/dev/null | head -20 || true")}. Use classifyBranchRisk for each branch with its ahead/behind numbers. Return the risk classification for every branch.`, | ||
| output: s.object({ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/grill-with-docs] The output schema uses 💡 Use s.enum in the branches arraybranches: s.array(s.object({ name: s.string, ahead: s.int, behind: s.int, risk: s.enum(["safe", "caution", "risky", "critical"]) })), |
||
| branches: s.array(s.object({ name: s.string, ahead: s.int, behind: s.int, risk: s.string })), | ||
| riskyCritical: s.array(s.string), | ||
| summary: s.string, | ||
| }), | ||
| tools: [classifyBranchRisk], | ||
| addons: [steering(), repair()], | ||
| }); | ||
|
|
||
| export default gitBranchRiskClassifier; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # 499 - Test File Coverage Pipeline | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, workflow, p, s } from "rig"; | ||
|
|
||
| // Agent role: discover all test and spec files in the workspace. | ||
| const testDiscoverer = agent({ | ||
| model: "small", | ||
| instructions: p`Find test files with ${p.glob("**/*.test.ts")} and spec files with ${p.bash("find . -name '*.spec.ts' -not -path '*/node_modules/*' 2>/dev/null | head -30 || true")}. Return distinct lists and total count.`, | ||
| output: s.object({ | ||
| testFiles: s.array(s.path), | ||
| specFiles: s.array(s.path), | ||
| totalCount: s.int, | ||
| }), | ||
| }); | ||
|
|
||
| const classifyTest = defineTool("classifyTest", { | ||
| description: "Classify a test file as unit, integration, or e2e based on its path and name", | ||
| parameters: s.object({ file: s.path, lineCount: s.int }), | ||
| handler({ file }): "unit" | "integration" | "e2e" { | ||
| if (file.includes("e2e") || file.includes("end-to-end")) return "e2e" as const; | ||
| if (file.includes("integration") || file.includes("integ")) return "integration" as const; | ||
| return "unit" as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: classify test files by type and produce counts. | ||
| const testClassifier = agent({ | ||
| model: "small", | ||
| input: s.object({ testFiles: s.array(s.path), specFiles: s.array(s.path), totalCount: s.int }), | ||
| instructions: p`Use classifyTest for each file in input.testFiles and input.specFiles. Also check file sizes using ${p.bash("wc -l $(find . -name '*.test.ts' -o -name '*.spec.ts' 2>/dev/null | head -10 | tr '\\n' ' ') 2>/dev/null || true")}. Return classification map and counts.`, | ||
| output: s.object({ | ||
| classifications: s.record(s.string), | ||
| unitCount: s.int, | ||
| integrationCount: s.int, | ||
| e2eCount: s.int, | ||
| }), | ||
| tools: [classifyTest], | ||
| }); | ||
|
|
||
| // Workflow role: discover test files, classify them, and produce a report. | ||
| const testFileCoveragePipeline = workflow({ | ||
| meta: { name: "test-file-coverage-pipeline", description: "Discover and classify test files in a three-stage pipeline" }, | ||
| body: async ({ call }) => { | ||
| const discovered = await call(testDiscoverer, ""); | ||
| const classified = await call(testClassifier, discovered ?? { testFiles: [], specFiles: [], totalCount: 0 }); | ||
| return { | ||
| totalFiles: discovered?.totalCount ?? 0, | ||
| unitCount: classified?.unitCount ?? 0, | ||
| integrationCount: classified?.integrationCount ?? 0, | ||
| e2eCount: classified?.e2eCount ?? 0, | ||
| classifications: classified?.classifications ?? {}, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| export default testFileCoveragePipeline; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # 500 - Json Schema Type Distribution | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
|
|
||
| const analyzeSchemaTypes = defineTool("analyzeSchemaTypes", { | ||
| description: "Analyze a JSON schema file and count property types", | ||
| parameters: s.object({ schemaFile: s.path, content: s.string }), | ||
| handler({ content }) { | ||
| try { | ||
| const schema = JSON.parse(content); | ||
| const props = schema.properties ?? {}; | ||
| const typeCounts: Record<string, number> = {}; | ||
| for (const v of Object.values(props) as Array<{ type?: string }>) { | ||
| const t = v.type ?? "unknown"; | ||
| typeCounts[t] = (typeCounts[t] ?? 0) + 1; | ||
| } | ||
| return { propertyCount: Object.keys(props).length, typeCounts }; | ||
| } catch { | ||
| return { propertyCount: 0, typeCounts: {} }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: discover JSON schema files and compute the distribution of property types across all schemas. | ||
| const jsonSchemaTypeDistribution = agent({ | ||
| model: "small", | ||
| instructions: p`Find JSON schema files with ${p.glob("**/*.schema.json")} and also using ${p.bash("find . -name '*.schema.json' -not -path '*/node_modules/*' 2>/dev/null | head -10 || true")}. Read and analyze each file using analyzeSchemaTypes. Aggregate property type counts across all schemas. Return the type distribution and most common type.`, | ||
| output: s.object({ | ||
| schemaFiles: s.array(s.path), | ||
| totalProperties: s.int, | ||
| typeDistribution: s.record(s.int), | ||
| mostCommonType: s.string, | ||
| }), | ||
| tools: [analyzeSchemaTypes], | ||
| }); | ||
|
|
||
| export default jsonSchemaTypeDistribution; | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/grill-with-docs] The output schema has a
status: s.stringfield but theclassifyEnvKeytool returns a typed union ("declared_and_used" | "declared_not_used" | "used_not_declared"). Usings.stringinstead ofs.enum([...])loses the type constraint and misses a chance to demonstrates.enum— a schema helper this sample is well-suited to showcase.💡 Use s.enum to match the tool return type