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
29 changes: 29 additions & 0 deletions skills/rig/samples/230-git-rename-tracker-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 230 - Git Rename Tracker V3

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

// Agent role: track file renames in recent git history and return a structured report.
const gitRenameTracker = agent({
model: "small",
instructions: p`Analyze the following git log output for file renames:

${p.bash("git log --diff-filter=R --summary --oneline -50")}

Parse each rename line (format: "rename old/path => new/path (similarity%)").
Extract the commit hash from the preceding commit line, and the old and new paths.
Return the structured list of renames with totals.`,
output: s.object({
renames: s.array(s.object({
hash: s.string,
oldPath: s.path,
newPath: s.path,
})),
totalRenames: s.int,
mostRenamed: s.optional(s.path),
}),
addons: [repair()],
});

export default gitRenameTracker;
```
45 changes: 45 additions & 0 deletions skills/rig/samples/231-yaml-workflow-linter-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 231 - Yaml Workflow Linter V2

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

const validateWorkflow = defineTool("validateWorkflow", {
description: "Validate a GitHub Actions workflow YAML file for required top-level keys.",
parameters: { filePath: s.path },
handler: async ({ filePath }) => {
const content = await readFile(filePath, "utf8");
const hasName = /^name:/m.test(content);
const hasOn = /^on:/m.test(content);
const hasJobs = /^jobs:/m.test(content);
const issues: string[] = [];
if (!hasName) issues.push("Missing 'name' key");
if (!hasOn) issues.push("Missing 'on' key");
if (!hasJobs) issues.push("Missing 'jobs' key");
const status = issues.length === 0 ? "pass" : issues.length < 2 ? "warn" : "fail";
return { hasName, hasOn, hasJobs, status, issues };
},
});

// Agent role: lint GitHub Actions workflow YAML files for required top-level keys.
const yamlWorkflowLinter = agent({
model: "small",
instructions: p`Validate each GitHub Actions workflow file found in the workspace.

Workflow files: ${p.glob(".github/workflows/**/*.yml")}

For each file path listed above, call the validateWorkflow tool to check for required keys.
Return a record keyed by filename with validation results.`,
output: s.record(s.object({
hasName: s.boolean,
hasOn: s.boolean,
hasJobs: s.boolean,
status: s.enum("pass", "warn", "fail"),
issues: s.array(s.string),
})),
tools: [validateWorkflow],
addons: [repair()],
});

export default yamlWorkflowLinter;
```
73 changes: 73 additions & 0 deletions skills/rig/samples/232-two-phase-complexity-review-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 232 - Two Phase Complexity Review V3

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

// Agent role: extract TypeScript files with line counts from the workspace.
const extractor = agent({
name: "extractor",
model: "small",
instructions: p`List TypeScript source files and their line counts.

Files found: ${p.glob("**/*.ts")}
Line counts: ${p.bash("wc -l $(find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' | head -20) 2>/dev/null | tail -21")}

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 wc -l command uses shell word-splitting on the $(find ...) subshell — file paths with spaces will be split into multiple arguments causing wc to error on them. This is a shell injection risk in samples (readers may copy this pattern into production tools).

💡 Fix

Use xargs or a while read loop to handle paths safely, or replace with find ... -exec wc -l:

find . -name "*.ts" -not -path "*/node_modules/*" -not -path "*/.git/*" | head -20 | xargs wc -l 2>/dev/null | tail -21


Return a list of files with their line counts. Exclude totals and node_modules.`,
output: s.array(s.object({
file: s.path,
lines: s.int,
})),
});

// Agent role: classify TypeScript files by complexity tier based on line counts.
const reviewer = agent({
name: "reviewer",
model: "small",
instructions: p`You will receive a JSON list of TypeScript files with line counts.
Classify each file's complexity:
- trivial: 0-20 lines
- small: 21-100 lines
- medium: 101-300 lines
- large: 301-600 lines
- huge: 600+ lines

Return the classified list plus a summary.`,
input: s.array(s.object({ file: s.path, lines: s.int })),
output: s.object({
files: s.array(s.object({
file: s.path,
lines: s.int,
complexity: s.enum("trivial", "small", "medium", "large", "huge"),
})),
summary: s.object({
totalFiles: s.int,
largestFile: s.optional(s.path),
mostComplex: s.optional(s.path),
}),
}),
});

// Agent role: coordinate two-phase TypeScript complexity analysis.
const coordinator = agent({
model: "small",
instructions: p`Perform a two-phase TypeScript file complexity analysis:
1. Use the extractor subagent to get a list of TypeScript files with line counts.
2. Pass the extractor's output as input to the reviewer subagent for complexity classification.
3. Return the reviewer's output as your final result.`,
output: s.object({
files: s.array(s.object({
file: s.path,
lines: s.int,
complexity: s.enum("trivial", "small", "medium", "large", "huge"),
})),
summary: s.object({
totalFiles: s.int,
largestFile: s.optional(s.path),
mostComplex: s.optional(s.path),
}),
}),
agents: { extractor, reviewer },
});

export default coordinator;
```
43 changes: 43 additions & 0 deletions skills/rig/samples/233-barrel-file-generator-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 233 - Barrel File Generator V3

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

const detectExports = defineTool("detectExports", {
description: "Detect exported symbols from a TypeScript source file.",
parameters: { filePath: s.path },
handler: async ({ filePath }) => {
const content = await readFile(filePath, "utf8");
const exportMatches = content.match(/^export\s+(?:default\s+)?(?:const|function|class|type|interface|enum)\s+(\w+)/gm) ?? [];

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 match(/^export\s+.../gm) regex captures the full export ... line, then a second .match(/(\w+)\s*$/) extracts the last word. For export default function foo this correctly returns foo, but for export const myVar = the trailing = is not in the pattern so the last word captured is correctly myVar — but export type { Foo } (re-export braces syntax) and export * from patterns are silently missed.

💡 Suggestion

This is a sample so perfect correctness is not required, but it would improve the sample quality (and reader trust) to note the limitation in a comment:

// Note: only detects direct declaration exports; re-exports (`export { Foo }`, `export * from`)
// are not captured by this regex.

const symbols = exportMatches.map(m => {
const match = m.match(/(\w+)\s*$/);
return match ? match[1] : "";
}).filter(Boolean);
return { filePath, symbols, count: symbols.length };
},
});

// Agent role: scan TypeScript source files and generate a barrel index.ts with all exports.
const barrelFileGenerator = agent({
model: "small",
instructions: p`Generate a TypeScript barrel file (index.ts) that re-exports all symbols from source files.

Source files: ${p.glob("src/**/*.ts")}

For each file path above, call the detectExports tool to find exported symbols.
Then generate the barrel file content as a series of export statements.
Write it to ${p.writeOutput("barrelContent", "src/index.ts")}.
Return the file count, export count, barrel content, and output path.`,
output: s.object({
filesScanned: s.int,
exportCount: s.int,
barrelContent: s.string,
outputPath: s.path,
}),
tools: [detectExports],
addons: [repair()],
});

export default barrelFileGenerator;
```
48 changes: 48 additions & 0 deletions skills/rig/samples/234-git-hook-inventory-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 234 - Git Hook Inventory V3

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

const classifyHook = defineTool("classifyHook", {
description: "Classify a git hook as active, stub, or missing based on its content.",
parameters: { hookName: s.string },
handler: async ({ hookName }) => {
const hookPath = `.git/hooks/${hookName}`;
try {
await access(hookPath, constants.F_OK);
const content = await readFile(hookPath, "utf8");
const isSample = content.includes("sample") || hookPath.endsWith(".sample");

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] Dead branch: hookPath.endsWith(".sample") is always falsehookPath is built as .git/hooks/${hookName} and hookName is never a .sample-suffixed string.

💡 Fix

Remove the dead branch or adjust the logic to also list .sample files:

// Option A: drop the dead condition
const isSample = content.includes("sample");

// Option B: if you want to catch .git/hooks/pre-commit.sample files,
// pass them as hookName values and the check works naturally

As written the intent (skip .sample files) is silently lost.

const isAsync = content.includes("&") || content.includes("nohup");
const summary = isSample
? `Sample/stub hook at ${hookPath}`
: `Active hook: ${content.split("\n")[1]?.trim() ?? ""}`;
return { status: isSample ? "stub" : "active", isAsync, summary };
} catch {
return { status: "missing", isAsync: false, summary: `No hook at ${hookPath}` };
}
},
});

// Agent role: inventory and classify all standard git hooks in this repository.
const gitHookInventory = agent({
model: "small",
instructions: p`Inspect all standard git hooks and classify each one.

Hook directory listing: ${p.bash("ls -la .git/hooks/ 2>/dev/null || echo 'No .git/hooks directory'")}

Standard hooks to check: pre-commit, pre-push, commit-msg, post-commit, post-merge,
pre-rebase, post-checkout, prepare-commit-msg, pre-receive, post-receive.

For each hook name, call the classifyHook tool to determine its status.
Return a record keyed by hook name with status, isAsync, and summary.`,
output: s.record(s.object({
status: s.enum("active", "stub", "missing"),
isAsync: s.boolean,
summary: s.string,
})),
tools: [classifyHook],
});

export default gitHookInventory;
```
34 changes: 34 additions & 0 deletions skills/rig/samples/235-pr-review-checklist-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 235 - Pr Review Checklist V3

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

// Agent role: generate a structured PR review checklist from the current git diff.
const prReviewChecklist = agent({
model: "small",
instructions: p`Generate a structured PR review checklist based on the git diff below.

Changed files summary:
${p.bash("git diff HEAD~1 --stat 2>/dev/null || git diff --stat 2>/dev/null || echo 'No diff available'")}

Diff content (first 200 lines):
${p.bash("git diff HEAD~1 -- . 2>/dev/null | head -200 || git diff -- . | head -200 || echo 'No diff available'")}

Analyze the diff and produce:
1. A checklist of specific review items, each categorized and prioritized.
2. The list of changed file paths.
3. Whether the PR appears ready to merge (ready: true if no high-priority issues).`,
output: s.object({
checklist: s.array(s.object({
item: s.string,
category: s.enum("security", "performance", "correctness", "style", "docs", "tests"),
priority: s.enum("high", "medium", "low"),
})),
changedFiles: s.array(s.path),
ready: s.boolean,
}),
addons: [repair()],
});

export default prReviewChecklist;
```
65 changes: 65 additions & 0 deletions skills/rig/samples/236-vitest-coverage-parser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 236 - Vitest Coverage Parser

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

const parseCoverageSummary = defineTool("parseCoverageSummary", {
description: "Parse a vitest coverage-summary.json file to extract per-file coverage metrics.",
parameters: { filePath: s.path },
handler: async ({ filePath }) => {
const raw = await readFile(filePath, "utf8");

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 tool reads the file but the agent passes the path via the prompt instruction (line 41) as the hard-coded string "coverage/coverage-summary.json" — the filePath parameter is effectively unused for path selection. If the coverage file is at a different location this pattern silently breaks.

💡 Fix

Have the agent pass the detected path to the tool, or remove the parameter and hard-code the path inside the tool:

// Option A: remove filePath parameter, embed the path
handler: async () => {
  const raw = await readFile("coverage/coverage-summary.json", "utf8");
  ...
}

// Option B (better): let the agent derive the path from p.readOptional and pass it
parameters: { filePath: s.path },
// and instruction says: call parseCoverageSummary with the file path shown above

const json = JSON.parse(raw) as Record<string, { lines: { pct: number }; branches: { pct: number }; functions: { pct: number } }>;
const files: Record<string, { lines: number; branches: number; functions: number; status: string }> = {};
let totalLines = 0, totalBranches = 0, totalFunctions = 0, count = 0;
for (const [file, data] of Object.entries(json)) {
if (file === "total") continue;
const lines = data.lines?.pct ?? 0;
const branches = data.branches?.pct ?? 0;
const functions = data.functions?.pct ?? 0;
const avg = (lines + branches + functions) / 3;
const status = avg >= 80 ? "covered" : avg >= 40 ? "partial" : "uncovered";
files[file] = { lines, branches, functions, status };
totalLines += lines; totalBranches += branches; totalFunctions += functions; count++;
}
const n = count || 1;
return {
files,
overall: { lines: totalLines / n, branches: totalBranches / n, functions: totalFunctions / n },
totalFiles: count,
};
},
});

// Agent role: parse vitest coverage output and return a per-file coverage report.
const vitestCoverageParser = agent({
model: "small",
instructions: p`Parse the vitest coverage report for this project.

Coverage summary file: ${p.readOptional("coverage/coverage-summary.json")}

If the coverage summary file is available, call parseCoverageSummary with path "coverage/coverage-summary.json".
Otherwise, report all counts as 0 and status as "uncovered".

Count files with lines coverage >= 80% as "well covered".
Return the per-file metrics, overall averages, total file count, and well-covered count.`,
output: s.object({
files: s.record(s.object({
lines: s.number,
branches: s.number,
functions: s.number,
status: s.enum("covered", "partial", "uncovered"),
})),
overall: s.object({
lines: s.number,
branches: s.number,
functions: s.number,
}),
totalFiles: s.int,
wellCoveredCount: s.int,
}),
tools: [parseCoverageSummary],
});

export default vitestCoverageParser;
```
52 changes: 52 additions & 0 deletions skills/rig/samples/237-git-log-stats-aggregator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 237 - Git Log Stats Aggregator

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

const aggregateStats = defineTool("aggregateStats", {
description: "Aggregate git log lines into per-author and per-day-of-week commit counts.",
parameters: { logLines: s.string },
handler: ({ logLines }) => {
const perAuthor: Record<string, number> = {};
const perDayOfWeek: Record<string, number> = {};
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
let totalCommits = 0;
for (const line of logLines.split("\n")) {
if (!line.trim()) continue;
const parts = line.split("|");
if (parts.length < 4) continue;
const author = parts[1].trim();
const dateStr = parts[3].trim();
perAuthor[author] = (perAuthor[author] ?? 0) + 1;
const day = days[new Date(dateStr).getDay()];

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] new Date(dateStr).getDay() returns NaN when dateStr is empty or malformed, so days[NaN] is undefined — the if (day) guard silently drops those commits from perDayOfWeek but still increments totalCommits, causing a mismatch.

💡 Fix

Validate dateStr before parsing, or guard on the date being valid:

const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
  const day = days[date.getDay()];
  if (day) perDayOfWeek[day] = (perDayOfWeek[day] ?? 0) + 1;
}
totalCommits++;

The current guard on if (day) only prevents the undefined key insertion but does not prevent the totalCommits inflation.

if (day) perDayOfWeek[day] = (perDayOfWeek[day] ?? 0) + 1;
totalCommits++;
}
const mostActiveAuthor = Object.entries(perAuthor).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
const mostActiveDay = Object.entries(perDayOfWeek).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
return { perAuthor, perDayOfWeek, totalCommits, mostActiveAuthor, mostActiveDay };
},
});

// Agent role: aggregate git log statistics into per-author and per-day-of-week commit counts.
const gitLogStatsAggregator = agent({
model: "small",
instructions: p`Aggregate git commit statistics from the following log:

${p.bash("git log --format='%H|%an|%ae|%ad|%s' --date=short -100 2>/dev/null || echo ''")}

Call aggregateStats with the full log text above as logLines.
Return the result including perAuthor counts, perDayOfWeek counts, totalCommits,
mostActiveAuthor, and mostActiveDay.`,
output: s.object({
perAuthor: s.record(s.int),
perDayOfWeek: s.record(s.int),
totalCommits: s.int,
mostActiveAuthor: s.optional(s.string),
mostActiveDay: s.optional(s.string),
}),
tools: [aggregateStats],
});

export default gitLogStatsAggregator;
```
Loading
Loading