-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-08-25 #487
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,64 @@ | ||
| # 461 - Package Scripts Trio Workflow | ||
|
|
||
| ```rig | ||
| import { agent, p, s, workflow } from "rig"; | ||
|
|
||
| // Agent role: list all npm scripts from package.json. | ||
| const scriptsLister = agent({ | ||
| name: "scriptsLister", | ||
| model: "small", | ||
| instructions: p`List all npm scripts from this project. | ||
| ${p.read("package.json")} | ||
| Return all script names from the "scripts" field as an array.`, | ||
| output: s.object({ scripts: s.array(s.string) }), | ||
| }); | ||
|
|
||
| // Agent role: categorize npm scripts by purpose. | ||
| const scriptsCategorizer = agent({ | ||
| name: "scriptsCategorizer", | ||
| model: "small", | ||
| input: s.object({ scripts: s.array(s.string) }), | ||
| instructions: `Classify each script name into build, test, lint, release, utility, or other. | ||
| Return a record mapping category to the list of script names in it, plus the dominantCategory.`, | ||
| output: s.object({ | ||
| categories: s.record(s.array(s.string)), | ||
| dominantCategory: s.enum("build", "test", "lint", "release", "utility", "other"), | ||
| }), | ||
| }); | ||
|
|
||
| // Agent role: check installed dependency health via npm ls. | ||
| const scriptsHealthChecker = agent({ | ||
| name: "scriptsHealthChecker", | ||
| model: "small", | ||
| instructions: p`Check installed npm dependency health. | ||
| ${p.bash("npm ls --depth=0 2>&1 | tail -30")} | ||
| List any packages that appear missing or broken, and classify overall health.`, | ||
| output: s.object({ | ||
| missingDeps: s.array(s.string), | ||
| dependencyHealth: s.enum("ok", "warnings", "errors"), | ||
| }), | ||
| }); | ||
|
|
||
| // Workflow role: run three package.json analysis agents and produce an overall health verdict. | ||
| export default workflow({ | ||
| meta: { name: "pkg-scripts-trio", description: "Three-agent package.json scripts and dependency analysis." }, | ||
| body: async ({ call, phase }) => { | ||
| phase("Collect"); | ||
| const [listed, health] = await Promise.all([ | ||
| call(scriptsLister, "list scripts"), | ||
| call(scriptsHealthChecker, "check dependency health"), | ||
| ]); | ||
| const scripts = listed?.scripts ?? []; | ||
| phase("Categorize"); | ||
| const categorized = await call(scriptsCategorizer, { scripts }); | ||
| phase("Summarize"); | ||
| return call.json( | ||
| `scripts=${JSON.stringify(scripts)} categories=${JSON.stringify(categorized?.categories ?? {})} missingDeps=${JSON.stringify(health?.missingDeps ?? [])} dependencyHealth=${health?.dependencyHealth ?? "ok"}. Determine overallHealth: healthy if dependencyHealth=ok and scripts non-empty, needs-attention if warnings or empty scripts, critical if errors or missing deps.`, | ||
| s.object({ | ||
| overallHealth: s.enum("healthy", "needs-attention", "critical"), | ||
| summary: s.string, | ||
| }), | ||
| ); | ||
| }, | ||
| }); | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # 462 - TypeScript Const Enum Extractor | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, steering } from "rig"; | ||
|
|
||
|
|
||
| const extractConstEnums = defineTool("extractConstEnums", { | ||
| description: "Read a TypeScript file and extract all const enum declarations with their members.", | ||
| parameters: s.object({ filePath: s.path("TypeScript file path") }), | ||
| async handler({ filePath }) { | ||
| const { readFile } = await import("node:fs/promises"); | ||
| const src = await readFile(filePath, "utf8"); | ||
| const enumRe = /const\s+enum\s+(\w+)\s*\{([^}]*)\}/g; | ||
| const result: Record<string, { name: string; value: string | null }[]> = {}; | ||
| let m: RegExpExecArray | null; | ||
| while ((m = enumRe.exec(src)) !== null) { | ||
| const enumName = m[1]; | ||
| const body = m[2]; | ||
| const members = body | ||
| .split(",") | ||
| .map((line: string) => line.trim()) | ||
| .filter((line: string) => line.length > 0) | ||
| .map((line: string) => { | ||
| const [name, value] = line.split("=").map((s: string) => s.trim()); | ||
| return { name, value: value ?? null }; | ||
|
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 regex 💡 Suggested approachUse the const enumRe = /const\s+enum\s+(\w+)\s*\{([\s\S]*?)\}/g;This is a sample, so a note in the instructions or a comment in the code acknowledging this limitation would also be acceptable. |
||
| }); | ||
| result[enumName] = members; | ||
| } | ||
| return JSON.stringify(result); | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: find and extract all TypeScript const enum declarations across the workspace. | ||
| const tsConstEnumExtractor = agent({ | ||
| name: "tsConstEnumExtractor", | ||
| model: "small", | ||
| instructions: p`Scan all TypeScript source files for const enum declarations. | ||
| Files: ${p.glob("src/**/*.ts")} | ||
| Use extractConstEnums on each file. Aggregate results: enums record (members array, memberCount, sourceFile), totalEnums, totalMembers, largestEnum (name with most members, if any).`, | ||
| output: s.object({ | ||
| enums: s.record(s.object({ | ||
| members: s.array(s.object({ name: s.string, value: s.optional(s.string) })), | ||
| memberCount: s.int, | ||
| sourceFile: s.path, | ||
| })), | ||
| totalEnums: s.int, | ||
| totalMembers: s.int, | ||
| largestEnum: s.optional(s.string), | ||
| }), | ||
| tools: [extractConstEnums], | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default tsConstEnumExtractor; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # 463 - HTTP Access Log Stats | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, repair, s } from "rig"; | ||
|
|
||
|
|
||
| const parseLogLine = defineTool("parseLogLine", { | ||
| description: "Parse a single HTTP access log line and classify its HTTP status.", | ||
| parameters: s.object({ line: s.string("Raw access log line") }), | ||
| handler({ line }) { | ||
| const parts = line.split(" "); | ||
| const statusStr = parts.find((p: string) => /^\d{3}$/.test(p)) ?? "0"; | ||
| const status = parseInt(statusStr, 10); | ||
| let statusClass: "2xx" | "3xx" | "4xx" | "5xx" | "other"; | ||
| if (status >= 200 && status < 300) statusClass = "2xx"; | ||
| else if (status >= 300 && status < 400) statusClass = "3xx"; | ||
| else if (status >= 400 && status < 500) statusClass = "4xx"; | ||
| else if (status >= 500 && status < 600) statusClass = "5xx"; | ||
| else statusClass = "other"; | ||
|
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] 💡 Suggested alternativePass the path to the tool and read line-by-line inside the handler instead of via |
||
| const pathMatch = line.match(/"(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([^\s"]+)/); | ||
| const path = pathMatch ? pathMatch[1] : "/"; | ||
| return JSON.stringify({ status, statusClass, path }); | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: parse an HTTP access log file and compute request statistics. | ||
| const httpAccessLogStats = agent({ | ||
| name: "httpAccessLogStats", | ||
| model: "small", | ||
| input: s.object({ logFile: s.path("Path to HTTP access log file") }), | ||
| instructions: p`Read the HTTP access log at the specified path using ${p.readInput("logFile")}. | ||
| Use parseLogLine on each non-empty line to classify its status. Aggregate: | ||
| - statusCounts: count per status class (2xx, 3xx, 4xx, 5xx, other) | ||
| - topPaths: top 5 most frequent request paths | ||
| - totalRequests: total line count | ||
| - errorRate: (4xx + 5xx) / total as a fraction 0-1`, | ||
| output: s.object({ | ||
| statusCounts: s.record(s.int), | ||
| topPaths: s.array(s.string), | ||
| totalRequests: s.int, | ||
| errorRate: s.number, | ||
| }), | ||
| tools: [parseLogLine], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default httpAccessLogStats; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # 464 - TypeScript Spread Usage Counter | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, steering } from "rig"; | ||
|
|
||
|
|
||
| const countSpreadPatterns = defineTool("countSpreadPatterns", { | ||
| description: "Count object spread and array spread usages in a TypeScript file.", | ||
| parameters: s.object({ filePath: s.path("TypeScript file path") }), | ||
| async handler({ filePath }) { | ||
| const { readFile } = await import("node:fs/promises"); | ||
| const src = await readFile(filePath, "utf8"); | ||
| const objectSpreads = (src.match(/\.\.\.[a-zA-Z_$][a-zA-Z0-9_$]*(?=\s*[,}])/g) ?? []).length; | ||
| const arraySpreads = (src.match(/\.\.\.[a-zA-Z_$][a-zA-Z0-9_$]*(?=\s*[,\]])/g) ?? []).length; | ||
|
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] Both regex lookaheads include 💡 Suggested fixUse mutually exclusive lookaheads — const objectSpreads = (src.match(/\.\.\.[a-zA-Z_$][a-zA-Z0-9_$]*(?=\s*})/g) ?? []).length;
const arraySpreads = (src.match(/\.\.\.[a-zA-Z_$][a-zA-Z0-9_$]*(?=\s*\])/g) ?? []).length;Spreads followed by |
||
| return JSON.stringify({ objectSpreads, arraySpreads }); | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: count object spread and array spread patterns across all TypeScript source files. | ||
| const tsSpreadUsageCounter = agent({ | ||
| name: "tsSpreadUsageCounter", | ||
| model: "small", | ||
| instructions: p`Scan all TypeScript source files for spread operator usage. | ||
| Files: ${p.glob("src/**/*.ts")} | ||
| Use countSpreadPatterns on each file. Aggregate into: | ||
| - files: record mapping filePath to { objectSpreadCount, arraySpreadCount } | ||
| - totalObjectSpreads: sum of all object spreads | ||
| - totalArraySpreads: sum of all array spreads | ||
| - mostSpreadFile: path of the file with the highest combined spread count (null if none)`, | ||
| output: s.object({ | ||
| files: s.record(s.object({ objectSpreadCount: s.int, arraySpreadCount: s.int })), | ||
| totalObjectSpreads: s.int, | ||
| totalArraySpreads: s.int, | ||
| mostSpreadFile: s.optional(s.path), | ||
| }), | ||
| tools: [countSpreadPatterns], | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default tsSpreadUsageCounter; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # 465 - Git Hook File Scanner | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, repair, s } from "rig"; | ||
|
|
||
|
|
||
| const analyzeHookFile = defineTool("analyzeHookFile", { | ||
| description: "Read a git hook file and detect its shebang, executability, and type.", | ||
| parameters: s.object({ hookPath: s.path("Full path to the hook file") }), | ||
| async handler({ hookPath }) { | ||
| const { readFile, stat } = await import("node:fs/promises"); | ||
| try { | ||
| const [content, info] = await Promise.all([readFile(hookPath, "utf8"), stat(hookPath)]); | ||
| const shebang = content.split("\n")[0] ?? ""; | ||
| const executable = !!(info.mode & 0o111); | ||
| const name = hookPath.split("/").pop() ?? hookPath; | ||
| const knownTypes = ["pre-commit", "commit-msg", "post-commit", "pre-push", "pre-receive"]; | ||
| const hookType = knownTypes.includes(name) ? name : "other"; | ||
| const lineCount = content.split("\n").length; | ||
| return JSON.stringify({ shebang, executable, hookType, lineCount }); | ||
| } catch { | ||
| return JSON.stringify({ error: "could not read hook" }); | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: scan .git/hooks for installed hook scripts and report their properties. | ||
| const gitHookFileScanner = agent({ | ||
|
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] When 💡 Options
As a sample this matters because readers will copy the error-handling pattern. |
||
| name: "gitHookFileScanner", | ||
| model: "small", | ||
| instructions: p`List all files in the .git/hooks directory. | ||
| ${p.bash("ls -1 .git/hooks/ 2>/dev/null || echo 'no hooks directory'")} | ||
| For each non-sample file, use analyzeHookFile passing the full path (.git/hooks/<name>). | ||
| Return hooks as a record keyed by hook name, activeCount (executable hooks), and totalHooks.`, | ||
| output: s.object({ | ||
| hooks: s.record(s.object({ | ||
| shebang: s.string, | ||
| executable: s.boolean, | ||
| hookType: s.enum("pre-commit", "commit-msg", "post-commit", "pre-push", "pre-receive", "other"), | ||
| lineCount: s.int, | ||
| })), | ||
| activeCount: s.int, | ||
| totalHooks: s.int, | ||
| }), | ||
| tools: [analyzeHookFile], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default gitHookFileScanner; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # 466 - Merge Strategy Selector | ||
|
|
||
| ```rig | ||
| import { agent, p, s, workflow } from "rig"; | ||
|
|
||
| // Agent role: analyze git diff --stat to determine which area of the codebase changed most. | ||
| const branchDiffAgent = agent({ | ||
| name: "branchDiffAgent", | ||
| model: "small", | ||
| instructions: p`Analyze the git diff statistics for the current branch vs main. | ||
| ${p.bash("git diff --stat origin/main...HEAD 2>/dev/null || git diff --stat HEAD~1...HEAD 2>/dev/null || echo 'no diff available'")} | ||
| Determine which area of the codebase was changed most (src/test/config/docs/mixed).`, | ||
| output: s.object({ | ||
| changedFiles: s.int, | ||
| insertions: s.int, | ||
| deletions: s.int, | ||
| dominantArea: s.enum("src", "test", "config", "docs", "mixed"), | ||
| }), | ||
| }); | ||
|
|
||
| // Agent role: assess merge conflict risk from git status. | ||
| const conflictRiskAgent = agent({ | ||
| name: "conflictRiskAgent", | ||
| model: "small", | ||
| instructions: p`Check git working tree status for conflicts. | ||
| ${p.bash("git status --short 2>/dev/null | head -30")} | ||
| Count conflict markers (lines starting with UU, AA, DD) and report conflict risk.`, | ||
| output: s.object({ | ||
| conflictCount: s.int, | ||
| hasConflicts: s.boolean, | ||
| }), | ||
| }); | ||
|
|
||
| // Workflow role: analyze branch diff and conflict risk, then recommend a merge strategy. | ||
| export default workflow({ | ||
| meta: { name: "merge-strategy-selector", description: "Select optimal merge strategy based on diff and conflict analysis." }, | ||
| body: async ({ call, phase }) => { | ||
| phase("Analyze"); | ||
| const [diffResult, conflictResult] = await Promise.all([ | ||
| call(branchDiffAgent, "analyze diff"), | ||
| call(conflictRiskAgent, "check conflicts"), | ||
| ]); | ||
| phase("Recommend"); | ||
| return call.json( | ||
| `dominantArea=${diffResult?.dominantArea} changedFiles=${diffResult?.changedFiles} hasConflicts=${conflictResult?.hasConflicts} conflictCount=${conflictResult?.conflictCount}. Choose mergeRecommendation: fast-forward (few files, no conflicts, src-only), squash (many small commits, clean), merge (mixed areas), rebase (linear history preferred, no conflicts).`, | ||
| s.object({ | ||
| mergeRecommendation: s.enum("fast-forward", "squash", "merge", "rebase"), | ||
| rationale: s.string, | ||
| }), | ||
| ); | ||
| }, | ||
| }); | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # 467 - INI Config Parser | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, repair, s } from "rig"; | ||
|
|
||
|
|
||
| const parseIniSection = defineTool("parseIniSection", { | ||
| description: "Parse key-value pairs from an INI config file section.", | ||
| parameters: s.object({ content: s.string("Full INI file content"), section: s.string("Section name to parse") }), | ||
| handler({ content, section }) { | ||
| const lines = content.split("\n"); | ||
| const sectionRe = new RegExp(`^\\[${section}\\]`); | ||
| const result: Record<string, string> = {}; | ||
| let inSection = false; | ||
| for (const line of lines) { | ||
| if (sectionRe.test(line.trim())) { inSection = true; continue; } | ||
| if (/^\[/.test(line.trim())) { if (inSection) break; continue; } | ||
| if (!inSection) continue; | ||
| const eqIdx = line.indexOf("="); | ||
| if (eqIdx < 0) continue; | ||
| const key = line.slice(0, eqIdx).trim(); | ||
| const val = line.slice(eqIdx + 1).trim(); | ||
| if (key) result[key] = val; | ||
| } | ||
|
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] 💡 Alternative designA |
||
| return JSON.stringify(result); | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: parse an INI configuration file and extract all sections and their key-value pairs. | ||
| const iniConfigParser = agent({ | ||
| name: "iniConfigParser", | ||
| model: "small", | ||
| input: s.object({ configFile: s.path("Path to the INI config file") }), | ||
| instructions: p`Read the INI configuration file at the specified path. | ||
| ${p.readInput("configFile")} | ||
| Use parseIniSection for each section header you find (lines matching [SectionName]). | ||
| Return sections as a record, totalKeys count, and sectionCount.`, | ||
| output: s.object({ | ||
| sections: s.record(s.record(s.string)), | ||
| sectionCount: s.int, | ||
| totalKeys: s.int, | ||
| }), | ||
| tools: [parseIniSection], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default iniConfigParser; | ||
| ``` | ||
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]
call.jsonreceives a large prose prompt that embeds JSON via template literals. If anyJSON.stringifyvalue is long or contains special characters the resulting string becomes hard to read and can drift from the intended semantics as a sample.💡 Preferred pattern: structured input agent
Other workflow samples (e.g. 469) pass structured data to a typed
input:agent instead of embedding it in a freeformcall.jsonstring. Consider makingSummarizea proper agent withinput: s.object({...})so the pattern being demonstrated is consistent with the rest of the samples collection.