-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-08-27 #496
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,44 @@ | ||
| # 471 - Git Hook Scanner | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, repair, s } from "rig"; | ||
|
|
||
| const analyzeHookFile = defineTool("analyzeHookFile", { | ||
| description: "Read a git hook file and determine its shebang line and executability.", | ||
| parameters: s.object({ hookPath: s.path }), | ||
| handler: async ({ hookPath }) => { | ||
| const { readFile, stat } = await import("node:fs/promises"); | ||
| const info = await stat(hookPath); | ||
| const isExecutable = (info.mode & 0o111) !== 0; | ||
| let shebang: string | undefined; | ||
| try { | ||
| const content = await readFile(hookPath, "utf-8"); | ||
| const firstLine = content.split("\n")[0] ?? ""; | ||
| if (firstLine.startsWith("#!")) shebang = firstLine; | ||
| } catch { | ||
| // unreadable | ||
| } | ||
| return { isExecutable, shebang }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: scan .git/hooks/ and classify each hook file by type. | ||
| const gitHookScanner = agent({ | ||
| model: "small", | ||
| instructions: p`List all files in ${p.bash("ls -la .git/hooks/ 2>/dev/null || echo ''")}. For each hook file (skip .sample files and README), call analyzeHookFile with its full path (.git/hooks/<name>). Classify hookType as one of: pre-commit, commit-msg, post-commit, pre-push, pre-receive, other. Return hooks as a record keyed by hook name, plus activeCount (executable hooks) and totalHooks.`, | ||
| output: s.object({ | ||
| hooks: s.record(s.object({ | ||
| hookType: s.enum("pre-commit", "commit-msg", "post-commit", "pre-push", "pre-receive", "other"), | ||
| shebang: s.optional(s.string), | ||
| isExecutable: s.boolean, | ||
| })), | ||
| activeCount: s.int, | ||
| totalHooks: s.int, | ||
| }), | ||
| tools: [analyzeHookFile], | ||
| maxTurns: 6, | ||
| addons: repair(), | ||
| }); | ||
|
|
||
| export default gitHookScanner; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # 472 - Merge Strategy Selector | ||
|
|
||
| ```rig | ||
| import { agent, p, s } from "rig"; | ||
|
|
||
| // Agent role: analyze git diff --stat to determine the dominant changed area. | ||
| const branchDiffAgent = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze the output of ${p.bash("git diff --stat HEAD~1 2>/dev/null || git diff --stat HEAD 2>/dev/null || echo 'no diff'")}. Determine the dominant area of changes: src (source code), test (test files), config (config files), docs (documentation), or mixed (multiple areas equally).`, | ||
| output: s.object({ | ||
| dominantArea: s.enum("src", "test", "config", "docs", "mixed"), | ||
| changedFiles: s.int, | ||
| }), | ||
| }); | ||
|
|
||
| // Agent role: check for conflicts in the working tree. | ||
| const conflictRiskAgent = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze ${p.bash("git status --short 2>/dev/null || echo ''")} and count lines containing conflict markers (<<<<<<, >>>>>>). Return conflictCount and hasConflicts.`, | ||
|
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] grep -rl '<<<<<<<' . --include='*.ts' --include='*.js' 2>/dev/null | wc -l |
||
| output: s.object({ | ||
| conflictCount: s.int, | ||
| hasConflicts: s.boolean, | ||
| }), | ||
| }); | ||
|
|
||
| // Agent role: recommend a merge strategy based on diff area and conflict risk. | ||
| const mergeSelectorCoordinator = agent({ | ||
| model: "small", | ||
| agents: { branchDiffAgent, conflictRiskAgent }, | ||
| instructions: "Call branchDiffAgent first to get the dominant area, then call conflictRiskAgent to assess conflict risk. Based on results, choose mergeRecommendation: fast-forward (no conflicts, few files), squash (many small changes, no conflicts), merge (mixed areas or some conflicts), rebase (single area, no conflicts, clean history). Return all combined fields plus a one-sentence rationale.", | ||
| output: s.object({ | ||
| mergeRecommendation: s.enum("fast-forward", "squash", "merge", "rebase"), | ||
| dominantArea: s.enum("src", "test", "config", "docs", "mixed"), | ||
| conflictCount: s.int, | ||
| hasConflicts: s.boolean, | ||
| rationale: s.string, | ||
| }), | ||
| }); | ||
|
|
||
| export default mergeSelectorCoordinator; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # 473 - Git Tag Message Extractor | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, repair, s } from "rig"; | ||
|
|
||
| const fetchTagMessage = defineTool("fetchTagMessage", { | ||
| description: "Fetch the annotation message for a git tag. Returns empty string if lightweight.", | ||
| parameters: s.object({ tagName: s.string }), | ||
| handler: async ({ tagName }) => { | ||
| const { execSync } = await import("node:child_process"); | ||
| try { | ||
| const output = execSync(`git cat-file tag ${tagName} 2>/dev/null`, { encoding: "utf8" }); | ||
| const lines = output.split("\n"); | ||
| const msgStart = lines.findIndex((line: string) => line === "") + 1; | ||
| const message = msgStart > 0 ? lines.slice(msgStart).join("\n").trim() : ""; | ||
| return { isAnnotated: true, message }; | ||
| } catch { | ||
| return { isAnnotated: false, message: "" }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: list all git tags and extract their annotation messages. | ||
| const gitTagMessageExtractor = agent({ | ||
| model: "small", | ||
| instructions: p`Get the list of tags from ${p.bash("git tag -l 2>/dev/null || echo ''")}. For each tag, call fetchTagMessage to determine if it is annotated and get its message. Return tags as an array with name, optional message, and isAnnotated. Include totalTags and annotatedCount.`, | ||
| output: s.object({ | ||
| tags: s.array(s.object({ | ||
| name: s.string, | ||
| message: s.optional(s.string), | ||
| isAnnotated: s.boolean, | ||
| })), | ||
| totalTags: s.int, | ||
| annotatedCount: s.int, | ||
| }), | ||
| tools: [fetchTagMessage], | ||
| maxTurns: 6, | ||
| addons: repair(), | ||
| }); | ||
|
|
||
| export default gitTagMessageExtractor; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # 474 - TS Import Alias Resolver | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, steering, repair } from "rig"; | ||
|
|
||
| const resolveAlias = defineTool("resolveAlias", { | ||
| description: "Resolve a TypeScript import alias prefix to its real path using tsconfig paths config.", | ||
| parameters: s.object({ | ||
| aliasPrefix: s.string, | ||
| tsconfigContent: s.string, | ||
| }), | ||
| handler: async ({ aliasPrefix, tsconfigContent }) => { | ||
| try { | ||
| const tsconfig = JSON.parse(tsconfigContent); | ||
| const paths: Record<string, string[]> = tsconfig?.compilerOptions?.paths ?? {}; | ||
| const key = Object.keys(paths).find((k: string) => k.startsWith(aliasPrefix.replace(/\*$/, ""))); | ||
| if (key) { | ||
| const targets = paths[key] ?? []; | ||
| return { realPath: targets[0]?.replace(/\*$/, "") ?? null, found: true }; | ||
| } | ||
| return { realPath: null, found: false }; | ||
| } catch { | ||
| return { realPath: null, found: false }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: resolve TypeScript import aliases by reading tsconfig paths and grepping src/ for usages. | ||
| const tsImportAliasResolver = agent({ | ||
| model: "small", | ||
| instructions: p`Read the project tsconfig: ${p.readOptional("tsconfig.json", "{}")}. Then check grep output: ${p.bash("grep -rn \"from '@\" src/ --include='*.ts' 2>/dev/null | head -100 || echo ''")}. For each unique alias prefix found (e.g. @utils/, @lib/), call resolveAlias with the prefix and the tsconfig content. Count usages per alias and list the files. Return aliases as a record keyed by prefix, plus totalAliasUsages and unmappedAliases (aliases not found in tsconfig).`, | ||
| output: s.object({ | ||
| aliases: s.record(s.object({ | ||
| realPath: s.optional(s.string), | ||
| usageCount: s.int, | ||
| files: s.array(s.string), | ||
| })), | ||
| totalAliasUsages: s.int, | ||
| unmappedAliases: s.array(s.string), | ||
| }), | ||
| tools: [resolveAlias], | ||
| maxTurns: 6, | ||
| addons: [steering(), repair()], | ||
| }); | ||
|
|
||
| export default tsImportAliasResolver; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # 475 - JSON Schema Property Counter | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, repair, s } from "rig"; | ||
|
|
||
| const countPropertiesAtLevel = defineTool("countPropertiesAtLevel", { | ||
| description: "Recursively count JSON Schema properties at each depth level.", | ||
| parameters: s.object({ schemaJson: s.string }), | ||
| handler: async ({ schemaJson }) => { | ||
| const schema = JSON.parse(schemaJson); | ||
|
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's 💡 Suggested alternativeconst countPropertiesAtLevel = defineTool('countPropertiesAtLevel', {
parameters: s.object({ filePath: s.path }),
handler: async ({ filePath }) => {
const { readFile } = await import('node:fs/promises');
const schema = JSON.parse(await readFile(filePath, 'utf-8'));
// ... same walk logic
}
});Then the instruction can just say: call |
||
| const levels: Record<string, number> = {}; | ||
| let total = 0; | ||
| let maxDepth = 0; | ||
| let hasRequired = false; | ||
|
|
||
| function walk(node: unknown, depth: number): void { | ||
| if (!node || typeof node !== "object") return; | ||
| const obj = node as Record<string, unknown>; | ||
| if (obj["required"]) hasRequired = true; | ||
| if (obj["properties"] && typeof obj["properties"] === "object") { | ||
| const props = obj["properties"] as Record<string, unknown>; | ||
| const count = Object.keys(props).length; | ||
| levels[String(depth)] = (levels[String(depth)] ?? 0) + count; | ||
| total += count; | ||
| if (depth > maxDepth) maxDepth = depth; | ||
| for (const val of Object.values(props)) walk(val, depth + 1); | ||
| } | ||
| if (obj["items"]) walk(obj["items"], depth); | ||
| } | ||
|
|
||
| walk(schema, 0); | ||
| return { levels, totalProperties: total, maxDepth, hasRequired }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: count JSON schema properties at each nesting depth given a schema file path. | ||
| const jsonSchemaPropertyCounter = agent({ | ||
| model: "small", | ||
| input: s.object({ schemaFile: s.string }), | ||
| instructions: p`Read the JSON schema file at the path provided in input.schemaFile: ${p.readInput("schemaFile")}. Pass its full content to countPropertiesAtLevel. Return levels (depth->count), totalProperties, maxDepth, and hasRequired.`, | ||
| output: s.object({ | ||
| levels: s.record(s.int), | ||
| totalProperties: s.int, | ||
| maxDepth: s.int, | ||
| hasRequired: s.boolean, | ||
| }), | ||
| tools: [countPropertiesAtLevel], | ||
| maxTurns: 4, | ||
| addons: repair(), | ||
| }); | ||
|
|
||
| export default jsonSchemaPropertyCounter; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # 476 - Changelog Entry Workflow | ||
|
|
||
| ```rig | ||
| import { agent, p, s } from "rig"; | ||
|
|
||
| // Agent role: collect recent git commits as raw log lines. | ||
| const commitCollector = agent({ | ||
| model: "small", | ||
| instructions: p`Run ${p.bash("git log --oneline -20 2>/dev/null || echo ''")} and return each line as a commit entry with hash and message.`, | ||
| output: s.object({ | ||
| commits: s.array(s.object({ hash: s.string, message: s.string })), | ||
| }), | ||
| }); | ||
|
|
||
| // Agent role: classify each commit by conventional commit category. | ||
| const commitClassifier = agent({ | ||
| model: "small", | ||
| input: s.object({ commits: s.array(s.object({ hash: s.string, message: s.string })) }), | ||
| instructions: "For each commit in input.commits, classify it as feat, fix, chore, docs, or other based on the commit message prefix. Return classified commits array and a categories count record.", | ||
| output: s.object({ | ||
| classified: s.array(s.object({ | ||
| hash: s.string, | ||
| message: s.string, | ||
| category: s.enum("feat", "fix", "chore", "docs", "other"), | ||
| })), | ||
| categories: s.record(s.int), | ||
| }), | ||
| }); | ||
|
|
||
| // Agent role: format classified commits into a markdown changelog entry. | ||
| const changelogFormatter = agent({ | ||
| model: "small", | ||
| input: s.object({ | ||
| classified: s.array(s.object({ | ||
| hash: s.string, | ||
| message: s.string, | ||
| category: s.enum("feat", "fix", "chore", "docs", "other"), | ||
| })), | ||
| categories: s.record(s.int), | ||
| }), | ||
| instructions: "Format input.classified into a markdown changelog entry grouped by category. Return changelogEntry (markdown string), commitCount, and categories (the input categories record).", | ||
| output: s.object({ | ||
| changelogEntry: s.string, | ||
| commitCount: s.int, | ||
| categories: s.record(s.int), | ||
| }), | ||
| }); | ||
|
|
||
| // Agent role: orchestrate changelog generation by running collector, classifier, then formatter. | ||
| const changelogCoordinator = agent({ | ||
| model: "small", | ||
| agents: { commitCollector, commitClassifier, changelogFormatter }, | ||
|
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 coordinator passes data between subagents via prompt instructions ("pass the commits array to commitClassifier"), but 💡 Pattern noteThe rig docs call this chaining via the coordinator's agent calls. Each subagent call passes structured output as the next agent's input, reducing hallucination risk. The current instruction prose "pass the commits array" works but is fragile for larger payloads. |
||
| instructions: "Step 1: call commitCollector to get recent commits. Step 2: pass the commits array to commitClassifier. Step 3: pass classified and categories to changelogFormatter. Return changelogFormatter's output.", | ||
| output: s.object({ | ||
| changelogEntry: s.string, | ||
| commitCount: s.int, | ||
| categories: s.record(s.int), | ||
| }), | ||
| }); | ||
|
|
||
| export default changelogCoordinator; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # 477 - Node Module Path Resolver | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, repair, s } from "rig"; | ||
|
|
||
| const resolveModulePath = defineTool("resolveModulePath", { | ||
| description: "Resolve the filesystem path of an installed npm package using node:module.", | ||
| parameters: s.object({ moduleName: s.string }), | ||
| handler: async ({ moduleName }) => { | ||
| const { createRequire } = await import("node:module"); | ||
| const { isBuiltin } = await import("node:module"); | ||
| if (isBuiltin(moduleName)) { | ||
| return { resolvedPath: null, isBuiltin: true }; | ||
| } | ||
| try { | ||
| const req = createRequire(process.cwd() + "/index.js"); | ||
| const resolved = req.resolve(moduleName); | ||
| return { resolvedPath: resolved, isBuiltin: false }; | ||
| } catch { | ||
| return { resolvedPath: null, isBuiltin: false }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: resolve an npm module path and extract its package.json metadata. | ||
| const nodeModulePathResolver = 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] 💡 Suggested fixUse |
||
| input: s.object({ moduleName: s.string }), | ||
| instructions: p`The module to resolve is provided in input.moduleName. Call resolveModulePath with it. If resolvedPath is returned, find and read the package.json near that path: ${p.readOptional("package.json", "{}")}. Extract name, version, main, and types fields. Return resolvedPath, packageName, version, main, types, and isBuiltin.`, | ||
| output: s.object({ | ||
| resolvedPath: s.string, | ||
| packageName: s.string, | ||
| version: s.string, | ||
|
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] 💡 Suggested fixChange the output schema to reflect the nullable reality: resolvedPath: s.optional(s.string),And update the instructions to handle the built-in case gracefully. |
||
| main: s.optional(s.string), | ||
| types: s.optional(s.string), | ||
| isBuiltin: s.boolean, | ||
| }), | ||
| tools: [resolveModulePath], | ||
| maxTurns: 4, | ||
| addons: repair(), | ||
| }); | ||
|
|
||
| export default nodeModulePathResolver; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # 478 - Source File Checksum Reporter | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, steering, repair } from "rig"; | ||
|
|
||
| const computeChecksum = defineTool("computeChecksum", { | ||
| description: "Compute the MD5 checksum and byte size of a file.", | ||
| parameters: s.object({ filePath: 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] MD5 is not collision-resistant and is deprecated for integrity use cases. Since this is a sample demonstrating 💡 One-line fixconst checksum = createHash('sha256').update(buf).digest('hex');Samples teach patterns; teaching MD5 normalises a weak algorithm when |
||
| handler: async ({ filePath }) => { | ||
| const { createHash } = await import("node:crypto"); | ||
| const { readFile } = await import("node:fs/promises"); | ||
| const buf = await readFile(filePath); | ||
| const checksum = createHash("md5").update(buf).digest("hex"); | ||
| return { checksum, sizeBytes: buf.length }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: compute MD5 checksums for all TypeScript source files and report totals. | ||
| const sourceFileChecksumReporter = agent({ | ||
| model: "small", | ||
| instructions: p`Find TypeScript source files with ${p.bash("find src -name '*.ts' -not -path '*/node_modules/*' 2>/dev/null | head -50 || echo ''")}. For each file path, call computeChecksum. Return files as a record keyed by path with checksum and sizeBytes, plus totalFiles, totalBytes, and largestFile (the path with the most bytes, or omit if none).`, | ||
| output: s.object({ | ||
| files: s.record(s.object({ checksum: s.string, sizeBytes: s.int })), | ||
| totalFiles: s.int, | ||
| totalBytes: s.int, | ||
| largestFile: s.optional(s.string), | ||
| }), | ||
| tools: [computeChecksum], | ||
| maxTurns: 8, | ||
| addons: [steering(), repair()], | ||
| }); | ||
|
|
||
| export default sourceFileChecksumReporter; | ||
| ``` | ||
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]
stat(hookPath)will throw if the hook file doesn't exist (e.g. a path constructed fromlsoutput with trailing whitespace or encoding issues). The error bubbles out of thedefineToolhandler uncaught, while the innerreadFileerror is silently swallowed. These two error-handling levels are inconsistent.💡 Suggested fix
Wrap
statin the same try/catch asreadFile, or guard both at the top level: