-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-08-07 #365
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,54 @@ | ||
| # 371 - 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 commit history for a file and return ownership info.", | ||
| parameters: { filePath: s.path }, | ||
| handler: ({ filePath }: { filePath: string }) => { | ||
| try { | ||
| const output = execSync(`git log --format="%ae" -- "${filePath}" 2>/dev/null`, { encoding: "utf-8" }).trim(); | ||
| const emails = output ? output.split("\n").filter(Boolean) : []; | ||
| const counts: Record<string, number> = {}; | ||
| for (const email of emails) counts[email] = (counts[email] ?? 0) + 1; | ||
| const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]); | ||
| const primaryOwner = sorted[0]?.[0] ?? "unknown"; | ||
| const commitCount = emails.length; | ||
| const contributors = sorted.map(([email]) => email); | ||
| return { primaryOwner, commitCount, contributors }; | ||
| } catch { | ||
| return { primaryOwner: "unknown", commitCount: 0, contributors: [] }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: map git file ownership for all TypeScript source files. | ||
| const gitFileOwnershipMapper = agent({ | ||
| model: "small", | ||
| instructions: p`Map git file ownership for TypeScript source files. | ||
|
|
||
| Source files: | ||
| ${p.glob("src/**/*.ts")} | ||
|
|
||
| Steps: | ||
| 1. For each file path listed above, call getFileOwner to retrieve primaryOwner, commitCount, contributors. | ||
| 2. Build ownership record keyed by file path. | ||
| 3. Find mostActiveContributor: the email with the highest total commit count 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; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # 372 - Workflow Input Validator | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const validateWorkflowInputs = defineTool("validateWorkflowInputs", { | ||
| description: "Parse a GitHub Actions workflow YAML file and extract workflow_dispatch inputs.", | ||
| parameters: { filePath: s.path }, | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const inputsMatch = content.match(/workflow_dispatch:\s*\n(?:\s+.*\n)*?\s+inputs:([\s\S]*?)(?=\n\w|\n\s{0,2}\w|$)/); | ||
|
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 💡 Suggestion: use a YAML parser or loosen the indentation constraintThe simplest fix is to widen the indent range in const inputPattern = /^\s{2,}(\w+):\s*\n((?:\s{3,}.+\n?)*)/gm;A more robust fix would be to use a lightweight YAML parser (e.g. |
||
| if (!inputsMatch) return { inputs: {}, inputCount: 0, hasRequiredWithoutDefault: false }; | ||
| const inputsBlock = inputsMatch[1]; | ||
| const inputEntries: Record<string, { type: string; required: boolean; default?: string | undefined }> = {}; | ||
| const inputPattern = /^\s{4,8}(\w+):\s*\n((?:\s{6,12}.+\n?)*)/gm; | ||
| let match: RegExpExecArray | null; | ||
| while ((match = inputPattern.exec(inputsBlock)) !== null) { | ||
| const name = match[1]; | ||
| const block = match[2]; | ||
| const typeM = block.match(/type:\s*(.+)/); | ||
| const requiredM = block.match(/required:\s*(true|false)/); | ||
| const defaultM = block.match(/default:\s*(.+)/); | ||
| inputEntries[name] = { | ||
| type: typeM ? typeM[1].trim() : "string", | ||
| required: requiredM ? requiredM[1] === "true" : false, | ||
| default: defaultM ? defaultM[1].trim() : undefined, | ||
| }; | ||
| } | ||
| const inputCount = Object.keys(inputEntries).length; | ||
| const hasRequiredWithoutDefault = Object.values(inputEntries).some( | ||
| (v) => v.required && v.default === undefined | ||
| ); | ||
| return { inputs: inputEntries, inputCount, hasRequiredWithoutDefault }; | ||
| }, | ||
| }); | ||
|
|
||
| // 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 GitHub Actions workflow files. | ||
|
|
||
| Workflow files: | ||
| ${p.glob(".github/workflows/*.yml")} | ||
|
|
||
| Steps: | ||
| 1. For each file path listed above, call validateWorkflowInputs to extract inputs, inputCount, hasRequiredWithoutDefault. | ||
| 2. Build workflows record keyed by filename. | ||
| 3. Set totalWorkflows to the count of workflow files processed.`, | ||
| output: s.object({ | ||
| workflows: s.record( | ||
| s.object({ | ||
| inputCount: s.int, | ||
| hasRequiredWithoutDefault: s.boolean, | ||
| }) | ||
| ), | ||
| totalWorkflows: s.int, | ||
| }), | ||
| tools: [validateWorkflowInputs], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default workflowInputValidator; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # 373 - TypeScript 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 in other files.", | ||
| parameters: { filePath: s.path, allFiles: s.array(s.string) }, | ||
| handler: async ({ filePath, allFiles }: { filePath: string; allFiles: string[] }) => { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const exportPattern = /export\s+(?:const|function|class|type|interface|enum)\s+(\w+)/g; | ||
| const exported: string[] = []; | ||
| let m: RegExpExecArray | null; | ||
| while ((m = exportPattern.exec(content)) !== null) exported.push(m[1]); | ||
| const unused: string[] = []; | ||
| for (const sym of exported) { | ||
| let found = false; | ||
| for (const other of allFiles) { | ||
| if (other === filePath) continue; | ||
| try { | ||
| const otherContent = await readFile(other, "utf-8"); | ||
| if (otherContent.includes(sym)) { found = true; break; } | ||
|
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] False negative: 💡 Fix: use a word-boundary regex matchconst usagePattern = new RegExp(`\\b${sym}\\b`);
if (usagePattern.test(otherContent)) { found = true; break; }Word-boundary matching is the minimum needed to avoid false negatives on short symbol names. |
||
| } catch { /* skip */ } | ||
| } | ||
| if (!found) unused.push(sym); | ||
| } | ||
| return { unused }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: find unused TypeScript exports across the codebase. | ||
| const tsDeadExportFinder = agent({ | ||
| model: "small", | ||
| instructions: p`Find TypeScript exported symbols that are never imported elsewhere. | ||
|
|
||
| TypeScript files: | ||
| ${p.glob("src/**/*.ts")} | ||
|
|
||
| All TypeScript files (for cross-reference): | ||
| ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' 2>/dev/null | head -100")} | ||
|
|
||
| Steps: | ||
| 1. For each file in the source list, call findUnusedExports with that file and the full allFiles list. | ||
| 2. Build unusedExports record keyed by file path, value is array of unused symbol names. | ||
| 3. Omit files with no unused exports. | ||
| 4. totalUnused = total count of unused symbols across all files. | ||
| 5. hasDeadCode = totalUnused > 0.`, | ||
| output: s.object({ | ||
| unusedExports: s.record(s.array(s.string)), | ||
| totalUnused: s.int, | ||
| hasDeadCode: s.boolean, | ||
| }), | ||
| tools: [findUnusedExports], | ||
| addons: [steering(), repair()], | ||
| }); | ||
|
|
||
| export default tsDeadExportFinder; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # 374 - Package Scripts Documenter | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const inferScriptPurpose = defineTool("inferScriptPurpose", { | ||
| description: "Classify a package.json script by its command into a category.", | ||
| parameters: { name: s.string, command: s.string }, | ||
| handler: ({ name, command }: { name: string; command: string }) => { | ||
| const cmd = command.toLowerCase(); | ||
| const nm = name.toLowerCase(); | ||
| let category: "build" | "test" | "lint" | "release" | "dev" | "other" = "other"; | ||
| if (/\btest\b|jest|vitest|mocha/.test(cmd) || /\btest/.test(nm)) category = "test"; | ||
| else if (/\bbuild\b|tsc|webpack|vite|rollup|esbuild/.test(cmd) || /\bbuild/.test(nm)) category = "build"; | ||
| else if (/\blint\b|eslint|prettier|biome/.test(cmd) || /\blint/.test(nm)) category = "lint"; | ||
| else if (/\brelease\b|publish|changeset|version/.test(cmd) || /\brelease\b|\bpublish/.test(nm)) category = "release"; | ||
| else if (/\bdev\b|watch|start\b|nodemon/.test(cmd) || /\bdev\b|\bstart\b|\bwatch/.test(nm)) category = "dev"; | ||
| const purpose = `Runs ${name}: ${command.slice(0, 60)}`; | ||
| return { purpose, category } as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: document all package.json scripts with purpose and category, then write SCRIPTS.md. | ||
| const packageScriptsDocumenter = agent({ | ||
| model: "small", | ||
| instructions: p`Document all scripts in package.json and write SCRIPTS.md. | ||
|
|
||
| package.json contents: | ||
| ${p.read("package.json")} | ||
|
|
||
| Steps: | ||
| 1. Parse the scripts object from the package.json content above. | ||
| 2. For each script name and command, call inferScriptPurpose to get purpose and category. | ||
| 3. Build the scripts record keyed by script name. | ||
| 4. Write SCRIPTS.md using p.write with a markdown table listing each script, its category, and purpose. | ||
| 5. documentedCount = number of scripts processed. | ||
| 6. outputFile = "SCRIPTS.md".`, | ||
| 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, | ||
|
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] Schema/tool mismatch: the output schema declares 💡 Fix: either return `command` from the tool or remove it from the output schemaOption A — return return { purpose, category, command } as const;Option B — drop The output schema should be the single source of truth; every field the LLM must populate should have a clear data source in the instructions or tool returns. |
||
| outputFile: s.path, | ||
| }), | ||
| tools: [inferScriptPurpose], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default packageScriptsDocumenter; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # 375 - Git Diff Stats Summarizer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const classifyDiffEntry = defineTool("classifyDiffEntry", { | ||
| description: "Classify a git diff --numstat entry as added, modified, deleted, or renamed.", | ||
| parameters: { additions: s.int, deletions: s.int, path: s.string }, | ||
| handler: ({ additions, deletions, path }: { additions: number; deletions: number; path: string }) => { | ||
| let changeType: "added" | "modified" | "deleted" | "renamed" = "modified"; | ||
| if (path.includes(" => ") || path.includes("{")) changeType = "renamed"; | ||
| else if (deletions === 0 && additions > 0) changeType = "added"; | ||
| else if (additions === 0 && deletions > 0) changeType = "deleted"; | ||
| return { changeType } as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: summarize git diff statistics between HEAD~1 and HEAD. | ||
| const gitDiffStatsSummarizer = agent({ | ||
| model: "small", | ||
| instructions: p`Summarize file changes between the last two commits. | ||
|
|
||
| Diff numstat output: | ||
| ${p.bash("git diff --numstat HEAD~1 HEAD 2>/dev/null || echo ''")} | ||
|
|
||
| Steps: | ||
| 1. Parse each line of the numstat output (format: additions TAB deletions TAB path). | ||
| 2. For each entry, call classifyDiffEntry with additions, deletions, path to get changeType. | ||
| 3. Build files array with path, additions, deletions, changeType. | ||
| 4. totalAdditions = sum of all additions. | ||
| 5. totalDeletions = sum of all deletions. | ||
| 6. mostChangedFile = path with highest additions+deletions (omit if no files).`, | ||
| output: s.object({ | ||
| files: s.array( | ||
| s.object({ | ||
| path: s.string, | ||
| additions: s.int, | ||
| deletions: s.int, | ||
| changeType: 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,53 @@ | ||
| # 376 - Dotenv Template Generator | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const extractEnvReferences = defineTool("extractEnvReferences", { | ||
| description: "Extract process.env.X references from a TypeScript file.", | ||
| parameters: { filePath: s.path }, | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| try { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const pattern = /process\.env\.([A-Z_][A-Z0-9_]*)/g; | ||
| const keys = new Set<string>(); | ||
| let m: RegExpExecArray | null; | ||
| while ((m = pattern.exec(content)) !== null) keys.add(m[1]); | ||
| return { keys: Array.from(keys) }; | ||
| } catch { | ||
| return { keys: [] }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: generate a .env.template from process.env references in source files. | ||
| const dotenvTemplateGenerator = agent({ | ||
| model: "small", | ||
| instructions: p`Generate a .env.template file from process.env references in TypeScript source files. | ||
|
|
||
| Existing .env file (if present): | ||
| ${p.readOptional(".env", "(no .env file found)")} | ||
|
|
||
| TypeScript source files: | ||
| ${p.glob("src/**/*.ts")} | ||
|
|
||
| Steps: | ||
| 1. For each TypeScript file path listed above, call extractEnvReferences to get the list of env keys. | ||
| 2. Collect all unique keys referenced across all files → envKeys. | ||
| 3. Parse the existing .env content to find documented keys. | ||
| 4. undocumentedKeys = envKeys not already in .env. | ||
| 5. Write .env.template with each key as KEY= (one per line, with a comment header). | ||
| 6. templatePath = ".env.template", templateGenerated = true.`, | ||
| output: s.object({ | ||
| templatePath: s.path, | ||
| envKeys: s.array(s.string), | ||
| undocumentedKeys: s.array(s.string), | ||
| templateGenerated: s.boolean, | ||
| }), | ||
| tools: [extractEnvReferences], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default dotenvTemplateGenerator; | ||
| ``` |
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.
[/diagnosing-bugs] Shell injection risk:
filePathis interpolated directly into the shell command string without sanitization — paths containing shell metacharacters (spaces,$(), backticks) will be interpreted by the shell.💡 Fix: use spawnSync with argument array
Switch from a template-string
execSynccall tospawnSyncwith a proper argument array, so the path is never parsed by the shell:Agent-generated tool inputs are a realistic injection vector since the LLM controls the
filePathargument.