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
64 changes: 64 additions & 0 deletions skills/rig/samples/461-pkg-scripts-trio-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# 461 - Package Scripts Trio Workflow

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

// Agent role: list all npm scripts from package.json.
const scriptsLister = agent({
name: "scriptsLister",
model: "small",
instructions: p`List all npm scripts from this project.
${p.read("package.json")}
Return all script names from the "scripts" field as an array.`,
output: s.object({ scripts: s.array(s.string) }),
});

// Agent role: categorize npm scripts by purpose.
const scriptsCategorizer = agent({
name: "scriptsCategorizer",
model: "small",
input: s.object({ scripts: s.array(s.string) }),
instructions: `Classify each script name into build, test, lint, release, utility, or other.
Return a record mapping category to the list of script names in it, plus the dominantCategory.`,
output: s.object({
categories: s.record(s.array(s.string)),
dominantCategory: s.enum("build", "test", "lint", "release", "utility", "other"),
}),
});

// Agent role: check installed dependency health via npm ls.
const scriptsHealthChecker = agent({
name: "scriptsHealthChecker",
model: "small",
instructions: p`Check installed npm dependency health.
${p.bash("npm ls --depth=0 2>&1 | tail -30")}
List any packages that appear missing or broken, and classify overall health.`,
output: s.object({
missingDeps: s.array(s.string),
dependencyHealth: s.enum("ok", "warnings", "errors"),
}),
});

// Workflow role: run three package.json analysis agents and produce an overall health verdict.
export default workflow({
meta: { name: "pkg-scripts-trio", description: "Three-agent package.json scripts and dependency analysis." },
body: async ({ call, phase }) => {
phase("Collect");
const [listed, health] = await Promise.all([
call(scriptsLister, "list scripts"),
call(scriptsHealthChecker, "check dependency health"),
]);
const scripts = listed?.scripts ?? [];
phase("Categorize");
const categorized = await call(scriptsCategorizer, { scripts });
phase("Summarize");
return call.json(
`scripts=${JSON.stringify(scripts)} categories=${JSON.stringify(categorized?.categories ?? {})} missingDeps=${JSON.stringify(health?.missingDeps ?? [])} dependencyHealth=${health?.dependencyHealth ?? "ok"}. Determine overallHealth: healthy if dependencyHealth=ok and scripts non-empty, needs-attention if warnings or empty scripts, critical if errors or missing deps.`,
s.object({
overallHealth: s.enum("healthy", "needs-attention", "critical"),
summary: s.string,
}),
);
},

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] call.json receives a large prose prompt that embeds JSON via template literals. If any JSON.stringify value is long or contains special characters the resulting string becomes hard to read and can drift from the intended semantics as a sample.

💡 Preferred pattern: structured input agent

Other workflow samples (e.g. 469) pass structured data to a typed input: agent instead of embedding it in a freeform call.json string. Consider making Summarize a proper agent with input: s.object({...}) so the pattern being demonstrated is consistent with the rest of the samples collection.

});
```
55 changes: 55 additions & 0 deletions skills/rig/samples/462-ts-const-enum-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 462 - TypeScript Const Enum Extractor

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


const extractConstEnums = defineTool("extractConstEnums", {
description: "Read a TypeScript file and extract all const enum declarations with their members.",
parameters: s.object({ filePath: s.path("TypeScript file path") }),
async handler({ filePath }) {
const { readFile } = await import("node:fs/promises");
const src = await readFile(filePath, "utf8");
const enumRe = /const\s+enum\s+(\w+)\s*\{([^}]*)\}/g;
const result: Record<string, { name: string; value: string | null }[]> = {};
let m: RegExpExecArray | null;
while ((m = enumRe.exec(src)) !== null) {
const enumName = m[1];
const body = m[2];
const members = body
.split(",")
.map((line: string) => line.trim())
.filter((line: string) => line.length > 0)
.map((line: string) => {
const [name, value] = line.split("=").map((s: string) => s.trim());
return { name, value: value ?? null };

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 regex /const\s+enum\s+(\w+)\s*\{([^}]*)\}/g doesn't handle multi-line enum bodies where the closing } appears on its own line — common in TypeScript. [^}]* is greedy but stops at the first }, so enums with nested expressions or trailing commas followed by whitespace may be skipped or incorrectly truncated.

💡 Suggested approach

Use the s (dotAll) flag so . matches newlines, or replace [^}]* with [\s\S]*? for a non-greedy match across lines:

const enumRe = /const\s+enum\s+(\w+)\s*\{([\s\S]*?)\}/g;

This is a sample, so a note in the instructions or a comment in the code acknowledging this limitation would also be acceptable.

});
result[enumName] = members;
}
return JSON.stringify(result);
},
});

// Agent role: find and extract all TypeScript const enum declarations across the workspace.
const tsConstEnumExtractor = agent({
name: "tsConstEnumExtractor",
model: "small",
instructions: p`Scan all TypeScript source files for const enum declarations.
Files: ${p.glob("src/**/*.ts")}
Use extractConstEnums on each file. Aggregate results: enums record (members array, memberCount, sourceFile), totalEnums, totalMembers, largestEnum (name with most members, if any).`,
output: s.object({
enums: s.record(s.object({
members: s.array(s.object({ name: s.string, value: s.optional(s.string) })),
memberCount: s.int,
sourceFile: s.path,
})),
totalEnums: s.int,
totalMembers: s.int,
largestEnum: s.optional(s.string),
}),
tools: [extractConstEnums],
addons: [steering()],
});

export default tsConstEnumExtractor;
```
48 changes: 48 additions & 0 deletions skills/rig/samples/463-http-access-log-stats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 463 - HTTP Access Log Stats

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


const parseLogLine = defineTool("parseLogLine", {
description: "Parse a single HTTP access log line and classify its HTTP status.",
parameters: s.object({ line: s.string("Raw access log line") }),
handler({ line }) {
const parts = line.split(" ");
const statusStr = parts.find((p: string) => /^\d{3}$/.test(p)) ?? "0";
const status = parseInt(statusStr, 10);
let statusClass: "2xx" | "3xx" | "4xx" | "5xx" | "other";
if (status >= 200 && status < 300) statusClass = "2xx";
else if (status >= 300 && status < 400) statusClass = "3xx";
else if (status >= 400 && status < 500) statusClass = "4xx";
else if (status >= 500 && status < 600) statusClass = "5xx";
else statusClass = "other";

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] p.readInput("logFile") inlines the log file content into the prompt, which is unbounded. A real access log can be hundreds of MB — the intent is clearly to use parseLogLine on each line, but the instructions ask the model to first read the whole file into context before calling the tool.

💡 Suggested alternative

Pass the path to the tool and read line-by-line inside the handler instead of via p.readInput. The tool signature already accepts a line string, so add a companion readLogLines tool (or use p.bash("head -1000 ...") to bound the input) so the sample doesn't imply full-file inlining as the pattern.

const pathMatch = line.match(/"(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([^\s"]+)/);
const path = pathMatch ? pathMatch[1] : "/";
return JSON.stringify({ status, statusClass, path });
},
});

// Agent role: parse an HTTP access log file and compute request statistics.
const httpAccessLogStats = agent({
name: "httpAccessLogStats",
model: "small",
input: s.object({ logFile: s.path("Path to HTTP access log file") }),
instructions: p`Read the HTTP access log at the specified path using ${p.readInput("logFile")}.
Use parseLogLine on each non-empty line to classify its status. Aggregate:
- statusCounts: count per status class (2xx, 3xx, 4xx, 5xx, other)
- topPaths: top 5 most frequent request paths
- totalRequests: total line count
- errorRate: (4xx + 5xx) / total as a fraction 0-1`,
output: s.object({
statusCounts: s.record(s.int),
topPaths: s.array(s.string),
totalRequests: s.int,
errorRate: s.number,
}),
tools: [parseLogLine],
addons: [repair()],
});

export default httpAccessLogStats;
```
41 changes: 41 additions & 0 deletions skills/rig/samples/464-ts-spread-usage-counter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 464 - TypeScript Spread Usage Counter

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


const countSpreadPatterns = defineTool("countSpreadPatterns", {
description: "Count object spread and array spread usages in a TypeScript file.",
parameters: s.object({ filePath: s.path("TypeScript file path") }),
async handler({ filePath }) {
const { readFile } = await import("node:fs/promises");
const src = await readFile(filePath, "utf8");
const objectSpreads = (src.match(/\.\.\.[a-zA-Z_$][a-zA-Z0-9_$]*(?=\s*[,}])/g) ?? []).length;
const arraySpreads = (src.match(/\.\.\.[a-zA-Z_$][a-zA-Z0-9_$]*(?=\s*[,\]])/g) ?? []).length;

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] Both regex lookaheads include , so every ...foo, (spread before a comma) is counted in both objectSpreads and arraySpreads, silently doubling all spreads in multi-element literals.

💡 Suggested fix

Use mutually exclusive lookaheads — } for object, ] for array:

const objectSpreads = (src.match(/\.\.\.[a-zA-Z_$][a-zA-Z0-9_$]*(?=\s*})/g) ?? []).length;
const arraySpreads  = (src.match(/\.\.\.[a-zA-Z_$][a-zA-Z0-9_$]*(?=\s*\])/g) ?? []).length;

Spreads followed by , are context-ambiguous from a pure-regex perspective; document that limitation or tally them separately as ambiguousSpreads.

return JSON.stringify({ objectSpreads, arraySpreads });
},
});

// Agent role: count object spread and array spread patterns across all TypeScript source files.
const tsSpreadUsageCounter = agent({
name: "tsSpreadUsageCounter",
model: "small",
instructions: p`Scan all TypeScript source files for spread operator usage.
Files: ${p.glob("src/**/*.ts")}
Use countSpreadPatterns on each file. Aggregate into:
- files: record mapping filePath to { objectSpreadCount, arraySpreadCount }
- totalObjectSpreads: sum of all object spreads
- totalArraySpreads: sum of all array spreads
- mostSpreadFile: path of the file with the highest combined spread count (null if none)`,
output: s.object({
files: s.record(s.object({ objectSpreadCount: s.int, arraySpreadCount: s.int })),
totalObjectSpreads: s.int,
totalArraySpreads: s.int,
mostSpreadFile: s.optional(s.path),
}),
tools: [countSpreadPatterns],
addons: [steering()],
});

export default tsSpreadUsageCounter;
```
50 changes: 50 additions & 0 deletions skills/rig/samples/465-git-hook-file-scanner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 465 - Git Hook File Scanner

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


const analyzeHookFile = defineTool("analyzeHookFile", {
description: "Read a git hook file and detect its shebang, executability, and type.",
parameters: s.object({ hookPath: s.path("Full path to the hook file") }),
async handler({ hookPath }) {
const { readFile, stat } = await import("node:fs/promises");
try {
const [content, info] = await Promise.all([readFile(hookPath, "utf8"), stat(hookPath)]);
const shebang = content.split("\n")[0] ?? "";
const executable = !!(info.mode & 0o111);
const name = hookPath.split("/").pop() ?? hookPath;
const knownTypes = ["pre-commit", "commit-msg", "post-commit", "pre-push", "pre-receive"];
const hookType = knownTypes.includes(name) ? name : "other";
const lineCount = content.split("\n").length;
return JSON.stringify({ shebang, executable, hookType, lineCount });
} catch {
return JSON.stringify({ error: "could not read hook" });
}
},
});

// Agent role: scan .git/hooks for installed hook scripts and report their properties.
const gitHookFileScanner = agent({

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] When analyzeHookFile catches a filesystem error it returns { error: "could not read hook" }, but the tool's return type is inferred as string (JSON). The output schema for hooks is s.record(s.object({ shebang, executable, hookType, lineCount })), so if the model faithfully includes errored hooks in the record the validation will fail and trigger repair.

💡 Options
  1. Return a sentinel object that still matches the schema (e.g. shebang: "", executable: false, hookType: "other", lineCount: 0) and set an error flag outside the record.
  2. Return an empty string / throw so the model skips the path entirely.
  3. Add an optional error field to the schema so the repair cycle knows what happened.

As a sample this matters because readers will copy the error-handling pattern.

name: "gitHookFileScanner",
model: "small",
instructions: p`List all files in the .git/hooks directory.
${p.bash("ls -1 .git/hooks/ 2>/dev/null || echo 'no hooks directory'")}
For each non-sample file, use analyzeHookFile passing the full path (.git/hooks/<name>).
Return hooks as a record keyed by hook name, activeCount (executable hooks), and totalHooks.`,
output: s.object({
hooks: s.record(s.object({
shebang: s.string,
executable: s.boolean,
hookType: s.enum("pre-commit", "commit-msg", "post-commit", "pre-push", "pre-receive", "other"),
lineCount: s.int,
})),
activeCount: s.int,
totalHooks: s.int,
}),
tools: [analyzeHookFile],
addons: [repair()],
});

export default gitHookFileScanner;
```
53 changes: 53 additions & 0 deletions skills/rig/samples/466-merge-strategy-selector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 466 - Merge Strategy Selector

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

// Agent role: analyze git diff --stat to determine which area of the codebase changed most.
const branchDiffAgent = agent({
name: "branchDiffAgent",
model: "small",
instructions: p`Analyze the git diff statistics for the current branch vs main.
${p.bash("git diff --stat origin/main...HEAD 2>/dev/null || git diff --stat HEAD~1...HEAD 2>/dev/null || echo 'no diff available'")}
Determine which area of the codebase was changed most (src/test/config/docs/mixed).`,
output: s.object({
changedFiles: s.int,
insertions: s.int,
deletions: s.int,
dominantArea: s.enum("src", "test", "config", "docs", "mixed"),
}),
});

// Agent role: assess merge conflict risk from git status.
const conflictRiskAgent = agent({
name: "conflictRiskAgent",
model: "small",
instructions: p`Check git working tree status for conflicts.
${p.bash("git status --short 2>/dev/null | head -30")}
Count conflict markers (lines starting with UU, AA, DD) and report conflict risk.`,
output: s.object({
conflictCount: s.int,
hasConflicts: s.boolean,
}),
});

// Workflow role: analyze branch diff and conflict risk, then recommend a merge strategy.
export default workflow({
meta: { name: "merge-strategy-selector", description: "Select optimal merge strategy based on diff and conflict analysis." },
body: async ({ call, phase }) => {
phase("Analyze");
const [diffResult, conflictResult] = await Promise.all([
call(branchDiffAgent, "analyze diff"),
call(conflictRiskAgent, "check conflicts"),
]);
phase("Recommend");
return call.json(
`dominantArea=${diffResult?.dominantArea} changedFiles=${diffResult?.changedFiles} hasConflicts=${conflictResult?.hasConflicts} conflictCount=${conflictResult?.conflictCount}. Choose mergeRecommendation: fast-forward (few files, no conflicts, src-only), squash (many small commits, clean), merge (mixed areas), rebase (linear history preferred, no conflicts).`,
s.object({
mergeRecommendation: s.enum("fast-forward", "squash", "merge", "rebase"),
rationale: s.string,
}),
);
},
});
```
48 changes: 48 additions & 0 deletions skills/rig/samples/467-ini-config-parser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 467 - INI Config Parser

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


const parseIniSection = defineTool("parseIniSection", {
description: "Parse key-value pairs from an INI config file section.",
parameters: s.object({ content: s.string("Full INI file content"), section: s.string("Section name to parse") }),
handler({ content, section }) {
const lines = content.split("\n");
const sectionRe = new RegExp(`^\\[${section}\\]`);
const result: Record<string, string> = {};
let inSection = false;
for (const line of lines) {
if (sectionRe.test(line.trim())) { inSection = true; continue; }
if (/^\[/.test(line.trim())) { if (inSection) break; continue; }
if (!inSection) continue;
const eqIdx = line.indexOf("=");
if (eqIdx < 0) continue;
const key = line.slice(0, eqIdx).trim();
const val = line.slice(eqIdx + 1).trim();
if (key) result[key] = val;
}

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] parseIniSection takes the full file content as a string parameter plus a target section name. The agent must first read the file (via p.readInput), then pass the raw string to the tool for each section — meaning the full content is sent once in the prompt and once per tool call. For large configs this multiplies token usage.

💡 Alternative design

A parseAllSections tool that reads the file path and returns all sections at once would be more efficient and a better demonstration of the "deep tool" principle from /codebase-design: one tool call, rich return value. The current design is useful as an educational example of iterative tool use, but a brief comment acknowledging the trade-off would help readers.

return JSON.stringify(result);
},
});

// Agent role: parse an INI configuration file and extract all sections and their key-value pairs.
const iniConfigParser = agent({
name: "iniConfigParser",
model: "small",
input: s.object({ configFile: s.path("Path to the INI config file") }),
instructions: p`Read the INI configuration file at the specified path.
${p.readInput("configFile")}
Use parseIniSection for each section header you find (lines matching [SectionName]).
Return sections as a record, totalKeys count, and sectionCount.`,
output: s.object({
sections: s.record(s.record(s.string)),
sectionCount: s.int,
totalKeys: s.int,
}),
tools: [parseIniSection],
addons: [repair()],
});

export default iniConfigParser;
```
Loading