-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-07-27 #221
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,25 @@ | ||
| # 220 - Git Rename Tracker V2 | ||
|
|
||
| ```rig | ||
| import { agent, p, s, repair } from "rig"; | ||
|
|
||
| // Agent role: track file renames in git history and produce a structured rename log. | ||
| const gitRenameTrackerV2 = agent({ | ||
| model: "small", | ||
| instructions: p`Retrieve git file rename history: ${p.bash("git log --diff-filter=R --summary --pretty=format:'%h %ai' | head -60 2>/dev/null || echo 'No renames found'")}. Parse each rename entry (lines matching "rename ... => ...") and associate it with the nearest commit hash and date. Return all renames sorted newest first, the total count, and the path that appears most often as either old or new.`, | ||
| output: s.object({ | ||
| renames: s.array(s.object({ | ||
| hash: s.string, | ||
| date: s.string, | ||
| oldPath: s.path, | ||
| newPath: s.path, | ||
| })), | ||
| totalRenames: s.int, | ||
| mostRenamedFile: s.optional(s.path), | ||
| }), | ||
| maxTurns: 4, | ||
| addons: repair(), | ||
| }); | ||
|
|
||
| export default gitRenameTrackerV2; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # 221 - Yaml Workflow Linter | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool } from "rig"; | ||
| import { readFileSync } from "node:fs"; | ||
|
|
||
| const validateYaml = defineTool("validateYaml", { | ||
| description: "Validate a YAML file for structural issues and required top-level keys", | ||
| parameters: s.object({ filePath: s.path }), | ||
| handler({ filePath }) { | ||
| let content: string; | ||
| try { | ||
| content = readFileSync(filePath, "utf-8"); | ||
| } catch { | ||
| return { issues: ["File not readable"], lineCount: 0, status: "fail" as const }; | ||
| } | ||
| const issues: string[] = []; | ||
| const lines = content.split("\n"); | ||
| const lineCount = lines.length; | ||
| const isWorkflow = filePath.includes(".github/workflows"); | ||
| if (isWorkflow) { | ||
| if (!content.includes("name:")) issues.push("Missing 'name' key"); | ||
| if (!content.includes("on:") && !content.includes('"on":')) issues.push("Missing 'on' trigger"); | ||
| if (!content.includes("jobs:")) issues.push("Missing 'jobs' key"); | ||
| } | ||
| if (content.includes("\t")) issues.push("Contains tab characters (use spaces in YAML)"); | ||
| const status = issues.length === 0 ? "pass" : issues.some(i => i.startsWith("Missing")) ? "fail" : "warn"; | ||
| return { issues, lineCount, status } as { issues: string[]; lineCount: number; status: "pass" | "warn" | "fail" }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: lint YAML files and report structural issues per file. | ||
| const yamlWorkflowLinter = agent({ | ||
| model: "small", | ||
| instructions: p`Find YAML files: ${p.bash("find . \\( -name '*.yml' -o -name '*.yaml' \\) | grep -v node_modules | grep -v .git | head -20")}. For each file path, call validateYaml and collect the result. Return a record keyed by file path.`, | ||
| output: s.record(s.object({ | ||
| issues: s.array(s.string), | ||
| lineCount: s.int, | ||
| status: s.enum("pass", "warn", "fail"), | ||
| })), | ||
| tools: [validateYaml], | ||
| }); | ||
|
|
||
| export default yamlWorkflowLinter; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # 222 - Two Phase Complexity Review V2 | ||
|
|
||
| ```rig | ||
| import { agent, p, s } from "rig"; | ||
|
|
||
| // Agent role: extract TypeScript function names and approximate line counts from source files. | ||
| const extractor = agent({ | ||
| name: "extractor", | ||
| model: "small", | ||
| instructions: p`Scan TypeScript source files for function definitions: ${p.bash("grep -rn --include='*.ts' 'function \\|=> {\\|async ' . | grep -v node_modules | grep -v '.test.' | head -60 2>/dev/null || echo 'no ts files'")}. For each distinct function name found, estimate its line count from context. Return an array of objects with functionName, lineCount, and file.`, | ||
| output: s.array(s.object({ | ||
| functionName: s.string, | ||
| lineCount: s.int, | ||
| file: s.path, | ||
| })), | ||
| }); | ||
|
|
||
|
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] The extractor estimates line counts "from context" in the LLM prompt, but this is inherently unreliable — the model cannot accurately count lines from a grep summary. The sample teaches readers that line counts come from real tool calls, yet the extractor uses no tool for counting. 💡 Use a defineTool for actual line countingAdd a const countLines = defineTool("countLines", {
parameters: s.object({ file: s.path }),
async handler({ file }) {
const { readFile } = await import("node:fs/promises");
const content = await readFile(file, "utf-8");
return { lineCount: content.split("
").length };
},
});This makes the sample more accurate and teaches a better pattern. |
||
| // Agent role: rate each extracted function for complexity and produce a summary. | ||
| const reviewer = agent({ | ||
| name: "reviewer", | ||
| model: "small", | ||
| instructions: p`You will receive a list of functions with their line counts. Classify each as: simple (<10 lines), moderate (10-29), complex (30-59), or critical (≥60). Return one rating object per function plus a summary with total and complex+critical count.`, | ||
| output: s.object({ | ||
| analysis: s.record(s.object({ | ||
| lineCount: s.int, | ||
| complexity: s.enum("simple", "moderate", "complex", "critical"), | ||
| })), | ||
| summary: s.object({ | ||
| totalFunctions: s.int, | ||
| complexCount: s.int, | ||
| }), | ||
| }), | ||
| agents: { extractor }, | ||
| }); | ||
|
|
||
| export default reviewer; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # 223 - Barrel File Generator V2 | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
|
|
||
| const detectExports = defineTool("detectExports", { | ||
| description: "Detect export declarations in a TypeScript source file", | ||
| parameters: s.object({ filePath: s.path }), | ||
| async handler({ filePath }) { | ||
| const { readFile } = await import("node:fs/promises"); | ||
| try { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const exports = (content.match(/^export\s+(default\s+)?(function|class|const|let|var|type|interface|enum)\s+\w+/gm) ?? []); | ||
| return { filePath, exports, exportCount: exports.length }; | ||
| } catch { | ||
| return { filePath, exports: [], exportCount: 0 }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: generate barrel index.ts content for TypeScript source directories. | ||
| const barrelFileGenerator = agent({ | ||
| model: "small", | ||
|
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] The output field 💡 Rename to reflect what is actually computedRename to |
||
| instructions: p`Find TypeScript source files: ${p.bash("find src -name '*.ts' ! -name 'index.ts' ! -name '*.test.ts' ! -name '*.spec.ts' 2>/dev/null | head -30 || find . -name '*.ts' ! -name 'index.ts' ! -name '*.test.ts' ! -path '*/node_modules/*' | head -20")}. Use detectExports for each file. Tally filesScanned, barrelFilesWritten (directories that have at least one export), exportCount (total exports found), and directories (unique directory paths). Return only the declared output.`, | ||
| tools: [detectExports], | ||
| output: s.object({ | ||
| filesScanned: s.int, | ||
| barrelFilesWritten: s.int, | ||
| exportCount: s.int, | ||
| directories: s.array(s.string), | ||
| }), | ||
| }); | ||
|
|
||
| export default barrelFileGenerator; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # 224 - Git Hook Inventory V2 | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
|
|
||
| const classifyHook = defineTool("classifyHook", { | ||
| description: "Classify a git hook by inspecting its content", | ||
| parameters: s.object({ name: s.string, content: s.string }), | ||
| handler({ content }) { | ||
| if (!content || content.trim() === "missing") { | ||
| return { status: "missing" as const, isAsync: false, summary: "Hook file not present" }; | ||
|
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] The 💡 Either use or remove the parameterIf handler({ name, content }) {
// ...
const summary = isSample ? "Sample/placeholder hook" : `Active hook '${name}' (${lines} lines)`;
} |
||
| } | ||
| const isSample = content.includes("sample") || content.trim() === "#!/bin/sh" || content.trim() === "#!/bin/bash"; | ||
| const isAsync = content.includes(" &") || content.includes("async "); | ||
| const status = isSample ? "stub" : "active"; | ||
| const summary = isSample ? "Sample/placeholder hook — not active" : `Active hook (${content.split("\n").length} lines)`; | ||
| return { status, isAsync, summary } as { status: "active" | "stub" | "missing"; isAsync: boolean; summary: string }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: inventory git hooks and classify each as active, stub, or missing. | ||
| const gitHookInventory = agent({ | ||
| model: "small", | ||
| instructions: p`List and inspect git hooks: ${p.bash("ls .git/hooks/ 2>/dev/null || echo 'no hooks directory'")}. ${p.bash("for f in pre-commit commit-msg pre-push post-commit prepare-commit-msg pre-rebase; do echo \"=== $f ===\"; cat \".git/hooks/$f\" 2>/dev/null || echo 'missing'; done")}. Use classifyHook for each hook name with its content. Return a record keyed by hook name.`, | ||
| tools: [classifyHook], | ||
| output: s.record(s.object({ | ||
| summary: s.string, | ||
| status: s.enum("active", "stub", "missing"), | ||
| isAsync: s.boolean, | ||
| })), | ||
| }); | ||
|
|
||
| export default gitHookInventory; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # 225 - Pr Review Checklist V2 | ||
|
|
||
| ```rig | ||
| import { agent, p, s, repair } from "rig"; | ||
|
|
||
| // Agent role: generate a PR review checklist from the current git diff. | ||
| const prReviewChecklist = agent({ | ||
| model: "small", | ||
| maxTurns: 6, | ||
| addons: repair(), | ||
| instructions: p`Generate a PR review checklist based on these changes. | ||
|
|
||
| Changed files: | ||
| ${p.bash("git diff --stat HEAD~1 2>/dev/null || git diff --stat HEAD 2>/dev/null || echo 'no diff available'")} | ||
|
|
||
| Diff: | ||
| ${p.bash("git diff HEAD~1 -- . 2>/dev/null | head -300 || git diff HEAD -- . 2>/dev/null | head -300 || echo 'no diff'")} | ||
|
|
||
| For each concern, produce a checklist item with a clear description, category (security/performance/correctness/style/testing), and priority (must/should/nice-to-have). List changed files and indicate if the PR is ready for review.`, | ||
| output: s.object({ | ||
| checklist: s.array(s.object({ | ||
| item: s.string, | ||
| category: s.enum("security", "performance", "correctness", "style", "testing"), | ||
| priority: s.enum("must", "should", "nice-to-have"), | ||
| })), | ||
| changedFiles: s.array(s.path), | ||
| ready: s.boolean, | ||
| }), | ||
| }); | ||
|
|
||
| export default prReviewChecklist; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # 226 - Js Ast Node Counter | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s } from "rig"; | ||
|
|
||
| const countAstNodes = defineTool("countAstNodes", { | ||
| description: "Count syntax node patterns in a JavaScript/TypeScript file using regex heuristics", | ||
| parameters: s.object({ filePath: s.path }), | ||
| async handler({ filePath }) { | ||
| const { readFile } = await import("node:fs/promises"); | ||
| try { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const functionCount = (content.match(/\bfunction\s+\w+\s*\(/g) ?? []).length; | ||
| const arrowFunctionCount = (content.match(/=>\s*[{(]/g) ?? []).length; | ||
| const classCount = (content.match(/\bclass\s+\w+/g) ?? []).length; | ||
| const importCount = (content.match(/^\s*import\s+/gm) ?? []).length; | ||
| const exportCount = (content.match(/^\s*export\s+/gm) ?? []).length; | ||
| return { filePath, functionCount, arrowFunctionCount, classCount, importCount, exportCount }; | ||
| } catch { | ||
| return { filePath, functionCount: 0, arrowFunctionCount: 0, classCount: 0, importCount: 0, exportCount: 0 }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: count AST-like syntax node patterns in JavaScript files. | ||
| const jsAstNodeCounter = agent({ | ||
| model: "small", | ||
| instructions: p`Find JavaScript files: ${p.bash("find . -name '*.js' ! -path '*/node_modules/*' ! -path '*/.git/*' | head -10 2>/dev/null || echo 'no js files found'")}. For each file, call countAstNodes and collect results. Tally totalFunctions across all files and identify mostComplexFile (highest combined function + arrowFunction count). Return only the declared output.`, | ||
| tools: [countAstNodes], | ||
| output: s.object({ | ||
| files: s.record(s.object({ | ||
| functionCount: s.int, | ||
| arrowFunctionCount: s.int, | ||
| classCount: s.int, | ||
| importCount: s.int, | ||
| exportCount: s.int, | ||
| })), | ||
| totalFunctions: s.int, | ||
| mostComplexFile: s.optional(s.path), | ||
| }), | ||
| }); | ||
|
|
||
| export default jsAstNodeCounter; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # 227 - Git Tag Date Mapper | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, steering } from "rig"; | ||
|
|
||
| const classifyTagAge = defineTool("classifyTagAge", { | ||
| description: "Classify a tag's age based on its ISO date string", | ||
| parameters: s.object({ dateStr: s.string }), | ||
| handler({ dateStr }) { | ||
| const date = new Date(dateStr); | ||
| const now = new Date(); | ||
| const daysDiff = (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24); | ||
| let ageClass: "recent" | "stable" | "old" | "ancient"; | ||
| if (daysDiff < 30) ageClass = "recent"; | ||
| else if (daysDiff < 180) ageClass = "stable"; | ||
| else if (daysDiff < 730) ageClass = "old"; | ||
| else ageClass = "ancient"; | ||
| return { ageClass }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: map git tags to their commit dates and classify their age. | ||
| const gitTagDateMapper = agent({ | ||
| model: "small", | ||
| addons: steering(), | ||
| instructions: p`Inspect git tags and their dates. | ||
|
|
||
| Tags: | ||
| ${p.bash("git tag -l --sort=-version:refname | head -20 2>/dev/null || echo 'no tags'")} | ||
|
|
||
| Tag log: | ||
| ${p.bash("git log --tags --simplify-by-decoration --pretty='%D|%ai|%s' | head -30 2>/dev/null || echo 'no tag log'")} | ||
|
|
||
| For each tag, extract its ISO date and call classifyTagAge. Return a record keyed by tag name with date, subject, and ageClass. Include totalTags and latestTag.`, | ||
| tools: [classifyTagAge], | ||
| output: s.object({ | ||
| tags: s.record(s.object({ | ||
| date: s.string, | ||
| subject: s.string, | ||
| ageClass: s.enum("recent", "stable", "old", "ancient"), | ||
| })), | ||
| totalTags: s.int, | ||
| latestTag: s.optional(s.string), | ||
| }), | ||
| }); | ||
|
|
||
| export default gitTagDateMapper; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # 228 - Circular Import Detector | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, steering } from "rig"; | ||
|
|
||
| const buildImportGraph = defineTool("buildImportGraph", { | ||
| description: "Build an import graph from TypeScript files in a directory by parsing relative import statements", | ||
| parameters: s.object({ directory: s.path }), | ||
| async handler({ directory }) { | ||
| const { readdir, readFile } = await import("node:fs/promises"); | ||
| const { join } = await import("node:path"); | ||
| const graph: Record<string, string[]> = {}; | ||
| try { | ||
| const entries = await readdir(directory, { recursive: true, encoding: "utf-8" }); | ||
| const tsFiles = entries.filter(f => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts")); | ||
| for (const file of tsFiles.slice(0, 20)) { | ||
| const fullPath = join(directory, file); | ||
| try { | ||
| const content = await readFile(fullPath, "utf-8"); | ||
| const imports = [...content.matchAll(/from\s+['"](\.[^'"]+)['"]/g)].map(m => m[1]); | ||
| graph[file] = imports; | ||
| } catch { | ||
| graph[file] = []; | ||
| } | ||
| } | ||
| } catch { | ||
| // directory not found | ||
| } | ||
| return { graph }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: detect circular imports in TypeScript source files. | ||
| const circularImportDetector = agent({ | ||
| model: "small", | ||
| addons: steering(), | ||
| instructions: p`Detect circular imports in TypeScript files. | ||
|
|
||
| Import overview: | ||
|
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] The agent instruction asks the LLM to "analyse the graph to find cycles" from the returned 💡 Move cycle detection into the tool handlerExtend function findCycles(graph: Record<string, string[]>): string[][] {
const cycles: string[][] = [];
const visited = new Set<string>();
const stack: string[] = [];
// ... DFS implementation
return cycles;
}This makes the sample deterministic and teaches the right design: use tools for computation, LLM for orchestration. |
||
| ${p.bash("grep -rn \"from '\\./\\|from \\\"\\./\" src/ --include='*.ts' 2>/dev/null | head -40 || grep -rn \"from '\\./\" . --include='*.ts' | grep -v node_modules | head -40 || echo 'no relative imports found'")} | ||
|
|
||
| Call buildImportGraph on the source directory (try "src" first, then "."). Analyse the graph to find cycles (A imports B imports A, etc.). Return cycles as arrays of file paths, hasCycles, totalFiles, and cycleCount.`, | ||
| tools: [buildImportGraph], | ||
| output: s.object({ | ||
| cycles: s.array(s.array(s.string)), | ||
| hasCycles: s.boolean, | ||
| totalFiles: s.int, | ||
| cycleCount: s.int, | ||
| }), | ||
| }); | ||
|
|
||
| export default circularImportDetector; | ||
| ``` | ||
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] Substring matching with
includes("name:")will produce false positives for YAML keys likecontainer-name:orusername:. This makes the linter unreliable and isn't a good pattern to demonstrate in a sample.💡 Use anchored regex instead
Regex anchored to line start avoids false positives from partial key matches.