Skip to content

Commit ce58ef5

Browse files
Add 10 rig samples 331-340 (2026-07-31) (#318)
1 parent afe5524 commit ce58ef5

10 files changed

Lines changed: 415 additions & 0 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# 331 - TS Generic Type Extractor
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
import { readFile } from "node:fs/promises";
6+
7+
const extractGenerics = defineTool("extractGenerics", {
8+
description: "Extract generic type parameters from a TypeScript file",
9+
parameters: s.object({ filePath: s.path }),
10+
handler: async ({ filePath }: { filePath: string }) => {
11+
const src = await readFile(filePath, "utf-8");
12+
const matches = src.match(/<[A-Z][A-Za-z0-9, ]*>/g) ?? [];
13+
const generics = [...new Set(matches.map((m: string) => m.slice(1, -1).trim()))];
14+
const count = generics.length;
15+
const complexity =
16+
count === 0 ? "none" as const
17+
: count <= 2 ? "simple" as const
18+
: count <= 5 ? "moderate" as const
19+
: "complex" as const;
20+
return { generics, count, complexity };
21+
},
22+
});
23+
24+
// Agent role: find TypeScript files and extract generic type parameters, classifying complexity per file.
25+
const tsGenericTypeExtractor = agent({
26+
model: "small",
27+
instructions: p`TypeScript files found: ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | head -30")}
28+
For each file, call extractGenerics to get its generic types and return the results keyed by filename.`,
29+
output: s.record(s.object({
30+
generics: s.array(s.string),
31+
count: s.int,
32+
complexity: s.enum("none", "simple", "moderate", "complex"),
33+
})),
34+
tools: [extractGenerics],
35+
addons: [repair()],
36+
maxTurns: 6,
37+
});
38+
39+
export default tsGenericTypeExtractor;
40+
```
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# 332 - Shell Script Validator
2+
3+
```rig
4+
import { agent, p, s, defineTool, steering } from "rig";
5+
import { readFile } from "node:fs/promises";
6+
7+
const validateScript = defineTool("validateScript", {
8+
description: "Validate a shell script for safety best practices",
9+
parameters: s.object({ filePath: s.path }),
10+
handler: async ({ filePath }: { filePath: string }) => {
11+
const src = await readFile(filePath, "utf-8");
12+
const hasShebang = src.startsWith("#!");
13+
const hasSafeFlags = /set\s+-[euo]*[euo]/.test(src);
14+
const hasEval = /\beval\b/.test(src);
15+
const functionCount = (src.match(/\bfunction\s+\w+|^\w+\s*\(\s*\)/gm) ?? []).length;
16+
let quality: "excellent" | "good" | "fair" | "poor";
17+
if (hasShebang && hasSafeFlags && !hasEval) quality = "excellent" as const;
18+
else if (hasShebang && hasSafeFlags) quality = "good" as const;
19+
else if (hasShebang) quality = "fair" as const;
20+
else quality = "poor" as const;
21+
return { hasShebang, hasSafeFlags, hasEval, functionCount, quality };
22+
},
23+
});
24+
25+
// Agent role: find shell scripts and validate each for shebang, safe flags, eval usage, and function count.
26+
const shellScriptValidator = agent({
27+
model: "small",
28+
instructions: p`Shell scripts found: ${p.bash("find . -name '*.sh' -not -path '*/node_modules/*' | head -30")}
29+
Call validateScript for each path and return results keyed by file path.`,
30+
output: s.record(s.object({
31+
hasShebang: s.boolean,
32+
hasSafeFlags: s.boolean,
33+
hasEval: s.boolean,
34+
functionCount: s.int,
35+
quality: s.enum("excellent", "good", "fair", "poor"),
36+
})),
37+
tools: [validateScript],
38+
addons: [steering()],
39+
maxTurns: 6,
40+
});
41+
42+
export default shellScriptValidator;
43+
```
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# 333 - CI Flake Triager
2+
3+
```rig
4+
import { agent, p, s, repair } from "rig";
5+
6+
// Agent role: triage the most recent CI test failure and classify it by root cause.
7+
const ciFlakeTriager = agent({
8+
model: "small",
9+
instructions: p`Recent test log:
10+
${p.bash("cat test-results/last-failure.log 2>/dev/null || echo 'No test log found'")}
11+
12+
Workflow file:
13+
${p.readOptional(".github/workflows/ci.yml", "(no ci.yml found)")}
14+
15+
Classify the failure. Return failureClass, confidence (0-1), retryAdvice, and affected test names.`,
16+
output: s.object({
17+
failureClass: s.enum("infrastructure", "assertion", "timeout", "unknown"),
18+
confidence: s.number,
19+
retryAdvice: s.string,
20+
affectedTests: s.array(s.string),
21+
}),
22+
addons: [repair()],
23+
maxTurns: 2,
24+
});
25+
26+
export default ciFlakeTriager;
27+
```
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# 334 - Config Drift Reconciler
2+
3+
```rig
4+
import { agent, p, s } from "rig";
5+
6+
// Agent role: compare baseline and active config, write a corrected patch, and report drifted keys.
7+
const configDriftReconciler = agent({
8+
model: "small",
9+
instructions: p`Baseline config:
10+
${p.readOptional("config/baseline.json", "{}")}
11+
12+
Active config:
13+
${p.readOptional("config/active.json", "{}")}
14+
15+
Compare the two configs. For each key that differs, record baseline and actual values.
16+
Produce a patch JSON with corrections and return the analysis.
17+
${p.writeOutput("patch", "config/patch.json")}`,
18+
output: s.object({
19+
patch: s.string,
20+
changedKeys: s.record(s.object({
21+
baseline: s.unknown,
22+
actual: s.unknown,
23+
})),
24+
summary: s.object({
25+
totalDrifted: s.int,
26+
totalChecked: s.int,
27+
normalized: s.boolean,
28+
}),
29+
}),
30+
maxTurns: 4,
31+
});
32+
33+
export default configDriftReconciler;
34+
```
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# 335 - Release Note Enricher
2+
3+
```rig
4+
import { agent, p, s, defineTool, steering, repair } from "rig";
5+
6+
const lookupTicketMetadata = defineTool("lookupTicketMetadata", {
7+
description: "Extract ticket references (#NNN or PROJ-NNN) from text",
8+
parameters: s.object({ text: s.string }),
9+
handler: ({ text }: { text: string }) => {
10+
const githubRefs = text.match(/#\d+/g) ?? [];
11+
const projectRefs = text.match(/[A-Z]+-\d+/g) ?? [];
12+
return { githubRefs, projectRefs, found: githubRefs.length + projectRefs.length };
13+
},
14+
});
15+
16+
// Agent role: enrich raw release notes by extracting ticket references, categorizing sections, and assessing risk.
17+
const releaseNoteEnricher = agent({
18+
model: "small",
19+
input: s.object({ rawNotes: s.string }),
20+
instructions: p`Enrich the provided release notes. Call lookupTicketMetadata to extract ticket references.
21+
Then organize notes into sections (features/bugfixes/breaking/chores), assign a risk label, and list any unresolved references.`,
22+
output: s.object({
23+
sections: s.array(s.object({
24+
title: s.string,
25+
items: s.array(s.string),
26+
})),
27+
riskLabel: s.enum("low", "medium", "high", "critical"),
28+
missingReferences: s.array(s.string),
29+
}),
30+
tools: [lookupTicketMetadata],
31+
addons: [steering(), repair()],
32+
maxTurns: 6,
33+
});
34+
35+
export default releaseNoteEnricher;
36+
```
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# 336 - Docs Refactor Coordinator
2+
3+
```rig
4+
import { agent, p, s } from "rig";
5+
6+
// Agent role: extract API names from README.
7+
const apiExtractor = agent({
8+
name: "apiExtractor",
9+
model: "small",
10+
instructions: p`Read the README: ${p.read("README.md")}
11+
Extract all API function and class names mentioned. Return a list of API names.`,
12+
output: s.object({
13+
apiNames: s.array(s.string),
14+
}),
15+
});
16+
17+
// Agent role: rewrite prose sections of README for clarity.
18+
const proseCleanup = agent({
19+
name: "proseCleanup",
20+
model: "small",
21+
instructions: p`Read the README: ${p.read("README.md")}
22+
Rewrite the prose sections for clarity, removing jargon and improving readability. Return cleaned prose.`,
23+
output: s.object({
24+
cleanedProse: s.string,
25+
changesApplied: s.int,
26+
}),
27+
});
28+
29+
// Agent role: coordinate API extraction and prose cleanup, then emit a refactored README.
30+
const docsRefactorCoordinator = agent({
31+
model: "small",
32+
instructions: p`Coordinate the documentation refactor:
33+
1. Ask apiExtractor to extract all API names from README.md
34+
2. Ask proseCleanup to rewrite the prose
35+
3. Combine results into a refactoredContent string
36+
${p.writeOutput("refactoredContent", "refactored.md")}
37+
Return extractedApis, changesApplied, and outputPath.`,
38+
output: s.object({
39+
extractedApis: s.array(s.string),
40+
changesApplied: s.int,
41+
outputPath: s.path,
42+
refactoredContent: s.string,
43+
}),
44+
agents: { apiExtractor, proseCleanup },
45+
maxTurns: 6,
46+
});
47+
48+
export default docsRefactorCoordinator;
49+
```
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# 337 - Markdown Frontmatter Extractor
2+
3+
```rig
4+
import { agent, p, s, defineTool, repair } from "rig";
5+
import { readFile } from "node:fs/promises";
6+
7+
const parseFrontmatter = defineTool("parseFrontmatter", {
8+
description: "Parse YAML frontmatter from a markdown file",
9+
parameters: s.object({ filePath: s.path }),
10+
handler: async ({ filePath }: { filePath: string }) => {
11+
const src = await readFile(filePath, "utf-8");
12+
const match = src.match(/^---\n([\s\S]*?)\n---/);
13+
if (!match) return { hasFrontmatter: false, title: null, date: null, tags: null };
14+
const fm = match[1];
15+
const title = (fm.match(/^title:\s*(.+)$/m) ?? [])[1]?.trim() ?? null;
16+
const date = (fm.match(/^date:\s*(.+)$/m) ?? [])[1]?.trim() ?? null;
17+
const tagsMatch = fm.match(/^tags:\s*\[(.+)\]/m);
18+
const tags = tagsMatch ? tagsMatch[1].split(",").map((t: string) => t.trim().replace(/['"]/g, "")) : null;
19+
return { hasFrontmatter: true, title, date, tags };
20+
},
21+
});
22+
23+
// Agent role: extract YAML frontmatter from all markdown files in the workspace.
24+
const markdownFrontmatterExtractor = agent({
25+
model: "small",
26+
instructions: p`Markdown files: ${p.glob("**/*.md")}
27+
Call parseFrontmatter for each file and return results keyed by file path.`,
28+
output: s.record(s.object({
29+
hasFrontmatter: s.boolean,
30+
title: s.optional(s.string),
31+
date: s.optional(s.string),
32+
tags: s.optional(s.array(s.string)),
33+
})),
34+
tools: [parseFrontmatter],
35+
addons: [repair()],
36+
maxTurns: 8,
37+
});
38+
39+
export default markdownFrontmatterExtractor;
40+
```
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# 338 - Git Diff Word Frequency
2+
3+
```rig
4+
import { agent, p, s, defineTool } from "rig";
5+
6+
const countWordChanges = defineTool("countWordChanges", {
7+
description: "Count added and deleted words from a git word-diff porcelain output",
8+
parameters: s.object({ diffOutput: s.string }),
9+
handler: ({ diffOutput }: { diffOutput: string }) => {
10+
const lines = diffOutput.split("\n");
11+
const addedWords: Record<string, number> = {};
12+
const deletedWords: Record<string, number> = {};
13+
for (const line of lines) {
14+
if (line.startsWith("+") && !line.startsWith("+++")) {
15+
line.slice(1).split(/\s+/).filter(Boolean).forEach((w: string) => {
16+
addedWords[w] = (addedWords[w] ?? 0) + 1;
17+
});
18+
} else if (line.startsWith("-") && !line.startsWith("---")) {
19+
line.slice(1).split(/\s+/).filter(Boolean).forEach((w: string) => {
20+
deletedWords[w] = (deletedWords[w] ?? 0) + 1;
21+
});
22+
}
23+
}
24+
const sortByCount = (m: Record<string, number>) =>
25+
Object.entries(m).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([w]) => w);
26+
return {
27+
topAddedWords: sortByCount(addedWords),
28+
topDeletedWords: sortByCount(deletedWords),
29+
totalAdditions: Object.values(addedWords).reduce((a, b) => a + b, 0),
30+
totalDeletions: Object.values(deletedWords).reduce((a, b) => a + b, 0),
31+
};
32+
},
33+
});
34+
35+
// Agent role: analyze word-level changes in the last git diff and report top added/deleted words.
36+
const gitDiffWordFrequency = agent({
37+
model: "small",
38+
instructions: p`Word diff output: ${p.bash("git diff --word-diff=porcelain HEAD~1 HEAD 2>/dev/null || git diff --word-diff=porcelain HEAD 2>/dev/null || echo 'no diff'")}
39+
Call countWordChanges with the diff output and return the word frequency analysis.`,
40+
output: s.object({
41+
topAddedWords: s.array(s.string),
42+
topDeletedWords: s.array(s.string),
43+
totalAdditions: s.int,
44+
totalDeletions: s.int,
45+
mostFrequentAddition: s.optional(s.string),
46+
}),
47+
tools: [countWordChanges],
48+
maxTurns: 4,
49+
});
50+
51+
export default gitDiffWordFrequency;
52+
```
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# 339 - TS Async Function Finder
2+
3+
```rig
4+
import { agent, p, s, defineTool, steering } from "rig";
5+
import { readFile } from "node:fs/promises";
6+
7+
const scanAsyncFunctions = defineTool("scanAsyncFunctions", {
8+
description: "Find async function and method signatures in a TypeScript file",
9+
parameters: s.object({ filePath: s.path }),
10+
handler: async ({ filePath }: { filePath: string }) => {
11+
const src = await readFile(filePath, "utf-8");
12+
const matches = src.match(/async\s+(?:function\s+(\w+)|\*?\s*(\w+)\s*\()/g) ?? [];
13+
const asyncFunctions = matches.map((m: string) => m.trim());
14+
return { asyncFunctions, count: asyncFunctions.length };
15+
},
16+
});
17+
18+
// Agent role: scan TypeScript files for async functions and return per-file counts with totals.
19+
const tsAsyncFunctionFinder = agent({
20+
model: "small",
21+
instructions: p`TypeScript files: ${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' | head -40")}
22+
Call scanAsyncFunctions for each file. Return files record, totalAsync, and mostAsyncFile.`,
23+
output: s.object({
24+
files: s.record(s.object({
25+
asyncFunctions: s.array(s.string),
26+
count: s.int,
27+
})),
28+
totalAsync: s.int,
29+
mostAsyncFile: s.optional(s.path),
30+
}),
31+
tools: [scanAsyncFunctions],
32+
addons: [steering()],
33+
maxTurns: 8,
34+
});
35+
36+
export default tsAsyncFunctionFinder;
37+
```

0 commit comments

Comments
 (0)