Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions skills/rig/samples/491-dotenv-process-drift-detector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 491 - Dotenv Process Drift Detector

```rig
import { agent, defineTool, p, s, repair } from "rig";

const classifyEnvKey = defineTool("classifyEnvKey", {
description: "Classify an env key based on whether it's in .env.example and used in code",
parameters: s.object({ key: s.string, inExample: s.boolean, usedInCode: s.boolean }),
handler({ inExample, usedInCode }): "declared_and_used" | "declared_not_used" | "used_not_declared" {
if (inExample && usedInCode) return "declared_and_used" as const;
if (inExample && !usedInCode) return "declared_not_used" as const;
return "used_not_declared" as const;
},
});

// Agent role: detect drift between .env.example declarations and actual process.env usages in source code.
const dotenvDriftDetector = agent({
model: "small",
instructions: p`Compare declared env keys in ${p.read(".env.example")} with actual usages from ${p.bash("grep -rn 'process\\.env\\.' src/ 2>/dev/null || true")}. Use classifyEnvKey for each key found. Return the full drift report.`,
output: s.object({
keys: s.record(s.object({ inExample: s.boolean, usedInCode: s.boolean, status: s.string })),
totalKeys: s.int,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The output schema has a status: s.string field but the classifyEnvKey tool returns a typed union ("declared_and_used" | "declared_not_used" | "used_not_declared"). Using s.string instead of s.enum([...]) loses the type constraint and misses a chance to demonstrate s.enum — a schema helper this sample is well-suited to showcase.

💡 Use s.enum to match the tool return type
keys: s.record(s.object({
  inExample: s.boolean,
  usedInCode: s.boolean,
  status: s.enum(["declared_and_used", "declared_not_used", "used_not_declared"]),
})),

missingFromExample: s.array(s.string),
unusedDeclarations: s.array(s.string),
}),
tools: [classifyEnvKey],
addons: [repair()],
});

export default dotenvDriftDetector;
```
43 changes: 43 additions & 0 deletions skills/rig/samples/492-vitest-snapshot-count-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 492 - Vitest Snapshot Count Workflow

```rig
import { agent, workflow, p, s } from "rig";

// Agent role: scan for Vitest snapshot files and count lines.
const snapshotScanner = agent({
model: "small",
instructions: p`Find snapshot files with ${p.bash("find . -path '*/__snapshots__/*.snap' -not -path '*/node_modules/*' 2>/dev/null | head -20 || true")} and count lines in each with ${p.bash("find . -path '*/__snapshots__/*.snap' -not -path '*/node_modules/*' 2>/dev/null | xargs wc -l 2>/dev/null || echo '0 total'")}. Return the file list and total count.`,
output: s.object({
files: s.array(s.object({ path: s.path, lineCount: s.int })),
totalFiles: s.int,
}),
});

// Agent role: count snapshot export entries across snapshot files.
const snapshotParser = agent({
model: "small",
instructions: p`Find snapshot files containing exports using ${p.bash("grep -rl 'exports\\[' . --include='*.snap' 2>/dev/null | head -20 || true")}. Count total export entries with ${p.bash("grep -r 'exports\\[' . --include='*.snap' 2>/dev/null | wc -l || echo 0")}. Return file list and count.`,
output: s.object({
snapshotFiles: s.array(s.path),
exportCount: s.int,
}),
});

// Workflow role: run snapshot scanner and parser in parallel and combine results.
const vitestSnapshotWorkflow = workflow({
meta: { name: "vitest-snapshot-count-workflow", description: "Count Vitest snapshots in parallel" },
body: async ({ call }) => {
const [scanResult, parseResult] = await Promise.all([
call(snapshotScanner, ""),
call(snapshotParser, ""),
]);
return {
totalSnapshotFiles: scanResult?.totalFiles ?? 0,
totalLines: (scanResult?.files ?? []).reduce((sum: number, f: { lineCount: number }) => sum + f.lineCount, 0),
snapshotFiles: parseResult?.snapshotFiles ?? [],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] exportCount from snapshotParser is silently dropped — the workflow never surfaces it in its return value, so the parser agent runs but its key metric goes unused. As a sample this is misleading: readers may conclude exportCount is an optional output rather than the parser's primary purpose.

💡 Include exportCount in the return value
return {
  totalSnapshotFiles: scanResult?.totalFiles ?? 0,
  totalLines: (scanResult?.files ?? []).reduce((sum: number, f: { lineCount: number }) => sum + f.lineCount, 0),
  snapshotFiles: parseResult?.snapshotFiles ?? [],
  exportCount: parseResult?.exportCount ?? 0,  // add this
};

};
},
});

export default vitestSnapshotWorkflow;
```
32 changes: 32 additions & 0 deletions skills/rig/samples/493-git-stale-branch-reporter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 493 - Git Stale Branch Reporter

```rig
import { agent, defineTool, p, s } from "rig";

const classifyBranchAge = defineTool("classifyBranchAge", {
description: "Classify a branch by age based on its last activity description",
parameters: s.object({ branch: s.string, lastActivity: s.string }),
handler({ lastActivity }): "fresh" | "aging" | "stale" | "ancient" {
const l = lastActivity.toLowerCase();
if (l.includes("second") || l.includes("minute") || l.includes("hour")) return "fresh" as const;
if (l.includes("day") && !l.includes("week")) return "aging" as const;
if (l.includes("week") || l.includes("month")) return "stale" as const;
return "ancient" as const;
},
});

// Agent role: report on git remote branches sorted by last commit date and classify their age.
const gitStaleBranchReporter = agent({
model: "small",
instructions: p`Analyze git branches from ${p.bash("git branch -r --sort=-committerdate --format='%(refname:short) %(committerdate:relative)' 2>/dev/null | head -30 || true")}. Use classifyBranchAge for each branch to determine its freshness status. Return the full report.`,
output: s.object({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] Same pattern: status: s.string in the output schema but classifyBranchAge returns a typed union. And similarly for 494's branches array. Consider s.enum(["fresh", "aging", "stale", "ancient"]) to keep output schemas honest about the values they represent — this also teaches readers that s.enum is the right schema helper when a tool returns a discriminated string union.

branches: s.array(s.object({ name: s.string, lastActivity: s.string, status: s.string })),
staleBranches: s.array(s.string),
freshCount: s.int,
staleCount: s.int,
}),
tools: [classifyBranchAge],
});

export default gitStaleBranchReporter;
```
29 changes: 29 additions & 0 deletions skills/rig/samples/494-ts-abstract-class-finder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 494 - Ts Abstract Class Finder

```rig
import { agent, defineTool, p, s } from "rig";

const extractAbstractClass = defineTool("extractAbstractClass", {
description: "Extract abstract class name and export status from a declaration line",
parameters: s.object({ filePath: s.path, line: s.int, declaration: s.string }),
handler({ declaration }) {
const exported = declaration.trim().startsWith("export");
const match = declaration.match(/abstract\s+class\s+(\w+)/);
return { className: match?.[1] ?? "Unknown", isExported: exported };
},
});

// Agent role: find all TypeScript abstract classes in the source tree and report their export status.
const tsAbstractClassFinder = agent({
model: "small",
instructions: p`Search for TypeScript abstract classes using ${p.bash("grep -rn 'abstract class' src/ 2>/dev/null || true")}. Also list source files with ${p.glob("src/**/*.ts")}. Use extractAbstractClass for each match to get the class name and export status. Return the full list.`,
output: s.object({
abstractClasses: s.array(s.object({ className: s.string, filePath: s.path, line: s.int, isExported: s.boolean })),
totalCount: s.int,
exportedCount: s.int,
}),
tools: [extractAbstractClass],
});

export default tsAbstractClassFinder;
```
30 changes: 30 additions & 0 deletions skills/rig/samples/495-dockerfile-env-inspector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 495 - Dockerfile Env Inspector

```rig
import { agent, defineTool, p, s } from "rig";

const parseEnvInstruction = defineTool("parseEnvInstruction", {
description: "Parse a Dockerfile ENV instruction line and extract key, value, and whether a default is set",
parameters: s.object({ file: s.path, line: s.int, raw: s.string }),
handler({ raw }) {
const parts = raw.replace(/^ENV\s+/, "").split(/\s+|=/, 2);
const key = parts[0] ?? "";
const value = parts[1] ?? "";
return { key, value, hasDefault: value.length > 0 };
},
});

// Agent role: locate Dockerfiles and extract all ENV instructions with their key-value details.
const dockerfileEnvInspector = agent({
model: "small",
instructions: p`Find Dockerfiles using ${p.bash("find . -name 'Dockerfile*' -not -path '*/node_modules/*' 2>/dev/null | head -20 || true")}. Extract ENV instructions with ${p.bash("grep -rn '^ENV ' $(find . -name 'Dockerfile*' -not -path '*/node_modules/*' 2>/dev/null | head -10 | tr '\\n' ' ') 2>/dev/null || true")}. Use parseEnvInstruction for each ENV line found. Return all env vars.`,
output: s.object({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The nested shell substitution in the grep command — grep -rn '^ENV ' $(find ...) — is shell-injected via p.bash(). If any Dockerfile path contains spaces or special characters this will silently misbehave or error. Since p.bash() is a declarative placeholder (not in-process execution), showing fragile shell composition in a sample teaches an unsafe pattern.

💡 Safer alternative: use xargs
p.bash("find . -name 'Dockerfile*' -not -path '*/node_modules/*' 2>/dev/null | head -10 | xargs -I{} grep -n '^ENV ' {} 2>/dev/null || true")

Using xargs -I{} avoids the subshell word-splitting problem.

envVars: s.array(s.object({ key: s.string, value: s.string, hasDefault: s.boolean, file: s.path, line: s.int })),
totalCount: s.int,
dockerfileCount: s.int,
}),
tools: [parseEnvInstruction],
});

export default dockerfileEnvInspector;
```
33 changes: 33 additions & 0 deletions skills/rig/samples/496-npm-script-prefix-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 496 - Npm Script Prefix Analyzer

```rig
import { agent, defineTool, p, s } from "rig";

const classifyScriptPrefix = defineTool("classifyScriptPrefix", {
description: "Classify an npm script by its command prefix",
parameters: s.object({ scriptName: s.string, command: s.string }),
handler({ command }): "node" | "npm" | "npx" | "ts-node" | "sh" | "other" {
const c = command.trim().split(" ")[0] ?? "";
if (c === "node") return "node" as const;
if (c === "npm") return "npm" as const;
if (c === "npx") return "npx" as const;
if (c === "ts-node" || c === "tsx") return "ts-node" as const;
if (c === "sh" || c === "bash") return "sh" as const;
return "other" as const;
},
});

// Agent role: read package.json scripts and classify each by command prefix, returning counts per prefix.
const npmScriptPrefixAnalyzer = agent({
model: "small",
instructions: p`Read scripts from ${p.read("package.json")}. Use classifyScriptPrefix for each script entry to determine its command prefix. Return the per-script details and prefix counts.`,
output: s.object({
scripts: s.record(s.object({ command: s.string, prefix: s.string })),
prefixCounts: s.record(s.int),
totalScripts: s.int,
}),
tools: [classifyScriptPrefix],
});

export default npmScriptPrefixAnalyzer;
```
34 changes: 34 additions & 0 deletions skills/rig/samples/497-ts-default-param-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 497 - Ts Default Param Extractor

```rig
import { agent, defineTool, p, s, repair } from "rig";

const extractDefaultParam = defineTool("extractDefaultParam", {
description: "Extract function name, parameter name, and default value from a TypeScript line",
parameters: s.object({ file: s.path, line: s.int, raw: s.string }),
handler({ raw }) {
const fnMatch = raw.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=)/);
const paramMatch = raw.match(/(\w+)\s*=\s*([^,)]+)/);
return {
functionName: fnMatch?.[1] ?? fnMatch?.[2] ?? "anonymous",
paramName: paramMatch?.[1] ?? "unknown",
defaultValue: paramMatch?.[2]?.trim() ?? "unknown",
};
},
});

// Agent role: find TypeScript functions with default parameter values and report them.
const tsDefaultParamExtractor = agent({
model: "small",
instructions: p`Find TypeScript files with ${p.glob("src/**/*.ts")}. Search for function parameters with defaults using ${p.bash("grep -rn 'function\\|const.*=>' src/ 2>/dev/null | grep '=' | grep -v '=>' | head -50 || true")}. Use extractDefaultParam for each match. Return the full list of default parameters.`,
output: s.object({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The bash grep filter is ambiguous and will produce many false positives. The pattern grep 'function\|const.*=>' | grep '=' | grep -v '=>' is contradictory — it greps for arrow functions (const.*=>) then excludes =>, meaning it only captures const x = ... declarations, not actual default parameters like fn(x = 1). The sample would return mostly variable assignments rather than function defaults.

💡 More precise grep for default parameters
grep -rn '([^)]*\w\+\s*=\s*[^=][^)]*)' src/ --include='*.ts' 2>/dev/null | head -50 || true

Or target function declarations and arrow function signatures specifically and filter for = inside parameter lists.

defaults: s.array(s.object({ functionName: s.string, paramName: s.string, defaultValue: s.string, file: s.path, line: s.int })),
totalDefaults: s.int,
filesWithDefaults: s.array(s.path),
}),
tools: [extractDefaultParam],
addons: [repair()],
});

export default tsDefaultParamExtractor;
```
32 changes: 32 additions & 0 deletions skills/rig/samples/498-git-branch-risk-classifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 498 - Git Branch Risk Classifier

```rig
import { agent, defineTool, p, s, steering, repair } from "rig";

const classifyBranchRisk = defineTool("classifyBranchRisk", {
description: "Classify a branch merge risk based on ahead/behind commit counts",
parameters: s.object({ branch: s.string, ahead: s.int, behind: s.int }),
handler({ ahead, behind }): "safe" | "caution" | "risky" | "critical" {
const total = ahead + behind;
if (total === 0) return "safe" as const;
if (total <= 5) return "caution" as const;
if (total <= 20) return "risky" as const;
return "critical" as const;
},
});

// Agent role: list local git branches with ahead/behind counts and classify each by merge risk.
const gitBranchRiskClassifier = agent({
model: "small",
instructions: p`List branches with ${p.bash("git branch --format='%(refname:short) %(ahead-behind:HEAD)' 2>/dev/null | head -20 || true")}. Use classifyBranchRisk for each branch with its ahead/behind numbers. Return the risk classification for every branch.`,
output: s.object({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The output schema uses risk: s.string but classifyBranchRisk returns a typed union "safe" | "caution" | "risky" | "critical". Same pattern as 491 — s.enum would make the sample more faithful to the tool's contract and demonstrate the schema helper.

💡 Use s.enum in the branches array
branches: s.array(s.object({ name: s.string, ahead: s.int, behind: s.int, risk: s.enum(["safe", "caution", "risky", "critical"]) })),

branches: s.array(s.object({ name: s.string, ahead: s.int, behind: s.int, risk: s.string })),
riskyCritical: s.array(s.string),
summary: s.string,
}),
tools: [classifyBranchRisk],
addons: [steering(), repair()],
});

export default gitBranchRiskClassifier;
```
58 changes: 58 additions & 0 deletions skills/rig/samples/499-test-file-coverage-pipeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 499 - Test File Coverage Pipeline

```rig
import { agent, defineTool, workflow, p, s } from "rig";

// Agent role: discover all test and spec files in the workspace.
const testDiscoverer = agent({
model: "small",
instructions: p`Find test files with ${p.glob("**/*.test.ts")} and spec files with ${p.bash("find . -name '*.spec.ts' -not -path '*/node_modules/*' 2>/dev/null | head -30 || true")}. Return distinct lists and total count.`,
output: s.object({
testFiles: s.array(s.path),
specFiles: s.array(s.path),
totalCount: s.int,
}),
});

const classifyTest = defineTool("classifyTest", {
description: "Classify a test file as unit, integration, or e2e based on its path and name",
parameters: s.object({ file: s.path, lineCount: s.int }),
handler({ file }): "unit" | "integration" | "e2e" {
if (file.includes("e2e") || file.includes("end-to-end")) return "e2e" as const;
if (file.includes("integration") || file.includes("integ")) return "integration" as const;
return "unit" as const;
},
});

// Agent role: classify test files by type and produce counts.
const testClassifier = agent({
model: "small",
input: s.object({ testFiles: s.array(s.path), specFiles: s.array(s.path), totalCount: s.int }),
instructions: p`Use classifyTest for each file in input.testFiles and input.specFiles. Also check file sizes using ${p.bash("wc -l $(find . -name '*.test.ts' -o -name '*.spec.ts' 2>/dev/null | head -10 | tr '\\n' ' ') 2>/dev/null || true")}. Return classification map and counts.`,
output: s.object({
classifications: s.record(s.string),
unitCount: s.int,
integrationCount: s.int,
e2eCount: s.int,
}),
tools: [classifyTest],
});

// Workflow role: discover test files, classify them, and produce a report.
const testFileCoveragePipeline = workflow({
meta: { name: "test-file-coverage-pipeline", description: "Discover and classify test files in a three-stage pipeline" },
body: async ({ call }) => {
const discovered = await call(testDiscoverer, "");
const classified = await call(testClassifier, discovered ?? { testFiles: [], specFiles: [], totalCount: 0 });
return {
totalFiles: discovered?.totalCount ?? 0,
unitCount: classified?.unitCount ?? 0,
integrationCount: classified?.integrationCount ?? 0,
e2eCount: classified?.e2eCount ?? 0,
classifications: classified?.classifications ?? {},
};
},
});

export default testFileCoveragePipeline;
```
39 changes: 39 additions & 0 deletions skills/rig/samples/500-json-schema-type-distribution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 500 - Json Schema Type Distribution

```rig
import { agent, defineTool, p, s } from "rig";

const analyzeSchemaTypes = defineTool("analyzeSchemaTypes", {
description: "Analyze a JSON schema file and count property types",
parameters: s.object({ schemaFile: s.path, content: s.string }),
handler({ content }) {
try {
const schema = JSON.parse(content);
const props = schema.properties ?? {};
const typeCounts: Record<string, number> = {};
for (const v of Object.values(props) as Array<{ type?: string }>) {
const t = v.type ?? "unknown";
typeCounts[t] = (typeCounts[t] ?? 0) + 1;
}
return { propertyCount: Object.keys(props).length, typeCounts };
} catch {
return { propertyCount: 0, typeCounts: {} };
}
},
});

// Agent role: discover JSON schema files and compute the distribution of property types across all schemas.
const jsonSchemaTypeDistribution = agent({
model: "small",
instructions: p`Find JSON schema files with ${p.glob("**/*.schema.json")} and also using ${p.bash("find . -name '*.schema.json' -not -path '*/node_modules/*' 2>/dev/null | head -10 || true")}. Read and analyze each file using analyzeSchemaTypes. Aggregate property type counts across all schemas. Return the type distribution and most common type.`,
output: s.object({
schemaFiles: s.array(s.path),
totalProperties: s.int,
typeDistribution: s.record(s.int),
mostCommonType: s.string,
}),
tools: [analyzeSchemaTypes],
});

export default jsonSchemaTypeDistribution;
```