-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-07-29 #272
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,46 @@ | ||
| # 300 - Server Uptime Log Parser | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering, repair } from "rig"; | ||
|
|
||
| const parseUptimeEvent = defineTool("parseUptimeEvent", { | ||
| description: "Parse a log line to extract uptime lifecycle events", | ||
| parameters: s.object({ line: s.string }), | ||
| handler({ line }) { | ||
| const startedMatch = line.match(/(\S+\s+\S+\s+\S+).*\s(\S+(?:\[\d+\])?): .*[Ss]tarted/); | ||
| const stoppedMatch = line.match(/(\S+\s+\S+\s+\S+).*\s(\S+(?:\[\d+\])?): .*[Ss]topped/); | ||
| const crashMatch = line.match(/(\S+\s+\S+\s+\S+).*\s(\S+(?:\[\d+\])?): .*(?:[Cc]rash|[Kk]ill|exit.*code [^0])/); | ||
| if (crashMatch) return { timestamp: crashMatch[1] ?? "", service: crashMatch[2] ?? "", event: "crashed" as const }; | ||
| if (startedMatch) return { timestamp: startedMatch[1] ?? "", service: startedMatch[2] ?? "", event: "started" as const }; | ||
| if (stoppedMatch) return { timestamp: stoppedMatch[1] ?? "", service: stoppedMatch[2] ?? "", event: "stopped" as const }; | ||
| return { timestamp: "", service: "", event: "unknown" as const }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Parse system logs to identify service lifecycle events, crash counts, and uptime statistics. | ||
| const uptimeLogParser = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze the following system log lines to identify service lifecycle events. | ||
| Log lines: | ||
| ${p.bash("journalctl -n 200 --no-pager 2>/dev/null || tail -n 200 /var/log/syslog 2>/dev/null || echo 'no logs'")} | ||
|
|
||
| Use the parseUptimeEvent tool on relevant log lines that mention service starts, stops, or crashes. | ||
| Calculate totalUptimeHours as a rough estimate based on timestamps found. | ||
| Set hasCrashLoop to true if crashCount >= 3. | ||
| Return the structured output.`, | ||
| output: s.object({ | ||
| uptimeEvents: s.array(s.object({ | ||
| timestamp: s.string, | ||
| service: s.string, | ||
| event: s.enum("started", "stopped", "crashed", "unknown"), | ||
| })), | ||
| crashCount: s.int, | ||
| totalUptimeHours: s.number, | ||
| hasCrashLoop: s.boolean, | ||
| }), | ||
| tools: [parseUptimeEvent], | ||
| addons: [steering(), repair()], | ||
| }); | ||
|
|
||
| export default uptimeLogParser; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # 301 - CSS Variable Extractor | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering, repair } from "rig"; | ||
|
|
||
| const parseCssVar = defineTool("parseCssVar", { | ||
| description: "Parse a CSS line to extract custom property (CSS variable) declarations", | ||
| parameters: s.object({ line: s.string }), | ||
| handler({ line }) { | ||
| const match = line.match(/^\s*(--[\w-]+)\s*:\s*(.+?)\s*;?\s*$/); | ||
| if (!match) return { name: "", value: "", found: false }; | ||
| return { name: match[1] ?? "", value: match[2] ?? "", found: true }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Scan CSS files to extract CSS custom property declarations and detect unused variables. | ||
| const cssVariableExtractor = agent({ | ||
| model: "small", | ||
| instructions: p`Scan CSS files and extract all custom property (CSS variable) declarations. | ||
|
|
||
| CSS file content (declarations and usages): | ||
| ${p.bash("find . -name '*.css' -not -path '*/node_modules/*' | head -20 | xargs grep -h -- '--' 2>/dev/null | head -100 || echo 'no css files'")} | ||
|
|
||
| Use the parseCssVar tool on each line that may contain a CSS variable declaration (--variable: value). | ||
| For each variable found, count how many times it appears as a usage (var(--name)). | ||
| A variable is unused if it is declared but never used via var(). | ||
| Return the structured output.`, | ||
| output: s.object({ | ||
| variables: s.record(s.object({ | ||
| value: s.string, | ||
| usageCount: s.int, | ||
| })), | ||
| unusedVars: s.array(s.string), | ||
| totalVars: s.int, | ||
| }), | ||
| tools: [parseCssVar], | ||
| addons: [steering(), repair()], | ||
| }); | ||
|
|
||
| export default cssVariableExtractor; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # 302 - OpenAPI Route Coverage | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const extractRoutes = defineTool("extractRoutes", { | ||
| description: "Extract HTTP route patterns from router handler code", | ||
| parameters: s.object({ content: s.string }), | ||
| handler({ content }) { | ||
| const routes: Array<{ path: string; method: string }> = []; | ||
| const pattern = /router\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi; | ||
| let m: RegExpExecArray | null; | ||
| while ((m = pattern.exec(content)) !== null) { | ||
| routes.push({ method: (m[1] ?? "get").toUpperCase(), path: m[2] ?? "/" }); | ||
| } | ||
| return routes; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Compare OpenAPI spec routes against implemented router handlers to measure coverage. | ||
| const openapiRouteCoverage = agent({ | ||
| model: "small", | ||
| instructions: p`Compare OpenAPI spec routes against implemented router handlers to determine coverage. | ||
|
|
||
| OpenAPI spec: | ||
| ${p.readOptional("openapi.json", "{}")} | ||
|
|
||
| Router handler code: | ||
| ${p.bash("grep -rn 'router\\.' --include='*.ts' --include='*.js' . 2>/dev/null | head -50 || echo 'no routes'")} | ||
|
|
||
| Use the extractRoutes tool on the router handler code to discover implemented routes. | ||
| Cross-reference with paths defined in the OpenAPI spec. | ||
| Return routes array showing which are defined (in spec) vs implemented (in code). | ||
| Calculate coveragePercent as (totalImplemented / totalDefined * 100) or 0 if no spec.`, | ||
| output: s.object({ | ||
| routes: s.array(s.object({ | ||
| path: s.string, | ||
| method: s.string, | ||
| defined: s.boolean, | ||
| implemented: s.boolean, | ||
| })), | ||
| coveragePercent: s.number, | ||
| totalDefined: s.int, | ||
| totalImplemented: s.int, | ||
| }), | ||
| tools: [extractRoutes], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default openapiRouteCoverage; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # 303 - Import Style Checker | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const classifyStyle = defineTool("classifyStyle", { | ||
| description: "Classify a file's module style based on require vs import counts", | ||
| parameters: s.object({ file: s.string, requireCount: s.int, importCount: s.int }), | ||
| handler({ requireCount, importCount }) { | ||
| if (requireCount === 0 && importCount === 0) return "unknown" as const; | ||
| if (requireCount > 0 && importCount > 0) return "mixed" as const; | ||
| if (requireCount > 0) return "cjs" as const; | ||
| return "esm" as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Classify each JavaScript file by its module system (ESM vs CJS) based on import/require usage. | ||
| const importStyleChecker = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze JavaScript files to classify their module system style. | ||
|
|
||
| Files with require/import patterns: | ||
| ${p.bash("find . -name '*.js' -not -path '*/node_modules/*' | head -10 | xargs grep -hn '^require\\|^import ' 2>/dev/null | head -80 || echo 'no imports'")} | ||
|
|
||
| For each file found, count occurrences of require(...) and import statements. | ||
| Use the classifyStyle tool to determine the module style per file. | ||
| Return counts per file and summary totals.`, | ||
| output: s.object({ | ||
| files: s.record(s.object({ | ||
| style: s.enum("esm", "cjs", "mixed", "unknown"), | ||
| requireCount: s.int, | ||
| importCount: s.int, | ||
| })), | ||
| esmCount: s.int, | ||
| cjsCount: s.int, | ||
| mixedCount: s.int, | ||
| unknownCount: s.int, | ||
| }), | ||
| tools: [classifyStyle], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default importStyleChecker; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # 304 - Git Tag Semver Validator | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering } from "rig"; | ||
|
|
||
| const parseSemver = defineTool("parseSemver", { | ||
| description: "Parse a git tag to determine if it follows semver and extract components", | ||
| parameters: s.object({ tag: s.string }), | ||
| handler({ tag }) { | ||
| const match = tag.match(/^v?(\d+)\.(\d+)\.(\d+)/); | ||
| if (!match) return { valid: false }; | ||
| return { | ||
| valid: true, | ||
| major: parseInt(match[1] ?? "0", 10), | ||
| minor: parseInt(match[2] ?? "0", 10), | ||
| patch: parseInt(match[3] ?? "0", 10), | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Validate git tags against semver format and identify the latest valid version. | ||
| const gitTagSemverValidator = agent({ | ||
| model: "small", | ||
| instructions: p`Validate git tags to check semver compliance. | ||
|
|
||
| Git tags: | ||
| ${p.bash("git tag --list 2>/dev/null | head -30 || echo 'no tags'")} | ||
|
|
||
| Use the parseSemver tool on each tag to validate format and extract major/minor/patch. | ||
| Identify invalidCount (tags that don't follow semver). | ||
| Set latestValid to the highest valid semver tag, or omit if none. | ||
| Return the structured output.`, | ||
| output: s.object({ | ||
| tags: s.array(s.object({ | ||
| name: s.string, | ||
| valid: s.boolean, | ||
| parsed: s.optional(s.object({ | ||
| major: s.int, | ||
| minor: s.int, | ||
| patch: s.int, | ||
| })), | ||
| })), | ||
| invalidCount: s.int, | ||
| latestValid: s.optional(s.string), | ||
| }), | ||
| tools: [parseSemver], | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default gitTagSemverValidator; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # 305 - Shell Script Safety Analyzer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const analyzeShellScript = defineTool("analyzeShellScript", { | ||
| description: "Analyze a shell script file for safety flags (set -e, set -u, set -o pipefail) and shebang", | ||
| parameters: s.object({ filePath: s.string }), | ||
| async handler({ filePath }) { | ||
| const content = await readFile(filePath, "utf8").catch(() => ""); | ||
| const lines = content.split("\n"); | ||
| const shebangLine = lines[0] ?? ""; | ||
| const shebang = shebangLine.startsWith("#!") ? shebangLine.slice(2).trim() : undefined; | ||
| const hasSetE = /\bset\b.*-[a-zA-Z]*e/.test(content) || /\bset\b.*-e/.test(content); | ||
| const hasSetU = /\bset\b.*-[a-zA-Z]*u/.test(content) || /\bset\b.*-u/.test(content); | ||
| const hasPipefail = /set\s+-o\s+pipefail/.test(content); | ||
| let safetyLevel: "strict" | "partial" | "unsafe"; | ||
| if (hasSetE && hasSetU && hasPipefail) safetyLevel = "strict"; | ||
| else if (hasSetE || hasSetU || hasPipefail) safetyLevel = "partial"; | ||
| else safetyLevel = "unsafe"; | ||
| return { shebang, hasSetE, hasSetU, hasPipefail, safetyLevel }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Scan shell scripts for safety flags and classify each by safety level. | ||
| const shellScriptSafetyAnalyzer = agent({ | ||
| model: "small", | ||
| instructions: p`Find all shell scripts and analyze each for safety flags. | ||
|
|
||
| Shell script files: | ||
| ${p.bash("find . -name '*.sh' -not -path '*/node_modules/*' | head -20 2>/dev/null || echo 'no shell scripts'")} | ||
|
|
||
| Use the analyzeShellScript tool on each .sh file path listed above. | ||
| Return a record keyed by file path with safety analysis for each script.`, | ||
| output: s.record(s.object({ | ||
| shebang: s.optional(s.string), | ||
| hasSetE: s.boolean, | ||
| hasSetU: s.boolean, | ||
| hasPipefail: s.boolean, | ||
| safetyLevel: s.enum("strict", "partial", "unsafe"), | ||
| })), | ||
| tools: [analyzeShellScript], | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default shellScriptSafetyAnalyzer; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # 306 - Git Diff Stats Summarizer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const classifyDiffEntry = defineTool("classifyDiffEntry", { | ||
| description: "Classify a diff entry based on addition/deletion counts", | ||
| parameters: s.object({ path: s.string, additions: s.int, deletions: s.int }), | ||
| handler({ additions, deletions }) { | ||
| if (additions > 0 && deletions === 0) return "added" as const; | ||
| if (additions === 0 && deletions > 0) return "deleted" as const; | ||
| if (additions > 0 && deletions > 0) return "modified" as const; | ||
| return "renamed" as const; | ||
|
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 fixhandler({ additions, deletions }) {
if (additions > 0 && deletions === 0) return "added" as const;
if (additions === 0 && deletions > 0) return "deleted" as const;
if (additions > 0 && deletions > 0) return "modified" as const;
return "unchanged" as const; // 0/0 means no net line changes, not a rename
},Rename detection would require inspecting the file path string (e.g. |
||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Summarize git diff statistics between the last two commits, classifying each changed file. | ||
| const gitDiffStatsSummarizer = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze git diff statistics for the most recent commit. | ||
|
|
||
| Diff numstat (additions deletions file): | ||
| ${p.bash("git diff --numstat HEAD~1 HEAD 2>/dev/null | head -30 || echo 'no diff'")} | ||
|
|
||
| Diff summary: | ||
| ${p.bash("git diff --stat HEAD~1 HEAD 2>/dev/null | tail -5 || echo 'no stat'")} | ||
|
|
||
| Parse the numstat output (format: additions<TAB>deletions<TAB>path). | ||
| Use the classifyDiffEntry tool on each file entry. | ||
| Sum up totalAdditions and totalDeletions. | ||
| Set mostChangedFile to the file with the highest combined additions+deletions, or omit if empty. | ||
| Return the structured output.`, | ||
| output: s.object({ | ||
| files: s.array(s.object({ | ||
| path: s.string, | ||
| additions: s.int, | ||
| deletions: s.int, | ||
| change: s.enum("added", "modified", "deleted", "renamed"), | ||
| })), | ||
| totalAdditions: s.int, | ||
| totalDeletions: s.int, | ||
| mostChangedFile: s.optional(s.string), | ||
| }), | ||
| tools: [classifyDiffEntry], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default gitDiffStatsSummarizer; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # 307 - Dotenv Template Generator | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const extractEnvReferences = defineTool("extractEnvReferences", { | ||
| description: "Extract all process.env.VAR_NAME references from a TypeScript source file", | ||
| parameters: s.object({ filePath: s.string }), | ||
| async handler({ filePath }) { | ||
| const content = await readFile(filePath, "utf8").catch(() => ""); | ||
| const pattern = /process\.env\.([A-Z_][A-Z0-9_]*)/g; | ||
| const keys = new Set<string>(); | ||
| let m: RegExpExecArray | null; | ||
| while ((m = pattern.exec(content)) !== null) { | ||
| if (m[1]) keys.add(m[1]); | ||
| } | ||
| return Array.from(keys); | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: Generate a .env.template file by scanning source files for process.env references and comparing against existing .env keys. | ||
| const dotenvTemplateGenerator = agent({ | ||
| model: "small", | ||
| instructions: p`Generate a .env.template file documenting all required environment variables. | ||
|
|
||
| Existing .env file: | ||
| ${p.readOptional(".env", "# no .env file found")} | ||
|
|
||
| TypeScript source files to scan: | ||
| ${p.glob("src/**/*.ts")} | ||
|
|
||
| Use the extractEnvReferences tool on each source file to find process.env.VAR references. | ||
| Collect all unique env keys found across all files (envKeys). | ||
| Compare with keys present in .env — undocumentedKeys are those referenced in code but missing from .env. | ||
| Write a .env.template with each key as KEY=<placeholder>. | ||
| ${p.write(".env.template", "# Generated .env.template\n# Replace <placeholder> with actual values\n")} | ||
| Return templatePath as ".env.template", envKeys array, undocumentedKeys array, and templateGenerated as true.`, | ||
| output: s.object({ | ||
| templatePath: s.path, | ||
| envKeys: s.array(s.string), | ||
| undocumentedKeys: s.array(s.string), | ||
| templateGenerated: s.boolean, | ||
|
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 💡 ConsiderIf the intent is to show |
||
| }), | ||
| tools: [extractEnvReferences], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default dotenvTemplateGenerator; | ||
| ``` | ||
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]
extractRoutesonly matches Express-stylerouter.METHOD()calls and returns them directly as the tool's return value, but the instructions pass the raw grep output (not the parsed code) to the tool. The tool receives the entire concatenated grep lines ascontent, which includes line numbers and file prefixes (file.ts:42:router.get(...)). The regexrouter\.will work on this string, but the file-prefixed format isn't called out — a reader adapting this sample may get silent mismatches.💡 Suggestion
Either document in a comment that
grep -hstrips the file prefix (which the bash command already uses), or adjust the regex to anchor more carefully. A small clarifying comment near thep.bashline would close the gap for readers: