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/280-commit-churn-classifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 280 - Commit Churn Classifier

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

// Agent role: classify repository files by commit churn frequency and assign a risk level.
const commitChurnClassifier = agent({
model: "small",
addons: steering({ message: "Be precise: assign riskLevel based on churnCount: >20=critical, >10=volatile, >5=active, else stable." }),

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] The steering message duplicates the classification thresholds that are already spelled out in instructions. If the thresholds ever change, they'd need to be updated in two places.

💡 Suggestion

Either remove the steering addon and rely on the clear rules in instructions, or keep only the steering message and shorten the instructions. Compare sample 113 (git-hotspot-analyzer.md) where steering is used for emphasis, not duplication.

If the steering is genuinely needed to keep the LLM on-spec, consider combining repair with a single concise steering message that doesn't restate the full threshold table.

instructions: p`Classify repository files by how frequently they are committed (churn).

File churn counts from last 200 commits:
${p.bash("git log --name-only --format='' HEAD~200..HEAD 2>/dev/null | grep -v '^$' | sort | uniq -c | sort -rn | head -40")}

For each file, parse the churn count and assign a riskLevel:
- critical: churnCount > 20
- volatile: churnCount > 10
- active: churnCount > 5
- stable: churnCount <= 5

Return a record keyed by file path with churnCount (integer) and riskLevel.`,
output: s.record(
s.object({
churnCount: s.int,
riskLevel: s.enum("stable", "active", "volatile", "critical"),
})
),
});

export default commitChurnClassifier;
```
41 changes: 41 additions & 0 deletions skills/rig/samples/281-npm-package-size-estimator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 281 - NPM Package Size Estimator

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

const classifySize = defineTool("classifySize", {
description: "Classify a package size in KB into a tier rating.",
parameters: s.object({ sizeKb: s.number }),
handler({ sizeKb }: { sizeKb: number }) {
if (sizeKb < 10) return "tiny" as const;
if (sizeKb < 100) return "small" as const;
if (sizeKb < 500) return "medium" as const;
if (sizeKb < 2000) return "large" as const;
return "xlarge" as const;
},
});

// Agent role: estimate the NPM package size and rate it by tier.
const npmPackageSizeEstimator = agent({
model: "small",
instructions: p`Estimate the NPM package size for this project.

Pack dry-run output:
${p.bash("npm pack --dry-run 2>&1 | tail -20")}

Directory size:
${p.bash("du -sh . 2>/dev/null | head -5")}

Use classifySize tool with the estimated total size in KB to get the sizeRating.
List the top files by size and provide a brief recommendation.`,
tools: [classifySize],
output: s.object({
estimatedSizeKb: s.number,
topFiles: s.array(s.object({ name: s.string, sizeKb: s.number })),
sizeRating: s.enum("tiny", "small", "medium", "large", "xlarge"),
recommendation: s.string,
}),
});

export default npmPackageSizeEstimator;
```
44 changes: 44 additions & 0 deletions skills/rig/samples/282-ts-branch-coverage-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 282 - TS Branch Coverage Analyzer

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

const branchItem = s.object({
functionName: s.string,
line: s.int,
branchType: s.enum("if", "ternary", "switch", "nullish"),
covered: s.boolean,
});

// Agent role: extract all branch points from the provided TypeScript source code.
const branchAnalyzer = agent({
model: "small",
instructions: p`Analyze the TypeScript source code provided in the input and list every branch point (if/ternary/switch/nullish coalescing). For each branch, identify the enclosing function name, line number, branch type, and whether it appears covered (has a test or usage path). Return an array of branch objects.`,

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] The branchAnalyzer subagent declares input: s.object({ filePath: s.path }) but its instructions never call p.readInput("filePath") — the model never sees the source to analyze.

💡 Fix

Follow the existing pattern from sample 159 (ts-branch-coverage.md) — add p.readInput to the subagent's instructions:

instructions: p`Analyze the TypeScript source code:

${p.readInput("filePath")}

For each branch...`,

Without this the subagent receives no file content; its covered flags will be unreliable.

input: s.object({ filePath: s.path }),
output: s.array(branchItem),
});

// Agent role: coordinate branch coverage analysis for a TypeScript file.
const tsBranchCoverageAnalyzer = agent({
model: "small",
input: s.object({ filePath: s.path }),
instructions: p`Perform branch coverage analysis on a TypeScript file.

File content:
${p.readInput("filePath")}

Delegate the analysis to the branchAnalyzer subagent, passing the filePath.
Collect its output (an array of branch objects), then compute a summary with totalBranches, coveredBranches, and uncoveredBranches counts.`,
output: s.object({
branches: s.array(branchItem),
summary: s.object({
totalBranches: s.int,
coveredBranches: s.int,
uncoveredBranches: s.int,
}),
}),
agents: { branchAnalyzer },
});

export default tsBranchCoverageAnalyzer;
```
41 changes: 41 additions & 0 deletions skills/rig/samples/283-env-variable-type-inferrer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 283 - Env Variable Type Inferrer

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

const inferVarType = defineTool("inferVarType", {
description: "Infer the type of an environment variable value using regex heuristics.",
parameters: s.object({ value: s.string }),
handler({ value }: { value: string }) {
if (/^https?:\/\//i.test(value)) return "url" as const;
if (/^(true|false|yes|no|1|0)$/i.test(value)) return "boolean" as const;
if (/^\d+(\.\d+)?$/.test(value)) return "number" as const;
if (/^(\/|\.\/|~\/)/.test(value) || /\.(txt|json|yaml|yml|pem|key|crt)$/.test(value)) return "path" as const;
if (value === "") return "unknown" as const;
return "string" as const;
},
});

// Agent role: infer types for all environment variables defined in .env.example.
const envVariableTypeInferrer = agent({
model: "small",
instructions: p`Infer the type of each environment variable defined in the .env.example file.

.env.example contents:
${p.readOptional(".env.example", "(no .env.example found)")}

For each KEY=VALUE line, call inferVarType with the value to get its type. Generate a short description of what each variable likely controls. Set allDocumented to true only if every variable has a non-empty value or inline comment hint.`,
tools: [inferVarType],
output: s.object({
vars: s.record(
s.object({
type: s.enum("string", "number", "boolean", "url", "path", "unknown"),
description: s.string,
})
),
allDocumented: s.boolean,
}),
});

export default envVariableTypeInferrer;
```
62 changes: 62 additions & 0 deletions skills/rig/samples/284-makefile-target-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 284 - Makefile Target Extractor

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

const parseTargets = defineTool("parseTargets", {
description: "Parse Makefile content to extract phony and real targets with descriptions.",
parameters: s.object({ content: s.string }),
handler({ content }: { content: string }) {
const lines = content.split("\n");
const phonySet = new Set<string>();
const targets: Array<{ name: string; isPhony: boolean; hasHelp: boolean; description?: string }> = [];

for (const line of lines) {
const phonyMatch = line.match(/^\.PHONY\s*:\s*(.+)/);
if (phonyMatch) {
for (const t of phonyMatch[1].split(/\s+/)) phonySet.add(t.trim());
}
}

const targetRe = /^([a-zA-Z0-9_\-./]+)\s*:/;
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(targetRe);
if (m && !m[1].startsWith(".")) {
const name = m[1];
const helpLine = i > 0 ? lines[i - 1] : "";
const hasHelp = /##/.test(helpLine) || /##/.test(lines[i]);
const description = helpLine.match(/##\s*(.+)/)?.[1]?.trim();
targets.push({ name, isPhony: phonySet.has(name), hasHelp, ...(description ? { description } : {}) });
}
}
return targets;
},
});

// Agent role: extract and classify Makefile targets.
const makefileTargetExtractor = agent({
model: "small",
addons: repair(),
instructions: p`Extract and classify targets from the project Makefile.

Makefile contents:
${p.readOptional("Makefile", "(no Makefile found)")}

Call parseTargets with the full Makefile content. Return the array of targets and include totalCount (all targets) and phonyCount (targets where isPhony is true).`,
tools: [parseTargets],
output: s.object({
targets: s.array(
s.object({
name: s.string,
isPhony: s.boolean,
hasHelp: s.boolean,
description: s.optional(s.string),
})
),
totalCount: s.int,
phonyCount: s.int,
}),
});

export default makefileTargetExtractor;
```
41 changes: 41 additions & 0 deletions skills/rig/samples/285-test-fixture-generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 285 - Test Fixture Generator

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

// Agent role: analyze a function signature and return its parameters and return type.
const signatureAnalyzer = agent({
model: "small",
instructions: p`Analyze the TypeScript source provided in the input. Extract the signature details for the specified function: its parameters (name and type), return type, and any JSDoc description. Return structured signature details.`,

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] The signatureAnalyzer subagent declares input: s.object({ sourceFile: s.path, functionName: s.string }) but its instructions don't inject the file content with p.readInput("sourceFile") — the model analyzes nothing.

💡 Fix

The subagent needs to read the file to extract the signature:

instructions: p`Analyze the TypeScript source:

${p.readInput("sourceFile")}

Extract the signature of the function named in the input...`,

The parent agent already reads the source at line 26, but the subagent doesn't — it will produce hallucinated signatures.

input: s.object({ sourceFile: s.path, functionName: s.string }),
output: s.object({
functionName: s.string,
parameters: s.array(s.object({ name: s.string, type: s.string })),
returnType: s.string,
description: s.optional(s.string),
}),
});

// Agent role: generate a test fixture for a specific function in a TypeScript source file.
const testFixtureGenerator = agent({
model: "small",
input: s.object({ sourceFile: s.path, functionName: s.string }),
instructions: p`Generate a test fixture file for the specified function.

Source file content:
${p.readInput("sourceFile")}

1. Delegate to signatureAnalyzer to get the function signature for the function named in the input.
2. Using the signature, generate fixture code: example inputs, mock helpers, and a vitest describe block.
3. Write the fixture code to fixture.ts: ${p.writeOutput("fixtureCode", "fixture.ts")}

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] p.writeOutput("fixtureCode", "fixture.ts") writes a hardcoded filename fixture.ts, ignoring the input functionName. Every call will overwrite the same file, making the agent unusable for generating fixtures for multiple functions.

💡 Suggestion

Use a dynamic path or make suggestedFileName the write target. Since p.writeOutput requires a literal path at definition time, document that the caller should provide the output path via input, or hardcode a pattern like {functionName}.fixture.ts in the suggested filename and let the caller move the file.

Alternatively, remove p.writeOutput and rely solely on the fixtureCode string output — the suggestedFileName field already conveys where to write it.

4. Return fixtureCode (the full fixture source), the required imports, and suggestedFileName.`,
output: s.object({
fixtureCode: s.string,
imports: s.array(s.string),
suggestedFileName: s.path,
}),
agents: { signatureAnalyzer },
});

export default testFixtureGenerator;
```
44 changes: 44 additions & 0 deletions skills/rig/samples/286-toml-config-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 286 - TOML Config Analyzer

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

const analyzeTomlFile = defineTool("analyzeTomlFile", {
description: "Read a TOML file and extract its top-level section headers and key count.",
parameters: s.object({ filePath: s.path }),
async handler({ filePath }: { filePath: string }) {
const { readFile } = await import("node:fs/promises");
try {
const content = await readFile(filePath, "utf8");
const sections = [...content.matchAll(/^\[([^\]]+)\]/gm)].map((m: RegExpMatchArray) => m[1] as string);
const keyCount = (content.match(/^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*=/gm) ?? []).length;
const hasRequired = sections.includes("package") || sections.includes("tool") || keyCount > 0;
return { sections, keyCount, hasRequired };
} catch {
return { sections: [], keyCount: 0, hasRequired: false };
}
},
});

// Agent role: analyze all TOML config files in the workspace and summarize their sections.
const tomlConfigAnalyzer = agent({
model: "small",
addons: repair(),
instructions: p`Analyze all TOML config files found in the workspace.

TOML files found:
${p.glob("**/*.toml")}

For each file path listed above, call analyzeTomlFile to extract its sections, keyCount, and hasRequired flag. Return a record keyed by file path.`,
tools: [analyzeTomlFile],
output: s.record(
s.object({
sections: s.array(s.string),
keyCount: s.int,
hasRequired: s.boolean,
})
),
});

export default tomlConfigAnalyzer;
```
44 changes: 44 additions & 0 deletions skills/rig/samples/287-git-checkpoint-summarizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 287 - Git Checkpoint Summarizer

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

const classifyCheckpoint = defineTool("classifyCheckpoint", {
description: "Classify a git checkpoint line as stash, commit, tag, or branch.",
parameters: s.object({ line: s.string }),
handler({ line }: { line: string }) {
if (line.startsWith("stash@{")) return "stash" as const;
if (/^refs\/tags\//.test(line)) return "tag" as const;
if (/^refs\/heads\//.test(line)) return "branch" as const;
return "commit" as const;
},
});

// Agent role: summarize git checkpoints (stashes and recent commits) in the repository.
const gitCheckpointSummarizer = agent({
model: "small",
instructions: p`Summarize the git checkpoints (stashes and recent commits) for this repository.

Stash list:
${p.bash("git stash list 2>/dev/null || echo '(no stashes)'")}

Recent commits:
${p.bash("git log --oneline -20 2>/dev/null || echo '(no commits)'")}

For each stash entry and each commit line, call classifyCheckpoint to determine its type. Build the checkpoints array with ref, type, and message fields. Set latestCheckpoint to the most recent commit hash or stash ref, and totalCount to the combined count.`,
tools: [classifyCheckpoint],
output: s.object({
checkpoints: s.array(
s.object({
ref: s.string,
type: s.enum("stash", "commit", "tag", "branch"),
message: s.string,
})
),
latestCheckpoint: s.optional(s.string),
totalCount: s.int,
}),
});

export default gitCheckpointSummarizer;
```
Loading
Loading