-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-07-27 #227
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,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; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) ?? []; | ||
|
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 💡 SuggestionThis is a sample so perfect correctness is not required, but it would improve the sample quality (and reader trust) to note the limitation in a comment: // Note: only detects direct declaration exports; re-exports (`export { Foo }`, `export * from`)
// are not captured by this regex. |
||
| 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; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"); | ||
|
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] Dead branch: 💡 FixRemove the dead branch or adjust the logic to also list // Option A: drop the dead condition
const isSample = content.includes("sample");
// Option B: if you want to catch .git/hooks/pre-commit.sample files,
// pass them as hookName values and the check works naturallyAs written the intent (skip |
||
| 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; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"); | ||
|
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 tool reads the file but the agent passes the path via the prompt instruction (line 41) as the hard-coded string 💡 FixHave the agent pass the detected path to the tool, or remove the parameter and hard-code the path inside the tool: // Option A: remove filePath parameter, embed the path
handler: async () => {
const raw = await readFile("coverage/coverage-summary.json", "utf8");
...
}
// Option B (better): let the agent derive the path from p.readOptional and pass it
parameters: { filePath: s.path },
// and instruction says: call parseCoverageSummary with the file path shown above |
||
| const json = JSON.parse(raw) as Record<string, { lines: { pct: number }; branches: { pct: number }; functions: { pct: number } }>; | ||
| const files: Record<string, { lines: number; branches: number; functions: number; status: string }> = {}; | ||
| 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; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, number> = {}; | ||
| const perDayOfWeek: Record<string, number> = {}; | ||
| 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()]; | ||
|
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] 💡 FixValidate const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
const day = days[date.getDay()];
if (day) perDayOfWeek[day] = (perDayOfWeek[day] ?? 0) + 1;
}
totalCommits++;The current guard on |
||
| 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; | ||
| ``` | ||
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
wc -lcommand uses shell word-splitting on the$(find ...)subshell — file paths with spaces will be split into multiple arguments causingwcto error on them. This is a shell injection risk in samples (readers may copy this pattern into production tools).💡 Fix
Use
xargsor awhile readloop to handle paths safely, or replace withfind ... -exec wc -l: