-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-07-28 #232
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,40 @@ | ||
| # 240 - Git Commit Annotator | ||
|
|
||
| ```rig | ||
| import { agent, p, s } from "rig"; | ||
|
|
||
| // Agent role: annotate each of the last 20 git commits with a category and impact level. | ||
| const commitSummarizer = agent({ | ||
| name: "commitSummarizer", | ||
| model: "small", | ||
| instructions: p`Classify the following git commit line into a category and impact level. | ||
| Commit: ${p.readInput("commit")}`, | ||
| input: s.object({ commit: s.string }), | ||
| output: s.object({ | ||
| category: s.enum("feat", "fix", "chore", "docs", "refactor", "test", "perf"), | ||
| impact: s.enum("low", "medium", "high"), | ||
| }), | ||
| }); | ||
|
|
||
| // Agent role: fetch the last 20 git commits and annotate each with category and impact using the commitSummarizer subagent. | ||
| const gitCommitAnnotator = agent({ | ||
| model: "small", | ||
| instructions: p`You are a git commit annotator. Here are the last 20 commits: | ||
| ${p.bash("git log --oneline -20")} | ||
|
|
||
| For each commit line, call the commitSummarizer subagent with the full commit line. | ||
| Then assemble the annotations array and a short prose report summarizing the distribution of categories and impacts.`, | ||
| output: s.object({ | ||
| annotations: s.array(s.object({ | ||
| hash: s.string, | ||
| message: s.string, | ||
| category: s.enum("feat", "fix", "chore", "docs", "refactor", "test", "perf"), | ||
| impact: s.enum("low", "medium", "high"), | ||
| })), | ||
| report: s.string, | ||
| }), | ||
| agents: { commitSummarizer }, | ||
| }); | ||
|
|
||
| export default gitCommitAnnotator; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # 241 - Npm Script Dep Mapper | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const parseScriptDeps = defineTool("parseScriptDeps", { | ||
| description: "Parse npm scripts JSON and extract per-script dependency metadata.", | ||
| parameters: { scriptsJson: s.string }, | ||
| handler: ({ scriptsJson }: { scriptsJson: string }) => { | ||
| const scripts: Record<string, string> = JSON.parse(scriptsJson); | ||
| const result: Record<string, { | ||
| command: string; | ||
| dependsOn: string[]; | ||
| category: string; | ||
| hasPreHook: boolean; | ||
| hasPostHook: boolean; | ||
| }> = {}; | ||
| for (const [name, command] of Object.entries(scripts)) { | ||
| const refs = [...command.matchAll(/npm run ([a-z0-9:_-]+)/gi)].map((m: RegExpMatchArray) => m[1]); | ||
| const hasPreHook = (`pre${name}`) in scripts; | ||
| const hasPostHook = (`post${name}`) in scripts; | ||
| let category = "util"; | ||
| if (/build|compile|bundle/.test(name)) category = "build"; | ||
| else if (/test|spec|coverage/.test(name)) category = "test"; | ||
| else if (/lint|format|check/.test(name)) category = "lint"; | ||
| else if (/dev|start|watch/.test(name)) category = "dev"; | ||
| else if (/deploy|publish|release/.test(name)) category = "deploy"; | ||
| else if (/^pre|^post/.test(name)) category = "hook"; | ||
| result[name] = { command, dependsOn: refs, category, hasPreHook, hasPostHook }; | ||
| } | ||
| return result; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: map npm script dependencies in the workspace package.json. | ||
| const npmScriptDepMapper = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze the npm scripts in the workspace package.json. | ||
| package.json content: ${p.read("package.json")} | ||
|
|
||
| Steps: | ||
| 1. Extract the "scripts" field from the package.json content as a JSON string. | ||
| 2. Call parseScriptDeps with that JSON string. | ||
| 3. Return the result directly as the output record.`, | ||
| output: s.record(s.object({ | ||
| command: s.string, | ||
| dependsOn: s.array(s.string), | ||
| category: s.enum("build", "test", "lint", "dev", "deploy", "util", "hook", "other"), | ||
| hasPreHook: s.boolean, | ||
| hasPostHook: s.boolean, | ||
| })), | ||
| tools: [parseScriptDeps], | ||
| maxTurns: 6, | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default npmScriptDepMapper; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # 242 - Markdown Link Checker | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool } from "rig"; | ||
| import { exec } from "node:child_process"; | ||
| import { promisify } from "node:util"; | ||
|
|
||
| const execAsync = promisify(exec); | ||
|
|
||
| const checkUrl = defineTool("checkUrl", { | ||
| description: "Check an HTTP URL with curl and return its status.", | ||
| parameters: { url: s.string }, | ||
| handler: async ({ url }: { url: string }) => { | ||
| try { | ||
| const { stdout } = await execAsync( | ||
| `curl -s -o /dev/null -w "%{http_code}" --max-time 5 --location "${url}"`, | ||
| ); | ||
| const code = parseInt(stdout.trim(), 10); | ||
|
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] Shell injection risk: 💡 Suggested fixUse import { execFile } from "node:child_process";
const { stdout } = await promisify(execFile)("curl", ["-s","-o","/dev/null","-w","%{http_code}","--max-time","5","--location",url]);Samples teach patterns — unsafe shell interpolation here will be replicated by readers. |
||
| let status: string; | ||
| if (code >= 200 && code < 300) status = "ok"; | ||
| else if (code >= 300 && code < 400) status = "redirect"; | ||
| else if (code === 0) status = "timeout"; | ||
| else status = "broken"; | ||
| return { httpCode: code, status }; | ||
| } catch { | ||
| return { httpCode: 0, status: "timeout" }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: check all HTTP/HTTPS links found in markdown files in the workspace. | ||
| const markdownLinkChecker = agent({ | ||
| model: "small", | ||
| instructions: p`Check all HTTP/HTTPS links found in markdown files. | ||
|
|
||
| Markdown files: ${p.bash("find . -name '*.md' -not -path '*/node_modules/*' | head -30")} | ||
| Extracted URLs: ${p.bash("grep -rh 'https\\?://[^ )>\"]*' --include='*.md' --no-filename . 2>/dev/null | grep -oE 'https?://[^ )>\"]+' | sort -u | head -50")} | ||
|
|
||
| Steps: | ||
| 1. For each unique URL extracted, call checkUrl. | ||
| 2. Build the links array with url, status, httpCode, and foundInFile (leave as null if not determinable). | ||
| 3. Count brokenCount (status = "broken" or "timeout"). | ||
| 4. Set allValid to true only if brokenCount is 0.`, | ||
| output: s.object({ | ||
| links: s.array(s.object({ | ||
| url: s.string, | ||
| status: s.enum("ok", "broken", "redirect", "timeout", "skipped"), | ||
| httpCode: s.optional(s.number), | ||
| foundInFile: s.optional(s.string), | ||
| })), | ||
| brokenCount: s.number, | ||
| allValid: s.boolean, | ||
| }), | ||
| tools: [checkUrl], | ||
| maxTurns: 8, | ||
| }); | ||
|
|
||
| export default markdownLinkChecker; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # 243 - Package Version Drift | ||
|
|
||
| ```rig | ||
| import { agent, p, s } from "rig"; | ||
|
|
||
| // Agent role: report package version drift by comparing package.json with npm outdated results. | ||
| const packageVersionDrift = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze package version drift for this workspace. | ||
|
|
||
| package.json: ${p.read("package.json")} | ||
| npm outdated (JSON, may be empty if all current): ${p.bash("npm outdated --json 2>/dev/null || echo '{}'")} | ||
|
|
||
| Steps: | ||
| 1. Parse the npm outdated JSON — each key is a package name with current/wanted/latest fields. | ||
| 2. For each outdated package determine driftLevel: | ||
| - "ok" if current === latest | ||
| - "patch" if only patch differs | ||
| - "minor" if minor version differs | ||
| - "major" if major version differs | ||
| 3. Write a markdown drift report to drift-report.md summarizing the table. | ||
| 4. Set reportWritten to true after writing.`, | ||
| output: s.object({ | ||
|
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 instructions say "Write a markdown drift report to drift-report.md" but the agent has no write tool and no 💡 OptionsOption A — remove the phantom write: // Remove "3. Write a markdown drift report to drift-report.md" from instructions
// Remove reportWritten from the output schema
output: s.object({
packages: s.array(...),
}),Option B — add a write intent: instructions: p`...
${p.write("drift-report.md", "<the generated markdown table>")}
`, |
||
| packages: s.array(s.object({ | ||
| name: s.string, | ||
| current: s.string, | ||
| latest: s.string, | ||
| driftLevel: s.enum("ok", "patch", "minor", "major"), | ||
| })), | ||
| reportWritten: s.boolean, | ||
| }), | ||
| }); | ||
|
|
||
| export default packageVersionDrift; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # 244 - Git Author Stats | ||
|
|
||
| ```rig | ||
| import { agent, p, s } from "rig"; | ||
|
|
||
| // Agent role: aggregate git author statistics including commit counts and first/last commit dates. | ||
| const gitAuthorStats = agent({ | ||
| model: "small", | ||
| instructions: p`Aggregate git commit statistics by author. | ||
|
|
||
| Commit counts per author: ${p.bash("git shortlog -sne --all 2>/dev/null || echo ''")} | ||
| Per-commit author and date: ${p.bash("git log --format='%ae|%ai' --all 2>/dev/null | head -500")} | ||
|
|
||
| Steps: | ||
| 1. Parse the shortlog output to extract each author email and commit count. | ||
| 2. From the per-commit log, compute firstCommit (earliest date) and lastCommit (latest date) per author email. | ||
| 3. Build the authors record keyed by email with commitCount, firstCommit, lastCommit. | ||
| 4. Set topAuthor to the email with the highest commitCount. | ||
| 5. Set totalCommits to the sum of all commitCounts.`, | ||
| output: s.object({ | ||
| authors: s.record(s.object({ | ||
| commitCount: s.number, | ||
| firstCommit: s.string, | ||
| lastCommit: s.string, | ||
| })), | ||
| topAuthor: s.string, | ||
| totalCommits: s.number, | ||
| }), | ||
| }); | ||
|
|
||
| export default gitAuthorStats; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # 245 - Ts Path Alias Validator | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool } from "rig"; | ||
| import { existsSync } from "node:fs"; | ||
|
|
||
| const checkAlias = defineTool("checkAlias", { | ||
| description: "Check whether a TypeScript path alias target directory exists on disk.", | ||
| parameters: { alias: s.string, target: s.string }, | ||
| handler: ({ target }: { alias: string; target: string }) => { | ||
| const resolved = target.replace(/\/\*$/, ""); | ||
| return existsSync(resolved) ? "valid" : "broken"; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: validate all TypeScript path aliases defined in tsconfig.json against the workspace. | ||
| const tsPathAliasValidator = agent({ | ||
| model: "small", | ||
| instructions: p`Validate TypeScript path aliases defined in tsconfig.json. | ||
|
|
||
| tsconfig.json: ${p.read("tsconfig.json")} | ||
| Import statements in .ts files: ${p.bash("grep -rh --include='*.ts' 'from \"' . 2>/dev/null | grep -v node_modules | sort -u | head -100")} | ||
|
|
||
| Steps: | ||
| 1. Parse the compilerOptions.paths from tsconfig.json — each key is an alias pattern, values are target arrays. | ||
| 2. For each alias, call checkAlias with the alias key and its first target path. | ||
| 3. Check whether each alias is actually used in the import statements. | ||
| 4. Set status: "broken" if target doesn't exist, "unused" if never imported, "valid" otherwise. | ||
| 5. Count brokenCount as the number of broken aliases.`, | ||
| output: s.object({ | ||
| aliases: s.record(s.object({ | ||
| target: s.string, | ||
| status: s.enum("valid", "broken", "unused"), | ||
| })), | ||
| totalAliases: s.number, | ||
| brokenCount: s.number, | ||
| }), | ||
| tools: [checkAlias], | ||
| maxTurns: 6, | ||
| }); | ||
|
|
||
| export default tsPathAliasValidator; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # 246 - Stale Branch Detector | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool } from "rig"; | ||
|
|
||
| const classifyBranchAge = defineTool("classifyBranchAge", { | ||
| description: "Classify a branch as fresh, stale, or dead based on its last commit date.", | ||
| parameters: { branchName: s.string, lastCommitDate: s.string }, | ||
| handler: ({ lastCommitDate }: { branchName: string; lastCommitDate: string }) => { | ||
| const ageMs = Date.now() - new Date(lastCommitDate).getTime(); | ||
| const ageDays = ageMs / (1000 * 60 * 60 * 24); | ||
| if (ageDays < 30) return "fresh"; | ||
| if (ageDays < 90) return "stale"; | ||
| return "dead"; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: detect stale and dead local git branches and recommend candidates for deletion. | ||
| const staleBranchDetector = agent({ | ||
| model: "small", | ||
| instructions: p`Detect stale and dead local git branches. | ||
|
|
||
| Branch list with last commit dates: | ||
| ${p.bash("git for-each-ref --format='%(refname:short)|%(committerdate:iso8601)' refs/heads 2>/dev/null || echo ''")} | ||
|
|
||
| Steps: | ||
| 1. Parse each line as branchName|lastCommitDate. | ||
| 2. For each branch, call classifyBranchAge to get its age category. | ||
| 3. Build the branches array with name, lastCommit, and age. | ||
| 4. Count staleCount (age = "stale") and deadCount (age = "dead"). | ||
| 5. Set recommendedForDeletion to branch names where age is "dead".`, | ||
| output: s.object({ | ||
| branches: s.array(s.object({ | ||
| name: s.string, | ||
| lastCommit: s.string, | ||
| age: s.enum("fresh", "stale", "dead"), | ||
| })), | ||
| staleCount: s.number, | ||
| deadCount: s.number, | ||
| recommendedForDeletion: s.array(s.string), | ||
| }), | ||
| tools: [classifyBranchAge], | ||
| maxTurns: 6, | ||
| }); | ||
|
|
||
| export default staleBranchDetector; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # 247 - License Header Checker | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const checkLicenseHeader = defineTool("checkLicenseHeader", { | ||
| description: "Check whether a TypeScript file starts with the expected license header.", | ||
| parameters: { filePath: s.string, expectedHeader: s.string }, | ||
| handler: async ({ filePath, expectedHeader }: { filePath: string; expectedHeader: string }) => { | ||
| try { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const lines = content.split("\n"); | ||
| const headerLines = expectedHeader.split("\n"); | ||
| const fileStart = lines.slice(0, headerLines.length).join("\n"); | ||
| if (fileStart === expectedHeader) return "ok"; | ||
| if (content.includes(expectedHeader.split("\n")[0])) return "wrong"; | ||
| return "missing"; | ||
| } catch { | ||
| return "missing"; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: check that all TypeScript source files contain the expected license header. | ||
| const licenseHeaderChecker = agent({ | ||
| model: "small", | ||
| input: s.object({ expectedHeader: s.string }), | ||
| instructions: p`Check each TypeScript file for the expected license header. | ||
|
|
||
| TypeScript files in the workspace: | ||
| ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' | head -60")} | ||
|
|
||
| Steps: | ||
| 1. Read expectedHeader from the input. | ||
| 2. For each file path, call checkLicenseHeader with the filePath and expectedHeader. | ||
| 3. Build a files record keyed by file path with hasHeader and status. | ||
| 4. Count missingCount (status = "missing" or "wrong"). | ||
| 5. Set allCompliant to true only if missingCount is 0.`, | ||
| output: s.object({ | ||
| files: s.record(s.object({ | ||
| hasHeader: s.boolean, | ||
| status: s.enum("ok", "missing", "wrong"), | ||
| })), | ||
| missingCount: s.number, | ||
| allCompliant: s.boolean, | ||
| }), | ||
| tools: [checkLicenseHeader], | ||
| maxTurns: 8, | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default licenseHeaderChecker; | ||
| ``` |
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] The output schema includes
"other"in the category enum, but theparseScriptDepstool handler never produces that value — it always falls through to"util". Either remove"other"from the schema or add an explicit else branch that returns it.💡 Fix