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

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

// Agent role: track file renames in git history and produce a structured rename log.
const gitRenameTrackerV2 = agent({
model: "small",
instructions: p`Retrieve git file rename history: ${p.bash("git log --diff-filter=R --summary --pretty=format:'%h %ai' | head -60 2>/dev/null || echo 'No renames found'")}. Parse each rename entry (lines matching "rename ... => ...") and associate it with the nearest commit hash and date. Return all renames sorted newest first, the total count, and the path that appears most often as either old or new.`,
output: s.object({
renames: s.array(s.object({
hash: s.string,
date: s.string,
oldPath: s.path,
newPath: s.path,
})),
totalRenames: s.int,
mostRenamedFile: s.optional(s.path),
}),
maxTurns: 4,
addons: repair(),
});

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

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

const validateYaml = defineTool("validateYaml", {
description: "Validate a YAML file for structural issues and required top-level keys",
parameters: s.object({ filePath: s.path }),
handler({ filePath }) {
let content: string;
try {
content = readFileSync(filePath, "utf-8");
} catch {
return { issues: ["File not readable"], lineCount: 0, status: "fail" as const };
}
const issues: string[] = [];
const lines = content.split("\n");
const lineCount = lines.length;
const isWorkflow = filePath.includes(".github/workflows");
if (isWorkflow) {
if (!content.includes("name:")) issues.push("Missing 'name' key");
if (!content.includes("on:") && !content.includes('"on":')) issues.push("Missing 'on' trigger");

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] Substring matching with includes("name:") will produce false positives for YAML keys like container-name: or username:. This makes the linter unreliable and isn't a good pattern to demonstrate in a sample.

💡 Use anchored regex instead
if (!/^name:/m.test(content)) issues.push("Missing 'name' key");
if (!/^on:/m.test(content) && !content.includes('"on":')) issues.push("Missing 'on' trigger");
if (!/^jobs:/m.test(content)) issues.push("Missing 'jobs' key");

Regex anchored to line start avoids false positives from partial key matches.

if (!content.includes("jobs:")) issues.push("Missing 'jobs' key");
}
if (content.includes("\t")) issues.push("Contains tab characters (use spaces in YAML)");
const status = issues.length === 0 ? "pass" : issues.some(i => i.startsWith("Missing")) ? "fail" : "warn";
return { issues, lineCount, status } as { issues: string[]; lineCount: number; status: "pass" | "warn" | "fail" };
},
});

// Agent role: lint YAML files and report structural issues per file.
const yamlWorkflowLinter = agent({
model: "small",
instructions: p`Find YAML files: ${p.bash("find . \\( -name '*.yml' -o -name '*.yaml' \\) | grep -v node_modules | grep -v .git | head -20")}. For each file path, call validateYaml and collect the result. Return a record keyed by file path.`,
output: s.record(s.object({
issues: s.array(s.string),
lineCount: s.int,
status: s.enum("pass", "warn", "fail"),
})),
tools: [validateYaml],
});

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

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

// Agent role: extract TypeScript function names and approximate line counts from source files.
const extractor = agent({
name: "extractor",
model: "small",
instructions: p`Scan TypeScript source files for function definitions: ${p.bash("grep -rn --include='*.ts' 'function \\|=> {\\|async ' . | grep -v node_modules | grep -v '.test.' | head -60 2>/dev/null || echo 'no ts files'")}. For each distinct function name found, estimate its line count from context. Return an array of objects with functionName, lineCount, and file.`,
output: s.array(s.object({
functionName: s.string,
lineCount: s.int,
file: s.path,
})),
});

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 extractor estimates line counts "from context" in the LLM prompt, but this is inherently unreliable — the model cannot accurately count lines from a grep summary. The sample teaches readers that line counts come from real tool calls, yet the extractor uses no tool for counting.

💡 Use a defineTool for actual line counting

Add a countLines tool that reads the file and returns an exact count:

const countLines = defineTool("countLines", {
  parameters: s.object({ file: s.path }),
  async handler({ file }) {
    const { readFile } = await import("node:fs/promises");
    const content = await readFile(file, "utf-8");
    return { lineCount: content.split("
").length };
  },
});

This makes the sample more accurate and teaches a better pattern.

// Agent role: rate each extracted function for complexity and produce a summary.
const reviewer = agent({
name: "reviewer",
model: "small",
instructions: p`You will receive a list of functions with their line counts. Classify each as: simple (<10 lines), moderate (10-29), complex (30-59), or critical (≥60). Return one rating object per function plus a summary with total and complex+critical count.`,
output: s.object({
analysis: s.record(s.object({
lineCount: s.int,
complexity: s.enum("simple", "moderate", "complex", "critical"),
})),
summary: s.object({
totalFunctions: s.int,
complexCount: s.int,
}),
}),
agents: { extractor },
});

export default reviewer;
```
35 changes: 35 additions & 0 deletions skills/rig/samples/223-barrel-file-generator-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 223 - Barrel File Generator V2

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

const detectExports = defineTool("detectExports", {
description: "Detect export declarations in a TypeScript source file",
parameters: s.object({ filePath: s.path }),
async handler({ filePath }) {
const { readFile } = await import("node:fs/promises");
try {
const content = await readFile(filePath, "utf-8");
const exports = (content.match(/^export\s+(default\s+)?(function|class|const|let|var|type|interface|enum)\s+\w+/gm) ?? []);
return { filePath, exports, exportCount: exports.length };
} catch {
return { filePath, exports: [], exportCount: 0 };
}
},
});

// Agent role: generate barrel index.ts content for TypeScript source directories.
const barrelFileGenerator = agent({
model: "small",

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 field barrelFilesWritten counts directories with exports but the agent never actually writes any barrel files — it only detects exports. The name is misleading and doesn't match what the sample demonstrates. This could confuse readers about what the agent does.

💡 Rename to reflect what is actually computed

Rename to directoriesWithExports (or similar) to match the actual computation. If the intent is to simulate writing, add a p.write intent or note in the instruction that this is a dry-run count.

instructions: p`Find TypeScript source files: ${p.bash("find src -name '*.ts' ! -name 'index.ts' ! -name '*.test.ts' ! -name '*.spec.ts' 2>/dev/null | head -30 || find . -name '*.ts' ! -name 'index.ts' ! -name '*.test.ts' ! -path '*/node_modules/*' | head -20")}. Use detectExports for each file. Tally filesScanned, barrelFilesWritten (directories that have at least one export), exportCount (total exports found), and directories (unique directory paths). Return only the declared output.`,
tools: [detectExports],
output: s.object({
filesScanned: s.int,
barrelFilesWritten: s.int,
exportCount: s.int,
directories: s.array(s.string),
}),
});

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

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

const classifyHook = defineTool("classifyHook", {
description: "Classify a git hook by inspecting its content",
parameters: s.object({ name: s.string, content: s.string }),
handler({ content }) {
if (!content || content.trim() === "missing") {
return { status: "missing" as const, isAsync: false, summary: "Hook file not present" };

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 parameters but ignored in the handler — only content is used. This is a misleading sample: readers expect declared parameters to be used, and silently ignoring name could cause confusion.

💡 Either use or remove the parameter

If name is not needed in the handler, remove it from parameters and pass it from the instruction context only. If it's useful for the summary, use it:

handler({ name, content }) {
  // ...
  const summary = isSample ? "Sample/placeholder hook" : `Active hook '${name}' (${lines} lines)`;
}

}
const isSample = content.includes("sample") || content.trim() === "#!/bin/sh" || content.trim() === "#!/bin/bash";
const isAsync = content.includes(" &") || content.includes("async ");
const status = isSample ? "stub" : "active";
const summary = isSample ? "Sample/placeholder hook — not active" : `Active hook (${content.split("\n").length} lines)`;
return { status, isAsync, summary } as { status: "active" | "stub" | "missing"; isAsync: boolean; summary: string };
},
});

// Agent role: inventory git hooks and classify each as active, stub, or missing.
const gitHookInventory = agent({
model: "small",
instructions: p`List and inspect git hooks: ${p.bash("ls .git/hooks/ 2>/dev/null || echo 'no hooks directory'")}. ${p.bash("for f in pre-commit commit-msg pre-push post-commit prepare-commit-msg pre-rebase; do echo \"=== $f ===\"; cat \".git/hooks/$f\" 2>/dev/null || echo 'missing'; done")}. Use classifyHook for each hook name with its content. Return a record keyed by hook name.`,
tools: [classifyHook],
output: s.record(s.object({
summary: s.string,
status: s.enum("active", "stub", "missing"),
isAsync: s.boolean,
})),
});

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

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

// Agent role: generate a PR review checklist from the current git diff.
const prReviewChecklist = agent({
model: "small",
maxTurns: 6,
addons: repair(),
instructions: p`Generate a PR review checklist based on these changes.

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

Diff:
${p.bash("git diff HEAD~1 -- . 2>/dev/null | head -300 || git diff HEAD -- . 2>/dev/null | head -300 || echo 'no diff'")}

For each concern, produce a checklist item with a clear description, category (security/performance/correctness/style/testing), and priority (must/should/nice-to-have). List changed files and indicate if the PR is ready for review.`,
output: s.object({
checklist: s.array(s.object({
item: s.string,
category: s.enum("security", "performance", "correctness", "style", "testing"),
priority: s.enum("must", "should", "nice-to-have"),
})),
changedFiles: s.array(s.path),
ready: s.boolean,
}),
});

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

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

const countAstNodes = defineTool("countAstNodes", {
description: "Count syntax node patterns in a JavaScript/TypeScript file using regex heuristics",
parameters: s.object({ filePath: s.path }),
async handler({ filePath }) {
const { readFile } = await import("node:fs/promises");
try {
const content = await readFile(filePath, "utf-8");
const functionCount = (content.match(/\bfunction\s+\w+\s*\(/g) ?? []).length;
const arrowFunctionCount = (content.match(/=>\s*[{(]/g) ?? []).length;
const classCount = (content.match(/\bclass\s+\w+/g) ?? []).length;
const importCount = (content.match(/^\s*import\s+/gm) ?? []).length;
const exportCount = (content.match(/^\s*export\s+/gm) ?? []).length;
return { filePath, functionCount, arrowFunctionCount, classCount, importCount, exportCount };
} catch {
return { filePath, functionCount: 0, arrowFunctionCount: 0, classCount: 0, importCount: 0, exportCount: 0 };
}
},
});

// Agent role: count AST-like syntax node patterns in JavaScript files.
const jsAstNodeCounter = agent({
model: "small",
instructions: p`Find JavaScript files: ${p.bash("find . -name '*.js' ! -path '*/node_modules/*' ! -path '*/.git/*' | head -10 2>/dev/null || echo 'no js files found'")}. For each file, call countAstNodes and collect results. Tally totalFunctions across all files and identify mostComplexFile (highest combined function + arrowFunction count). Return only the declared output.`,
tools: [countAstNodes],
output: s.object({
files: s.record(s.object({
functionCount: s.int,
arrowFunctionCount: s.int,
classCount: s.int,
importCount: s.int,
exportCount: s.int,
})),
totalFunctions: s.int,
mostComplexFile: s.optional(s.path),
}),
});

export default jsAstNodeCounter;
```
48 changes: 48 additions & 0 deletions skills/rig/samples/227-git-tag-date-mapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 227 - Git Tag Date Mapper

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

const classifyTagAge = defineTool("classifyTagAge", {
description: "Classify a tag's age based on its ISO date string",
parameters: s.object({ dateStr: s.string }),
handler({ dateStr }) {
const date = new Date(dateStr);
const now = new Date();
const daysDiff = (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24);
let ageClass: "recent" | "stable" | "old" | "ancient";
if (daysDiff < 30) ageClass = "recent";
else if (daysDiff < 180) ageClass = "stable";
else if (daysDiff < 730) ageClass = "old";
else ageClass = "ancient";
return { ageClass };
},
});

// Agent role: map git tags to their commit dates and classify their age.
const gitTagDateMapper = agent({
model: "small",
addons: steering(),
instructions: p`Inspect git tags and their dates.

Tags:
${p.bash("git tag -l --sort=-version:refname | head -20 2>/dev/null || echo 'no tags'")}

Tag log:
${p.bash("git log --tags --simplify-by-decoration --pretty='%D|%ai|%s' | head -30 2>/dev/null || echo 'no tag log'")}

For each tag, extract its ISO date and call classifyTagAge. Return a record keyed by tag name with date, subject, and ageClass. Include totalTags and latestTag.`,
tools: [classifyTagAge],
output: s.object({
tags: s.record(s.object({
date: s.string,
subject: s.string,
ageClass: s.enum("recent", "stable", "old", "ancient"),
})),
totalTags: s.int,
latestTag: s.optional(s.string),
}),
});

export default gitTagDateMapper;
```
53 changes: 53 additions & 0 deletions skills/rig/samples/228-circular-import-detector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 228 - Circular Import Detector

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

const buildImportGraph = defineTool("buildImportGraph", {
description: "Build an import graph from TypeScript files in a directory by parsing relative import statements",
parameters: s.object({ directory: s.path }),
async handler({ directory }) {
const { readdir, readFile } = await import("node:fs/promises");
const { join } = await import("node:path");
const graph: Record<string, string[]> = {};
try {
const entries = await readdir(directory, { recursive: true, encoding: "utf-8" });
const tsFiles = entries.filter(f => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts"));
for (const file of tsFiles.slice(0, 20)) {
const fullPath = join(directory, file);
try {
const content = await readFile(fullPath, "utf-8");
const imports = [...content.matchAll(/from\s+['"](\.[^'"]+)['"]/g)].map(m => m[1]);
graph[file] = imports;
} catch {
graph[file] = [];
}
}
} catch {
// directory not found
}
return { graph };
},
});

// Agent role: detect circular imports in TypeScript source files.
const circularImportDetector = agent({
model: "small",
addons: steering(),
instructions: p`Detect circular imports in TypeScript files.

Import overview:

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 agent instruction asks the LLM to "analyse the graph to find cycles" from the returned graph record, but cycle detection is a graph algorithm that should be in a defineTool handler — not left to the model. Delegating DFS/BFS cycle detection to an LLM is unreliable and produces inconsistent results.

💡 Move cycle detection into the tool handler

Extend buildImportGraph (or add a detectCycles tool) to run a proper DFS and return cycles directly:

function findCycles(graph: Record<string, string[]>): string[][] {
  const cycles: string[][] = [];
  const visited = new Set<string>();
  const stack: string[] = [];
  // ... DFS implementation
  return cycles;
}

This makes the sample deterministic and teaches the right design: use tools for computation, LLM for orchestration.

${p.bash("grep -rn \"from '\\./\\|from \\\"\\./\" src/ --include='*.ts' 2>/dev/null | head -40 || grep -rn \"from '\\./\" . --include='*.ts' | grep -v node_modules | head -40 || echo 'no relative imports found'")}

Call buildImportGraph on the source directory (try "src" first, then "."). Analyse the graph to find cycles (A imports B imports A, etc.). Return cycles as arrays of file paths, hasCycles, totalFiles, and cycleCount.`,
tools: [buildImportGraph],
output: s.object({
cycles: s.array(s.array(s.string)),
hasCycles: s.boolean,
totalFiles: s.int,
cycleCount: s.int,
}),
});

export default circularImportDetector;
```
Loading