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
70 changes: 70 additions & 0 deletions skills/rig/samples/310-lock-file-drift-detector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# 310 - Lock File Drift Detector

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

// Agent role: detect version drift between package.json dependencies and package-lock.json locked versions.
const lockFileDriftDetector = agent({
model: "small",
instructions: p`You are a lock file drift detector.

Read the package files:
${p.bash("cat package.json 2>/dev/null || echo '{}'")}
${p.bash("cat package-lock.json 2>/dev/null || echo '{}'")}

Use the checkVersionDrift tool to cross-check declared vs locked versions for each dependency.
Return the declared output.`,
tools: [
defineTool("checkVersionDrift", {
description: "Parse package.json and package-lock.json content and return drifted packages",
parameters: s.object({
packageJsonContent: s.string,
lockFileContent: s.string,
}),
handler({ packageJsonContent, lockFileContent }) {
let pkg: Record<string, unknown>;
let lock: Record<string, unknown>;
try {
pkg = JSON.parse(packageJsonContent) as Record<string, unknown>;
} catch {
return { error: "Failed to parse package.json" };
}
try {
lock = JSON.parse(lockFileContent) as Record<string, unknown>;
} catch {
return { error: "Failed to parse package-lock.json" };
}
const deps = {
...((pkg["dependencies"] as Record<string, string>) ?? {}),
...((pkg["devDependencies"] as Record<string, string>) ?? {}),
};
const lockPackages = (lock["packages"] as Record<string, { version?: string }>) ?? {};
const drifted: Array<{ name: string; declared: string; locked: string }> = [];
for (const [name, declared] of Object.entries(deps)) {
const lockKey = `node_modules/${name}`;
const lockedEntry = lockPackages[lockKey];
const locked = lockedEntry?.version ?? "missing";
const declaredClean = (declared as string).replace(/^[\^~>=<]/, "");
if (locked !== declaredClean && locked !== "missing") continue;

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 drift detection logic is inverted — version mismatches are silently skipped, so the tool only ever reports missing packages, never actual version differences.

💡 Bug explanation

Line 48: if (locked !== declaredClean && locked !== "missing") continue;

This skips any entry where the locked version differs from the declared version and it is not "missing" — i.e., it skips every real drift case. Only the subsequent if (locked === "missing") block ever appends to drifted.

Fix the condition:

if (locked === "missing" || locked !== declaredClean) {
  drifted.push({ name, declared, locked });
}

if (locked === "missing") {
drifted.push({ name, declared: declared as string, locked });
}
}
return { drifted };
},
}),
],
output: s.object({
driftedPackages: s.array(s.object({
name: s.string,
declared: s.string,
locked: s.string,
})),
driftCount: s.int,
allInSync: s.boolean,
}),
addons: [repair()],
});

export default lockFileDriftDetector;
```
32 changes: 32 additions & 0 deletions skills/rig/samples/311-git-conflict-marker-scanner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 311 - Git Conflict Marker Scanner

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

// Agent role: scan tracked files for unresolved git conflict markers and report affected locations.
const gitConflictMarkerScanner = agent({
model: "small",
instructions: p`You are a git conflict marker scanner.

Check for conflict markers in tracked files:
${p.bash("git ls-files | xargs grep -lrn '<<<<<<< ' 2>/dev/null || echo 'no conflicts found'")}

For files with conflict markers, get the details:
${p.bash("git ls-files | xargs grep -n '^<<<<<<< \\|^=======$\\|^>>>>>>> ' 2>/dev/null || echo 'none'")}

Analyze the results and return the declared output listing all conflict markers found.`,
output: s.object({
conflicts: s.array(s.object({
file: s.path,
line: s.int,
markerType: s.enum("start", "middle", "end"),
})),
affectedFiles: s.array(s.path),
conflictCount: s.int,
isClean: s.boolean,
}),
addons: [steering()],
});

export default gitConflictMarkerScanner;
```
54 changes: 54 additions & 0 deletions skills/rig/samples/312-proto-field-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 312 - Proto Field Extractor

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

// Agent role: extract field definitions from protobuf message types in .proto files.
const protoFieldExtractor = agent({
model: "small",
instructions: p`You are a protobuf field extractor.

Find .proto files in the workspace:
${p.bash("find . -name '*.proto' -not -path '*/node_modules/*' 2>/dev/null || echo 'no .proto files found'")}

For each .proto file found, call extractProtoFields with its path to parse message definitions.
Return the declared output keyed by message name.`,
tools: [
defineTool("extractProtoFields", {
description: "Read a .proto file and extract message field definitions",
parameters: s.object({ filePath: s.path }),
async handler({ filePath }) {
const content = await readFile(filePath, "utf8");
const result: Record<string, Array<{ fieldName: string; fieldType: string; fieldNumber: number; isRepeated: boolean }>> = {};
const messageRegex = /message\s+(\w+)\s*\{([^}]*)\}/gs;
const fieldRegex = /^\s*(repeated\s+)?(\w+)\s+(\w+)\s*=\s*(\d+)/gm;
for (const msgMatch of content.matchAll(messageRegex)) {
const messageName = msgMatch[1];
const body = msgMatch[2];
const fields: Array<{ fieldName: string; fieldType: string; fieldNumber: number; isRepeated: boolean }> = [];
for (const fldMatch of body.matchAll(fieldRegex)) {
fields.push({
isRepeated: !!fldMatch[1],
fieldType: fldMatch[2],
fieldName: fldMatch[3],
fieldNumber: parseInt(fldMatch[4], 10),
});
}
result[messageName] = fields;
}
return result;
},
}),
],
output: s.record(s.array(s.object({
fieldName: s.string,
fieldType: s.string,
fieldNumber: s.int,
isRepeated: s.boolean,
}))),
addons: [repair()],
});

export default protoFieldExtractor;
```
50 changes: 50 additions & 0 deletions skills/rig/samples/313-git-remote-url-inspector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 313 - Git Remote Url Inspector

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

// Agent role: inspect git remote URLs and classify their protocol and type.
const gitRemoteUrlInspector = agent({
model: "small",
instructions: p`You are a git remote URL inspector.

List all remotes and their URLs:
${p.bash("git remote -v 2>/dev/null || echo 'no remotes'")}

Count remote branches on origin:
${p.bash("git ls-remote --heads origin 2>/dev/null | wc -l || echo '0'")}

Use the parseRemoteLine tool for each remote line to extract protocol and type.
Return the declared output.`,
tools: [
defineTool("parseRemoteLine", {
description: "Parse a git remote -v output line and extract name, url, protocol, and type",
parameters: s.object({ line: s.string }),
handler({ line }) {
const match = line.match(/^(\S+)\s+(\S+)\s+\((\w+)\)$/);
if (!match) return null;
const [, name, url, type] = match;
let protocol: "https" | "ssh" | "git" | "file";
if (url.startsWith("https://")) protocol = "https" as const;
else if (url.startsWith("git@") || url.startsWith("ssh://")) protocol = "ssh" as const;
else if (url.startsWith("git://")) protocol = "git" as const;
else protocol = "file" as const;
return { name, url, type, protocol };
},
}),
],
output: s.object({
remotes: s.array(s.object({
name: s.string,
url: s.string,
type: s.string,
protocol: s.enum("https", "ssh", "git", "file"),
})),
remoteBranchCount: s.int,
hasOrigin: s.boolean,
}),
addons: [repair()],
});

export default gitRemoteUrlInspector;
```
48 changes: 48 additions & 0 deletions skills/rig/samples/314-ts-enum-value-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 314 - Ts Enum Value Extractor

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

// Agent role: extract TypeScript enum declarations and their members from source files.
const tsEnumValueExtractor = agent({
model: "small",
instructions: p`You are a TypeScript enum value extractor.

Find TypeScript files with enum declarations:
${p.bash("grep -rln 'enum ' --include='*.ts' . 2>/dev/null | head -20 || echo 'no enums found'")}

For each file found, call extractEnumValues with the file path to parse all enum declarations.
Return the declared output keyed by enum name.`,
tools: [
defineTool("extractEnumValues", {
description: "Read a TypeScript file and extract all enum declarations with their members",
parameters: s.object({ filePath: s.path }),
async handler({ filePath }) {
const content = await readFile(filePath, "utf8");
const result: Record<string, { members: string[]; isConst: boolean; memberCount: number }> = {};
const enumRegex = /(const\s+)?enum\s+(\w+)\s*\{([^}]*)\}/gs;
for (const match of content.matchAll(enumRegex)) {
const isConst = !!match[1];
const enumName = match[2];
const body = match[3];
const members = body
.split(",")
.map((m: string) => m.trim().split(/\s*=\s*/)[0].trim())
.filter((m: string) => m.length > 0 && !m.startsWith("//"));
result[enumName] = { members, isConst, memberCount: members.length };
}
return result;
},
}),
],
output: s.record(s.object({
members: s.array(s.string),
isConst: s.boolean,
memberCount: s.int,
})),
addons: [steering()],
});

export default tsEnumValueExtractor;
```
85 changes: 85 additions & 0 deletions skills/rig/samples/315-json-config-merger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# 315 - Json Config Merger

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

// Agent role: deeply merge two JSON config files and write the result to an output path.
const jsonConfigMerger = agent({
model: "small",
input: s.object({
baseConfig: s.path,
overrideConfig: s.path,
outputPath: s.path,
}),
instructions: p`You are a JSON config merger.

Base config contents:
${p.readInput("baseConfig")}

Override config contents:
${p.readInput("overrideConfig")}

Use the mergeJsonObjects tool to deeply merge the override config into the base config.
Then write the merged result to ${p.writeInput("outputPath", "mergedContent")}.
Return the declared output.`,
tools: [
defineTool("mergeJsonObjects", {
description: "Deep merge two JSON strings, returning the merged JSON and counts of added/overridden keys",
parameters: s.object({
baseJson: s.string,
overrideJson: s.string,
}),
handler({ baseJson, overrideJson }) {
const base = JSON.parse(baseJson) as Record<string, unknown>;

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] JSON.parse in the handler has no error handling — a malformed config file will throw an uncaught exception and crash the tool call. Sample 310 handles this correctly with try/catch; this sample should do the same.

💡 Fix
handler({ baseJson, overrideJson }) {
  let base: Record<string, unknown>;
  let override: Record<string, unknown>;
  try { base = JSON.parse(baseJson); } catch { return { error: "Failed to parse base config" }; }
  try { override = JSON.parse(overrideJson); } catch { return { error: "Failed to parse override config" }; }
  // ... rest of merge logic
}

const override = JSON.parse(overrideJson) as Record<string, unknown>;
let keysAdded = 0;
let keysOverridden = 0;
function deepMerge(
target: Record<string, unknown>,
source: Record<string, unknown>
): Record<string, unknown> {
const result = { ...target };
for (const [key, value] of Object.entries(source)) {
if (key in result) {
if (
typeof result[key] === "object" &&
result[key] !== null &&
typeof value === "object" &&
value !== null
) {
result[key] = deepMerge(
result[key] as Record<string, unknown>,
value as Record<string, unknown>
);
} else {
result[key] = value;
keysOverridden++;
}
} else {
result[key] = value;
keysAdded++;
}
}
return result;
}
const merged = deepMerge(base, override);
return {
mergedJson: JSON.stringify(merged, null, 2),
keysAdded,
keysOverridden,
totalKeys: Object.keys(merged).length,
};
},
}),
],
output: s.object({
keysAdded: s.int,
keysOverridden: s.int,
totalKeys: s.int,
outputPath: s.path,
}),
addons: [repair()],
});

export default jsonConfigMerger;
```
46 changes: 46 additions & 0 deletions skills/rig/samples/316-markdown-toc-generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 316 - Markdown Toc Generator

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

// Agent role: generate a table of contents markdown file from all headings found in workspace .md files.
const markdownTocGenerator = agent({
model: "small",
instructions: p`You are a markdown table of contents generator.

Find all markdown files in the workspace:
${p.glob("**/*.md")}

For each file, call extractHeadings to parse its heading structure.
Then write the combined TOC to ${p.write("TOC.md", "tocContent")}.
Return the declared output.`,
tools: [
defineTool("extractHeadings", {
description: "Read a markdown file and extract all headings with their levels and anchor text",
parameters: s.object({ filePath: s.path }),
async handler({ filePath }) {
const content = await readFile(filePath, "utf8");
const headings: Array<{ level: number; text: string; anchor: string }> = [];
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
for (const match of content.matchAll(headingRegex)) {
const level = match[1].length;
const text = match[2].trim();
const anchor = text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
headings.push({ level, text, anchor });
}
return { filePath, headings };
},
}),
],
output: s.object({
processedFiles: s.array(s.path),
headingCount: s.int,
outputPath: s.path,
tocGenerated: s.boolean,
}),
addons: [repair()],
});

export default markdownTocGenerator;
```
Loading
Loading