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
53 changes: 53 additions & 0 deletions skills/rig/samples/381-ts-dead-export-finder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 381 - TS Dead Export Finder

```rig
import { agent, p, s, defineTool } from "rig";
import { readFile } from "node:fs/promises";

const findUnusedExports = defineTool("findUnusedExports", {
description: "Find exported symbols in a TypeScript file that are not imported elsewhere in the codebase",
parameters: s.object({
filePath: s.path,
allFilePaths: s.array(s.path),
}),
handler: async ({ filePath, allFilePaths }: { filePath: string; allFilePaths: string[] }) => {
const content = await readFile(filePath, "utf-8");
const exportMatches = [...content.matchAll(/^export\s+(?:function|const|class|type|interface|enum)\s+(\w+)/gm)];
const exportedNames = exportMatches.map((m) => m[1]);

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 findUnusedExports tool reads every other file for every exported name — O(files × exports) sequential readFile calls with no caching. On a 200-file codebase with 20 exports each, this is 4000 file reads. The pattern also defeats the async runtime by await-ing inside nested loops.

💡 Cache file contents up front

Read all files once into a Map before the inner loop:

handler: async ({ filePath, allFilePaths }) => {
  const files = new Map(
    await Promise.all(allFilePaths.map(async p => [p, await readFile(p, "utf-8").catch(() => "")] as const))
  );
  const content = files.get(filePath) ?? "";
  const exportedNames = [...content.matchAll(/^export\s+(?:function|const|class|type|interface|enum)\s+(\w+)/gm)]
    .map(m => m[1]);
  return exportedNames.filter(name =>
    !allFilePaths.some(p => p !== filePath && new RegExp(`\\b${name}\\b`).test(files.get(p) ?? ""))
  );
},

This reads each file once via parallel Promise.all instead of re-reading every file for every exported name.


const unusedNames: string[] = [];
for (const name of exportedNames) {
let isImported = false;
for (const other of allFilePaths) {
if (other === filePath) continue;
const otherContent = await readFile(other, "utf-8").catch(() => "");
if (new RegExp(`\\b${name}\\b`).test(otherContent)) {

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.

[/diagnosing-bugs] The cross-file search uses \bName\b (word boundary regex) against raw file text, which produces false positives for any identifier that contains the exported name as a substring (e.g. export Button matches ButtonGroup), and misses renamed imports (import { Button as Btn }). This could cause all exports to appear "used" on a large codebase.

💡 More reliable search

A simple improvement: check for the name appearing in an import statement rather than anywhere in the file:

if (new RegExp(`[{,]\\s*${name}\\s*[},]`).test(otherContent)) {
  isImported = true;
  break;
}

For a production tool, consider using ts-morph or the TypeScript compiler API. For a sample this scope is acceptable, but the description should note the limitation so users know not to rely on this for critical dead-code decisions.

isImported = true;
break;
}
}
if (!isImported) unusedNames.push(name);
}
return unusedNames;
},
});

// Agent role: Scan TypeScript source files and identify exported symbols that are never imported elsewhere.
const tsDeadExportFinder = agent({
model: "small",
instructions: p`You are a dead code analyzer.
Discovered TypeScript files: ${p.glob("src/**/*.ts")}
Also check: ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/dist/*' | head -50")}

For each TypeScript file found, call findUnusedExports with that file path and the full list of all TypeScript file paths.
Aggregate results and return the full output schema.`,
output: s.object({
unusedExports: s.record(s.array(s.string)),
totalUnused: s.int,
hasDeadCode: s.boolean,
}),
tools: [findUnusedExports],
});

export default tsDeadExportFinder;
```
46 changes: 46 additions & 0 deletions skills/rig/samples/382-pkg-scripts-documenter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 382 - Pkg Scripts Documenter

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

const inferScriptPurpose = defineTool("inferScriptPurpose", {
description: "Classify an npm script by its command string into a category",
parameters: s.object({
name: s.string,
command: s.string,
}),
handler: ({ command }: { name: string; command: string }) => {
const lower = command.toLowerCase();
if (/\btest\b|jest|vitest|mocha/.test(lower)) return "test" as const;
if (/\bbuild\b|tsc|webpack|rollup|vite build/.test(lower)) return "build" as const;
if (/\blint\b|eslint|prettier/.test(lower)) return "lint" as const;
if (/\brelease\b|publish|changeset/.test(lower)) return "release" as const;
if (/\bdev\b|start|watch|serve/.test(lower)) return "dev" as const;
return "other" as const;
},
});

// Agent role: Read package.json scripts, classify each by purpose, and write a SCRIPTS.md documentation file.
const pkgScriptsDocumenter = agent({
model: "small",
instructions: p`You are a package.json scripts documenter.
Read the package.json: ${p.read("package.json")}

For each script in the "scripts" field, call inferScriptPurpose to classify it.

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 p.write intent here passes literal placeholder content ("## Scripts\n\n<!-- generated -->"), but the agent is supposed to write the actual classified script documentation. The p.write intent is a declarative placeholder for the path — the model fills in the real content at runtime. As written it reads as though the agent will always write the same boilerplate. The intent placeholder content should be a representative example or the actual format expected, not a static stub that misleads the reader.

💡 Use a representative placeholder
${p.write("SCRIPTS.md", "## Scripts\n\n| Script | Category | Command |\n|--------|----------|---------|\n| build | build | tsc |\n")}

This makes it clear to the model (and sample readers) what structure to produce, not just that a write will happen.

Then write a SCRIPTS.md file using ${p.write("SCRIPTS.md", "## Scripts\n\n<!-- generated -->")} as the write intent path.
Return the complete output schema with all scripts, their purposes, categories, and commands.`,
output: s.object({
scripts: s.record(s.object({
purpose: s.string,
category: s.enum("build", "test", "lint", "release", "dev", "other"),
command: s.string,
})),
documentedCount: s.int,
outputFile: s.string,
}),
tools: [inferScriptPurpose],
addons: [repair()],
});

export default pkgScriptsDocumenter;
```
45 changes: 45 additions & 0 deletions skills/rig/samples/383-git-diff-stats-summarizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 383 - Git Diff Stats Summarizer

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

const classifyDiffEntry = defineTool("classifyDiffEntry", {
description: "Classify a git diff --numstat line as added, modified, deleted, or renamed",
parameters: 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.

[/diagnosing-bugs] The classifyDiffEntry logic has "added" and "deleted" inverted — and the first branch is never reachable for actual new files. git diff --numstat uses additions<TAB>deletions<TAB>path, so a new file shows 5 0 file.ts; startsWith("0\t") matches zero additions (i.e. deleted files).

💡 Corrected classification

Actual outputs from the current code:

  • classifyDiffEntry("5\t0\tsrc/new.ts")"modified" ❌ (should be "added")
  • classifyDiffEntry("0\t3\tsrc/gone.ts")"added" ❌ (should be "deleted")

Fixed handler:

handler: ({ line }: { line: string }) => {
  const [adds, dels] = line.split("\t");
  if (line.includes("=>")) return "renamed" as const;
  if (adds !== "0" && dels === "0") return "added" as const;
  if (adds === "0" && dels !== "0") return "deleted" as const;
  return "modified" as const;
},

line: s.string,
}),
handler: ({ line }: { line: string }) => {
if (line.startsWith("0\t")) return "added" as const;
if (line.includes("\t") && line.split("\t")[0] !== "0" && line.split("\t")[1] !== "0") return "modified" as const;
if (line.split("\t")[0] === "0") return "deleted" as const;
if (line.includes("=>")) return "renamed" as const;
return "modified" as const;
},
});

// Agent role: Summarize git diff statistics between HEAD~1 and HEAD, classifying each changed file.
const gitDiffStatsSummarizer = agent({
model: "small",
instructions: p`You are a git diff statistics summarizer.
Run: ${p.bash("git diff --numstat HEAD~1 HEAD 2>/dev/null || git diff --numstat HEAD 2>/dev/null || echo 'no diff available'")}

For each output line, call classifyDiffEntry to get the category.
Parse additions and deletions counts from each line (format: additions deletions filepath).
Return the full output schema with all file stats.`,
output: s.object({
files: s.array(s.object({
path: s.string,
additions: s.int,
deletions: s.int,
category: s.enum("added", "modified", "deleted", "renamed"),
})),
totalAdditions: s.int,
totalDeletions: s.int,
mostChangedFile: s.optional(s.string),
}),
tools: [classifyDiffEntry],
addons: [repair()],
});

export default gitDiffStatsSummarizer;
```
42 changes: 42 additions & 0 deletions skills/rig/samples/384-dotenv-template-generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 384 - Dotenv Template Generator

```rig
import { agent, p, s, defineTool, repair } from "rig";
import { readFile } from "node:fs/promises";

const extractEnvReferences = defineTool("extractEnvReferences", {
description: "Extract all process.env.VAR_NAME references from a TypeScript file",
parameters: s.object({
filePath: s.path,
}),
handler: async ({ filePath }: { filePath: string }) => {
const content = await readFile(filePath, "utf-8").catch(() => "");
const matches = [...content.matchAll(/process\.env\.([A-Z_][A-Z0-9_]*)/g)];
return [...new Set(matches.map((m) => m[1]))];
},
});

// Agent role: Discover all environment variable references in TypeScript source files, compare with .env, and write a .env.template.
const dotenvTemplateGenerator = agent({
model: "small",
instructions: p`You are a dotenv template generator.
Existing .env file (if any): ${p.readOptional(".env")}
TypeScript source files to scan: ${p.glob("src/**/*.ts")}

For each TypeScript file found, call extractEnvReferences to get all process.env references.
Collect all unique env keys referenced across all files.
Compare with keys found in .env (lines starting with KEY=).
Write the template to: ${p.write(".env.template", "# Generated .env template")}
Return the output schema.`,
output: s.object({
templatePath: s.string,
envKeys: s.array(s.string),
undocumentedKeys: s.array(s.string),
templateGenerated: s.boolean,
}),
tools: [extractEnvReferences],
addons: [repair()],
});

export default dotenvTemplateGenerator;
```
52 changes: 52 additions & 0 deletions skills/rig/samples/385-ts-class-hierarchy-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 385 - TS Class Hierarchy Extractor

```rig
import { agent, p, s, defineTool, steering } from "rig";
import { readFile } from "node:fs/promises";

const extractClassInfo = defineTool("extractClassInfo", {
description: "Extract class declaration, extends, and implements info from a TypeScript file",
parameters: s.object({
filePath: s.path,
}),
handler: async ({ filePath }: { filePath: string }) => {
const content = await readFile(filePath, "utf-8").catch(() => "");
const classRegex = /(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s]+))?/g;
const classes: Array<{ name: string; parent: string | null; interfaces: string[]; isAbstract: boolean }> = [];
for (const m of content.matchAll(classRegex)) {
classes.push({
name: m[1],
parent: m[2] ?? null,
interfaces: m[3] ? m[3].split(",").map((s: string) => s.trim()) : [],

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.

[/diagnosing-bugs] The isAbstract detection looks 10 characters before the class keyword: content.slice(m.index - 10, m.index). But the regex already includes (?:abstract\s+)? as its own optional prefix — so when the regex does match an abstract class, m.index points to the start of abstract, not class, meaning the 10-char lookback window is looking at content before the keyword itself and the .includes("abstract") check can never be true for those matches.

💡 Fix: capture the abstract group directly from the regex match

The regex (?:abstract\s+)?class\s+(\w+)... already optionally matches abstract. Just check m[0].startsWith("abstract"):

classes.push({
  name: m[1],
  parent: m[2] ?? null,
  interfaces: m[3] ? m[3].split(",").map((s: string) => s.trim()) : [],
  isAbstract: m[0].startsWith("abstract"),
});

This is simpler and correct — no fragile index arithmetic needed.

isAbstract: content.slice(Math.max(0, m.index! - 10), m.index!).includes("abstract"),
});
}
return classes;
},
});

// Agent role: Extract class hierarchy information across all TypeScript files and report inheritance depth.
const tsClassHierarchyExtractor = agent({
model: "small",
instructions: p`You are a TypeScript class hierarchy extractor.
TypeScript files in project: ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/dist/*' | head -100")}

For each TypeScript file found, call extractClassInfo to get class declarations.
Aggregate all classes across files. Compute inheritance depth for each class by tracing parent chains.
Identify root classes (no parent). Return the output schema.`,
output: s.object({
classes: s.record(s.object({
parent: s.optional(s.string),
interfaces: s.array(s.string),
isAbstract: s.boolean,
depth: s.int,
})),
maxDepth: s.int,
rootClasses: s.array(s.string),
}),
tools: [extractClassInfo],
addons: [steering()],
});

export default tsClassHierarchyExtractor;
```
42 changes: 42 additions & 0 deletions skills/rig/samples/386-git-bisect-helper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 386 - Git Bisect Helper

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

const selectMidpoint = defineTool("selectMidpoint", {
description: "Select the midpoint commit hash from a list of commit hashes for binary search",
parameters: s.object({
commits: s.array(s.string),
}),
handler: ({ commits }: { commits: string[] }) => {
if (commits.length === 0) return null;
return commits[Math.floor(commits.length / 2)];
},
});

// Agent role: Help identify a suspect commit using binary search over the recent git log.
const gitBisectHelper = agent({
model: "small",
instructions: p`You are a git bisect helper that uses binary search to find a suspect commit.
Recent git log: ${p.bash("git log --oneline -20 2>/dev/null || echo 'no git log available'")}

Use the selectMidpoint tool to identify candidate commits step by step.
Perform up to 8 binary search steps over the commit list.
After narrowing down, return your best suspect commit, how many steps you took,
the commit range you searched, and your confidence level.`,
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 agent's prompt says "Perform up to 8 binary search steps" but the output schema has no way to communicate which commit was actually checked at each step, so the model can't justify how it narrowed from the full list to suspectCommit. The stepsRun field is returned but never grounded by tool calls — the model will fabricate a narrative. Consider tracking checkedCommits: s.array(s.string) in the output or using a loop structure that makes each selectMidpoint call observable.

💡 Add a checkedCommits field to the output
output: s.object({
  suspectCommit: s.optional(s.string),
  stepsRun: s.int,
  checkedCommits: s.array(s.string),  // add this
  commitRange: s.object({ start: s.string, end: s.string }),
  confidence: s.enum("high", "medium", "low"),
}),

This forces the model to materialise each binary search step as a real selectMidpoint call result rather than inventing a story.

suspectCommit: s.optional(s.string),
stepsRun: s.int,
commitRange: s.object({
start: s.string,
end: s.string,
}),
confidence: s.enum("high", "medium", "low"),
}),
tools: [selectMidpoint],
maxTurns: 8,
addons: [steering()],
});

export default gitBisectHelper;
```
50 changes: 50 additions & 0 deletions skills/rig/samples/387-shell-shebang-validator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 387 - Shell Shebang Validator

```rig
import { agent, p, s, defineTool, repair } from "rig";
import { readFile } from "node:fs/promises";

const checkShebangLine = defineTool("checkShebangLine", {
description: "Check if a shell script has a valid shebang line on the first line",
parameters: s.object({
filePath: s.path,
}),
handler: async ({ filePath }: { filePath: string }) => {
const content = await readFile(filePath, "utf-8").catch(() => "");
const firstLine = content.split("\n")[0] ?? "";
const hasShebang = firstLine.startsWith("#!");
const standardShebangs = ["#!/bin/sh", "#!/bin/bash", "#!/usr/bin/env bash", "#!/usr/bin/env sh"];
const isStandard = standardShebangs.some((s) => firstLine.startsWith(s));
return {
hasShebang,
shebangLine: hasShebang ? firstLine : undefined,
isStandard,
};
},
});

// Agent role: Validate shebang lines in all shell scripts found in the workspace.
const shellShebangValidator = agent({
model: "small",
instructions: p`You are a shell script shebang validator.
Shell scripts found: ${p.glob("**/*.sh")}

For each shell script, call checkShebangLine to verify the shebang.
Count files missing a shebang and those with a standard shebang.
Return the output schema.`,
output: s.object({
files: s.record(s.object({
hasShebang: s.boolean,
shebangLine: s.optional(s.string),
isStandard: s.boolean,
})),
missingShebangCount: s.int,
standardShebangCount: s.int,
totalFiles: s.int,
}),
tools: [checkShebangLine],
addons: [repair()],
});

export default shellShebangValidator;
```
48 changes: 48 additions & 0 deletions skills/rig/samples/388-git-log-graph-summarizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 388 - Git Log Graph Summarizer

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

const parseGraphLine = defineTool("parseGraphLine", {
description: "Parse a git log --graph line and classify its type",
parameters: s.object({
line: s.string,
}),
handler: ({ line }: { line: string }) => {
const hashMatch = line.match(/\b([0-9a-f]{7,})\b/);
const hash = hashMatch?.[1];
const messageMatch = line.match(/[0-9a-f]{7,}\s+(.+)$/);
const message = messageMatch?.[1];

if (/Merge/.test(line) && hash) return { type: "merge" as const, hash, message };
if (hash) return { type: "commit" as const, hash, message };
if (/[|\\\/]/.test(line) && !hash) return { type: "branch-point" as const, hash: undefined, message: undefined };
return { type: "decoration" as const, hash: undefined, message: undefined };
},
});

// Agent role: Parse and summarize a git log graph output to extract commit, merge, and branch-point counts.
const gitLogGraphSummarizer = agent({
model: "small",
instructions: p`You are a git log graph summarizer.
Git log graph: ${p.bash("git log --oneline --graph -20 2>/dev/null || echo 'no git log available'")}

For each line of the graph output, call parseGraphLine to classify it.
Count merges, commits, and branch-points.
Return the output schema.`,
output: s.object({
lines: s.array(s.object({
type: s.enum("merge", "commit", "branch-point", "decoration"),
hash: s.optional(s.string),
message: s.optional(s.string),
})),
mergeCount: s.int,
commitCount: s.int,
branchPoints: s.int,
}),
tools: [parseGraphLine],
addons: [steering()],
});

export default gitLogGraphSummarizer;
```
Loading
Loading