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
32 changes: 32 additions & 0 deletions skills/rig/samples/341-release-note-enricher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 341 - Release Note Enricher

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

const lookupTicketMetadata = defineTool("lookupTicketMetadata", {
description: "Extract ticket references (#NNN or PROJ-NNN) from text and return metadata",
parameters: s.object({ text: s.string }),
handler({ text }) {
const githubRefs = [...text.matchAll(/#(\d+)/g)].map((m) => `#${m[1]}`);
const jiraRefs = [...text.matchAll(/\b([A-Z]+-\d+)\b/g)].map((m) => m[1]);
return { githubRefs, jiraRefs, all: [...githubRefs, ...jiraRefs] };
},
});

// Agent role: enrich raw release notes by extracting ticket references, grouping into sections, assigning a risk label, and listing unresolved references.
const releaseNoteEnricher = agent({
model: "small",
input: s.object({ rawNotes: s.string }),
instructions: p`Use the lookupTicketMetadata tool to find all ticket references (#NNN, PROJ-NNN) in the raw release notes from the input. Group the notes into logical sections (e.g., Features, Bug Fixes, Breaking Changes). Assign a riskLabel based on content severity. List any ticket references that appear to be missing or unresolvable.`,
output: s.object({
sections: s.array(s.string),
riskLabel: s.enum("low", "medium", "high", "critical"),
missingReferences: s.array(s.string),
}),
tools: [lookupTicketMetadata],
maxTurns: 5,
addons: [steering(), repair()],
});

export default releaseNoteEnricher;
```
35 changes: 35 additions & 0 deletions skills/rig/samples/342-docs-refactor-coordinator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 342 - Docs Refactor Coordinator

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

// Agent role: extract every API name, function signature, and method reference from the documentation.
const apiExtractor = agent({
name: "apiExtractor",
model: "small",
instructions: p`Read: ${p.read("README.md")}. Extract all API names, function signatures, and method references. Return a flat list of unique identifiers.`,
output: s.array(s.string),
});

// Agent role: rewrite documentation prose to be clearer and more concise.
const proseCleanup = agent({
name: "proseCleanup",
model: "small",
instructions: p`Read: ${p.read("README.md")}. Rewrite the prose sections for clarity and conciseness without altering technical accuracy. Return the improved markdown text.`,
output: s.string,
});

// Agent role: coordinate API extraction and prose cleanup, then write a refactored docs file.
const docsRefactorCoordinator = agent({
model: "small",
instructions: p`Delegate to apiExtractor and proseCleanup subagents. Combine their outputs: extractedApis from apiExtractor and improved prose from proseCleanup. Write the refactored content to ${p.write("docs/refactored.md", "REFACTORED_CONTENT")}. Count the number of substantive prose changes made.`,
output: s.object({
extractedApis: s.array(s.string),
changesApplied: s.int,
outputPath: s.path,
}),
agents: { apiExtractor, proseCleanup },
});

export default docsRefactorCoordinator;
```
47 changes: 47 additions & 0 deletions skills/rig/samples/343-dockerfile-layer-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 343 - Dockerfile Layer Analyzer

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

const parseLayer = defineTool("parseLayer", {
description: "Classify a Dockerfile instruction line for cache-friendliness and weight",
parameters: s.object({ line: s.string }),
handler({ line }) {
const trimmed = line.trim();
const spaceIdx = trimmed.indexOf(" ");
const instruction = spaceIdx >= 0 ? trimmed.slice(0, spaceIdx).toUpperCase() : trimmed.toUpperCase();
const args = spaceIdx >= 0 ? trimmed.slice(spaceIdx + 1) : "";
const isHeavy = instruction === "RUN" && /apt-get|npm install|pip install|yarn|apk add/.test(args);
const cacheable = (instruction === "COPY" || instruction === "ADD") && /package\.json|requirements\.txt|go\.mod/.test(args);
const tip = isHeavy ? "Combine adjacent RUN commands to reduce layers" as const
: cacheable ? null
: instruction === "COPY" ? "Copy dependency manifests first for better cache" as const
: null;
return { instruction, cacheable, isHeavy, tip };
},
});

// Agent role: analyze Dockerfile layers for cache efficiency and suggest optimizations.
const dockerfileLayerAnalyzer = agent({
model: "small",
instructions: p`Dockerfile content: ${p.readOptional("Dockerfile", "# no Dockerfile found")}

Parse each non-empty, non-comment instruction line. Call parseLayer for each. Count totalLayers. Set optimizable to true if any layer has isHeavy true or a non-null tip. Collect all non-null tips into suggestions.`,
output: s.object({
layers: s.array(s.object({
instruction: s.string,
cacheable: s.boolean,
isHeavy: s.boolean,
tip: s.optional(s.string),
})),
totalLayers: s.int,
optimizable: s.boolean,
suggestions: s.array(s.string),
}),
tools: [parseLayer],
addons: [repair()],
maxTurns: 5,
});

export default dockerfileLayerAnalyzer;
```
38 changes: 38 additions & 0 deletions skills/rig/samples/344-git-worktree-status-reporter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 344 - Git Worktree Status Reporter

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

const classifyWorktree = defineTool("classifyWorktree", {
description: "Classify a git worktree entry as clean, dirty, bare, or detached",
parameters: s.object({ path: s.string, branch: s.string, statusOutput: s.string }),
handler({ branch, statusOutput }) {
if (branch === "(bare)") return { status: "bare" as const };
if (branch.startsWith("(HEAD detached")) return { status: "detached" as const };
if (statusOutput.trim().length > 0) return { status: "dirty" as const };
return { status: "clean" as const };
},
});

// Agent role: report the status of all git worktrees in the repository.
const gitWorktreeStatusReporter = agent({
model: "small",
instructions: p`Worktree list: ${p.bash("git worktree list --porcelain")}

Parse each worktree block (separated by blank lines). For each worktree, call classifyWorktree with its path, branch, and an empty statusOutput (use "dirty" heuristic based on porcelain output if available). Return the list of worktrees with their statuses, total count, and whether all are clean.`,
output: s.object({
worktrees: s.array(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] The classifyWorktree tool is never able to return "dirty" — the instructions always pass an empty statusOutput. The "dirty" branch in the handler is dead code, making the tool's description misleading.

💡 Suggested fix

Either thread real git status --porcelain <path> output through the instructions, or drop the statusOutput parameter and rely solely on the porcelain block fields the model already has:

// in instructions, for each worktree path:
// statusOutput: ${p.bash("git -C <path> status --porcelain")}

As written, readers who study the tool will expect dirty detection to work, but it never fires.

path: s.path,
branch: s.string,
status: s.enum("clean", "dirty", "bare", "detached"),
})),
totalWorktrees: s.int,
allClean: s.boolean,
}),
tools: [classifyWorktree],
addons: [repair()],
maxTurns: 4,
});

export default gitWorktreeStatusReporter;
```
50 changes: 50 additions & 0 deletions skills/rig/samples/345-npm-audit-simplifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 345 - NPM Audit Simplifier

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

const classifyVuln = defineTool("classifyVuln", {
description: "Classify an npm vulnerability as direct or transitive and produce a recommendation",
parameters: s.object({
name: s.string,
severity: s.string,
isDirect: s.boolean,
}),
handler({ name, severity, isDirect }) {
const sev = severity.toLowerCase();
const scope = isDirect ? "direct" : "transitive";
const recommendation =
sev === "critical" ? `Upgrade ${name} immediately (${scope} dependency)` :
sev === "high" ? `Schedule upgrade for ${name} (${scope} dependency)` :
isDirect ? `Review ${name} — ${sev} severity direct dependency` :
`Monitor ${name} — ${sev} severity transitive dependency`;
return { scope, recommendation };
},
});

// Agent role: simplify npm audit output into an actionable vulnerability summary.
const npmAuditSimplifier = agent({
model: "small",
instructions: p`npm audit output: ${p.bash("npm audit --json 2>/dev/null || echo '{\"vulnerabilities\":{}}'")
}

Parse the audit JSON. For each vulnerability, call classifyVuln with its name, severity, and whether it is a direct dependency. Count criticalCount and highCount. Choose action: urgent if any critical, scheduled if any high, monitor if any moderate/low, none if no vulnerabilities. Write a one-line summary.`,
output: s.object({
vulnerabilities: s.array(s.object({
name: s.string,
severity: s.string,
isDirect: s.boolean,
recommendation: s.string,
})),
criticalCount: s.int,
highCount: s.int,
summary: s.string,
action: s.enum("urgent", "scheduled", "monitor", "none"),
}),
tools: [classifyVuln],
addons: [repair()],
maxTurns: 4,
});

export default npmAuditSimplifier;
```
44 changes: 44 additions & 0 deletions skills/rig/samples/346-js-ast-node-counter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 346 - JS AST Node Counter

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

const countAstNodes = defineTool("countAstNodes", {
description: "Count function/arrow/class/import/export patterns in a TypeScript file using regex heuristics",
parameters: s.object({ filePath: s.path }),
handler: async ({ filePath }: { filePath: string }) => {
let source = "";
try { source = await readFile(filePath, "utf8"); } catch { return { functions: 0, arrows: 0, classes: 0, imports: 0, exports: 0 }; }
const functions = (source.match(/\bfunction\s+\w+/g) || []).length;
const arrows = (source.match(/=>\s*[{(]/g) || []).length;
const classes = (source.match(/\bclass\s+\w+/g) || []).length;
const imports = (source.match(/^import\b/gm) || []).length;
const exports = (source.match(/^export\b/gm) || []).length;
return { functions, arrows, classes, imports, exports };
},
});

// Agent role: count AST-like node patterns in each TypeScript source file and identify the most complex file.
const jsAstNodeCounter = agent({
model: "small",
instructions: p`TypeScript files in this workspace: ${p.glob("src/**/*.ts")}

For each file path listed, call countAstNodes to get pattern counts. Build a record keyed by file path. Sum all functions across files into totalFunctions. Set mostComplexFile to the file with the highest combined function+arrow+class count (omit if no files found).`,
output: s.object({
files: s.record(s.object({
functions: s.int,
arrows: s.int,
classes: s.int,
imports: s.int,
exports: s.int,
})),
totalFunctions: s.int,
mostComplexFile: s.optional(s.path),
}),
tools: [countAstNodes],
maxTurns: 6,
});

export default jsAstNodeCounter;
```
45 changes: 45 additions & 0 deletions skills/rig/samples/347-python-requirements-risk-mapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 347 - Python Requirements Risk Mapper

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

const classifyPackageRisk = defineTool("classifyPackageRisk", {
description: "Classify a Python package's risk level based on name and version heuristics",
parameters: s.object({ name: s.string, version: s.string }),
handler({ name, version }) {
const knownLegacy = ["django", "flask", "requests", "urllib3", "cryptography", "pillow", "numpy"];
const versionParts = version.split(".").map(Number);
const major = versionParts[0] ?? 0;
const isOutdated = major === 0 || (knownLegacy.includes(name.toLowerCase()) && major < 2);
const riskLevel: "low" | "medium" | "high" =

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 knownLegacy heuristic flags e.g. requests@2.31 as "high" risk because major < 2 is false — wait, actually the condition is major < 2, so requests@2.x would be isOutdated=false and riskLevel="low". But django@3.x (well above the < 2 threshold) would also be low. The threshold of major < 2 conflates "version 0/1" with "old", which gives false negatives for packages where the current major is 4+. This means a well-known package at 1.x that is outdated gets "high", but django@1.x (end-of-life since 2017) correctly gets "high". The logic works for django specifically but the variable name knownLegacy implies all listed packages follow the same versioning, which is misleading.

💡 Suggestion

Document the assumption inline so sample readers understand the intent:

// heuristic: packages in this list are considered outdated if still on major version 0 or 1
const legacyMajorThreshold: Record<string, number> = {
  django: 2, flask: 1, requests: 2, urllib3: 2, cryptography: 3, pillow: 9, numpy: 1,
};
const threshold = legacyMajorThreshold[name.toLowerCase()] ?? 1;
const isOutdated = major < threshold;

This avoids the misleading knownLegacy name and makes per-package thresholds explicit.

isOutdated && knownLegacy.includes(name.toLowerCase()) ? "high" as const
: isOutdated ? "medium" as const
: "low" as const;
return { isOutdated, riskLevel };
},
});

// Agent role: map Python requirements to risk levels and recommend an action.
const pythonRequirementsRiskMapper = agent({
model: "small",
instructions: p`requirements.txt: ${p.readOptional("requirements.txt", "# no requirements.txt found")}
installed packages: ${p.bash("pip list --format=json 2>/dev/null || echo '[]'")}

For each package found in requirements.txt or the installed list, call classifyPackageRisk with its name and version. Build a packages record. Count riskyCount (medium or high risk). Choose recommendedAction: audit if any high-risk, review if any medium-risk, ok otherwise.`,
output: s.object({
packages: s.record(s.object({
version: s.string,
isOutdated: s.boolean,
riskLevel: s.enum("low", "medium", "high"),
})),
totalPackages: s.int,
riskyCount: s.int,
recommendedAction: s.enum("audit", "review", "ok"),
}),
tools: [classifyPackageRisk],
addons: [steering()],
maxTurns: 5,
});

export default pythonRequirementsRiskMapper;
```
42 changes: 42 additions & 0 deletions skills/rig/samples/348-git-merge-complexity-scorer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 348 - Git Merge Complexity Scorer

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

const scoreMergeComplexity = defineTool("scoreMergeComplexity", {
description: "Classify a git merge commit as simple, moderate, or complex based on files changed",
parameters: s.object({ hash: s.string, message: s.string, filesChanged: s.int }),
handler({ hash, message, filesChanged }) {
const complexity: "simple" | "moderate" | "complex" =
filesChanged <= 3 ? "simple" as const
: filesChanged <= 10 ? "moderate" as const
: "complex" as const;
return { hash, message, filesChanged, complexity };
},
});

// Agent role: score git merge commits by complexity based on the number of files changed.
const gitMergeComplexityScorer = agent({
model: "small",
instructions: p`Recent merge commits: ${p.bash("git log --merges --oneline -20 2>/dev/null || echo ''")}
Merge diff stats: ${p.bash("git log --merges --oneline -20 --format='%H %s' 2>/dev/null | head -20 | while read hash msg; do count=$(git show --stat $hash 2>/dev/null | grep -E 'files? changed' | grep -oE '[0-9]+ files? changed' | grep -oE '^[0-9]+' || echo 0); echo \"$hash|$count|$msg\"; done")}

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] Unquoted $hash and $msg variables in the shell pipeline risk word-splitting: a commit message with spaces will break the read hash msg assignment, and a hash that expands to multiple tokens will silently corrupt git show --stat.

💡 Safer alternative

Use --format with a delimiter that avoids word-splitting, or quote the variables:

git log --merges -20 --format='%H|||%s' | while IFS='|||' read hash msg; do
  count=$(git show --stat "$hash" 2>/dev/null | grep -oE '^[0-9]+(?= files? changed)' || echo 0)
  echo "$hash|$count|$msg"
done

As a sample, this shell snippet is what readers will copy into real pipelines.

For each merge commit, call scoreMergeComplexity with its hash, message, and filesChanged count. Count totalMerges and complexMergeCount. Set mostComplexMerge to the hash with the highest filesChanged (omit if no merges).`,
output: s.object({
merges: s.array(s.object({
hash: s.string,
message: s.string,
filesChanged: s.int,
complexity: s.enum("simple", "moderate", "complex"),
})),
totalMerges: s.int,
complexMergeCount: s.int,
mostComplexMerge: s.optional(s.string),
}),
tools: [scoreMergeComplexity],
addons: [repair()],
maxTurns: 5,
});

export default gitMergeComplexityScorer;
```
42 changes: 42 additions & 0 deletions skills/rig/samples/349-ts-reexport-chain-tracer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 349 - TS Reexport Chain Tracer

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

const traceReexports = defineTool("traceReexports", {
description: "Scan a TypeScript file for re-export patterns and return referenced files and symbols",
parameters: s.object({ filePath: s.path }),
handler: async ({ filePath }: { filePath: string }) => {
let source = "";
try { source = await readFile(filePath, "utf8"); } catch { return { references: [] }; }
const starMatches = [...source.matchAll(/export\s+\*\s+from\s+['"]([^'"]+)['"]/g)];
const namedMatches = [...source.matchAll(/export\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g)];
const references = [

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 arrow-function parameter s in .map((s: string) => s.trim()) shadows the outer s schema import from rig, which is confusing and error-prone if this snippet is reused.

💡 Suggested fix

Rename to avoid the shadow:

...namedMatches.map((m) => ({ file: m[2], symbols: m[1].split(",").map((sym: string) => sym.trim()) })),

The TypeScript compiler won't catch this because both values are string, but it violates the principle of least surprise for readers.

...starMatches.map((m) => ({ file: m[1], symbols: ["*"] })),
...namedMatches.map((m) => ({ file: m[2], symbols: m[1].split(",").map((s: string) => s.trim()) })),
];
return { references };
},
});

// Agent role: trace the TypeScript re-export chain starting from an entry file and detect circular re-exports.
const tsReexportChainTracer = agent({
model: "small",
input: s.object({ entryFile: s.path }),
instructions: p`Starting from the entryFile in the input, call traceReexports to discover re-export chains. Follow the chain up to 3 levels deep. Build a list of chain entries (file + symbols). Detect if any file appears more than once in the chain (circularDetected). Report the total chain depth.`,
output: s.object({
chain: s.array(s.object({
file: s.string,
symbols: s.array(s.string),
})),
chainDepth: s.int,
circularDetected: s.boolean,
}),
tools: [traceReexports],
addons: [steering()],
maxTurns: 6,
});

export default tsReexportChainTracer;
```
Loading