-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-08-01 #331
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,32 @@ | ||
| # 341 - Release Note Enricher | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair, steering } from "rig"; | ||
|
|
||
| const lookupTicketMetadata = defineTool("lookupTicketMetadata", { | ||
| description: "Extract ticket references (#NNN or PROJ-NNN) from text and return metadata", | ||
| parameters: s.object({ text: s.string }), | ||
| handler({ text }) { | ||
| const githubRefs = [...text.matchAll(/#(\d+)/g)].map((m) => `#${m[1]}`); | ||
| const jiraRefs = [...text.matchAll(/\b([A-Z]+-\d+)\b/g)].map((m) => m[1]); | ||
| return { githubRefs, jiraRefs, all: [...githubRefs, ...jiraRefs] }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: enrich raw release notes by extracting ticket references, grouping into sections, assigning a risk label, and listing unresolved references. | ||
| const releaseNoteEnricher = agent({ | ||
| model: "small", | ||
| input: s.object({ rawNotes: s.string }), | ||
| instructions: p`Use the lookupTicketMetadata tool to find all ticket references (#NNN, PROJ-NNN) in the raw release notes from the input. Group the notes into logical sections (e.g., Features, Bug Fixes, Breaking Changes). Assign a riskLabel based on content severity. List any ticket references that appear to be missing or unresolvable.`, | ||
| output: s.object({ | ||
| sections: s.array(s.string), | ||
| riskLabel: s.enum("low", "medium", "high", "critical"), | ||
| missingReferences: s.array(s.string), | ||
| }), | ||
| tools: [lookupTicketMetadata], | ||
| maxTurns: 5, | ||
| addons: [steering(), repair()], | ||
| }); | ||
|
|
||
| export default releaseNoteEnricher; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # 342 - Docs Refactor Coordinator | ||
|
|
||
| ```rig | ||
| import { agent, p, s } from "rig"; | ||
|
|
||
| // Agent role: extract every API name, function signature, and method reference from the documentation. | ||
| const apiExtractor = agent({ | ||
| name: "apiExtractor", | ||
| model: "small", | ||
| instructions: p`Read: ${p.read("README.md")}. Extract all API names, function signatures, and method references. Return a flat list of unique identifiers.`, | ||
| output: s.array(s.string), | ||
| }); | ||
|
|
||
| // Agent role: rewrite documentation prose to be clearer and more concise. | ||
| const proseCleanup = agent({ | ||
| name: "proseCleanup", | ||
| model: "small", | ||
| instructions: p`Read: ${p.read("README.md")}. Rewrite the prose sections for clarity and conciseness without altering technical accuracy. Return the improved markdown text.`, | ||
| output: s.string, | ||
| }); | ||
|
|
||
| // Agent role: coordinate API extraction and prose cleanup, then write a refactored docs file. | ||
| const docsRefactorCoordinator = agent({ | ||
| model: "small", | ||
| instructions: p`Delegate to apiExtractor and proseCleanup subagents. Combine their outputs: extractedApis from apiExtractor and improved prose from proseCleanup. Write the refactored content to ${p.write("docs/refactored.md", "REFACTORED_CONTENT")}. Count the number of substantive prose changes made.`, | ||
| output: s.object({ | ||
| extractedApis: s.array(s.string), | ||
| changesApplied: s.int, | ||
| outputPath: s.path, | ||
| }), | ||
| agents: { apiExtractor, proseCleanup }, | ||
| }); | ||
|
|
||
| export default docsRefactorCoordinator; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # 343 - Dockerfile Layer Analyzer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const parseLayer = defineTool("parseLayer", { | ||
| description: "Classify a Dockerfile instruction line for cache-friendliness and weight", | ||
| parameters: s.object({ line: s.string }), | ||
| handler({ line }) { | ||
| const trimmed = line.trim(); | ||
| const spaceIdx = trimmed.indexOf(" "); | ||
| const instruction = spaceIdx >= 0 ? trimmed.slice(0, spaceIdx).toUpperCase() : trimmed.toUpperCase(); | ||
| const args = spaceIdx >= 0 ? trimmed.slice(spaceIdx + 1) : ""; | ||
| const isHeavy = instruction === "RUN" && /apt-get|npm install|pip install|yarn|apk add/.test(args); | ||
| const cacheable = (instruction === "COPY" || instruction === "ADD") && /package\.json|requirements\.txt|go\.mod/.test(args); | ||
| const tip = isHeavy ? "Combine adjacent RUN commands to reduce layers" as const | ||
| : cacheable ? null | ||
| : instruction === "COPY" ? "Copy dependency manifests first for better cache" as const | ||
| : null; | ||
| return { instruction, cacheable, isHeavy, tip }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: analyze Dockerfile layers for cache efficiency and suggest optimizations. | ||
| const dockerfileLayerAnalyzer = agent({ | ||
| model: "small", | ||
| instructions: p`Dockerfile content: ${p.readOptional("Dockerfile", "# no Dockerfile found")} | ||
|
|
||
| Parse each non-empty, non-comment instruction line. Call parseLayer for each. Count totalLayers. Set optimizable to true if any layer has isHeavy true or a non-null tip. Collect all non-null tips into suggestions.`, | ||
| output: s.object({ | ||
| layers: s.array(s.object({ | ||
| instruction: s.string, | ||
| cacheable: s.boolean, | ||
| isHeavy: s.boolean, | ||
| tip: s.optional(s.string), | ||
| })), | ||
| totalLayers: s.int, | ||
| optimizable: s.boolean, | ||
| suggestions: s.array(s.string), | ||
| }), | ||
| tools: [parseLayer], | ||
| addons: [repair()], | ||
| maxTurns: 5, | ||
| }); | ||
|
|
||
| export default dockerfileLayerAnalyzer; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # 344 - Git Worktree Status Reporter | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const classifyWorktree = defineTool("classifyWorktree", { | ||
| description: "Classify a git worktree entry as clean, dirty, bare, or detached", | ||
| parameters: s.object({ path: s.string, branch: s.string, statusOutput: s.string }), | ||
| handler({ branch, statusOutput }) { | ||
| if (branch === "(bare)") return { status: "bare" as const }; | ||
| if (branch.startsWith("(HEAD detached")) return { status: "detached" as const }; | ||
| if (statusOutput.trim().length > 0) return { status: "dirty" as const }; | ||
| return { status: "clean" as const }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: report the status of all git worktrees in the repository. | ||
| const gitWorktreeStatusReporter = agent({ | ||
| model: "small", | ||
| instructions: p`Worktree list: ${p.bash("git worktree list --porcelain")} | ||
|
|
||
| Parse each worktree block (separated by blank lines). For each worktree, call classifyWorktree with its path, branch, and an empty statusOutput (use "dirty" heuristic based on porcelain output if available). Return the list of worktrees with their statuses, total count, and whether all are clean.`, | ||
| output: s.object({ | ||
| worktrees: s.array(s.object({ | ||
| path: s.path, | ||
| branch: s.string, | ||
| status: s.enum("clean", "dirty", "bare", "detached"), | ||
| })), | ||
| totalWorktrees: s.int, | ||
| allClean: s.boolean, | ||
| }), | ||
| tools: [classifyWorktree], | ||
| addons: [repair()], | ||
| maxTurns: 4, | ||
| }); | ||
|
|
||
| export default gitWorktreeStatusReporter; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # 345 - NPM Audit Simplifier | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const classifyVuln = defineTool("classifyVuln", { | ||
| description: "Classify an npm vulnerability as direct or transitive and produce a recommendation", | ||
| parameters: s.object({ | ||
| name: s.string, | ||
| severity: s.string, | ||
| isDirect: s.boolean, | ||
| }), | ||
| handler({ name, severity, isDirect }) { | ||
| const sev = severity.toLowerCase(); | ||
| const scope = isDirect ? "direct" : "transitive"; | ||
| const recommendation = | ||
| sev === "critical" ? `Upgrade ${name} immediately (${scope} dependency)` : | ||
| sev === "high" ? `Schedule upgrade for ${name} (${scope} dependency)` : | ||
| isDirect ? `Review ${name} — ${sev} severity direct dependency` : | ||
| `Monitor ${name} — ${sev} severity transitive dependency`; | ||
| return { scope, recommendation }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: simplify npm audit output into an actionable vulnerability summary. | ||
| const npmAuditSimplifier = agent({ | ||
| model: "small", | ||
| instructions: p`npm audit output: ${p.bash("npm audit --json 2>/dev/null || echo '{\"vulnerabilities\":{}}'") | ||
| } | ||
|
|
||
| Parse the audit JSON. For each vulnerability, call classifyVuln with its name, severity, and whether it is a direct dependency. Count criticalCount and highCount. Choose action: urgent if any critical, scheduled if any high, monitor if any moderate/low, none if no vulnerabilities. Write a one-line summary.`, | ||
| output: s.object({ | ||
| vulnerabilities: s.array(s.object({ | ||
| name: s.string, | ||
| severity: s.string, | ||
| isDirect: s.boolean, | ||
| recommendation: s.string, | ||
| })), | ||
| criticalCount: s.int, | ||
| highCount: s.int, | ||
| summary: s.string, | ||
| action: s.enum("urgent", "scheduled", "monitor", "none"), | ||
| }), | ||
| tools: [classifyVuln], | ||
| addons: [repair()], | ||
| maxTurns: 4, | ||
| }); | ||
|
|
||
| export default npmAuditSimplifier; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # 346 - JS AST Node Counter | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const countAstNodes = defineTool("countAstNodes", { | ||
| description: "Count function/arrow/class/import/export patterns in a TypeScript file using regex heuristics", | ||
| parameters: s.object({ filePath: s.path }), | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| let source = ""; | ||
| try { source = await readFile(filePath, "utf8"); } catch { return { functions: 0, arrows: 0, classes: 0, imports: 0, exports: 0 }; } | ||
| const functions = (source.match(/\bfunction\s+\w+/g) || []).length; | ||
| const arrows = (source.match(/=>\s*[{(]/g) || []).length; | ||
| const classes = (source.match(/\bclass\s+\w+/g) || []).length; | ||
| const imports = (source.match(/^import\b/gm) || []).length; | ||
| const exports = (source.match(/^export\b/gm) || []).length; | ||
| return { functions, arrows, classes, imports, exports }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: count AST-like node patterns in each TypeScript source file and identify the most complex file. | ||
| const jsAstNodeCounter = agent({ | ||
| model: "small", | ||
| instructions: p`TypeScript files in this workspace: ${p.glob("src/**/*.ts")} | ||
|
|
||
| For each file path listed, call countAstNodes to get pattern counts. Build a record keyed by file path. Sum all functions across files into totalFunctions. Set mostComplexFile to the file with the highest combined function+arrow+class count (omit if no files found).`, | ||
| output: s.object({ | ||
| files: s.record(s.object({ | ||
| functions: s.int, | ||
| arrows: s.int, | ||
| classes: s.int, | ||
| imports: s.int, | ||
| exports: s.int, | ||
| })), | ||
| totalFunctions: s.int, | ||
| mostComplexFile: s.optional(s.path), | ||
| }), | ||
| tools: [countAstNodes], | ||
| maxTurns: 6, | ||
| }); | ||
|
|
||
| export default jsAstNodeCounter; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # 347 - Python Requirements Risk Mapper | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering } from "rig"; | ||
|
|
||
| const classifyPackageRisk = defineTool("classifyPackageRisk", { | ||
| description: "Classify a Python package's risk level based on name and version heuristics", | ||
| parameters: s.object({ name: s.string, version: s.string }), | ||
| handler({ name, version }) { | ||
| const knownLegacy = ["django", "flask", "requests", "urllib3", "cryptography", "pillow", "numpy"]; | ||
| const versionParts = version.split(".").map(Number); | ||
| const major = versionParts[0] ?? 0; | ||
| const isOutdated = major === 0 || (knownLegacy.includes(name.toLowerCase()) && major < 2); | ||
| const riskLevel: "low" | "medium" | "high" = | ||
|
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 💡 SuggestionDocument the assumption inline so sample readers understand the intent: // heuristic: packages in this list are considered outdated if still on major version 0 or 1
const legacyMajorThreshold: Record<string, number> = {
django: 2, flask: 1, requests: 2, urllib3: 2, cryptography: 3, pillow: 9, numpy: 1,
};
const threshold = legacyMajorThreshold[name.toLowerCase()] ?? 1;
const isOutdated = major < threshold;This avoids the misleading |
||
| isOutdated && knownLegacy.includes(name.toLowerCase()) ? "high" as const | ||
| : isOutdated ? "medium" as const | ||
| : "low" as const; | ||
| return { isOutdated, riskLevel }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: map Python requirements to risk levels and recommend an action. | ||
| const pythonRequirementsRiskMapper = agent({ | ||
| model: "small", | ||
| instructions: p`requirements.txt: ${p.readOptional("requirements.txt", "# no requirements.txt found")} | ||
| installed packages: ${p.bash("pip list --format=json 2>/dev/null || echo '[]'")} | ||
|
|
||
| For each package found in requirements.txt or the installed list, call classifyPackageRisk with its name and version. Build a packages record. Count riskyCount (medium or high risk). Choose recommendedAction: audit if any high-risk, review if any medium-risk, ok otherwise.`, | ||
| output: s.object({ | ||
| packages: s.record(s.object({ | ||
| version: s.string, | ||
| isOutdated: s.boolean, | ||
| riskLevel: s.enum("low", "medium", "high"), | ||
| })), | ||
| totalPackages: s.int, | ||
| riskyCount: s.int, | ||
| recommendedAction: s.enum("audit", "review", "ok"), | ||
| }), | ||
| tools: [classifyPackageRisk], | ||
| addons: [steering()], | ||
| maxTurns: 5, | ||
| }); | ||
|
|
||
| export default pythonRequirementsRiskMapper; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # 348 - Git Merge Complexity Scorer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const scoreMergeComplexity = defineTool("scoreMergeComplexity", { | ||
| description: "Classify a git merge commit as simple, moderate, or complex based on files changed", | ||
| parameters: s.object({ hash: s.string, message: s.string, filesChanged: s.int }), | ||
| handler({ hash, message, filesChanged }) { | ||
| const complexity: "simple" | "moderate" | "complex" = | ||
| filesChanged <= 3 ? "simple" as const | ||
| : filesChanged <= 10 ? "moderate" as const | ||
| : "complex" as const; | ||
| return { hash, message, filesChanged, complexity }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: score git merge commits by complexity based on the number of files changed. | ||
| const gitMergeComplexityScorer = agent({ | ||
| model: "small", | ||
| instructions: p`Recent merge commits: ${p.bash("git log --merges --oneline -20 2>/dev/null || echo ''")} | ||
| Merge diff stats: ${p.bash("git log --merges --oneline -20 --format='%H %s' 2>/dev/null | head -20 | while read hash msg; do count=$(git show --stat $hash 2>/dev/null | grep -E 'files? changed' | grep -oE '[0-9]+ files? changed' | grep -oE '^[0-9]+' || echo 0); echo \"$hash|$count|$msg\"; done")} | ||
|
|
||
|
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] Unquoted 💡 Safer alternativeUse git log --merges -20 --format='%H|||%s' | while IFS='|||' read hash msg; do
count=$(git show --stat "$hash" 2>/dev/null | grep -oE '^[0-9]+(?= files? changed)' || echo 0)
echo "$hash|$count|$msg"
doneAs a sample, this shell snippet is what readers will copy into real pipelines. |
||
| For each merge commit, call scoreMergeComplexity with its hash, message, and filesChanged count. Count totalMerges and complexMergeCount. Set mostComplexMerge to the hash with the highest filesChanged (omit if no merges).`, | ||
| output: s.object({ | ||
| merges: s.array(s.object({ | ||
| hash: s.string, | ||
| message: s.string, | ||
| filesChanged: s.int, | ||
| complexity: s.enum("simple", "moderate", "complex"), | ||
| })), | ||
| totalMerges: s.int, | ||
| complexMergeCount: s.int, | ||
| mostComplexMerge: s.optional(s.string), | ||
| }), | ||
| tools: [scoreMergeComplexity], | ||
| addons: [repair()], | ||
| maxTurns: 5, | ||
| }); | ||
|
|
||
| export default gitMergeComplexityScorer; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # 349 - TS Reexport Chain Tracer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const traceReexports = defineTool("traceReexports", { | ||
| description: "Scan a TypeScript file for re-export patterns and return referenced files and symbols", | ||
| parameters: s.object({ filePath: s.path }), | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| let source = ""; | ||
| try { source = await readFile(filePath, "utf8"); } catch { return { references: [] }; } | ||
| const starMatches = [...source.matchAll(/export\s+\*\s+from\s+['"]([^'"]+)['"]/g)]; | ||
| const namedMatches = [...source.matchAll(/export\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g)]; | ||
| const references = [ | ||
|
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 arrow-function parameter 💡 Suggested fixRename to avoid the shadow: ...namedMatches.map((m) => ({ file: m[2], symbols: m[1].split(",").map((sym: string) => sym.trim()) })),The TypeScript compiler won't catch this because both values are |
||
| ...starMatches.map((m) => ({ file: m[1], symbols: ["*"] })), | ||
| ...namedMatches.map((m) => ({ file: m[2], symbols: m[1].split(",").map((s: string) => s.trim()) })), | ||
| ]; | ||
| return { references }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: trace the TypeScript re-export chain starting from an entry file and detect circular re-exports. | ||
| const tsReexportChainTracer = agent({ | ||
| model: "small", | ||
| input: s.object({ entryFile: s.path }), | ||
| instructions: p`Starting from the entryFile in the input, call traceReexports to discover re-export chains. Follow the chain up to 3 levels deep. Build a list of chain entries (file + symbols). Detect if any file appears more than once in the chain (circularDetected). Report the total chain depth.`, | ||
| output: s.object({ | ||
| chain: s.array(s.object({ | ||
| file: s.string, | ||
| symbols: s.array(s.string), | ||
| })), | ||
| chainDepth: s.int, | ||
| circularDetected: s.boolean, | ||
| }), | ||
| tools: [traceReexports], | ||
| addons: [steering()], | ||
| maxTurns: 6, | ||
| }); | ||
|
|
||
| export default tsReexportChainTracer; | ||
| ``` | ||
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
classifyWorktreetool is never able to return"dirty"— the instructions always pass an emptystatusOutput. The"dirty"branch in the handler is dead code, making the tool's description misleading.💡 Suggested fix
Either thread real
git status --porcelain <path>output through the instructions, or drop thestatusOutputparameter and rely solely on the porcelain block fields the model already has:As written, readers who study the tool will expect dirty detection to work, but it never fires.