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
18 changes: 18 additions & 0 deletions skills/rig/samples/88-git-contributor-mapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 88 - Git Contributor Mapper

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

// Agent role: map git contributors to their commit counts, primary work areas, and role classification.
const gitContributorMapper = agent({
model: "small",
instructions: p`Analyze git contributors using: ${p.bash("git shortlog -sn --no-merges")} and ${p.bash("git log --no-merges --name-only --pretty=format:'%an' | head -500")}. For each contributor, count their commits, identify which directories they primarily touch, and classify their role as core (many commits across many files), peripheral (few commits or limited scope), or single-file.`,
output: s.record(s.object({
commitCount: s.number,
primaryAreas: s.array(s.string),
role: s.enum("core", "peripheral", "single-file"),
})),
});

export default gitContributorMapper;
```
27 changes: 27 additions & 0 deletions skills/rig/samples/89-markdown-doc-summarizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 89 - Markdown Doc Summarizer

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

// Agent role: summarize each top-level section of a markdown doc, then compile into a full report.
const sectionSummarizer = agent({
name: "sectionSummarizer",
model: "nano",
instructions: p`Summarize the section of documentation provided in the input.`,
input: s.object({ heading: s.string, content: s.string }),
output: s.object({ heading: s.string, summary: s.string }),
});

// Agent role: read the project README, delegate per-section summarization, and write the final report.
const markdownDocSummarizer = agent({
model: "small",
instructions: p`Read the README: ${p.readOptional("README.md", "No README found.")}. Identify each top-level heading (##) and its content. Delegate summarization of each section to the sectionSummarizer agent. Compile all summaries into a report and write it to summaries/README-summary.md via ${p.writeOutput("reportPath", "summaries/README-summary.md")}.`,
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.

[/codebase-design] p.readOptional is used here, but README.md is inlined directly into the main agent's instructions. This means the entire file content is injected into the prompt at construction time — not at delegation time — which could balloon token usage for large READMEs before the agent even begins section extraction.

💡 Suggestion

Consider passing README.md as input to the orchestrator agent instead, so the caller controls what document is summarized and the sample is more reusable:

input: s.object({ docPath: s.string }),
instructions: p`Read the document: ${p.readInput("docPath")}. Identify each top-level heading (##)...`,

This also makes the sample demonstrate p.readInput — a distinct pattern not yet shown by surrounding samples.

sections: s.array(s.object({ heading: s.string, summary: s.string })),
reportPath: s.string,
}),
agents: { sectionSummarizer },
});

export default markdownDocSummarizer;
```
34 changes: 34 additions & 0 deletions skills/rig/samples/90-workspace-config-drift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 90 - Workspace Config Drift

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] This sample is a verbatim duplicate of 82-workspace-config-drift.md — same parseJson tool, same agent config, same instructions, same output schema, and same repair() addon. It adds no new pattern to the sample library.

💡 Suggestion

Either delete this and replace with a genuinely distinct sample, or differentiate it meaningfully (e.g. add steering() guidance, extend to YAML configs, or add a maxTurns rationale comment).

Duplicate samples dilute the sample library and confuse users learning by example.


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

const parseJson = defineTool("parseJson", {
description: "Parse a JSON string and return it, or report a parse error",
parameters: s.object({ content: s.string, filename: s.string }),
handler({ content, filename }) {
try {
const parsed = JSON.parse(content);
return { ok: true, parsed };
} catch (e) {
return { ok: false, error: String(e), filename };
}
},
});

// Agent role: detect drift in workspace config files by reading them and comparing against known defaults.
const workspaceConfigDrift = agent({
model: "small",
instructions: p`Read project config files: ${p.readOptional("tsconfig.json", "{}")} (tsconfig.json), ${p.readOptional(".eslintrc.json", "{}")} (.eslintrc.json), ${p.readOptional(".prettierrc", "{}")} (.prettierrc). Use the parseJson tool to parse each file. For each config, identify fields that deviate from sensible defaults and report them as drifted. Assign status ok if no drift, warning for minor issues, error for significant mismatches.`,
output: s.record(s.object({
driftedFields: s.array(s.string),
status: s.enum("ok", "warning", "error"),
})),
tools: [parseJson],
maxTurns: 4,
addons: repair(),
});

export default workspaceConfigDrift;
```
22 changes: 22 additions & 0 deletions skills/rig/samples/91-commit-format-suggester.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 91 - Commit Format Suggester

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] This sample is a near-verbatim duplicate of 83-commit-format-suggester.md — same instructions, same output schema, same [steering(), repair()] addon combo, same maxTurns: 5. The import order differs (repair, steering vs steering, repair) but produces identical runtime behaviour.

💡 Suggestion

Replace this with a distinct sample that covers a new concept not already in the library. If the intent was to refine the commit-format pattern, update 83 in place and document what changed.


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

// Agent role: review recent git commits and suggest conventional-format rewrites for each one.
const commitFormatSuggester = agent({
model: "small",
instructions: p`Review recent git commits: ${p.bash("git log --oneline -20 --no-merges")}. For each commit, check whether its message follows conventional commit format (type: description). Suggest a rewritten message in conventional format. Classify each commit as one of: feat, fix, chore, docs, test, refactor, style. Write the full report to commit-report.md via ${p.writeOutput("reportWritten", "commit-report.md")}.`,
output: s.array(s.object({
hash: s.string,
original: s.string,
suggested: s.string,
category: s.enum("feat", "fix", "chore", "docs", "test", "refactor", "style"),
})),
maxTurns: 5,
addons: [steering(), repair()],

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] steering() is called with no message argument, making it a no-op stub — the steering addon needs a message to steer the model toward a goal.

💡 Fix

Either remove steering() entirely, or provide a meaningful message like the existing sample 76-commit-msg-rewriter.md does:

addons: [steering({ message: "Ensure each suggested commit message uses conventional commit format: type(scope): description." }), repair()],

A steering() with no message wastes a turn without providing guidance.

});

export default commitFormatSuggester;
```
36 changes: 36 additions & 0 deletions skills/rig/samples/92-runtime-env-checker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 92 - Runtime Env Checker

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

const checkThresholds = defineTool("checkThresholds", {
description: "Validate environment values against minimum thresholds and return a list of issues",
parameters: s.object({
nodeVersion: s.string,
heapMB: s.number,
}),
handler({ nodeVersion, heapMB }) {
const issues: string[] = [];
const majorVersion = parseInt(nodeVersion.replace("v", "").split(".")[0], 10);
if (majorVersion < 18) issues.push(`Node.js ${nodeVersion} is below minimum v18`);
if (heapMB < 256) issues.push(`Heap ${heapMB}MB is below 256MB minimum`);
return { issues };
},
});

// Agent role: inspect the runtime environment and report overall health.
const runtimeEnvChecker = agent({
model: "small",
instructions: p`Inspect the runtime environment using: ${p.bash("node --version")}, ${p.bash("uname -a")}, and ${p.bash("node -e \"console.log(Math.round(process.memoryUsage().heapTotal/1024/1024))\"")}. Use the checkThresholds tool to validate versions and memory. Determine overall health as ok, degraded, or critical based on issues found.`,
output: s.object({
health: s.enum("ok", "degraded", "critical"),
nodeVersion: s.string,
os: s.string,
heapMB: s.number,
issues: s.array(s.string),
}),
tools: [checkThresholds],
});

export default runtimeEnvChecker;
```
21 changes: 21 additions & 0 deletions skills/rig/samples/93-hotspot-file-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 93 - Hotspot File Analyzer

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

// Agent role: analyze which source files are hot-spots by measuring churn and top contributors.
const hotspotFileAnalyzer = agent({
model: "small",
instructions: p`Analyze file churn in this repository. Get recently changed files using ${p.bash("git log --name-only --format='' HEAD~100..HEAD | sort | uniq -c | sort -rn | head -30")} and contributor data using ${p.bash("git shortlog -sn --no-merges HEAD~100..HEAD")}. For each hot-spot file, compute a churnScore 0–100 based on how often it changes, list topContributors, and classify riskLevel as low, medium, or high.`,
output: s.record(s.object({
churnScore: s.number,
topContributors: s.array(s.string),
riskLevel: s.enum("low", "medium", "high"),
})),
maxTurns: 5,
addons: steering({ message: "Ensure every file entry has a numeric churnScore and at least one topContributor." }),
});

export default hotspotFileAnalyzer;
```
50 changes: 50 additions & 0 deletions skills/rig/samples/94-ts-interface-conflict-checker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 94 - Ts Interface Conflict Checker

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

const scanInterfaces = defineTool("scanInterfaces", {
description: "Scan a TypeScript file for exported interface names using grep",
parameters: s.object({ filePath: s.string }),
async handler({ filePath }) {
const { execSync } = await import("node:child_process");
try {
const result = execSync(
`grep -n "^export interface\\|^interface " "${filePath}" 2>/dev/null || true`,
{ encoding: "utf8" }
);
const names = result
.split("\n")
.filter(Boolean)
.map((line) => {
const m = line.match(/interface\s+(\w+)/);
return m ? m[1] : null;
})
.filter(Boolean) as string[];
return { filePath, names };
} catch {
return { filePath, names: [] };
}
},
});

// Agent role: find duplicate TypeScript interface names across all source files in the project.
const tsInterfaceConflictChecker = agent({
model: "small",
instructions: p`Find all TypeScript source files using ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | head -80")}. Use the scanInterfaces tool on each file to collect interface names. Identify any interface name declared in more than one file. Classify each conflict as warning (same name, compatible) or error (likely clash). Set hasConflicts to true if any conflicts exist.`,
output: s.object({
conflicts: s.array(s.object({
interfaceName: s.string,
files: s.array(s.string),
severity: s.enum("warning", "error"),
})),
hasConflicts: s.boolean,
}),
tools: [scanInterfaces],
maxTurns: 6,
addons: repair(),
});

export default tsInterfaceConflictChecker;
```
48 changes: 48 additions & 0 deletions skills/rig/samples/95-git-worktree-mapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 95 - Git Worktree Mapper

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

const parseWorktreePorcelain = defineTool("parseWorktreePorcelain", {
description: "Parse the output of git worktree list --porcelain into structured entries",
parameters: s.object({ output: s.string }),
handler({ output }) {
const entries: Array<{ path: string; branch?: string; state: string }> = [];
const blocks = output.trim().split("\n\n");
for (const block of blocks) {
const lines = block.split("\n");
const pathLine = lines.find((l) => l.startsWith("worktree "));
const branchLine = lines.find((l) => l.startsWith("branch "));
const isLocked = lines.some((l) => l.startsWith("locked"));
const isBare = lines.some((l) => l.startsWith("bare"));
const entry: { path: string; branch?: string; state: string } = {
path: pathLine ? pathLine.replace("worktree ", "") : "",
state: isLocked ? "locked" : isBare ? "bare" : "clean",
};
if (branchLine) entry.branch = branchLine.replace("branch refs/heads/", "");
if (entry.path) entries.push(entry);
}
return entries;
},
});

// Agent role: list and classify all git worktrees in the current repository.
const gitWorktreeMapper = agent({
model: "small",
instructions: p`List all git worktrees using ${p.bash("git worktree list --porcelain")}. Use the parseWorktreePorcelain tool to parse the output. Determine the state of each worktree (locked, bare, clean, or dirty if there are uncommitted changes). Provide a summary with totalCount and activeCount (non-bare worktrees).`,
output: s.object({
worktrees: s.array(s.object({
path: s.string,
branch: s.optional(s.string),
state: s.enum("locked", "bare", "clean", "dirty"),
})),

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 output schema declares state: s.enum("locked", "bare", "clean", "dirty") including "dirty", but the parseWorktreePorcelain tool never sets state to "dirty" — it only checks for locked and bare flags, defaulting to "clean". The instructions say "Determine the state ... dirty if there are uncommitted changes" but the tool returns before any dirty-check can happen.

💡 Fix

Either:

  1. Remove "dirty" from the enum and the instructions (since the tool can't detect it), or
  2. Extend the tool to run git status --porcelain "<path>" inside the handler and set state to "dirty" when it returns output.

As-is, the LLM is told to classify dirty worktrees but has no tool mechanism to detect them — it will guess.

summary: s.object({
totalCount: s.number,
activeCount: s.number,
}),
}),
tools: [parseWorktreePorcelain],
});

export default gitWorktreeMapper;
```
23 changes: 23 additions & 0 deletions skills/rig/samples/96-test-naming-enforcer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 96 - Test Naming Enforcer

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

// Agent role: audit test file naming conventions and report files that violate the standard pattern.
const testNamingEnforcer = agent({
model: "small",
instructions: p`Find all test files in this project using ${p.bash("find . \\( -name '*.test.ts' -o -name '*.spec.ts' -o -name '*.test.js' -o -name '*.spec.js' \\) -not -path '*/node_modules/*' | head -60")}. For each file, check whether it follows the convention of <subject>.test.ts or <subject>.spec.ts. Classify as correct if it matches, wrong-prefix if the name before the extension separator is unusual, wrong-suffix if it ends differently, or missing-spec if it should be a test file but lacks the marker. Suggest a corrected name where applicable. Set allConform to true only if every file is classified as correct.`,
output: s.object({
files: s.record(s.object({
convention: s.enum("correct", "wrong-prefix", "wrong-suffix", "missing-spec"),
suggestedName: s.optional(s.string),
})),
allConform: s.boolean,
}),
maxTurns: 5,
addons: steering({ message: "Ensure every discovered test file has an entry in files and allConform is a boolean." }),
});

export default testNamingEnforcer;
```
40 changes: 40 additions & 0 deletions skills/rig/samples/97-pkg-dependency-graph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 97 - Pkg Dependency Graph

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

const classifyDependency = defineTool("classifyDependency", {
description: "Classify a dependency as runtime, dev, or peer based on its presence in package.json sections",
parameters: s.object({
name: s.string,
inDependencies: s.boolean,
inDevDependencies: s.boolean,
inPeerDependencies: s.boolean,
}),
handler({ inDependencies, inDevDependencies, inPeerDependencies }) {

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 name parameter is declared in the tool's parameters schema but never used in the handler body — only inDependencies, inDevDependencies, and inPeerDependencies are destructured.

💡 Fix
handler({ name, inDependencies, inDevDependencies, inPeerDependencies }) {
  // use name if you want to log or return it, otherwise remove it from parameters
}

Either remove name from parameters (since the handler doesn't use it), or destructure and use it in the return value so the LLM can correlate the classification back to a package name. As-is, the tool silently discards an input the LLM is instructed to pass.

if (inPeerDependencies) return "peer";
if (inDevDependencies) return "dev";
if (inDependencies) return "runtime";
return "dev";
},
});

// Agent role: extract and classify direct dependencies from package.json, then describe the overall tree shape.
const pkgDependencyGraph = agent({
model: "small",
instructions: p`Read the project manifest: ${p.read("package.json")}. Get the resolved dependency tree using ${p.bash("npm ls --json --depth=1 2>/dev/null || echo '{}'")}. Use the classifyDependency tool to classify each direct dependency as runtime, dev, or peer. Also list all devDependency names. Estimate the treeShape as flat (<5 deps), shallow (5–20), or deep (>20) and set depthScore to the total direct dependency count.`,

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 classifyDependency tool returns a plain string ("peer", "dev", or "runtime"), but the output schema wraps each dep in s.object({ name, version, type }). The tool result only provides typename and version must come entirely from the LLM's recall of the npm ls output. If the LLM hallucinates or drops a package, the tool gives no corrective signal.

💡 Suggestion

Return a richer object from the tool so the LLM can use it to build the output without needing to remember package names independently:

handler({ name, inDependencies, inDevDependencies, inPeerDependencies }) {
  const type = inPeerDependencies ? "peer" : inDevDependencies ? "dev" : inDependencies ? "runtime" : "dev";
  return { name, type };
}

This makes the tool a reliable anchor rather than a pure classification oracle.

output: s.object({
directDeps: s.array(s.object({
name: s.string,
version: s.string,
type: s.enum("runtime", "dev", "peer"),
})),
devDeps: s.array(s.string),
treeShape: s.enum("flat", "shallow", "deep"),
depthScore: s.number,
}),
tools: [classifyDependency],
});

export default pkgDependencyGraph;
```
Loading