-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-07-25 #156
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,43 @@ | ||
| # 150 - Git Hotspot Analyzer V3 | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
| import { steering } from "rig/addons"; | ||
|
|
||
| // Agent role: identify hot-spot files by git churn and classify risk level. | ||
| const gitHotspotAnalyzerV3 = agent({ | ||
| model: "small", | ||
| addons: steering({ message: "Focus on files that appear most frequently. Assign riskLevel based on churnScore: >20=critical, >10=high, >5=medium, else low." }), | ||
| instructions: p`Analyze git history to find frequently changed files and classify their risk. | ||
|
|
||
| Recently changed files (with churn counts): | ||
| ${p.bash("git log --name-only --format='' HEAD~50..HEAD | grep -v '^$' | sort | uniq -c | sort -rn | head -30")} | ||
|
|
||
| Top contributors overall: | ||
| ${p.bash("git shortlog -sn HEAD~50..HEAD 2>/dev/null | head -10")} | ||
|
|
||
| Use the countChurn tool to parse the churn output for each file. For each file, determine | ||
| the churnScore, assign topContributors from the shortlog output, and classify riskLevel. | ||
| Return a record keyed by file path with the declared output shape.`, | ||
| tools: [ | ||
| defineTool("countChurn", { | ||
| description: "Parse a line of uniq -c output to extract count and filename", | ||
| parameters: s.object({ line: s.string }), | ||
| handler({ line }) { | ||
| const m = line.trim().match(/^(\d+)\s+(.+)$/); | ||
| if (!m) return { count: 0, file: "" }; | ||
| return { count: parseInt(m[1], 10), file: m[2].trim() }; | ||
| }, | ||
| }), | ||
| ], | ||
| output: s.record( | ||
| s.object({ | ||
| churnScore: s.int, | ||
| topContributors: s.array(s.string), | ||
| riskLevel: s.enum("low", "medium", "high", "critical"), | ||
| }) | ||
| ), | ||
| }); | ||
|
|
||
| export default gitHotspotAnalyzerV3; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # 151 - Loc Statistics V2 | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
|
|
||
| // Agent role: gather lines-of-code statistics per file extension. | ||
| const locStatisticsV2 = agent({ | ||
| model: "small", | ||
| instructions: p`Count lines of code per file extension in this workspace. | ||
|
|
||
| TypeScript files found: | ||
| ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | head -50")} | ||
|
|
||
| Line counts for TypeScript files: | ||
| ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | head -20 | xargs wc -l 2>/dev/null | tail -1")} | ||
|
|
||
| JavaScript files found: | ||
| ${p.bash("find . -name '*.js' -not -path '*/node_modules/*' | head -20")} | ||
|
|
||
| Use the aggregateExtension tool to compute totals. Then classify complexity for each | ||
| extension: xlarge > 10000 lines, large > 5000, medium > 1000, small otherwise. | ||
| Return a record keyed by extension (e.g. ".ts", ".js") with lineCount, fileCount, | ||
| and complexity.`, | ||
| tools: [ | ||
| defineTool("aggregateExtension", { | ||
| description: "Count lines in files matching a given extension", | ||
| parameters: s.object({ extension: s.string, sampleFile: s.string }), | ||
| async handler({ extension, sampleFile }) { | ||
| const { execSync } = await import("node:child_process"); | ||
| try { | ||
| const count = execSync( | ||
| `find . -name '*${extension}' -not -path '*/node_modules/*' | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}'`, | ||
| { encoding: "utf-8" } | ||
| ).trim(); | ||
| const files = execSync( | ||
| `find . -name '*${extension}' -not -path '*/node_modules/*' | wc -l`, | ||
| { encoding: "utf-8" } | ||
| ).trim(); | ||
| return { extension, lineCount: parseInt(count) || 0, fileCount: parseInt(files) || 0, sampleFile }; | ||
| } catch { | ||
| return { extension, lineCount: 0, fileCount: 0, sampleFile }; | ||
| } | ||
| }, | ||
| }), | ||
| ], | ||
| output: s.record( | ||
| s.object({ | ||
| lineCount: s.int, | ||
| fileCount: s.int, | ||
| complexity: s.enum("small", "medium", "large", "xlarge"), | ||
| }) | ||
| ), | ||
| }); | ||
|
|
||
| export default locStatisticsV2; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # 152 - Import Cycle Detector V3 | ||
|
|
||
| ```rig | ||
| import { agent, p, s } from "rig"; | ||
| import { repair } from "rig/addons"; | ||
|
|
||
| // Agent role: detect circular import cycles in TypeScript source and classify severity. | ||
| const importCycleDetectorV3 = agent({ | ||
| model: "small", | ||
| maxTurns: 3, | ||
| addons: repair(), | ||
| instructions: p`Detect circular import cycles in this TypeScript project. | ||
|
|
||
| Circular dependency analysis: | ||
| ${p.bash("npx madge --circular --extensions ts src 2>/dev/null || echo 'No cycles found or madge not available'")} | ||
|
|
||
| TypeScript configuration: | ||
| ${p.readOptional("tsconfig.json", "{}")} | ||
|
|
||
| Analyze the output above. Each cycle is a group of files that import each other in a | ||
| circle. Classify severity: high if the cycle involves more than 3 files or core modules, | ||
| medium for 2-3 files, low for simple two-file cycles. | ||
|
|
||
| Return hasCycles (true if any cycles found), the cycles array with path (array of file | ||
| strings in the cycle) and severity, and totalCycles count.`, | ||
| output: s.object({ | ||
| hasCycles: s.boolean, | ||
| cycles: s.array( | ||
| s.object({ | ||
| path: s.array(s.string), | ||
| severity: s.enum("high", "medium", "low"), | ||
| }) | ||
| ), | ||
| totalCycles: s.int, | ||
| }), | ||
| }); | ||
|
|
||
| export default importCycleDetectorV3; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # 153 - Coverage Badge Updater V2 | ||
|
|
||
| ```rig | ||
| import { agent, p, s } from "rig"; | ||
|
|
||
| // Agent role: read coverage summary and update README with shields.io badge links. | ||
| const coverageBadgeUpdaterV2 = agent({ | ||
| model: "small", | ||
| instructions: p`Read coverage data and update the README with shields.io coverage badges. | ||
|
|
||
| Coverage summary (JSON): | ||
| ${p.readOptional("coverage/coverage-summary.json", "{}")} | ||
|
|
||
| Current README: | ||
| ${p.readOptional("README.md", "# Project\n")} | ||
|
|
||
| Compute the coverage percentage for each category (statements, branches, functions, lines) | ||
| from the total section. Compute overallPct as the average. Classify rating: green >= 80%, | ||
| yellow >= 60%, red < 60%. | ||
|
|
||
| Generate shields.io badge markdown for each category using URL format: | ||
| https://img.shields.io/badge/coverage-XX%25-green | ||
|
|
||
| Write the updated README with badges added at the top using p.write. | ||
|
|
||
| ${p.write("README.md", "<!-- badges will be written by agent -->")} | ||
|
|
||
| Return coverageByCategory (record of category to percentage), overallPct, rating, and | ||
| badgesWritten (true if README was updated).`, | ||
| output: s.object({ | ||
| coverageByCategory: s.record(s.number), | ||
|
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] 💡 Suggested fixThe invariant (documented in instructions: p`...Generate badge markdown.
${p.writeOutput("badgesMarkdown", "README.md")}`,
output: s.object({
coverageByCategory: s.record(s.number),
overallPct: s.number,
rating: s.enum("green", "yellow", "red"),
badgesMarkdown: s.string, // harness writes this to README.md
badgesWritten: s.boolean,
}),Existing samples 123, 133, 143 all use |
||
| overallPct: s.number, | ||
| rating: s.enum("green", "yellow", "red"), | ||
| badgesWritten: s.boolean, | ||
| }), | ||
| }); | ||
|
|
||
| export default coverageBadgeUpdaterV2; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # 154 - Dep License Auditor V2 | ||
|
|
||
| ```rig | ||
| import { agent, p, s } from "rig"; | ||
|
|
||
| // Agent role: audit npm dependency licenses and flag copyleft packages. | ||
| const depLicenseAuditorV2 = agent({ | ||
| model: "small", | ||
| instructions: p`Audit the licenses of npm dependencies and classify each one. | ||
|
|
||
| Installed dependencies (JSON tree): | ||
| ${p.bash("npm ls --json --depth=0 2>/dev/null || echo '{}'")} | ||
|
|
||
| Package dependency names: | ||
| ${p.bash("node -e \"try{const p=require('./package.json');console.log(JSON.stringify(Object.keys(p.dependencies||{})))}catch(e){console.log('[]')}\"")} | ||
|
|
||
| Package license fields from node_modules: | ||
| ${p.bash("node -e \"const fs=require('fs');const d='./node_modules';if(fs.existsSync(d)){const pkgs=fs.readdirSync(d).filter(x=>!x.startsWith('.'));pkgs.slice(0,30).forEach(p=>{try{const m=JSON.parse(fs.readFileSync(d+'/'+p+'/package.json','utf8'));console.log(p+':'+m.license)}catch(e){}});}\" 2>/dev/null || echo 'unavailable'")} | ||
|
|
||
| For each dependency, determine its license. Classify as: | ||
| - permissive: MIT, ISC, BSD, Apache, CC0, Unlicense, 0BSD | ||
| - copyleft: GPL, LGPL, AGPL, MPL, EUPL, CC-BY-SA | ||
| - unknown: anything else or unspecified | ||
|
|
||
| Return the packages array with name, license, and classification, hasCopyleft (true if any | ||
| copyleft found), and totalCount.`, | ||
| output: s.object({ | ||
| packages: s.array( | ||
| s.object({ | ||
| name: s.string, | ||
| license: s.string, | ||
| classification: s.enum("permissive", "copyleft", "unknown"), | ||
| }) | ||
| ), | ||
| hasCopyleft: s.boolean, | ||
| totalCount: s.int, | ||
| }), | ||
| }); | ||
|
|
||
| export default depLicenseAuditorV2; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # 155 - Test Coverage Mapper V2 | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
| import { repair } from "rig/addons"; | ||
|
|
||
| // Agent role: map source files to their test files using filename heuristics. | ||
| const testCoverageMapperV2 = agent({ | ||
| model: "small", | ||
| maxTurns: 2, | ||
| addons: repair(), | ||
| instructions: p`Map TypeScript source files to their corresponding test files. | ||
|
|
||
| Source files (non-test TypeScript): | ||
| ${p.bash("find . -name '*.ts' -not -name '*.test.ts' -not -name '*.spec.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' | head -30")} | ||
|
|
||
| Test files: | ||
| ${p.bash("find . -name '*.test.ts' -o -name '*.spec.ts' | grep -v node_modules | head -30")} | ||
|
|
||
| Use the matchTestFile tool for each source file to find its corresponding test file using | ||
| filename heuristics (e.g., src/foo.ts -> src/foo.test.ts or tests/foo.test.ts). | ||
|
|
||
| Return a record keyed by source file path with coverage ("covered" if test found, | ||
| "uncovered" if none, "partial" if indirect match), testFiles (array of matched test paths), | ||
| and reason explaining the determination.`, | ||
| tools: [ | ||
| defineTool("matchTestFile", { | ||
| description: "Find test files that match a source file using naming heuristics", | ||
| parameters: s.object({ sourceFile: s.string, testFiles: s.array(s.string) }), | ||
| handler({ sourceFile, testFiles }) { | ||
| const base = sourceFile.replace(/\.tsx?$/, "").replace(/^.*\//, ""); | ||
| const direct = testFiles.filter((t) => | ||
| t.includes(`${base}.test`) || t.includes(`${base}.spec`) | ||
| ); | ||
| const partial = direct.length === 0 | ||
| ? testFiles.filter((t) => t.toLowerCase().includes(base.toLowerCase())) | ||
| : []; | ||
| return { | ||
| matched: direct, | ||
| partial, | ||
| coverage: direct.length > 0 ? "covered" : partial.length > 0 ? "partial" : "uncovered", | ||
| }; | ||
| }, | ||
| }), | ||
| ], | ||
| output: s.record( | ||
| s.object({ | ||
| coverage: s.enum("covered", "uncovered", "partial"), | ||
| testFiles: s.array(s.string), | ||
| reason: s.string, | ||
| }) | ||
| ), | ||
| }); | ||
|
|
||
| export default testCoverageMapperV2; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # 156 - Regex Pattern Tester | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
| import { repair } from "rig/addons"; | ||
|
|
||
| // Agent role: run regex patterns against test cases and report pass/fail results. | ||
| const regexPatternTester = agent({ | ||
| model: "small", | ||
| addons: repair(), | ||
| input: s.object({ | ||
| patterns: s.array( | ||
| s.object({ | ||
| name: s.string, | ||
| regex: s.string, | ||
| testCases: s.array( | ||
| s.object({ | ||
| input: s.string, | ||
| expected: s.boolean, | ||
| }) | ||
| ), | ||
| }) | ||
| ), | ||
| }), | ||
| instructions: p`Run each regex pattern against its test cases and report results. | ||
|
|
||
| Input patterns and test cases: | ||
| ${p.json("input")} | ||
|
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] 💡 Suggested fixReplace: ${p.json("input")}With: Input patterns and test cases: ${p.inputField("patterns")}
|
||
|
|
||
| Use the runRegexTest tool for each (pattern, input) pair. Then build the results array | ||
| with patternName, input, expected, actual (from tool), and passed (expected === actual). | ||
| Count passCount and failCount. Set allPassed to true only if failCount is 0.`, | ||
| tools: [ | ||
| defineTool("runRegexTest", { | ||
| description: "Test a regex pattern against an input string", | ||
| parameters: s.object({ pattern: s.string, input: s.string }), | ||
| handler({ pattern, input }) { | ||
| try { | ||
| const matched = new RegExp(pattern).test(input); | ||
| return { matched }; | ||
| } catch { | ||
| return { matched: false }; | ||
| } | ||
| }, | ||
| }), | ||
| ], | ||
| output: s.object({ | ||
| results: s.array( | ||
| s.object({ | ||
| patternName: s.string, | ||
| input: s.string, | ||
| expected: s.boolean, | ||
| actual: s.boolean, | ||
| passed: s.boolean, | ||
| }) | ||
| ), | ||
| passCount: s.int, | ||
| failCount: s.int, | ||
| allPassed: s.boolean, | ||
| }), | ||
| }); | ||
|
|
||
| export default regexPatternTester; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # 157 - Commit Churn Classifier | ||
|
|
||
| ```rig | ||
| import { agent, p, s } from "rig"; | ||
| import { steering } from "rig/addons"; | ||
|
|
||
| // Agent role: classify 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 100 commits (count file): | ||
| ${p.bash("git log --name-only --format='' HEAD~100..HEAD 2>/dev/null | grep -v '^$' | sort | uniq -c | sort -rn | head -30")} | ||
|
|
||
| For each file in the output above, 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; | ||
| ``` |
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
extensionparameter in the shell command is interpolated without any sanitisation — passingextension = '.ts; rm -rf .')would execute arbitrary commands. Since this is a sample demonstratingdefineTool, the parameter should be validated or escaped before being interpolated into a shell string.💡 Suggested fix
Add an allowlist check before the shell call:
Samples are teaching material — they should model safe shell-interpolation practices.