-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-08-09 #382
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,53 @@ | ||
| # 381 - TS Dead Export Finder | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool } 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 in the codebase", | ||
| parameters: s.object({ | ||
| filePath: s.path, | ||
| allFilePaths: s.array(s.path), | ||
| }), | ||
| handler: async ({ filePath, allFilePaths }: { filePath: string; allFilePaths: string[] }) => { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const exportMatches = [...content.matchAll(/^export\s+(?:function|const|class|type|interface|enum)\s+(\w+)/gm)]; | ||
| const exportedNames = exportMatches.map((m) => m[1]); | ||
|
|
||
| const unusedNames: string[] = []; | ||
| for (const name of exportedNames) { | ||
| let isImported = false; | ||
| for (const other of allFilePaths) { | ||
| if (other === filePath) continue; | ||
| const otherContent = await readFile(other, "utf-8").catch(() => ""); | ||
| if (new RegExp(`\\b${name}\\b`).test(otherContent)) { | ||
|
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. [/diagnosing-bugs] The cross-file search uses 💡 More reliable searchA simple improvement: check for the name appearing in an import statement rather than anywhere in the file: if (new RegExp(`[{,]\\s*${name}\\s*[},]`).test(otherContent)) {
isImported = true;
break;
}For a production tool, consider using |
||
| isImported = true; | ||
| break; | ||
| } | ||
| } | ||
| if (!isImported) unusedNames.push(name); | ||
| } | ||
| return unusedNames; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Scan TypeScript source files and identify exported symbols that are never imported elsewhere. | ||
| const tsDeadExportFinder = agent({ | ||
| model: "small", | ||
| instructions: p`You are a dead code analyzer. | ||
| Discovered TypeScript files: ${p.glob("src/**/*.ts")} | ||
| Also check: ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/dist/*' | head -50")} | ||
|
|
||
| For each TypeScript file found, call findUnusedExports with that file path and the full list of all TypeScript file paths. | ||
| Aggregate results and return the full output schema.`, | ||
| output: s.object({ | ||
| unusedExports: s.record(s.array(s.string)), | ||
| totalUnused: s.int, | ||
| hasDeadCode: s.boolean, | ||
| }), | ||
| tools: [findUnusedExports], | ||
| }); | ||
|
|
||
| export default tsDeadExportFinder; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # 382 - Pkg Scripts Documenter | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const inferScriptPurpose = defineTool("inferScriptPurpose", { | ||
| description: "Classify an npm script by its command string into a category", | ||
| parameters: s.object({ | ||
| name: s.string, | ||
| command: s.string, | ||
| }), | ||
| handler: ({ command }: { name: string; command: string }) => { | ||
| const lower = command.toLowerCase(); | ||
| if (/\btest\b|jest|vitest|mocha/.test(lower)) return "test" as const; | ||
| if (/\bbuild\b|tsc|webpack|rollup|vite build/.test(lower)) return "build" as const; | ||
| if (/\blint\b|eslint|prettier/.test(lower)) return "lint" as const; | ||
| if (/\brelease\b|publish|changeset/.test(lower)) return "release" as const; | ||
| if (/\bdev\b|start|watch|serve/.test(lower)) return "dev" as const; | ||
| return "other" as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Read package.json scripts, classify each by purpose, and write a SCRIPTS.md documentation file. | ||
| const pkgScriptsDocumenter = agent({ | ||
| model: "small", | ||
| instructions: p`You are a package.json scripts documenter. | ||
| Read the package.json: ${p.read("package.json")} | ||
|
|
||
| For each script in the "scripts" field, call inferScriptPurpose to classify it. | ||
|
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 💡 Use a representative placeholder${p.write("SCRIPTS.md", "## Scripts\n\n| Script | Category | Command |\n|--------|----------|---------|\n| build | build | tsc |\n")}This makes it clear to the model (and sample readers) what structure to produce, not just that a write will happen. |
||
| Then write a SCRIPTS.md file using ${p.write("SCRIPTS.md", "## Scripts\n\n<!-- generated -->")} as the write intent path. | ||
| Return the complete output schema with all scripts, their purposes, categories, and commands.`, | ||
| 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.string, | ||
| }), | ||
| tools: [inferScriptPurpose], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default pkgScriptsDocumenter; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # 383 - Git Diff Stats Summarizer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const classifyDiffEntry = defineTool("classifyDiffEntry", { | ||
| description: "Classify a git diff --numstat line as added, modified, deleted, or renamed", | ||
| parameters: 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. [/diagnosing-bugs] The 💡 Corrected classificationActual outputs from the current code:
Fixed handler: handler: ({ line }: { line: string }) => {
const [adds, dels] = line.split("\t");
if (line.includes("=>")) return "renamed" as const;
if (adds !== "0" && dels === "0") return "added" as const;
if (adds === "0" && dels !== "0") return "deleted" as const;
return "modified" as const;
}, |
||
| line: s.string, | ||
| }), | ||
| handler: ({ line }: { line: string }) => { | ||
| if (line.startsWith("0\t")) return "added" as const; | ||
| if (line.includes("\t") && line.split("\t")[0] !== "0" && line.split("\t")[1] !== "0") return "modified" as const; | ||
| if (line.split("\t")[0] === "0") return "deleted" as const; | ||
| if (line.includes("=>")) return "renamed" as const; | ||
| return "modified" as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Summarize git diff statistics between HEAD~1 and HEAD, classifying each changed file. | ||
| const gitDiffStatsSummarizer = agent({ | ||
| model: "small", | ||
| instructions: p`You are a git diff statistics summarizer. | ||
| Run: ${p.bash("git diff --numstat HEAD~1 HEAD 2>/dev/null || git diff --numstat HEAD 2>/dev/null || echo 'no diff available'")} | ||
|
|
||
| For each output line, call classifyDiffEntry to get the category. | ||
| Parse additions and deletions counts from each line (format: additions deletions filepath). | ||
| Return the full output schema with all file stats.`, | ||
| output: s.object({ | ||
| files: s.array(s.object({ | ||
| path: s.string, | ||
| additions: s.int, | ||
| deletions: s.int, | ||
| category: s.enum("added", "modified", "deleted", "renamed"), | ||
| })), | ||
| totalAdditions: s.int, | ||
| totalDeletions: s.int, | ||
| mostChangedFile: s.optional(s.string), | ||
| }), | ||
| tools: [classifyDiffEntry], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default gitDiffStatsSummarizer; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # 384 - Dotenv Template Generator | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const extractEnvReferences = defineTool("extractEnvReferences", { | ||
| description: "Extract all process.env.VAR_NAME references from a TypeScript file", | ||
| parameters: s.object({ | ||
| filePath: s.path, | ||
| }), | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| const content = await readFile(filePath, "utf-8").catch(() => ""); | ||
| const matches = [...content.matchAll(/process\.env\.([A-Z_][A-Z0-9_]*)/g)]; | ||
| return [...new Set(matches.map((m) => m[1]))]; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Discover all environment variable references in TypeScript source files, compare with .env, and write a .env.template. | ||
| const dotenvTemplateGenerator = agent({ | ||
| model: "small", | ||
| instructions: p`You are a dotenv template generator. | ||
| Existing .env file (if any): ${p.readOptional(".env")} | ||
| TypeScript source files to scan: ${p.glob("src/**/*.ts")} | ||
|
|
||
| For each TypeScript file found, call extractEnvReferences to get all process.env references. | ||
| Collect all unique env keys referenced across all files. | ||
| Compare with keys found in .env (lines starting with KEY=). | ||
| Write the template to: ${p.write(".env.template", "# Generated .env template")} | ||
| Return the output schema.`, | ||
| output: s.object({ | ||
| templatePath: s.string, | ||
| envKeys: s.array(s.string), | ||
| undocumentedKeys: s.array(s.string), | ||
| templateGenerated: s.boolean, | ||
| }), | ||
| tools: [extractEnvReferences], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default dotenvTemplateGenerator; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # 385 - TS Class Hierarchy Extractor | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const extractClassInfo = defineTool("extractClassInfo", { | ||
| description: "Extract class declaration, extends, and implements info from a TypeScript file", | ||
| parameters: s.object({ | ||
| filePath: s.path, | ||
| }), | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| const content = await readFile(filePath, "utf-8").catch(() => ""); | ||
| const classRegex = /(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s]+))?/g; | ||
| const classes: Array<{ name: string; parent: string | null; interfaces: string[]; isAbstract: boolean }> = []; | ||
| for (const m of content.matchAll(classRegex)) { | ||
| classes.push({ | ||
| name: m[1], | ||
| parent: m[2] ?? null, | ||
| interfaces: m[3] ? m[3].split(",").map((s: string) => s.trim()) : [], | ||
|
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. [/diagnosing-bugs] The 💡 Fix: capture the abstract group directly from the regex matchThe regex classes.push({
name: m[1],
parent: m[2] ?? null,
interfaces: m[3] ? m[3].split(",").map((s: string) => s.trim()) : [],
isAbstract: m[0].startsWith("abstract"),
});This is simpler and correct — no fragile index arithmetic needed. |
||
| isAbstract: content.slice(Math.max(0, m.index! - 10), m.index!).includes("abstract"), | ||
| }); | ||
| } | ||
| return classes; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Extract class hierarchy information across all TypeScript files and report inheritance depth. | ||
| const tsClassHierarchyExtractor = agent({ | ||
| model: "small", | ||
| instructions: p`You are a TypeScript class hierarchy extractor. | ||
| TypeScript files in project: ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/dist/*' | head -100")} | ||
|
|
||
| For each TypeScript file found, call extractClassInfo to get class declarations. | ||
| Aggregate all classes across files. Compute inheritance depth for each class by tracing parent chains. | ||
| Identify root classes (no parent). Return the output schema.`, | ||
| output: s.object({ | ||
| classes: s.record(s.object({ | ||
| parent: s.optional(s.string), | ||
| interfaces: s.array(s.string), | ||
| isAbstract: s.boolean, | ||
| depth: s.int, | ||
| })), | ||
| maxDepth: s.int, | ||
| rootClasses: s.array(s.string), | ||
| }), | ||
| tools: [extractClassInfo], | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default tsClassHierarchyExtractor; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # 386 - Git Bisect Helper | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering } from "rig"; | ||
|
|
||
| const selectMidpoint = defineTool("selectMidpoint", { | ||
| description: "Select the midpoint commit hash from a list of commit hashes for binary search", | ||
| parameters: s.object({ | ||
| commits: s.array(s.string), | ||
| }), | ||
| handler: ({ commits }: { commits: string[] }) => { | ||
| if (commits.length === 0) return null; | ||
| return commits[Math.floor(commits.length / 2)]; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Help identify a suspect commit using binary search over the recent git log. | ||
| const gitBisectHelper = agent({ | ||
| model: "small", | ||
| instructions: p`You are a git bisect helper that uses binary search to find a suspect commit. | ||
| Recent git log: ${p.bash("git log --oneline -20 2>/dev/null || echo 'no git log available'")} | ||
|
|
||
| Use the selectMidpoint tool to identify candidate commits step by step. | ||
| Perform up to 8 binary search steps over the commit list. | ||
| After narrowing down, return your best suspect commit, how many steps you took, | ||
| the commit range you searched, and your confidence level.`, | ||
| 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 agent's prompt says "Perform up to 8 binary search steps" but the 💡 Add a checkedCommits field to the outputoutput: s.object({
suspectCommit: s.optional(s.string),
stepsRun: s.int,
checkedCommits: s.array(s.string), // add this
commitRange: s.object({ start: s.string, end: s.string }),
confidence: s.enum("high", "medium", "low"),
}),This forces the model to materialise each binary search step as a real |
||
| suspectCommit: s.optional(s.string), | ||
| stepsRun: s.int, | ||
| commitRange: s.object({ | ||
| start: s.string, | ||
| end: s.string, | ||
| }), | ||
| confidence: s.enum("high", "medium", "low"), | ||
| }), | ||
| tools: [selectMidpoint], | ||
| maxTurns: 8, | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default gitBisectHelper; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # 387 - Shell Shebang Validator | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const checkShebangLine = defineTool("checkShebangLine", { | ||
| description: "Check if a shell script has a valid shebang line on the first line", | ||
| parameters: s.object({ | ||
| filePath: s.path, | ||
| }), | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| const content = await readFile(filePath, "utf-8").catch(() => ""); | ||
| const firstLine = content.split("\n")[0] ?? ""; | ||
| const hasShebang = firstLine.startsWith("#!"); | ||
| const standardShebangs = ["#!/bin/sh", "#!/bin/bash", "#!/usr/bin/env bash", "#!/usr/bin/env sh"]; | ||
| const isStandard = standardShebangs.some((s) => firstLine.startsWith(s)); | ||
| return { | ||
| hasShebang, | ||
| shebangLine: hasShebang ? firstLine : undefined, | ||
| isStandard, | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Validate shebang lines in all shell scripts found in the workspace. | ||
| const shellShebangValidator = agent({ | ||
| model: "small", | ||
| instructions: p`You are a shell script shebang validator. | ||
| Shell scripts found: ${p.glob("**/*.sh")} | ||
|
|
||
| For each shell script, call checkShebangLine to verify the shebang. | ||
| Count files missing a shebang and those with a standard shebang. | ||
| Return the output schema.`, | ||
| output: s.object({ | ||
| files: s.record(s.object({ | ||
| hasShebang: s.boolean, | ||
| shebangLine: s.optional(s.string), | ||
| isStandard: s.boolean, | ||
| })), | ||
| missingShebangCount: s.int, | ||
| standardShebangCount: s.int, | ||
| totalFiles: s.int, | ||
| }), | ||
| tools: [checkShebangLine], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default shellShebangValidator; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # 388 - Git Log Graph Summarizer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering } from "rig"; | ||
|
|
||
| const parseGraphLine = defineTool("parseGraphLine", { | ||
| description: "Parse a git log --graph line and classify its type", | ||
| parameters: s.object({ | ||
| line: s.string, | ||
| }), | ||
| handler: ({ line }: { line: string }) => { | ||
| const hashMatch = line.match(/\b([0-9a-f]{7,})\b/); | ||
| const hash = hashMatch?.[1]; | ||
| const messageMatch = line.match(/[0-9a-f]{7,}\s+(.+)$/); | ||
| const message = messageMatch?.[1]; | ||
|
|
||
| if (/Merge/.test(line) && hash) return { type: "merge" as const, hash, message }; | ||
| if (hash) return { type: "commit" as const, hash, message }; | ||
| if (/[|\\\/]/.test(line) && !hash) return { type: "branch-point" as const, hash: undefined, message: undefined }; | ||
| return { type: "decoration" as const, hash: undefined, message: undefined }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Parse and summarize a git log graph output to extract commit, merge, and branch-point counts. | ||
| const gitLogGraphSummarizer = agent({ | ||
| model: "small", | ||
| instructions: p`You are a git log graph summarizer. | ||
| Git log graph: ${p.bash("git log --oneline --graph -20 2>/dev/null || echo 'no git log available'")} | ||
|
|
||
| For each line of the graph output, call parseGraphLine to classify it. | ||
| Count merges, commits, and branch-points. | ||
| Return the output schema.`, | ||
| output: s.object({ | ||
| lines: s.array(s.object({ | ||
| type: s.enum("merge", "commit", "branch-point", "decoration"), | ||
| hash: s.optional(s.string), | ||
| message: s.optional(s.string), | ||
| })), | ||
| mergeCount: s.int, | ||
| commitCount: s.int, | ||
| branchPoints: s.int, | ||
| }), | ||
| tools: [parseGraphLine], | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default gitLogGraphSummarizer; | ||
| ``` |
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.
[/codebase-design] The
findUnusedExportstool reads every other file for every exported name — O(files × exports) sequentialreadFilecalls with no caching. On a 200-file codebase with 20 exports each, this is 4000 file reads. The pattern also defeats the async runtime byawait-ing inside nested loops.💡 Cache file contents up front
Read all files once into a
Mapbefore the inner loop:This reads each file once via parallel
Promise.allinstead of re-reading every file for every exported name.