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/411-ts-decorator-usage-scanner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 411 - TS Decorator Usage Scanner

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

const scanDecorators = defineTool("scanDecorators", {
description: "Scan a TypeScript file for decorator usages matching @UpperCase patterns",
parameters: s.object({ filePath: s.path }),
handler: async ({ filePath }: { filePath: string }) => {
const content = await readFile(filePath, "utf8");
const matches = content.match(/@[A-Z]\w+/g) ?? [];
return { decorators: matches, filePath };
},
});

// Agent role: Scan TypeScript source files for decorator usages and report counts per decorator.
const tsDecoratorScanner = agent({
model: "small",
instructions: p`Scan all TypeScript source files for decorator usage.
Files to scan: ${p.glob("src/**/*.ts")}
Use the scanDecorators tool on each file path listed above.
Return an object with:
- decorators: a record mapping each decorator name to { usageCount, files }
- totalDecorated: total number of files that had at least one decorator
- mostUsedDecorator: the decorator name with the highest usageCount, or omit if none found`,
output: s.object({
decorators: s.record(s.object({
usageCount: s.int,
files: s.array(s.path),
})),
totalDecorated: s.int,
mostUsedDecorator: s.optional(s.string),
}),
tools: [scanDecorators],
addons: [repair()],
});

export default tsDecoratorScanner;
```
55 changes: 55 additions & 0 deletions skills/rig/samples/412-git-tag-semver-validator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 412 - Git Tag Semver Validator

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

const validateSemver = defineTool("validateSemver", {
description: "Validate a git tag against semver format and classify it",
parameters: s.object({ tag: s.string }),
handler: ({ tag }: { tag: string }) => {
const releaseRe = /^\d+\.\d+\.\d+$/;
const prereleaseRe = /^\d+\.\d+\.\d+-.+$/;
if (releaseRe.test(tag)) {
return { valid: true, normalized: tag, type: "release" as const };
} else if (prereleaseRe.test(tag)) {
return { valid: true, normalized: tag, type: "prerelease" as const };
}
// strip leading 'v' and retry
const stripped = tag.replace(/^v/, "");
if (releaseRe.test(stripped)) {
return { valid: true, normalized: stripped, type: "release" as const };
} else if (prereleaseRe.test(stripped)) {
return { valid: true, normalized: stripped, type: "prerelease" as const };
}
return { valid: false, normalized: tag, type: "invalid" as const };
},
});

// Agent role: Validate all git tags in the repository against semver format and produce a validity report.
const gitTagSemverValidator = agent({
model: "small",
instructions: p`Validate all git tags against semver format.
Git tags: ${p.bash("git tag --list")}
Use the validateSemver tool on each tag.
Return an object with:
- tags: array of objects each with { tag, valid, normalized, type }
- validCount: number of valid tags
- invalidCount: number of invalid tags
- allValid: true if invalidCount is 0`,
output: s.object({
tags: s.array(s.object({
tag: s.string,
valid: s.boolean,
normalized: s.string,
type: s.enum("release", "prerelease", "invalid"),
})),
validCount: s.int,
invalidCount: s.int,
allValid: s.boolean,
}),
tools: [validateSemver],
addons: [steering()],
});

export default gitTagSemverValidator;
```
51 changes: 51 additions & 0 deletions skills/rig/samples/413-package-json-completeness-scorer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 413 - Package JSON Completeness Scorer

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

const scoreField = defineTool("scoreField", {
description: "Classify a package.json field as required, recommended, or optional and assign a score",
parameters: s.object({ fieldName: s.string }),
handler: ({ fieldName }: { fieldName: string }) => {
const required = ["name", "version", "description", "main", "license"];
const recommended = ["repository", "keywords", "author", "bugs", "homepage", "engines", "files"];
if (required.includes(fieldName)) {
return { category: "required" as const, score: 3 };
} else if (recommended.includes(fieldName)) {
return { category: "recommended" as const, score: 2 };
}
return { category: "optional" as const, score: 1 };
},
});

// Agent role: Score package.json completeness by classifying its fields and computing a completeness percentage.
const packageJsonScorer = agent({
model: "small",
instructions: p`Score the completeness of package.json.
File contents: ${p.read("package.json")}
Use scoreField on each top-level field present in the file.
Also check for all required/recommended fields that are missing.
Return:
- fields: record mapping each field name to { present: true, category, score }
- For missing required/recommended fields add them with present: false
- totalScore: sum of scores for present fields
- maxScore: sum of all possible scores (required*3 + recommended*2)
- completenessPercent: (totalScore / maxScore) * 100
- missingRequired: array of required field names that are absent`,
output: s.object({
fields: s.record(s.object({
present: s.boolean,
category: s.enum("required", "recommended", "optional"),
score: s.number,
})),
totalScore: s.number,
maxScore: s.number,
completenessPercent: s.number,
missingRequired: s.array(s.string),
}),
tools: [scoreField],
addons: [repair()],
});

export default packageJsonScorer;
```
55 changes: 55 additions & 0 deletions skills/rig/samples/414-parallel-branch-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 414 - Parallel Branch Analysis Workflow

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

const repoMetric = s.object({
label: s.string,
value: s.number,
detail: s.string,
});

// Agent role: Analyze branch health and return a labeled metric summarizing branch counts.
const branchHealthAgent = agent({
model: "small",
instructions: p`Analyze git branches and report a summary metric.
Branches: ${p.bash("git branch -a")}
Count total and stale branches. Return:
- label: "branch-health"
- value: ratio of active to total branches (0.0 to 1.0)
- detail: JSON string with totalBranches, staleCount, activeBranches as a short summary`,
output: repoMetric,
});

// Agent role: Analyze commit frequency and return a labeled metric for commits per day.
const commitFrequencyAgent = agent({
model: "small",
instructions: p`Analyze git commit frequency over the last 30 days and return a metric.
Commits: ${p.bash("git log --since='30 days ago' --format='%ad' --date=short")}
Count unique commit dates. Return:
- label: "commit-frequency"
- value: average commits per day over 30 days
- detail: JSON string with totalCommits, activeDays`,
output: repoMetric,
});

// Workflow role: Run branch health and commit frequency analysis in parallel, then classify overall repository health.
const parallelBranchAnalysis = workflow({
meta: { name: "parallel-branch-analysis", description: "Parallel branch and commit frequency analysis" },
body: async ({ call, parallel }) => {
const [branchHealth, commitFrequency] = await parallel([
() => call(branchHealthAgent, "analyze"),
() => call(commitFrequencyAgent, "analyze"),

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 is used here to classify the overall health, but the inline comment says "Workflow role: ... classify overall repository health" without explaining to readers when to prefer call.json over spinning up a named agent(). Since this is teaching material, a one-line comment spelling out the trade-off (e.g. // call.json for lightweight one-shot LLM inference without a full agent definition) would make the sample significantly more instructive.

]);

const overallHealth = await call.json(
`Given metrics ${JSON.stringify([branchHealth, commitFrequency])}, classify overall repository health as "healthy", "needs-attention", or "critical".`,
s.enum("healthy", "needs-attention", "critical"),
);

return { branchHealth, commitFrequency, overallHealth };
},
});

export default parallelBranchAnalysis;
```
54 changes: 54 additions & 0 deletions skills/rig/samples/415-lockfile-integrity-checker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 415 - Lockfile Integrity Checker

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

const verifyLockEntry = defineTool("verifyLockEntry", {
description: "Verify that a package lock entry has required resolved and integrity fields",
parameters: s.object({ packageName: s.string, entryJson: s.string }),
handler: ({ packageName: _packageName, entryJson }: { packageName: string; entryJson: string }) => {
let entry: Record<string, unknown>;
try {
entry = JSON.parse(entryJson) as Record<string, unknown>;
} catch {
return { valid: false, hasIntegrity: false, hasResolved: false };
}
const integrityVal = entry["integrity"];
const resolvedVal = entry["resolved"];
const hasIntegrity = typeof integrityVal === "string" && integrityVal.length > 0;
const hasResolved = typeof resolvedVal === "string" && resolvedVal.length > 0;
return {
valid: hasIntegrity && hasResolved,
hasIntegrity,
hasResolved,
};
},
});

// Agent role: Check package-lock.json entries for completeness of resolved and integrity fields.
const lockfileIntegrityChecker = 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] The instruction tells the agent to serialize each package entry as a JSON string (entryJson), then immediately parse it inside the tool handler. This round-trips through JSON serialization only to deserialize it — the agent could just pass a typed object, or the tool could accept s.unknown and cast it. The current approach also puts the serialization burden on the LLM, which may produce invalid JSON for complex entries and trigger unnecessary repair() cycles.

model: "small",
instructions: p`Check the lockfile for integrity and resolved fields.
Contents: ${p.read("package-lock.json")}
For each package in the "packages" or "dependencies" section, use verifyLockEntry with the package name and its JSON entry (serialized as a JSON string).
Return:
- packages: record mapping package name to { valid, hasIntegrity, hasResolved }
- mismatchCount: count of packages where valid is false
- isClean: true if mismatchCount is 0
- totalChecked: total number of packages checked`,
output: s.object({
packages: s.record(s.object({
valid: s.boolean,
hasIntegrity: s.boolean,
hasResolved: s.boolean,
})),
mismatchCount: s.int,
isClean: s.boolean,
totalChecked: s.int,
}),
tools: [verifyLockEntry],
addons: [repair()],
});

export default lockfileIntegrityChecker;
```
47 changes: 47 additions & 0 deletions skills/rig/samples/416-graphql-schema-type-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 416 - GraphQL Schema Type Extractor

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

const extractGraphqlTypes = defineTool("extractGraphqlTypes", {
description: "Extract type declarations from a GraphQL schema file",
parameters: s.object({ filePath: s.path }),
handler: async ({ filePath }: { filePath: string }) => {
const content = await readFile(filePath, "utf8");
const results: Array<{ name: string; kind: string; fieldCount: number; fields: string[]; sourceFile: string }> = [];
const re = /^(type|input|enum|interface|union)\s+(\w+)[^{]*\{([^}]*)\}/gm;
let match: RegExpExecArray | null;
while ((match = re.exec(content)) !== 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.

[/codebase-design] The regex /^(type|input|enum|interface|union)\s+(\w+)[^{]*\{([^}]*)\}/gm captures only single-line-to-first-} bodies. A GraphQL type whose first field opens a nested brace (e.g. a directive with arguments) will be captured incorrectly, and multi-line types spanning multiple } characters are truncated at the first closing brace.

💡 Why this matters in samples

For simple flat schemas this works fine. But the sample presents itself as a general-purpose extractor; readers who reuse it on a real schema with directives or union-extends will get silently wrong field lists. Adding a brief comment like // works for flat schemas without nested braces would set the correct expectation without changing the logic.

const kind = match[1];
const name = match[2];
const body = match[3];
const fields = body.split("\n")
.map((l: string) => l.trim())
.filter((l: string) => l.length > 0 && !l.startsWith("#"));
results.push({ name, kind, fieldCount: fields.length, fields, sourceFile: filePath });
}
return results;
},
});

// Agent role: Extract and catalog all type declarations from GraphQL schema files in the workspace.
const graphqlSchemaTypeExtractor = agent({
model: "small",
instructions: p`Extract all GraphQL type declarations from schema files.
Schema files: ${p.bash("find . -name '*.graphql' -not -path '*/node_modules/*'")}
Use extractGraphqlTypes on each file path.
Aggregate all results into a single record keyed by type name.
Each entry should have: kind (type/input/enum/interface/union), fieldCount, fields (array), sourceFile.`,
output: s.record(s.object({
kind: s.enum("type", "input", "enum", "interface", "union"),
fieldCount: s.int,
fields: s.array(s.string),
sourceFile: s.string,
})),
tools: [extractGraphqlTypes],
addons: [steering()],
});

export default graphqlSchemaTypeExtractor;
```
57 changes: 57 additions & 0 deletions skills/rig/samples/417-git-stale-branch-reporter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 417 - Git Stale Branch Reporter

```rig
import { agent, p, s, defineTool, repair } from "rig";
import { execSync } from "node:child_process";

const getBranchAge = defineTool("getBranchAge", {
description: "Get the age of a git branch by looking at its last commit date",
parameters: s.object({ branchName: s.string }),
handler: ({ branchName }: { branchName: string }) => {
let lastCommit: string;
try {
lastCommit = execSync(`git log -1 --format=%ci "${branchName}" 2>/dev/null`, { encoding: "utf8" }).trim();
} catch {

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] Shell injection risk: branchName is interpolated directly into the shell command without sanitisation. A branch name containing shell metacharacters (e.g. "; rm -rf .) could execute arbitrary commands — a bad pattern for teaching material.

💡 Suggested fix: use spawnSync with an argument array
import { spawnSync } from "node:child_process";
const result = spawnSync("git", ["log", "-1", "--format=%ci", branchName], { encoding: "utf8" });
lastCommit = result.stdout.trim();

This avoids the shell entirely — no injection surface, clearer intent, and a better pattern for readers to learn from.

lastCommit = "";
}
if (!lastCommit) {
return { lastCommit: "unknown", ageDays: -1, ageClass: "ancient" as const };
}
const commitDate = new Date(lastCommit);
const ageDays = Math.floor((Date.now() - commitDate.getTime()) / (1000 * 60 * 60 * 24));
let ageClass: "fresh" | "recent" | "stale" | "ancient";
if (ageDays <= 7) ageClass = "fresh";
else if (ageDays <= 30) ageClass = "recent";
else if (ageDays <= 180) ageClass = "stale";
else ageClass = "ancient";
return { lastCommit, ageDays, ageClass };
},
});

// Agent role: Report the age and staleness classification of each remote git branch.
const gitStaleBranchReporter = agent({
model: "small",
instructions: p`Report the age of each remote git branch.
Remote branches: ${p.bash("git branch -r")}
Use getBranchAge on each branch name (strip leading whitespace and 'origin/' prefix if needed, but pass the full ref).
Return:
- branches: record mapping branch name to { lastCommit, ageDays, ageClass }
- staleCount: number of branches with ageClass "stale"
- ancientCount: number of branches with ageClass "ancient"
- totalBranches: total count`,
output: s.object({
branches: s.record(s.object({
lastCommit: s.string,
ageDays: s.int,
ageClass: s.enum("fresh", "recent", "stale", "ancient"),
})),
staleCount: s.int,
ancientCount: s.int,
totalBranches: s.int,
}),
tools: [getBranchAge],
addons: [repair()],
});

export default gitStaleBranchReporter;
```
Loading
Loading