-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-08-13 #415
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 @@ | ||
| # 411 - TS Generic Constraint Extractor | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| // Agent role: extract generic type constraints from TypeScript files and summarize per file. | ||
| const tsGenericConstraintExtractor = agent({ | ||
| model: "small", | ||
| instructions: p`Extract TypeScript generic type constraints (patterns like \`T extends ...\`) from source files. | ||
|
|
||
| TypeScript files: ${p.bash("find src -name '*.ts' 2>/dev/null | head -50 || echo ''")} | ||
|
|
||
| For each file path, call extractGenericConstraints. Then produce the declared output.`, | ||
| tools: [ | ||
| defineTool("extractGenericConstraints", { | ||
| description: "Extract generic type constraints from a TypeScript file", | ||
| parameters: s.object({ filePath: s.path }), | ||
| async handler({ filePath }) { | ||
| try { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const regex = /\bextends\s+([^\s,>{=]+(?:\s*[<(][^>)]*[>)])?)/g; | ||
| const constraints: string[] = []; | ||
| let m: RegExpExecArray | null; | ||
| while ((m = regex.exec(content)) !== null) { | ||
| constraints.push(m[1].trim()); | ||
| } | ||
| return { filePath, constraints }; | ||
| } catch { | ||
| return { filePath, constraints: [] }; | ||
| } | ||
| }, | ||
| }), | ||
| ], | ||
| output: s.object({ | ||
| constraints: s.record(s.array(s.string)), | ||
| totalConstraints: s.int, | ||
| constrainedCount: s.int, | ||
| mostConstrainedFile: s.optional(s.string), | ||
| }), | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default tsGenericConstraintExtractor; | ||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # 412 - README Badge Analyzer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering } from "rig"; | ||
|
|
||
| // Agent role: parse README.md badges and classify each by category. | ||
| const readmeBadgeAnalyzer = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze badge images in the README and classify each badge. | ||
|
|
||
| README content: | ||
| ${p.readOptional("README.md", "")} | ||
|
|
||
| Call parseBadge for each markdown image badge you find (pattern: [](link)). | ||
| Then return the declared output.`, | ||
| tools: [ | ||
| defineTool("parseBadge", { | ||
| description: "Parse a badge URL and classify it as ci, coverage, version, license, or other", | ||
| parameters: s.object({ url: s.string, label: s.string }), | ||
| handler({ url, label }: { url: string; label: string }) { | ||
| const u = url.toLowerCase(); | ||
| const l = label.toLowerCase(); | ||
| let category: "ci" | "coverage" | "version" | "license" | "other" = "other"; | ||
| if (/github.*action|travis|circleci|appveyor|workflow|build/.test(u + l)) category = "ci"; | ||
| else if (/coverage|codecov|coveralls/.test(u + l)) category = "coverage"; | ||
| else if (/version|release|npm|pypi/.test(u + l)) category = "version"; | ||
| else if (/license|mit|apache|gpl/.test(u + l)) category = "license"; | ||
| return { url, label, category }; | ||
| }, | ||
| }), | ||
| ], | ||
| output: s.object({ | ||
| badges: s.array(s.object({ | ||
| url: s.string, | ||
| label: s.string, | ||
| category: s.enum("ci", "coverage", "version", "license", "other"), | ||
| })), | ||
| totalBadges: s.int, | ||
| hasCiBadge: s.boolean, | ||
| }), | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default readmeBadgeAnalyzer; | ||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # 413 - Python Requirements Risk Mapper | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering } from "rig"; | ||
|
|
||
| // Agent role: classify Python package risk from requirements.txt and pip list output. | ||
| const pythonRequirementsRiskMapper = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze Python package risk based on requirements.txt and installed packages. | ||
|
|
||
| requirements.txt: | ||
| ${p.readOptional("requirements.txt", "(no requirements.txt found)")} | ||
|
|
||
| Installed packages (pip list): | ||
| ${p.bash("pip list --format=json 2>/dev/null || echo '[]'")} | ||
|
|
||
| For each package, call classifyPackageRisk. Packages that are very old (version < 1.0), | ||
| known for past vulnerabilities (e.g. requests<2.20, pyyaml<5.4, pillow<9), | ||
| or have no version pinned are considered high risk. | ||
| Produce the declared output.`, | ||
| tools: [ | ||
| defineTool("classifyPackageRisk", { | ||
| description: "Classify a Python package by risk level based on name and version", | ||
| parameters: s.object({ name: s.string, version: s.string }), | ||
| handler({ name, version }: { name: string; version: string }) { | ||
| const n = name.toLowerCase(); | ||
| const major = parseInt(version.split(".")[0] ?? "0", 10); | ||
| let risk: "low" | "medium" | "high" = "low"; | ||
| let reason = "stable package"; | ||
| if (!version || version === "unknown") { risk = "high"; reason = "no version pinned"; } | ||
| else if (major === 0) { risk = "medium"; reason = "pre-1.0 release"; } | ||
| if (/(pyyaml|pillow|requests|urllib3|cryptography)/.test(n) && major < 2) { | ||
| risk = "high"; reason = "known past vulnerability in old version"; | ||
| } | ||
| return { name, version, risk, reason }; | ||
| }, | ||
| }), | ||
| ], | ||
| output: s.object({ | ||
| packages: s.record(s.object({ | ||
| version: s.string, | ||
| risk: s.enum("low", "medium", "high"), | ||
| reason: s.string, | ||
| })), | ||
| totalPackages: s.int, | ||
| riskyCount: s.int, | ||
| recommendedAction: s.enum("audit", "review", "ok"), | ||
| }), | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default pythonRequirementsRiskMapper; | ||
|
|
||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # 414 - Git Merge Complexity Scorer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| // Agent role: score git merge commits by complexity based on files changed. | ||
| const gitMergeComplexityScorer = agent({ | ||
| model: "small", | ||
| instructions: p`Score the complexity of recent merge commits in this repository. | ||
|
|
||
| Recent merge commits: | ||
| ${p.bash("git log --merges --oneline -20 2>/dev/null || echo '(no merges found)'")} | ||
|
|
||
| For each merge commit hash, call scoreMergeComplexity. Then produce the declared output.`, | ||
| tools: [ | ||
| defineTool("scoreMergeComplexity", { | ||
| description: "Score a merge commit complexity by running git show --stat", | ||
| parameters: s.object({ hash: s.string, message: s.string }), | ||
| handler({ hash, message }: { hash: string; message: string }) { | ||
| const { execSync } = require("node:child_process"); | ||
|
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] Shell injection risk: 💡 Fix: use execFileSync with an args arrayReplace the dynamic import and execSync with a top-level import and import { execFileSync } from "node:child_process";
// inside handler:
const stat = execFileSync("git", ["show", "--stat", hash], { encoding: "utf-8" });This completely avoids shell parsing. As a bonus, also move the |
||
| try { | ||
| const stat = execSync(`git show --stat ${hash} 2>/dev/null`, { encoding: "utf-8" }); | ||
| const match = stat.match(/(\d+) files? changed/); | ||
| const filesChanged = match ? parseInt(match[1], 10) : 0; | ||
| const complexity: "simple" | "moderate" | "complex" = | ||
| filesChanged <= 3 ? "simple" : filesChanged <= 10 ? "moderate" : "complex"; | ||
| return { hash, message, filesChanged, complexity }; | ||
| } catch { | ||
| return { hash, message, filesChanged: 0, complexity: "simple" as const }; | ||
| } | ||
| }, | ||
| }), | ||
| ], | ||
| 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), | ||
| }), | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default gitMergeComplexityScorer; | ||
|
|
||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # 415 - TS Reexport Chain Tracer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| // Agent role: trace TypeScript re-export chains from an entry file. | ||
| const tsReexportChainTracer = agent({ | ||
| model: "small", | ||
| input: s.object({ entryFile: s.path }), | ||
| instructions: p`Trace TypeScript re-export chains starting from the entry file. | ||
| Use traceReexports to follow export * from and export { X } from chains. | ||
| Detect circular references. Produce the declared output.`, | ||
| tools: [ | ||
| defineTool("traceReexports", { | ||
| description: "Read a TypeScript file and extract its re-export paths", | ||
| parameters: s.object({ filePath: s.path }), | ||
| async handler({ filePath }) { | ||
| try { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const regex = /export\s+(?:\*|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]/g; | ||
| const exports: string[] = []; | ||
| let m: RegExpExecArray | null; | ||
| while ((m = regex.exec(content)) !== null) exports.push(m[1]); | ||
| return { filePath, reexports: exports }; | ||
| } catch { | ||
| return { filePath, reexports: [] }; | ||
|
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] Circular detection is declared in the output schema ( 💡 Implement tracking in the tool handlerPass a parameters: s.object({ filePath: s.path, visitedPaths: s.array(s.string) }),
async handler({ filePath, visitedPaths }) {
if (visitedPaths.includes(filePath)) return { filePath, reexports: [], circular: true };
// ... read and extract
return { filePath, reexports: exports, circular: false };
}Alternatively, document in the sample that |
||
| } | ||
| }, | ||
| }), | ||
| ], | ||
| output: s.object({ | ||
| chain: s.array(s.object({ file: s.path, symbols: s.array(s.string) })), | ||
| chainDepth: s.int, | ||
| circularDetected: s.boolean, | ||
| }), | ||
| maxTurns: 6, | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default tsReexportChainTracer; | ||
|
|
||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # 416 - Source Comment Density Reporter | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| // Agent role: measure comment density across TypeScript source files. | ||
| const sourceCommentDensityReporter = agent({ | ||
| model: "small", | ||
| instructions: p`Measure comment density for TypeScript source files. | ||
|
|
||
| Source files: ${p.glob("src/**/*.ts")} | ||
|
|
||
| For each file path, call measureCommentDensity. Then produce the declared output.`, | ||
| tools: [ | ||
| defineTool("measureCommentDensity", { | ||
| description: "Count comment lines vs code lines in a TypeScript file", | ||
| parameters: s.object({ filePath: s.path }), | ||
| async handler({ filePath }) { | ||
| try { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const lines = content.split("\n"); | ||
| let commentLines = 0; | ||
| let inBlock = false; | ||
| for (const line of lines) { | ||
| const t = line.trim(); | ||
| if (inBlock) { commentLines++; if (t.includes("*/")) inBlock = false; } | ||
| else if (t.startsWith("/*") || t.startsWith("*")) { commentLines++; if (!t.includes("*/")) inBlock = true; } | ||
| else if (t.startsWith("//")) commentLines++; | ||
| } | ||
|
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] False-positive comment detection: 💡 Suggested fixRestrict the continuation-line check to lines that start with optional whitespace followed by // Replace:
else if (t.startsWith("/*") || t.startsWith("*")) { ... }
// With:
else if (t.startsWith("/*") || (inBlock && t.startsWith("*"))) { ... }
// or simply:
else if (t.startsWith("/*")) { commentLines++; if (!t.includes("*/")) inBlock = true; }Counting |
||
| const codeLines = lines.length - commentLines; | ||
| const density = lines.length > 0 ? commentLines / lines.length : 0; | ||
| return { commentLines, codeLines, density }; | ||
| } catch { | ||
| return { commentLines: 0, codeLines: 0, density: 0 }; | ||
| } | ||
| }, | ||
| }), | ||
| ], | ||
| output: s.object({ | ||
| files: s.record(s.object({ | ||
| commentLines: s.int, | ||
| codeLines: s.int, | ||
| density: s.number, | ||
| })), | ||
| overall: s.object({ | ||
| totalFiles: s.int, | ||
| averageDensity: s.number, | ||
| highDensityFiles: s.array(s.path), | ||
| }), | ||
| }), | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default sourceCommentDensityReporter; | ||
|
|
||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # 417 - Git Blame Line Age Analyzer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| // Agent role: analyze git blame line ages for TypeScript source files. | ||
| const gitBlameLineAgeAnalyzer = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze the age of lines in TypeScript source files using git blame. | ||
|
|
||
| TypeScript files (sample): ${p.bash("find src -name '*.ts' 2>/dev/null | head -10 || echo ''")} | ||
|
|
||
| For each file, call analyzeBlameAge to get line age statistics. Then produce the declared output.`, | ||
| tools: [ | ||
| defineTool("analyzeBlameAge", { | ||
| description: "Run git blame on a file and compute line age statistics", | ||
| parameters: s.object({ filePath: s.path }), | ||
| handler({ filePath }: { filePath: string }) { | ||
| const { execSync } = require("node:child_process"); | ||
| try { | ||
| const out = execSync(`git blame --line-porcelain "${filePath}" 2>/dev/null`, { encoding: "utf-8" }); | ||
| const now = Date.now() / 1000; | ||
|
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] Shell injection risk: 💡 Fix: use execFileSync with an args arrayimport { execFileSync } from "node:child_process";
const out = execFileSync("git", ["blame", "--line-porcelain", filePath], { encoding: "utf-8" });Also move the |
||
| const timestamps: number[] = []; | ||
| for (const line of out.split("\n")) { | ||
| if (line.startsWith("author-time ")) timestamps.push(parseInt(line.slice(12), 10)); | ||
| } | ||
| if (timestamps.length === 0) return { filePath, avgAgeDays: 0, staleLines: 0, recentLines: 0, totalLines: 0 }; | ||
| const dayMs = 86400; | ||
| let stale = 0, recent = 0; | ||
| for (const ts of timestamps) { | ||
| const days = (now - ts) / dayMs; | ||
| if (days < 30) recent++; | ||
| else if (days > 365) stale++; | ||
| } | ||
| const avgAgeDays = Math.round(timestamps.reduce((a, t) => a + (now - t) / dayMs, 0) / timestamps.length); | ||
| return { filePath, avgAgeDays, staleLines: stale, recentLines: recent, totalLines: timestamps.length }; | ||
| } catch { | ||
| return { filePath, avgAgeDays: 0, staleLines: 0, recentLines: 0, totalLines: 0 }; | ||
| } | ||
| }, | ||
| }), | ||
| ], | ||
| output: s.object({ | ||
| files: s.record(s.object({ | ||
| avgAgeDays: s.int, | ||
| staleLines: s.int, | ||
| recentLines: s.int, | ||
| totalLines: s.int, | ||
| })), | ||
| oldestFile: s.optional(s.string), | ||
| newestFile: s.optional(s.string), | ||
| }), | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default gitBlameLineAgeAnalyzer; | ||
|
|
||
| ``` | ||
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] Vulnerability heuristic is inverted: the condition checks
major < 2for packages likerequestsandurllib3, butrequestshas had CVEs onmajor == 2(e.g. 2.18 → CVE-2018-18074).major < 2would wrongly markrequests==1.xas high-risk and passrequests==2.xas low. A version-range check (e.g.< 2.20) would be more accurate.💡 Example improvement
This is a sample, so a code comment acknowledging the simplification would also suffice.