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
51 changes: 51 additions & 0 deletions skills/rig/samples/320-git-blame-ownership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 320 - Git Blame Ownership

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

// Agent role: analyze git blame output to compute per-author line ownership statistics for a given file.
const gitBlameOwnership = agent({
model: "small",
input: s.object({ filePath: s.string }),
instructions: p`Analyze git blame for the file at input.filePath.
Run: ${p.bash("git blame --line-porcelain HEAD -- . 2>/dev/null | head -5 || echo 'no git'")}

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 p.bash probe runs git blame --line-porcelain HEAD -- . (the whole repo) as a static example hint, but the agent's input supplies filePath. A reader following this sample would expect the bash intent to target the specific file — but p.bash strings are static and cannot reference input.* values at build time.

💡 Suggestion

Make the limitation explicit: either add a comment noting that the bash hint is illustrative only, or remove it and rely on the parseBlameOutput tool receiving the blame text from the LLM (which will construct the real git blame command from the declared input.filePath). Mixing a static glob-level bash hint with a per-file input creates a conceptual mismatch for readers.

Use the parseBlameOutput tool with the blame output for the requested file path.
Return per-author statistics.`,
output: s.object({
authors: s.record(s.object({
lineCount: s.int,
percentage: s.number,
firstLine: s.int,
lastLine: s.int,
})),
topAuthor: s.string,
totalLines: s.int,
}),
tools: [
defineTool("parseBlameOutput", {
description: "Parse git blame --line-porcelain output and compute per-author line counts",
parameters: s.object({ blameOutput: s.string }),
handler({ blameOutput }) {
const lines = blameOutput.split("\n");
const authorCounts: Record<string, { count: number; first: number; last: number }> = {};
let lineNum = 0;
for (const line of lines) {
if (line.startsWith("author ")) {
lineNum++;
const author = line.slice(7).trim();
if (!authorCounts[author]) {
authorCounts[author] = { count: 0, first: lineNum, last: lineNum };
}
authorCounts[author].count++;
authorCounts[author].last = lineNum;
}
}
return authorCounts;
},
}),
],
addons: [steering()],
});

export default gitBlameOwnership;
```
39 changes: 39 additions & 0 deletions skills/rig/samples/321-ts-generic-type-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 321 - TypeScript Generic Type Extractor

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

// Agent role: scan TypeScript files to extract and classify generic type parameters used in each file.
const tsGenericTypeExtractor = agent({
model: "small",
instructions: p`Find all TypeScript files (excluding node_modules) and extract their generic type parameters.
Files found: ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | head -30")}
For each file use the extractGenerics tool. Return a record keyed by filename.`,
output: s.record(s.object({
generics: s.array(s.string),
count: s.int,
complexity: s.enum("none", "simple", "moderate", "complex"),
})),
tools: [
defineTool("extractGenerics", {
description: "Read a TypeScript file and extract generic type parameters using regex",
parameters: s.object({ filePath: s.string }),
async handler({ filePath }) {
const content = await readFile(filePath, "utf8");
const matches = content.match(/<[A-Z][A-Za-z0-9]*(?:\s*,\s*[A-Z][A-Za-z0-9]*)*>/g) ?? [];
const unique = [...new Set(matches)];
const count = unique.length;
const complexity = count === 0 ? "none" as const
: count <= 2 ? "simple" as const
: count <= 5 ? "moderate" as const
: "complex" as const;
return { generics: unique, count, complexity };
},
}),
],
addons: [repair()],
});

export default tsGenericTypeExtractor;
```
44 changes: 44 additions & 0 deletions skills/rig/samples/322-shell-script-validator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 322 - Shell Script Validator

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

// Agent role: validate shell scripts for safety practices and assign a quality rating.
const shellScriptValidator = agent({
model: "small",
instructions: p`Find all shell scripts (excluding node_modules) and validate each one.
Scripts found: ${p.bash("find . -name '*.sh' -not -path '*/node_modules/*'")}
For each script, read its content and use the validateScript tool.
Return a record keyed by file path.`,
output: s.record(s.object({
hasShebang: s.boolean,
hasSafeFlags: s.boolean,
hasEval: s.boolean,
functionCount: s.int,
quality: s.enum("excellent", "good", "fair", "poor"),
})),
tools: [
defineTool("validateScript", {
description: "Validate a shell script for safety practices",
parameters: s.object({ filePath: s.string }),
async handler({ filePath }) {
const content = await readFile(filePath, "utf8");
const lines = content.split("\n");
const hasShebang = lines[0]?.startsWith("#!") ?? false;
const hasSafeFlags = /set\s+-[eux]*e[eux]*/.test(content) || /set\s+-euo\s+pipefail/.test(content);
const hasEval = /\beval\b/.test(content);
const functionCount = (content.match(/\bfunction\s+\w+|\w+\s*\(\s*\)/g) ?? []).length;
const quality = hasShebang && hasSafeFlags && !hasEval ? "excellent" as const
: hasShebang && hasSafeFlags ? "good" as const
: hasShebang ? "fair" as const
: "poor" as const;
return { hasShebang, hasSafeFlags, hasEval, functionCount, quality };
},
}),
],
addons: [steering()],
});

export default shellScriptValidator;
```
24 changes: 24 additions & 0 deletions skills/rig/samples/323-ci-flake-triager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 323 - CI Flake Triager

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

// Agent role: triage a CI test failure and classify it by root cause.
const ciFlakeTriager = agent({
model: "small",
maxTurns: 2,
instructions: p`Triage the most recent CI test failure in this repository.
Last test run output: ${p.bash("npm test 2>&1 | tail -80 || echo 'no test output'")}
CI workflow file: ${p.readOptional(".github/workflows/ci.yml", "# no CI workflow found")}
Classify the failure type, assess confidence, give retry advice, and list affected tests.`,
output: s.object({
failureClass: s.enum("infrastructure", "assertion", "timeout", "unknown"),
confidence: s.enum("high", "medium", "low"),
retryAdvice: s.string,
affectedTests: s.array(s.string),
}),
addons: [repair()],
});

export default ciFlakeTriager;
```
33 changes: 33 additions & 0 deletions skills/rig/samples/324-config-drift-reconciler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 324 - Config Drift Reconciler

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

// Agent role: compare a baseline config file against an active config, identify drifted keys, and write a reconciled patch.
const configDriftReconciler = agent({
model: "small",
input: s.object({
baseFile: s.path,
activeFile: s.path,
outputFile: s.path,
}),
instructions: p`Compare the baseline config file against the active config file.
Baseline: ${p.readInput("baseFile")}
Active: ${p.readInput("activeFile")}
Identify keys that have drifted (changed values). Write a corrected patch to the output file: ${p.writeInput("outputFile", "patch")}

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] p.writeInput("outputFile", "patch") uses "patch" as the contentOutputField name, but the output schema has no field named patch — it has changedKeys and summary. The write intent will silently find nothing to write at runtime.

💡 Suggestion

Either rename the output field to patch and give it a s.string type, or change the intent to reference an actual output field:

// Option A: add a patch field to the output schema
output: s.object({
  patch: s.string,
  changedKeys: s.record(...),
  summary: s.object(...),
}),
// then reference it correctly:
${p.writeInput("outputFile", "patch")}

Without a matching output field, this sample incorrectly demonstrates p.writeInput.

Return the changed keys and a summary.`,
output: s.object({
changedKeys: s.record(s.object({
baseline: s.unknown,
actual: s.unknown,
})),
summary: s.object({
totalDrifted: s.int,
totalChecked: s.int,
normalized: s.boolean,
}),
}),
});

export default configDriftReconciler;
```
39 changes: 39 additions & 0 deletions skills/rig/samples/325-release-note-enricher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 325 - Release Note Enricher

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

// Agent role: enrich raw release notes by extracting ticket references and organizing entries into categorized sections.
const releaseNoteEnricher = agent({
model: "small",
input: s.object({ rawNotes: s.string }),
instructions: p`Enrich the following release notes from input.rawNotes.
Use the lookupTicketMetadata tool to resolve any ticket references (#NNN or PROJ-NNN).
Organize entries into sections (e.g., Features, Bug Fixes, Breaking Changes).
Assign a risk label and list any references that could not be resolved.`,
output: s.object({
sections: s.array(s.object({
heading: s.string,
items: s.array(s.string),
})),
riskLabel: s.enum("low", "medium", "high", "critical"),
missingReferences: s.array(s.string),
}),
tools: [
defineTool("lookupTicketMetadata", {
description: "Extract and return stub metadata for ticket references found in release notes text",
parameters: s.object({ references: s.array(s.string) }),
handler({ references }) {
return references.map((ref: string) => ({
ref,
title: `Title for ${ref}`,
status: "closed",
}));
},
}),
],
addons: [steering(), repair()],
});

export default releaseNoteEnricher;
```
48 changes: 48 additions & 0 deletions skills/rig/samples/326-ssh-config-host-parser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 326 - SSH Config Host Parser

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

// Agent role: parse the SSH config file and return a record of host blocks with their connection settings.
const sshConfigHostParser = agent({
model: "small",
instructions: p`Parse the SSH config file and extract all Host blocks.
SSH config: ${p.readOptional("~/.ssh/config", "# no ssh config found")}
Use the parseHostBlock tool on the config content. Return a record keyed by host pattern.`,
output: s.record(s.object({
hostName: s.optional(s.string),
user: s.optional(s.string),
port: s.optional(s.int),
identityFile: s.optional(s.path),
})),
tools: [
defineTool("parseHostBlock", {
description: "Parse SSH config content and extract Host blocks with their settings",
parameters: s.object({ configContent: s.string }),
handler({ configContent }) {
const result: Record<string, { hostName?: string; user?: string; port?: number; identityFile?: string }> = {};
const blocks = configContent.split(/^(?=Host\s)/m);
for (const block of blocks) {
const hostMatch = block.match(/^Host\s+(.+)/);
if (!hostMatch) continue;
const hostPattern = hostMatch[1].trim();
const entry: { hostName?: string; user?: string; port?: number; identityFile?: string } = {};
const hostnameMatch = block.match(/^\s*HostName\s+(.+)/m);
if (hostnameMatch) entry.hostName = hostnameMatch[1].trim();
const userMatch = block.match(/^\s*User\s+(.+)/m);
if (userMatch) entry.user = userMatch[1].trim();
const portMatch = block.match(/^\s*Port\s+(\d+)/m);
if (portMatch) entry.port = parseInt(portMatch[1], 10);
const idMatch = block.match(/^\s*IdentityFile\s+(.+)/m);
if (idMatch) entry.identityFile = idMatch[1].trim();
result[hostPattern] = entry;
}
return result;
},
}),
],
addons: [repair()],
});

export default sshConfigHostParser;
```
51 changes: 51 additions & 0 deletions skills/rig/samples/327-vitest-test-file-classifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 327 - Vitest Test File Classifier

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

// Agent role: classify vitest test files by type and gather test count statistics.
const vitestTestFileClassifier = agent({
model: "small",
instructions: p`Discover and classify all vitest test files in this workspace.
Test files: ${p.glob("**/*.test.ts")}
For each file use the classifyTestFile tool. Return a record keyed by file path plus aggregate counts.`,
output: s.object({
files: s.record(s.object({
testType: s.enum("unit", "integration", "e2e", "snapshot", "unknown"),
testCount: s.int,
usesMocks: s.boolean,
})),
totalFiles: s.int,
unitCount: s.int,
integrationCount: s.int,
}),
tools: [
defineTool("classifyTestFile", {
description: "Read a test file and classify its type and count tests",
parameters: s.object({ filePath: s.string }),
async handler({ filePath }) {
const content = await readFile(filePath, "utf8");
const usesMocks = /vi\.mock\(/.test(content);
const testCount = (content.match(/\b(?:test|it)\s*\(/g) ?? []).length;
let testType: "unit" | "integration" | "e2e" | "snapshot" | "unknown";
if (/toMatchSnapshot|toMatchInlineSnapshot/.test(content)) {
testType = "snapshot";
} else if (/e2e|end.to.end|playwright|cypress/i.test(content)) {
testType = "e2e";
} else if (/integration|supertest|request\(app/i.test(content)) {
testType = "integration";
} else if (testCount > 0) {
testType = "unit";
} else {
testType = "unknown";
}
return { testType, testCount, usesMocks };
},
}),
],
addons: [steering()],
});

export default vitestTestFileClassifier;
```
36 changes: 36 additions & 0 deletions skills/rig/samples/328-git-file-at-revision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 328 - Git File at Revision

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

// Agent role: retrieve the content of a file at a specific git revision and return metadata about the commit.
const gitFileAtRevision = agent({
model: "small",
input: s.object({ filePath: s.path, revision: s.string }),
instructions: p`Retrieve the file content at the given git revision.
File content: ${p.bash("git show HEAD:README.md 2>/dev/null | head -3 || echo 'example'")}

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 p.bash command hard-codes HEAD:README.md instead of using the agent's input.filePath and input.revision inputs — so the sample misleads readers about how to wire runtime inputs into prompt intents.

💡 Suggestion

p.bash is a static string resolved at prompt-build time; it cannot reference runtime input.* values. A cleaner approach: omit the inline bash hint and let the LLM construct git show <revision>:<path> itself from the declared input schema. This is consistent with the p.bash invariant (INV:prompt-intents: p.* are declarative placeholders, never executed in-process).

Use the extractRevisionMetadata tool on the commit log line.
Return the file content, commit metadata, and line count.`,
output: s.object({
fileContent: s.string,
commitHash: s.string,
commitMessage: s.string,
linesCount: s.int,
revision: s.string,
}),
tools: [
defineTool("extractRevisionMetadata", {
description: "Parse a git log --oneline line to extract commit hash and message",
parameters: s.object({ logLine: s.string }),
handler({ logLine }) {
const match = logLine.match(/^([0-9a-f]{6,40})\s+(.+)$/);
if (!match) return { commitHash: "unknown", commitMessage: logLine };
return { commitHash: match[1], commitMessage: match[2] };
},
}),
],
addons: [repair()],
});

export default gitFileAtRevision;
```
Loading
Loading