-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-08-04 #350
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,57 @@ | ||
| # 361 - TS Interface Inheritance Graph Builder | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const extractInterfaceExtends = defineTool("extractInterfaceExtends", { | ||
| description: "Parse a TypeScript file and return all interface declarations with their extends clauses.", | ||
| parameters: { filePath: s.path }, | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| try { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const results: Array<{ name: string; parents: string[] }> = []; | ||
| const re = /interface\s+(\w+)(?:\s+extends\s+([\w,\s]+))?/g; | ||
| let m: RegExpExecArray | null; | ||
| while ((m = re.exec(content)) !== null) { | ||
| const parents = m[2] | ||
| ? m[2].split(",").map((p) => p.trim()).filter(Boolean) | ||
| : []; | ||
| results.push({ name: m[1], parents }); | ||
| } | ||
| return results; | ||
| } catch { | ||
| return []; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| const tsInterfaceInheritanceGraphBuilder = agent({ | ||
| model: "small", | ||
| instructions: p`Build a TypeScript interface inheritance graph. | ||
|
|
||
| TypeScript files: | ||
| ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | head -20")} | ||
|
|
||
| Steps: | ||
| 1. For each file, call extractInterfaceExtends to get interface declarations and their parents. | ||
| 2. Aggregate all interfaces into a record keyed by interface name. Each entry: parents (array of parent interface names), depth classification. | ||
| 3. Classify depth: "root" = no parents, "derived" = has parents and at least one child, "leaf" = has parents but no children. | ||
| 4. totalInterfaces = total count across all files. | ||
| 5. maxDepth = maximum inheritance chain depth (BFS from each root).`, | ||
| output: s.object({ | ||
| interfaces: s.record( | ||
| s.object({ | ||
| parents: s.array(s.string), | ||
| depth: s.enum("root", "derived", "leaf"), | ||
| }) | ||
| ), | ||
| totalInterfaces: s.number, | ||
| maxDepth: s.number, | ||
| }), | ||
| tools: [extractInterfaceExtends], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default tsInterfaceInheritanceGraphBuilder; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # 362 - Dep License Compatibility Matrix | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const extractLicense = defineTool("extractLicense", { | ||
| description: "Read the LICENSE file for an npm package and classify its SPDX license identifier.", | ||
| parameters: { packageName: s.string }, | ||
| handler: async ({ packageName }: { packageName: string }) => { | ||
| const { execSync } = await import("node:child_process"); | ||
| try { | ||
| const license = execSync( | ||
| `node -e "const p=require('./node_modules/${packageName}/package.json');console.log(p.license||'')" 2>/dev/null`, | ||
| { encoding: "utf-8" } | ||
| ).trim(); | ||
| const permissive = ["MIT", "ISC", "BSD-2-Clause", "BSD-3-Clause", "Apache-2.0", "0BSD", "Unlicense"]; | ||
| const copyleft = ["GPL-2.0", "GPL-3.0", "LGPL-2.1", "LGPL-3.0", "AGPL-3.0", "MPL-2.0"]; | ||
| if (!license) return { spdxId: "unknown", compatibility: "unknown" as const }; | ||
| if (permissive.some((l) => license.includes(l))) return { spdxId: license, compatibility: "permissive" as const }; | ||
| if (copyleft.some((l) => license.includes(l))) return { spdxId: license, compatibility: "copyleft" as const }; | ||
| if (license.toLowerCase().includes("proprietary") || license.toLowerCase().includes("commercial")) | ||
| return { spdxId: license, compatibility: "proprietary" as const }; | ||
| return { spdxId: license, compatibility: "unknown" as const }; | ||
| } catch { | ||
| return { spdxId: "unknown", compatibility: "unknown" as const }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| const depLicenseCompatibilityMatrix = agent({ | ||
| model: "small", | ||
| instructions: p`Build a dependency license compatibility matrix. | ||
|
|
||
| package.json contents: | ||
| ${p.read("package.json")} | ||
|
|
||
| Steps: | ||
| 1. Parse the dependencies and devDependencies from package.json. | ||
| 2. For each package name, call extractLicense to get its spdxId and compatibility. | ||
| 3. Build a packages record keyed by package name with spdxId and compatibility. | ||
| 4. copyleftCount = number of packages classified as "copyleft". | ||
| 5. unknownCount = number classified as "unknown". | ||
| 6. hasConflicts = true if copyleftCount > 0.`, | ||
| output: s.object({ | ||
| packages: s.record( | ||
| s.object({ | ||
| spdxId: s.string, | ||
| compatibility: s.enum("permissive", "copyleft", "proprietary", "unknown"), | ||
| }) | ||
| ), | ||
| copyleftCount: s.number, | ||
| unknownCount: s.number, | ||
| hasConflicts: s.boolean, | ||
| }), | ||
| tools: [extractLicense], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default depLicenseCompatibilityMatrix; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # 363 - Tsconfig Options Analyzer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const parseTsConfigOption = defineTool("parseTsConfigOption", { | ||
| description: "Classify a TypeScript compiler option into a category.", | ||
| parameters: { key: s.string, value: s.unknown }, | ||
| handler: ({ key }: { key: string; value: unknown }) => { | ||
| const strict = ["strict", "strictNullChecks", "strictFunctionTypes", "strictBindCallApply", "strictPropertyInitialization", "noImplicitAny", "noImplicitThis", "alwaysStrict"]; | ||
| const perf = ["skipLibCheck", "skipDefaultLibCheck", "incremental", "tsBuildInfoFile"]; | ||
| const output = ["outDir", "outFile", "declaration", "declarationDir", "declarationMap", "sourceMap", "removeComments", "noEmit", "emitDeclarationOnly"]; | ||
| const paths = ["baseUrl", "paths", "rootDir", "rootDirs", "typeRoots", "types"]; | ||
| if (strict.includes(key)) return { category: "strict" as const, recommended: true }; | ||
| if (perf.includes(key)) return { category: "perf" as const, recommended: false }; | ||
| if (output.includes(key)) return { category: "output" as const, recommended: false }; | ||
| if (paths.includes(key)) return { category: "paths" as const, recommended: false }; | ||
| return { category: "misc" as const, recommended: false }; | ||
| }, | ||
| }); | ||
|
|
||
| const tsconfigOptionsAnalyzer = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze TypeScript compiler options across tsconfig files. | ||
|
|
||
| Main tsconfig.json: | ||
| ${p.read("tsconfig.json")} | ||
|
|
||
| Variant tsconfig files found: | ||
| ${p.bash("ls tsconfig.*.json 2>/dev/null || echo 'none'")} | ||
|
|
||
| Steps: | ||
| 1. Parse compilerOptions from the main tsconfig.json. | ||
| 2. For each compiler option key/value, call parseTsConfigOption to get category and recommended. | ||
| 3. Build an options record keyed by option name with value, category, recommended. | ||
| 4. strictCount = number of options with category "strict". | ||
| 5. hasIsolatedModules = true if "isolatedModules" option is present and set to true. | ||
| 6. configFilesFound = list of tsconfig files found (include tsconfig.json and any variants).`, | ||
| output: s.object({ | ||
| options: s.record( | ||
| s.object({ | ||
| value: s.unknown, | ||
| category: s.enum("strict", "perf", "output", "paths", "misc"), | ||
| recommended: s.boolean, | ||
| }) | ||
| ), | ||
| strictCount: s.number, | ||
| hasIsolatedModules: s.boolean, | ||
| configFilesFound: s.array(s.string), | ||
| }), | ||
| tools: [parseTsConfigOption], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default tsconfigOptionsAnalyzer; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # 364 - Git Patch Format Summarizer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool } from "rig"; | ||
| import { steering } from "rig"; | ||
|
|
||
| const extractPatchMetadata = defineTool("extractPatchMetadata", { | ||
| description: "Parse a git patch block and extract author, subject, files changed, insertions, and deletions.", | ||
| parameters: { patch: s.string }, | ||
| handler: ({ patch }: { patch: string }) => { | ||
| const author = (patch.match(/^From: (.+)$/m) ?? [])[1]?.trim() ?? "unknown"; | ||
| const subject = (patch.match(/^Subject: (.+)$/m) ?? [])[1]?.replace(/^\[PATCH[^\]]*\] /, "").trim() ?? "unknown"; | ||
| const filesChanged = parseInt((patch.match(/(\d+) file[s]? changed/) ?? [])[1] ?? "0", 10); | ||
| const insertions = parseInt((patch.match(/(\d+) insertion/) ?? [])[1] ?? "0", 10); | ||
| const deletions = parseInt((patch.match(/(\d+) deletion/) ?? [])[1] ?? "0", 10); | ||
| return { author, subject, filesChanged, insertions, deletions }; | ||
| }, | ||
| }); | ||
|
|
||
| const gitPatchFormatSummarizer = agent({ | ||
| model: "small", | ||
| instructions: p`Summarize the last 5 git commits as patch metadata. | ||
|
|
||
| Patch output: | ||
| ${p.bash("git format-patch -5 --stdout 2>/dev/null | head -500 || echo 'no patches'")} | ||
|
|
||
| Steps: | ||
| 1. Split the patch output into individual patch blocks (each starts with "From <hash>"). | ||
| 2. For each patch block, call extractPatchMetadata to get author, subject, filesChanged, insertions, deletions. | ||
| 3. Build patches array from results. | ||
| 4. totalPatches = patches.length. | ||
| 5. topContributor = author name that appears most frequently (omit if no patches).`, | ||
| output: s.object({ | ||
| patches: s.array( | ||
| s.object({ | ||
| author: s.string, | ||
| subject: s.string, | ||
| filesChanged: s.number, | ||
| insertions: s.number, | ||
| deletions: s.number, | ||
| }) | ||
| ), | ||
| totalPatches: s.number, | ||
| topContributor: s.optional(s.string), | ||
| }), | ||
| tools: [extractPatchMetadata], | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default gitPatchFormatSummarizer; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # 365 - NPM Lifecycle Script Analyzer | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const classifyScript = defineTool("classifyScript", { | ||
| description: "Classify an npm script by its lifecycle category.", | ||
| parameters: { name: s.string, command: s.string }, | ||
| handler: ({ name, command }: { name: string; command: string }) => { | ||
| const hookPrefixes = ["pre", "post"]; | ||
| const isHook = hookPrefixes.some((prefix) => | ||
| ["build", "test", "install", "pack", "publish", "start", "stop", "restart"].some( | ||
| (base) => name === `${prefix}${base}` | ||
| ) | ||
| ); | ||
| let category: "build" | "test" | "lint" | "release" | "hook" | "other" = "other"; | ||
| if (isHook) category = "hook"; | ||
| else if (/\b(build|compile|tsc|webpack|rollup|vite|esbuild)\b/.test(command)) category = "build"; | ||
| else if (/\b(test|jest|vitest|mocha|jasmine|tap)\b/.test(command)) category = "test"; | ||
| else if (/\b(lint|eslint|tslint|prettier|stylelint|biome)\b/.test(command)) category = "lint"; | ||
| else if (/\b(release|publish|deploy|version|changelog)\b/.test(command)) category = "release"; | ||
| return { category, isHook }; | ||
| }, | ||
| }); | ||
|
|
||
| const npmLifecycleScriptAnalyzer = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze npm lifecycle scripts from package.json. | ||
|
|
||
| package.json: | ||
| ${p.read("package.json")} | ||
|
|
||
| Steps: | ||
| 1. Parse the scripts field from package.json. | ||
| 2. For each script entry (name, command), call classifyScript to get category and isHook. | ||
| 3. Build scripts record keyed by script name with command, category, isHook. | ||
| 4. hookCount = number of scripts with isHook true. | ||
| 5. missingRecommended = array of recommended script names not present: ["test", "build", "lint"]. | ||
| 6. hasTestScript = "test" key exists in scripts. | ||
| 7. hasBuildScript = "build" key exists in scripts.`, | ||
| output: s.object({ | ||
| scripts: s.record( | ||
| s.object({ | ||
| command: s.string, | ||
| category: s.enum("build", "test", "lint", "release", "hook", "other"), | ||
| isHook: s.boolean, | ||
| }) | ||
| ), | ||
| hookCount: s.number, | ||
| missingRecommended: s.array(s.string), | ||
| hasTestScript: s.boolean, | ||
| hasBuildScript: s.boolean, | ||
| }), | ||
| tools: [classifyScript], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default npmLifecycleScriptAnalyzer; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # 366 - TS JSDoc Coverage Checker | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const analyzeFunctionComments = defineTool("analyzeFunctionComments", { | ||
| description: "Scan a TypeScript file for exported functions and count those with and without JSDoc comments.", | ||
| parameters: { filePath: s.path }, | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| try { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const lines = content.split("\n"); | ||
| let documentedCount = 0; | ||
| let undocumentedCount = 0; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i]; | ||
| if (/^export\s+(async\s+)?function\s+\w+/.test(line) || /^export\s+const\s+\w+\s*=\s*(async\s*)?\(/.test(line)) { | ||
| const prevLine = lines[i - 1]?.trim() ?? ""; | ||
| const prevPrevLine = lines[i - 2]?.trim() ?? ""; | ||
| if (prevLine.endsWith("*/") || prevPrevLine.endsWith("*/") || prevLine.startsWith("/**") || prevLine.startsWith("*")) { | ||
|
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] JSDoc detection heuristic is fragile: the tool checks the immediately previous 1–2 lines for 💡 Suggested fixScan backwards from let hasJsDoc = false;
for (let j = i - 1; j >= 0; j--) {
const t = lines[j].trim();
if (t.endsWith("*/") || t.startsWith("/**")) { hasJsDoc = true; break; }
if (t === "" || (!t.startsWith("*") && !t.startsWith("/"))) break;
}
if (hasJsDoc) documentedCount++; else undocumentedCount++; |
||
| documentedCount++; | ||
| } else { | ||
| undocumentedCount++; | ||
| } | ||
| } | ||
| } | ||
| const total = documentedCount + undocumentedCount; | ||
| const coverage = total > 0 ? documentedCount / total : 1; | ||
| return { documentedCount, undocumentedCount, coverage }; | ||
| } catch { | ||
| return { documentedCount: 0, undocumentedCount: 0, coverage: 0 }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| const tsJsDocCoverageChecker = agent({ | ||
| model: "small", | ||
| instructions: p`Check JSDoc coverage for exported TypeScript functions. | ||
|
|
||
| TypeScript files: | ||
| ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | head -20")} | ||
|
|
||
| Steps: | ||
| 1. For each file path, call analyzeFunctionComments to get documentedCount, undocumentedCount, and coverage. | ||
| 2. Build files record keyed by file path with those three values. | ||
| 3. overall: totalFunctions = sum of all (documentedCount + undocumentedCount), documentedFunctions = sum of all documentedCount, coveragePercent = (documentedFunctions / totalFunctions) * 100 (or 100 if no functions). | ||
| 4. wellDocumentedFiles = file paths where coverage >= 0.8.`, | ||
| output: s.object({ | ||
| files: s.record( | ||
| s.object({ | ||
| documentedCount: s.number, | ||
| undocumentedCount: s.number, | ||
| coverage: s.number, | ||
| }) | ||
| ), | ||
| overall: s.object({ | ||
| totalFunctions: s.number, | ||
| documentedFunctions: s.number, | ||
| coveragePercent: s.number, | ||
| }), | ||
| wellDocumentedFiles: s.array(s.string), | ||
| }), | ||
| tools: [analyzeFunctionComments], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default tsJsDocCoverageChecker; | ||
| ``` | ||
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] Shell injection risk:
packageNameis interpolated directly into anexecSyncshell string — a crafted package name (e.g. containing;or$()) could execute arbitrary commands.💡 Suggested fix
Read the package.json directly instead of using
execSync:Or at minimum validate
packageNameagainst a safe identifier pattern before interpolating.