-
Notifications
You must be signed in to change notification settings - Fork 0
[rig-tasks] Add 10 rig samples — 2026-08-02 #336
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,52 @@ | ||
| # 351 - TS Import Depth Analyzer V2 | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const analyzeImportDepth = defineTool("analyzeImportDepth", { | ||
| description: "Analyze relative import depth in a TypeScript file by counting '../' occurrences.", | ||
| parameters: { filePath: s.path }, | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| const content = await readFile(filePath, "utf8"); | ||
| const importRegex = /from\s+['"]([^'"]+)['"]/g; | ||
| const imports: string[] = []; | ||
| let match: RegExpExecArray | null; | ||
| while ((match = importRegex.exec(content)) !== null) { | ||
| imports.push(match[1]); | ||
| } | ||
| const relativeImports = imports.filter((i: string) => i.startsWith(".")); | ||
| const depths = relativeImports.map((i: string) => (i.match(/\.\.\//g) ?? []).length); | ||
| const maxDepth = depths.length > 0 ? Math.max(...depths) : 0; | ||
| const deepImports = relativeImports.filter((i: string) => (i.match(/\.\.\//g) ?? []).length >= 2); | ||
| return { maxDepth, deepImports, importCount: imports.length }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: analyze relative import depth across TypeScript files to find overly deep imports. | ||
| const tsImportDepthAnalyzer = agent({ | ||
| model: "small", | ||
| instructions: p`Analyze TypeScript import depth across all source files. | ||
|
|
||
| Source files to analyze: ${p.glob("**/*.ts")} | ||
|
|
||
| For each .ts file (excluding node_modules), call analyzeImportDepth to get maxDepth, deepImports, and importCount. | ||
| Calculate the average depth across all files (sum of maxDepths / file count). | ||
| Identify the file with the highest maxDepth as deepestFile (omit if no files). | ||
| Return the full per-file record plus summary stats.`, | ||
| output: s.object({ | ||
| files: s.record(s.object({ | ||
| maxDepth: s.int, | ||
| deepImports: s.array(s.string), | ||
| importCount: s.int, | ||
| })), | ||
| deepestFile: s.optional(s.path), | ||
| averageDepth: s.number, | ||
| }), | ||
| tools: [analyzeImportDepth], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default tsImportDepthAnalyzer; | ||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # 352 - Env File Completeness Checker V2 | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const parseEnvKeys = defineTool("parseEnvKeys", { | ||
| description: "Parse an env file string and return the list of key names.", | ||
| parameters: { content: s.string }, | ||
| handler: ({ content }: { content: string }) => { | ||
| return content | ||
| .split("\n") | ||
| .map((line: string) => line.trim()) | ||
| .filter((line: string) => line.length > 0 && !line.startsWith("#")) | ||
| .map((line: string) => line.split("=")[0].trim()) | ||
| .filter(Boolean); | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: compare .env.example with .env to check completeness of environment configuration. | ||
| const envFileCompletenessChecker = agent({ | ||
| model: "small", | ||
| instructions: p`Compare .env.example with .env to check that all required keys are present. | ||
|
|
||
| .env.example content: ${p.readOptional(".env.example")} | ||
|
|
||
| .env content: ${p.readOptional(".env")} | ||
|
|
||
| Steps: | ||
| 1. Call parseEnvKeys with the .env.example content to get exampleKeys. | ||
| 2. Call parseEnvKeys with the .env content to get presentKeys. | ||
| 3. Compute missingKeys (keys in exampleKeys but not presentKeys) and extraKeys (keys in presentKeys but not exampleKeys). | ||
| 4. Compute completeness as (exampleKeys.length - missingKeys.length) / exampleKeys.length, or 1.0 if exampleKeys is empty. | ||
| 5. Set status: "missing-example" if .env.example is absent, "missing-env" if .env is absent, "complete" if missingKeys is empty, "partial" otherwise.`, | ||
| output: s.object({ | ||
| exampleKeys: s.array(s.string), | ||
| presentKeys: s.array(s.string), | ||
| missingKeys: s.array(s.string), | ||
| extraKeys: s.array(s.string), | ||
| completeness: s.number, | ||
| status: s.enum("complete", "partial", "missing-example", "missing-env"), | ||
| }), | ||
| tools: [parseEnvKeys], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default envFileCompletenessChecker; | ||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # 353 - Stale Branch Detector V2 | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const classifyBranchAge = defineTool("classifyBranchAge", { | ||
| description: "Classify a git 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" as const; | ||
| if (ageDays < 90) return "stale" as const; | ||
| return "dead" as const; | ||
| }, | ||
| }); | ||
|
|
||
| // 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 classification. | ||
| 3. Build the branches array with name, lastCommit, and classification fields. | ||
| 4. Count staleCount (classification = "stale") and deadCount (classification = "dead"). | ||
| 5. Set recommendedForDeletion to the names of branches where classification is "dead".`, | ||
| output: s.object({ | ||
| branches: s.array(s.object({ | ||
| name: s.string, | ||
| lastCommit: s.string, | ||
| classification: s.enum("fresh", "stale", "dead"), | ||
| })), | ||
| staleCount: s.int, | ||
| deadCount: s.int, | ||
| recommendedForDeletion: s.array(s.string), | ||
| }), | ||
| tools: [classifyBranchAge], | ||
| maxTurns: 6, | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default staleBranchDetector; | ||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # 354 - License Header Checker V2 | ||
|
|
||
| ```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 headerLines = expectedHeader.split("\n"); | ||
| const fileStart = content.split("\n").slice(0, headerLines.length).join("\n"); | ||
| const hasHeader = fileStart === expectedHeader; | ||
| const status = hasHeader | ||
| ? ("ok" as const) | ||
| : content.includes(headerLines[0]) | ||
| ? ("wrong" as const) | ||
| : ("missing" as const); | ||
| return { hasHeader, status }; | ||
| } catch { | ||
| return { hasHeader: false, status: "missing" as const }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // 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 provided in the input. | ||
|
|
||
| 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 agent input. | ||
| 2. For each file path, call checkLicenseHeader with filePath and expectedHeader. | ||
| 3. Build a files record keyed by file path containing hasHeader and status. | ||
| 4. Count missingCount as files where status is "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.int, | ||
| allCompliant: s.boolean, | ||
| }), | ||
| tools: [checkLicenseHeader], | ||
| maxTurns: 8, | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default licenseHeaderChecker; | ||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # 355 - YAML Config Diff V2 | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
|
|
||
| const extractYamlKeys = defineTool("extractYamlKeys", { | ||
| description: "Extract top-level keys from a YAML string.", | ||
| parameters: { content: s.string }, | ||
| handler: ({ content }: { content: string }) => { | ||
| const keys: string[] = []; | ||
| for (const line of content.split("\n")) { | ||
| const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):/); | ||
| if (m) keys.push(m[1]); | ||
| } | ||
| return keys; | ||
| }, | ||
| }); | ||
|
|
||
| const diffKeys = defineTool("diffKeys", { | ||
| description: "Compute added and removed keys between two key arrays.", | ||
| parameters: { baseKeys: s.array(s.string), targetKeys: s.array(s.string) }, | ||
| handler: ({ baseKeys, targetKeys }: { baseKeys: string[]; targetKeys: string[] }) => { | ||
| const addedKeys = targetKeys.filter((k: string) => !baseKeys.includes(k)); | ||
| const removedKeys = baseKeys.filter((k: string) => !targetKeys.includes(k)); | ||
| return { addedKeys, removedKeys }; | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: diff top-level YAML keys between two config files and detect breaking changes. | ||
| const yamlConfigDiff = agent({ | ||
| model: "small", | ||
| input: s.object({ baseFile: s.path, targetFile: s.path }), | ||
| instructions: p`Diff top-level YAML keys between two config files. | ||
|
|
||
| Base file content: ${p.readInput("baseFile")} | ||
| Target file content: ${p.readInput("targetFile")} | ||
|
|
||
| Steps: | ||
| 1. Call extractYamlKeys on the base file content to get baseKeys. | ||
| 2. Call extractYamlKeys on the target file content to get targetKeys. | ||
| 3. Call diffKeys with baseKeys and targetKeys to get addedKeys and removedKeys. | ||
| 4. changedKeys: keys present in both but with differing values — estimate from content or leave empty. | ||
| 5. totalChanges = addedKeys.length + removedKeys.length + changedKeys.length. | ||
| 6. hasBreakingChanges = removedKeys.length > 0.`, | ||
| output: s.object({ | ||
| addedKeys: s.array(s.string), | ||
| removedKeys: s.array(s.string), | ||
| changedKeys: s.array(s.string), | ||
| totalChanges: s.int, | ||
| hasBreakingChanges: s.boolean, | ||
| }), | ||
| tools: [extractYamlKeys, diffKeys], | ||
| maxTurns: 6, | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default yamlConfigDiff; | ||
|
|
||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # 356 - Monorepo Workspace Lister V2 | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, steering } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const extractPackageInfo = defineTool("extractPackageInfo", { | ||
| description: "Read a nested package.json and extract name, version, dependency count, and private flag.", | ||
| parameters: { filePath: s.path }, | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| try { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const pkg = JSON.parse(content); | ||
| const dependencyCount = | ||
| Object.keys(pkg.dependencies ?? {}).length + | ||
| Object.keys(pkg.devDependencies ?? {}).length; | ||
| return { | ||
| name: (pkg.name ?? "(unnamed)") as string, | ||
| version: (pkg.version ?? undefined) as string | undefined, | ||
| dependencyCount, | ||
| hasPrivate: pkg.private === true, | ||
| }; | ||
| } catch { | ||
| return { name: "(error)", version: undefined, dependencyCount: 0, hasPrivate: false }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: discover all workspace packages in a monorepo and list their metadata. | ||
| const monorepoWorkspaceLister = agent({ | ||
| model: "small", | ||
| instructions: p`Discover all nested package.json files in this monorepo and extract package metadata. | ||
|
|
||
| Nested package.json paths (excluding node_modules): | ||
| ${p.bash("find . -name 'package.json' -not -path '*/node_modules/*' -mindepth 2 -maxdepth 4 2>/dev/null")} | ||
|
|
||
| Steps: | ||
| 1. For each file path in the list above, call extractPackageInfo to get its metadata. | ||
| 2. Assemble the packages array with name, version (optional), dependencyCount, hasPrivate, and path for each. | ||
| 3. Set totalPackages to the length of the packages array.`, | ||
| output: s.object({ | ||
| packages: s.array(s.object({ | ||
| name: s.string, | ||
| version: s.optional(s.string), | ||
| dependencyCount: s.int, | ||
| hasPrivate: s.boolean, | ||
| path: s.path, | ||
| })), | ||
| totalPackages: s.int, | ||
| }), | ||
| tools: [extractPackageInfo], | ||
| maxTurns: 8, | ||
| addons: [steering()], | ||
| }); | ||
|
|
||
| export default monorepoWorkspaceLister; | ||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # 357 - TS Decorator Usage Scanner | ||
|
|
||
| ```rig | ||
| import { agent, p, s, defineTool, repair } from "rig"; | ||
| import { readFile } from "node:fs/promises"; | ||
|
|
||
| const scanDecorators = defineTool("scanDecorators", { | ||
| description: "Scan a TypeScript file for decorator usage and return decorator names with their count.", | ||
| parameters: { filePath: s.path }, | ||
| handler: async ({ filePath }: { filePath: string }) => { | ||
| try { | ||
| const content = await readFile(filePath, "utf-8"); | ||
| const decoratorRegex = /@([A-Z][a-zA-Z0-9]*)/g; | ||
|
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 regex 💡 Suggested fixconst decoratorRegex = /@([a-zA-Z][a-zA-Z0-9]*)/g;Lowercase decorators (e.g. |
||
| const found: string[] = []; | ||
| let match: RegExpExecArray | null; | ||
| while ((match = decoratorRegex.exec(content)) !== null) { | ||
| found.push(match[1]); | ||
| } | ||
| return { decorators: found, hasDecorators: found.length > 0 }; | ||
| } catch { | ||
| return { decorators: [], hasDecorators: false }; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Agent role: scan TypeScript files for decorator usage and summarize which decorators are most common. | ||
| const tsDecoratorUsageScanner = agent({ | ||
| model: "small", | ||
| instructions: p`Scan TypeScript files for decorator usage (e.g. @Injectable, @Component). | ||
|
|
||
| TypeScript files: ${p.glob("src/**/*.ts")} | ||
|
|
||
| Steps: | ||
| 1. For each file path, call scanDecorators to get the list of decorator names used. | ||
| 2. Aggregate across all files: build a decorators record keyed by decorator name (without @), | ||
| with usageCount (total occurrences) and files (list of file paths where it appears). | ||
| 3. totalDecorated = number of files that had at least one decorator. | ||
| 4. mostUsedDecorator = decorator name with highest usageCount (omit if none found).`, | ||
| output: s.object({ | ||
| decorators: s.record(s.object({ | ||
| usageCount: s.int, | ||
| files: s.array(s.path), | ||
| })), | ||
| totalDecorated: s.int, | ||
| mostUsedDecorator: s.optional(s.string), | ||
| }), | ||
| tools: [scanDecorators], | ||
| addons: [repair()], | ||
| }); | ||
|
|
||
| export default tsDecoratorUsageScanner; | ||
|
|
||
| ``` | ||
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 instruction "estimate from content or leave empty" gives the LLM no reliable way to detect value changes — YAML parsing is needed to compare values, not text estimation. Consumers of this agent may assume
changedKeysis authoritative when it will frequently be empty or incorrect.💡 Suggested fix
Since
extractYamlKeysonly returns keys (not values), the agent cannot reliably detect changed values. Either:changedKeysfrom the output schema and note the limitation in the description, orextractYamlKeyValuestool that returns aRecord<string, string>and diffs values explicitly.Leaving the instruction as "estimate" teaches a bad pattern for agent samples.