-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-08-08 #370
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,59 @@ | ||
| # 371 - TypeScript Dead Export Finder | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, repair, steering } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const findUnusedExports = defineTool("findUnusedExports", { | ||
| description: "Given a list of TS file paths and their exported symbols, return which exports are not imported in any other file", | ||
| parameters: s.object({ | ||
| files: s.array(s.string("file path")), | ||
| }), | ||
| async handler({ files }) { | ||
| const exportedSymbols: Record<string, string[]> = {}; | ||
| const allContent: string[] = []; | ||
| for (const f of files) { | ||
| try { | ||
| const src = await readFile(f, "utf8"); | ||
| allContent.push(src); | ||
| const matches = [...src.matchAll(/^export\s+(?:const|function|class|type|interface|enum)\s+(\w+)/gm)]; | ||
| exportedSymbols[f] = matches.map((m) => m[1]); | ||
| } catch { | ||
| exportedSymbols[f] = []; | ||
| } | ||
| } | ||
| const combinedContent = allContent.join("\n"); | ||
| const unusedExports: Record<string, string[]> = {}; | ||
| for (const [file, symbols] of Object.entries(exportedSymbols)) { | ||
| const unused = symbols.filter((sym) => { | ||
| const importPattern = new RegExp(`import[^;]+\\b${sym}\\b`); | ||
| return !importPattern.test(combinedContent); | ||
| }); | ||
| if (unused.length > 0) unusedExports[file] = unused; | ||
| } | ||
| return JSON.stringify(unusedExports); | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: find TypeScript exported symbols that are not imported anywhere else in the project. | ||
| const deadExportFinder = agent({ | ||
| model: "small", | ||
| instructions: p`You are analyzing a TypeScript project for dead exports. | ||
|
|
||
| Files in the project: | ||
| ${p.glob("**/*.ts")} | ||
|
|
||
| Use the findUnusedExports tool with all discovered .ts file paths (excluding node_modules) to identify exported symbols that are never imported. | ||
|
|
||
| Return the results in the declared output schema.`, | ||
| output: s.object({ | ||
| unusedExports: s.record(s.array(s.string)), | ||
| totalUnused: s.int, | ||
| hasDeadCode: s.boolean, | ||
| }), | ||
| tools: [findUnusedExports], | ||
| addons: [steering(), repair()], | ||
| }); | ||
|
|
||
| export default deadExportFinder; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # 372 - Package Scripts Documenter | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, repair } from "rig"; | ||
|
|
||
| const inferScriptPurpose = defineTool("inferScriptPurpose", { | ||
| description: "Classify a npm script command into a category", | ||
| parameters: s.object({ | ||
| name: s.string("script name"), | ||
| command: s.string("script command"), | ||
| }), | ||
| handler({ name, command }) { | ||
| const cmd = command.toLowerCase(); | ||
| const nm = name.toLowerCase(); | ||
| if (/\bbuild\b|\btsc\b|\brollup\b|\bvite build\b/.test(cmd) || nm.includes("build")) return "build" as const; | ||
| if (/\btest\b|\bvitest\b|\bjest\b|\bmocha\b/.test(cmd) || nm.includes("test")) return "test" as const; | ||
| if (/\blint\b|\beslint\b|\bprettier\b/.test(cmd) || nm.includes("lint") || nm.includes("format")) return "lint" as const; | ||
| if (/\brelease\b|\bpublish\b|\bchangeset\b/.test(cmd) || nm.includes("release")) return "release" as const; | ||
| if (/\bdev\b|\bwatch\b|\bstart\b/.test(cmd) || nm.includes("dev") || nm.includes("start")) return "dev" as const; | ||
| return "other" as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: document all npm scripts from package.json into a SCRIPTS.md file. | ||
| const scriptsDocumenter = agent({ | ||
| model: "small", | ||
| instructions: p`Read the project package.json: | ||
| ${p.read("package.json")} | ||
|
|
||
| Use inferScriptPurpose to classify each script. Then write a SCRIPTS.md file documenting each script with its purpose, category, and command using ${p.write("SCRIPTS.md", "# Scripts\n\n<!-- generated by scripts-documenter -->\n")}. | ||
|
|
||
| Return the output schema with scripts metadata and the outputFile path.`, | ||
| output: s.object({ | ||
| scripts: s.record(s.object({ | ||
| purpose: s.enum("build", "test", "lint", "release", "dev", "other"), | ||
| category: s.string, | ||
| command: s.string, | ||
| })), | ||
| documentedCount: s.int, | ||
| outputFile: s.path, | ||
| }), | ||
| tools: [inferScriptPurpose], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default scriptsDocumenter; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # 373 - Git Diff Stats Summarizer | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, repair } from "rig"; | ||
|
|
||
| const classifyDiffEntry = defineTool("classifyDiffEntry", { | ||
| description: "Classify a diff file entry as added, modified, deleted, or renamed based on its path and stats", | ||
| parameters: s.object({ | ||
| path: s.string("file path from diff"), | ||
| additions: s.int, | ||
| deletions: s.int, | ||
| }), | ||
| handler({ path, additions, deletions }) { | ||
| if (path.includes(" => ")) return "renamed" as const; | ||
| if (additions > 0 && deletions === 0) return "added" as const; | ||
| if (deletions > 0 && additions === 0) return "deleted" as const; | ||
| return "modified" as const; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: summarize git diff stats between HEAD~1 and HEAD. | ||
| const diffStatsSummarizer = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze the git diff stats for the most recent commit: | ||
| ${p.bash("git diff --numstat HEAD~1 HEAD 2>/dev/null || git diff --numstat HEAD 2>/dev/null || echo 'no diff available'")} | ||
|
|
||
| For each file entry, use classifyDiffEntry to determine its classification. Return the full structured summary.`, | ||
| output: s.object({ | ||
| files: s.array(s.object({ | ||
| path: s.string, | ||
| additions: s.int, | ||
| deletions: s.int, | ||
| classification: s.enum("added", "modified", "deleted", "renamed"), | ||
| })), | ||
| totalAdditions: s.int, | ||
| totalDeletions: s.int, | ||
| mostChangedFile: s.optional(s.string), | ||
| }), | ||
| tools: [classifyDiffEntry], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default diffStatsSummarizer; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # 374 - Dotenv Template Generator | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const extractEnvReferences = defineTool("extractEnvReferences", { | ||
| description: "Extract all process.env.VARNAME references from a TypeScript file", | ||
| parameters: s.object({ filePath: s.string("path to TypeScript file") }), | ||
| async handler({ filePath }) { | ||
| try { | ||
| const src = await readFile(filePath, "utf8"); | ||
| const matches = [...src.matchAll(/process\.env\.([A-Z_][A-Z0-9_]*)/g)]; | ||
| return [...new Set(matches.map((m) => m[1]))].join(","); | ||
| } catch { | ||
| return ""; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: generate a .env.template file from process.env references found in source files. | ||
| const dotenvTemplateGenerator = agent({ | ||
| model: "small", | ||
| instructions: p`Generate a .env.template for this project. | ||
|
|
||
| Existing .env (if any): | ||
| ${p.readOptional(".env", "# no .env found")} | ||
|
|
||
| TypeScript source files: | ||
| ${p.glob("src/**/*.ts")} | ||
|
|
||
| For each TypeScript file path listed above, call extractEnvReferences to find process.env.VAR_NAME references. | ||
| Collect all unique variable names, compare with those already in .env, identify undocumented ones. | ||
| Then write a .env.template file using ${p.write(".env.template", "# Environment variables\n")}. | ||
|
|
||
| Return the output schema with templatePath, envKeys, undocumentedKeys, and templateGenerated.`, | ||
| output: s.object({ | ||
| templatePath: s.path, | ||
| envKeys: s.array(s.string), | ||
| undocumentedKeys: s.array(s.string), | ||
| templateGenerated: s.boolean, | ||
| }), | ||
| tools: [extractEnvReferences], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default dotenvTemplateGenerator; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # 375 - TypeScript Class Hierarchy Extractor | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, steering } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const extractClassInfo = defineTool("extractClassInfo", { | ||
| description: "Extract class declarations with extends/implements from a TypeScript file", | ||
| parameters: s.object({ filePath: s.string("path to TypeScript file") }), | ||
| async handler({ filePath }) { | ||
| try { | ||
| const src = await readFile(filePath, "utf8"); | ||
| const classPattern = /(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s]+))?/g; | ||
| const results: Array<{ name: string; parent: string | null; interfaces: string[]; isAbstract: boolean }> = []; | ||
| for (const m of src.matchAll(classPattern)) { | ||
| results.push({ | ||
| name: m[1], | ||
| parent: m[2] ?? null, | ||
| interfaces: m[3] ? m[3].split(",").map((s: string) => s.trim()).filter(Boolean) : [], | ||
| isAbstract: src.slice(Math.max(0, m.index! - 10), m.index!).includes("abstract"), | ||
| }); | ||
|
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 💡 Suggested fixUse the match itself: results.push({
name: m[1],
parent: m[2] ?? null,
interfaces: m[3] ? m[3].split(",").map((s: string) => s.trim()).filter(Boolean) : [],
isAbstract: m[0].startsWith("abstract"),
});This is unambiguous and does not require the unsafe index arithmetic. |
||
| } | ||
| return JSON.stringify(results); | ||
| } catch { | ||
| return "[]"; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: build a class hierarchy map from all TypeScript files in the project. | ||
| const classHierarchyExtractor = agent({ | ||
| model: "small", | ||
| maxTurns: 6, | ||
| instructions: p`Extract the class hierarchy from this TypeScript project. | ||
|
|
||
| TypeScript files found: | ||
| ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | head -50")} | ||
|
|
||
| For each file, use extractClassInfo to find class declarations. Collect all class info, build a hierarchy by computing inheritance depth (root classes = depth 0, subclasses = parent depth + 1). | ||
|
|
||
| Return the complete class hierarchy in the output schema.`, | ||
| output: s.object({ | ||
| classes: s.record(s.object({ | ||
| parent: s.optional(s.string), | ||
| interfaces: s.array(s.string), | ||
| isAbstract: s.boolean, | ||
| depth: s.int, | ||
| })), | ||
| maxDepth: s.int, | ||
| rootClasses: s.array(s.string), | ||
| }), | ||
| tools: [extractClassInfo], | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default classHierarchyExtractor; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # 376 - Git Bisect Helper | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, steering } from "rig"; | ||
|
|
||
| const selectMidpoint = defineTool("selectMidpoint", { | ||
| description: "Select the midpoint commit from a range of commits for binary search", | ||
| parameters: s.object({ | ||
| commits: s.array(s.string("commit hash oneline")), | ||
| }), | ||
| handler({ commits }) { | ||
| if (commits.length === 0) return JSON.stringify({ hash: null, index: -1 }); | ||
| const mid = Math.floor(commits.length / 2); | ||
| const line = commits[mid]; | ||
| const hash = line.split(" ")[0]; | ||
| return JSON.stringify({ hash, index: mid, total: commits.length }); | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: perform a simulated git bisect to identify a suspect bad commit. | ||
| const gitBisectHelper = agent({ | ||
| model: "small", | ||
| maxTurns: 8, | ||
| instructions: p`Perform a git bisect analysis on the recent commit history. | ||
|
|
||
| Recent commits: | ||
| ${p.bash("git log --oneline -20 2>/dev/null || echo 'no git history available'")} | ||
|
|
||
| Use selectMidpoint to perform binary search steps over the commit list. Simulate a bisect by selecting midpoints to narrow down the suspect commit range. After enough steps (3-4), pick the most suspect commit based on the bisect pattern. | ||
|
|
||
| Return the results in the output schema.`, | ||
| output: s.object({ | ||
| suspectCommit: s.optional(s.string), | ||
| stepsRun: s.int, | ||
| commitRange: s.object({ | ||
| start: s.string, | ||
| end: s.string, | ||
| }), | ||
| confidence: s.enum("high", "medium", "low"), | ||
| }), | ||
| tools: [selectMidpoint], | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default gitBisectHelper; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # 377 - YAML Key Presence Validator | ||
|
|
||
| ```rig | ||
| import { agent, defineTool, p, s, repair } from "rig"; | ||
|
|
||
| const checkYamlKey = defineTool("checkYamlKey", { | ||
| description: "Check whether a dot-notation key path exists in YAML content", | ||
| parameters: s.object({ | ||
| content: s.string("YAML file content"), | ||
| keyPath: s.string("dot-notation key path like 'server.port'"), | ||
| }), | ||
| handler({ content, keyPath }) { | ||
| const parts = keyPath.split("."); | ||
| let present = false; | ||
| // Check if all parts of the key path appear in order as indented keys | ||
| let remaining = content; | ||
| for (const part of parts) { | ||
|
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 key-path traversal is structurally unsound: it searches for each dot-notation segment as a regex anywhere in the remaining content, not within the correct indentation scope. For example, 💡 Suggested approachEither use a YAML parser (e.g., // Heuristic: searches for each key segment in remaining content.
// Does not track indentation scope — may produce false positives
// for identical key names in sibling sections.If the sample is meant to demonstrate a real-world pattern, prefer a proper YAML parse. |
||
| const pattern = new RegExp(`(?:^|\\n)\\s*${part}\\s*:`, "m"); | ||
| if (pattern.test(remaining)) { | ||
| const idx = remaining.search(pattern); | ||
| remaining = remaining.slice(idx); | ||
| present = true; | ||
| } else { | ||
| present = false; | ||
| break; | ||
| } | ||
| } | ||
| return present ? "present" : "missing"; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: validate that required keys are present in a YAML configuration file. | ||
| const yamlKeyValidator = agent({ | ||
| model: "small", | ||
| input: s.object({ | ||
| yamlFile: s.path, | ||
| requiredKeys: s.array(s.string), | ||
| }), | ||
| instructions: p`Validate a YAML file for required key presence. | ||
|
|
||
| YAML file content: | ||
| ${p.readInput("yamlFile")} | ||
|
|
||
| Required keys to check: use the input.requiredKeys array. | ||
|
|
||
| For each required key, call checkYamlKey with the full file content and the key path. Collect which keys are present and which are missing. | ||
|
|
||
| Return the output schema with missingKeys, presentKeys, allPresent, and checkedKeys.`, | ||
| output: s.object({ | ||
| missingKeys: s.array(s.string), | ||
| presentKeys: s.array(s.string), | ||
| allPresent: s.boolean, | ||
| checkedKeys: s.int, | ||
| }), | ||
| tools: [checkYamlKey], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default yamlKeyValidator; | ||
| ``` | ||
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] Self-reference bug: all file contents are joined into one
combinedContentstring, so if a symbol appears in an import statement within the same file, it passes the check and is never flagged as unused.💡 Suggested fix
When testing whether a symbol is imported, scan only the other files' content, not the exporting file itself:
A symbol that is only referenced in its own file is still dead from the outside.