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
40 changes: 40 additions & 0 deletions skills/rig/samples/240-git-commit-annotator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 240 - Git Commit Annotator

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

// Agent role: annotate each of the last 20 git commits with a category and impact level.
const commitSummarizer = agent({
name: "commitSummarizer",
model: "small",
instructions: p`Classify the following git commit line into a category and impact level.
Commit: ${p.readInput("commit")}`,
input: s.object({ commit: s.string }),
output: s.object({
category: s.enum("feat", "fix", "chore", "docs", "refactor", "test", "perf"),
impact: s.enum("low", "medium", "high"),
}),
});

// Agent role: fetch the last 20 git commits and annotate each with category and impact using the commitSummarizer subagent.
const gitCommitAnnotator = agent({
model: "small",
instructions: p`You are a git commit annotator. Here are the last 20 commits:
${p.bash("git log --oneline -20")}

For each commit line, call the commitSummarizer subagent with the full commit line.
Then assemble the annotations array and a short prose report summarizing the distribution of categories and impacts.`,
output: s.object({
annotations: s.array(s.object({
hash: s.string,
message: s.string,
category: s.enum("feat", "fix", "chore", "docs", "refactor", "test", "perf"),
impact: s.enum("low", "medium", "high"),
})),
report: s.string,
}),
agents: { commitSummarizer },
});

export default gitCommitAnnotator;
```
58 changes: 58 additions & 0 deletions skills/rig/samples/241-npm-script-dep-mapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 241 - Npm Script Dep Mapper

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

const parseScriptDeps = defineTool("parseScriptDeps", {
description: "Parse npm scripts JSON and extract per-script dependency metadata.",
parameters: { scriptsJson: s.string },
handler: ({ scriptsJson }: { scriptsJson: string }) => {
const scripts: Record<string, string> = JSON.parse(scriptsJson);
const result: Record<string, {
command: string;
dependsOn: string[];
category: string;
hasPreHook: boolean;
hasPostHook: boolean;
}> = {};
for (const [name, command] of Object.entries(scripts)) {
const refs = [...command.matchAll(/npm run ([a-z0-9:_-]+)/gi)].map((m: RegExpMatchArray) => m[1]);
const hasPreHook = (`pre${name}`) in scripts;
const hasPostHook = (`post${name}`) in scripts;
let category = "util";
if (/build|compile|bundle/.test(name)) category = "build";
else if (/test|spec|coverage/.test(name)) category = "test";
else if (/lint|format|check/.test(name)) category = "lint";
else if (/dev|start|watch/.test(name)) category = "dev";
else if (/deploy|publish|release/.test(name)) category = "deploy";
else if (/^pre|^post/.test(name)) category = "hook";
result[name] = { command, dependsOn: refs, category, hasPreHook, hasPostHook };
}
return result;
},
});

// Agent role: map npm script dependencies in the workspace package.json.
const npmScriptDepMapper = agent({
model: "small",
instructions: p`Analyze the npm scripts in the workspace package.json.
package.json content: ${p.read("package.json")}

Steps:
1. Extract the "scripts" field from the package.json content as a JSON string.
2. Call parseScriptDeps with that JSON string.
3. Return the result directly as the output record.`,
output: s.record(s.object({
command: s.string,
dependsOn: s.array(s.string),
category: s.enum("build", "test", "lint", "dev", "deploy", "util", "hook", "other"),
hasPreHook: s.boolean,

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 output schema includes "other" in the category enum, but the parseScriptDeps tool handler never produces that value — it always falls through to "util". Either remove "other" from the schema or add an explicit else branch that returns it.

💡 Fix
// In parseScriptDeps handler, the fallthrough produces "util", never "other".
// Option A: remove "other" from the output enum:
category: s.enum("build", "test", "lint", "dev", "deploy", "util", "hook"),

// Option B: rename the default to "other" in the handler:
let category = "other";

hasPostHook: s.boolean,
})),
tools: [parseScriptDeps],
maxTurns: 6,
addons: [repair()],
});

export default npmScriptDepMapper;
```
59 changes: 59 additions & 0 deletions skills/rig/samples/242-markdown-link-checker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 242 - Markdown Link Checker

```rig
import { agent, p, s, defineTool } from "rig";
import { exec } from "node:child_process";
import { promisify } from "node:util";

const execAsync = promisify(exec);

const checkUrl = defineTool("checkUrl", {
description: "Check an HTTP URL with curl and return its status.",
parameters: { url: s.string },
handler: async ({ url }: { url: string }) => {
try {
const { stdout } = await execAsync(
`curl -s -o /dev/null -w "%{http_code}" --max-time 5 --location "${url}"`,
);
const code = parseInt(stdout.trim(), 10);

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] Shell injection risk: url is interpolated directly into a shell string passed to execAsync. A URL containing shell metacharacters (e.g. $(...), backticks) will execute arbitrary commands.

💡 Suggested fix

Use execFile to pass the URL as a separate argument, bypassing shell interpretation:

import { execFile } from "node:child_process";
const { stdout } = await promisify(execFile)("curl", ["-s","-o","/dev/null","-w","%{http_code}","--max-time","5","--location",url]);

Samples teach patterns — unsafe shell interpolation here will be replicated by readers.

let status: string;
if (code >= 200 && code < 300) status = "ok";
else if (code >= 300 && code < 400) status = "redirect";
else if (code === 0) status = "timeout";
else status = "broken";
return { httpCode: code, status };
} catch {
return { httpCode: 0, status: "timeout" };
}
},
});

// Agent role: check all HTTP/HTTPS links found in markdown files in the workspace.
const markdownLinkChecker = agent({
model: "small",
instructions: p`Check all HTTP/HTTPS links found in markdown files.

Markdown files: ${p.bash("find . -name '*.md' -not -path '*/node_modules/*' | head -30")}
Extracted URLs: ${p.bash("grep -rh 'https\\?://[^ )>\"]*' --include='*.md' --no-filename . 2>/dev/null | grep -oE 'https?://[^ )>\"]+' | sort -u | head -50")}

Steps:
1. For each unique URL extracted, call checkUrl.
2. Build the links array with url, status, httpCode, and foundInFile (leave as null if not determinable).
3. Count brokenCount (status = "broken" or "timeout").
4. Set allValid to true only if brokenCount is 0.`,
output: s.object({
links: s.array(s.object({
url: s.string,
status: s.enum("ok", "broken", "redirect", "timeout", "skipped"),
httpCode: s.optional(s.number),
foundInFile: s.optional(s.string),
})),
brokenCount: s.number,
allValid: s.boolean,
}),
tools: [checkUrl],
maxTurns: 8,
});

export default markdownLinkChecker;
```
35 changes: 35 additions & 0 deletions skills/rig/samples/243-package-version-drift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 243 - Package Version Drift

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

// Agent role: report package version drift by comparing package.json with npm outdated results.
const packageVersionDrift = agent({
model: "small",
instructions: p`Analyze package version drift for this workspace.

package.json: ${p.read("package.json")}
npm outdated (JSON, may be empty if all current): ${p.bash("npm outdated --json 2>/dev/null || echo '{}'")}

Steps:
1. Parse the npm outdated JSON — each key is a package name with current/wanted/latest fields.
2. For each outdated package determine driftLevel:
- "ok" if current === latest
- "patch" if only patch differs
- "minor" if minor version differs
- "major" if major version differs
3. Write a markdown drift report to drift-report.md summarizing the table.
4. Set reportWritten to true after writing.`,
output: 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.

[/grill-with-docs] The instructions say "Write a markdown drift report to drift-report.md" but the agent has no write tool and no p.write(...) prompt intent. The reportWritten output field will always be a hallucinated true. Either add a p.write intent or remove the file-writing step and the reportWritten field.

💡 Options

Option A — remove the phantom write:

// Remove "3. Write a markdown drift report to drift-report.md" from instructions
// Remove reportWritten from the output schema
output: s.object({
  packages: s.array(...),
}),

Option B — add a write intent:

instructions: p`...
${p.write("drift-report.md", "<the generated markdown table>")}
`,

packages: s.array(s.object({
name: s.string,
current: s.string,
latest: s.string,
driftLevel: s.enum("ok", "patch", "minor", "major"),
})),
reportWritten: s.boolean,
}),
});

export default packageVersionDrift;
```
32 changes: 32 additions & 0 deletions skills/rig/samples/244-git-author-stats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 244 - Git Author Stats

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

// Agent role: aggregate git author statistics including commit counts and first/last commit dates.
const gitAuthorStats = agent({
model: "small",
instructions: p`Aggregate git commit statistics by author.

Commit counts per author: ${p.bash("git shortlog -sne --all 2>/dev/null || echo ''")}
Per-commit author and date: ${p.bash("git log --format='%ae|%ai' --all 2>/dev/null | head -500")}

Steps:
1. Parse the shortlog output to extract each author email and commit count.
2. From the per-commit log, compute firstCommit (earliest date) and lastCommit (latest date) per author email.
3. Build the authors record keyed by email with commitCount, firstCommit, lastCommit.
4. Set topAuthor to the email with the highest commitCount.
5. Set totalCommits to the sum of all commitCounts.`,
output: s.object({
authors: s.record(s.object({
commitCount: s.number,
firstCommit: s.string,
lastCommit: s.string,
})),
topAuthor: s.string,
totalCommits: s.number,
}),
});

export default gitAuthorStats;
```
43 changes: 43 additions & 0 deletions skills/rig/samples/245-ts-path-alias-validator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 245 - Ts Path Alias Validator

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

const checkAlias = defineTool("checkAlias", {
description: "Check whether a TypeScript path alias target directory exists on disk.",
parameters: { alias: s.string, target: s.string },
handler: ({ target }: { alias: string; target: string }) => {
const resolved = target.replace(/\/\*$/, "");
return existsSync(resolved) ? "valid" : "broken";
},
});

// Agent role: validate all TypeScript path aliases defined in tsconfig.json against the workspace.
const tsPathAliasValidator = agent({
model: "small",
instructions: p`Validate TypeScript path aliases defined in tsconfig.json.

tsconfig.json: ${p.read("tsconfig.json")}
Import statements in .ts files: ${p.bash("grep -rh --include='*.ts' 'from \"' . 2>/dev/null | grep -v node_modules | sort -u | head -100")}

Steps:
1. Parse the compilerOptions.paths from tsconfig.json — each key is an alias pattern, values are target arrays.
2. For each alias, call checkAlias with the alias key and its first target path.
3. Check whether each alias is actually used in the import statements.
4. Set status: "broken" if target doesn't exist, "unused" if never imported, "valid" otherwise.
5. Count brokenCount as the number of broken aliases.`,
output: s.object({
aliases: s.record(s.object({
target: s.string,
status: s.enum("valid", "broken", "unused"),
})),
totalAliases: s.number,
brokenCount: s.number,
}),
tools: [checkAlias],
maxTurns: 6,
});

export default tsPathAliasValidator;
```
47 changes: 47 additions & 0 deletions skills/rig/samples/246-stale-branch-detector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 246 - Stale Branch Detector

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

const classifyBranchAge = defineTool("classifyBranchAge", {
description: "Classify a branch as fresh, stale, or dead based on its last commit date.",
parameters: { branchName: s.string, lastCommitDate: s.string },
handler: ({ lastCommitDate }: { branchName: string; lastCommitDate: string }) => {
const ageMs = Date.now() - new Date(lastCommitDate).getTime();
const ageDays = ageMs / (1000 * 60 * 60 * 24);
if (ageDays < 30) return "fresh";
if (ageDays < 90) return "stale";
return "dead";
},
});

// Agent role: detect stale and dead local git branches and recommend candidates for deletion.
const staleBranchDetector = agent({
model: "small",
instructions: p`Detect stale and dead local git branches.

Branch list with last commit dates:
${p.bash("git for-each-ref --format='%(refname:short)|%(committerdate:iso8601)' refs/heads 2>/dev/null || echo ''")}

Steps:
1. Parse each line as branchName|lastCommitDate.
2. For each branch, call classifyBranchAge to get its age category.
3. Build the branches array with name, lastCommit, and age.
4. Count staleCount (age = "stale") and deadCount (age = "dead").
5. Set recommendedForDeletion to branch names where age is "dead".`,
output: s.object({
branches: s.array(s.object({
name: s.string,
lastCommit: s.string,
age: s.enum("fresh", "stale", "dead"),
})),
staleCount: s.number,
deadCount: s.number,
recommendedForDeletion: s.array(s.string),
}),
tools: [classifyBranchAge],
maxTurns: 6,
});

export default staleBranchDetector;
```
54 changes: 54 additions & 0 deletions skills/rig/samples/247-license-header-checker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 247 - License Header Checker

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

const checkLicenseHeader = defineTool("checkLicenseHeader", {
description: "Check whether a TypeScript file starts with the expected license header.",
parameters: { filePath: s.string, expectedHeader: s.string },
handler: async ({ filePath, expectedHeader }: { filePath: string; expectedHeader: string }) => {
try {
const content = await readFile(filePath, "utf-8");
const lines = content.split("\n");
const headerLines = expectedHeader.split("\n");
const fileStart = lines.slice(0, headerLines.length).join("\n");
if (fileStart === expectedHeader) return "ok";
if (content.includes(expectedHeader.split("\n")[0])) return "wrong";
return "missing";
} catch {
return "missing";
}
},
});

// Agent role: check that all TypeScript source files contain the expected license header.
const licenseHeaderChecker = agent({
model: "small",
input: s.object({ expectedHeader: s.string }),
instructions: p`Check each TypeScript file for the expected license header.

TypeScript files in the workspace:
${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' | head -60")}

Steps:
1. Read expectedHeader from the input.
2. For each file path, call checkLicenseHeader with the filePath and expectedHeader.
3. Build a files record keyed by file path with hasHeader and status.
4. Count missingCount (status = "missing" or "wrong").
5. Set allCompliant to true only if missingCount is 0.`,
output: s.object({
files: s.record(s.object({
hasHeader: s.boolean,
status: s.enum("ok", "missing", "wrong"),
})),
missingCount: s.number,
allCompliant: s.boolean,
}),
tools: [checkLicenseHeader],
maxTurns: 8,
addons: [repair()],
});

export default licenseHeaderChecker;
```
Loading
Loading