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
46 changes: 46 additions & 0 deletions skills/rig/samples/300-server-uptime-log-parser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 300 - Server Uptime Log Parser

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

const parseUptimeEvent = defineTool("parseUptimeEvent", {
description: "Parse a log line to extract uptime lifecycle events",
parameters: s.object({ line: s.string }),
handler({ line }) {
const startedMatch = line.match(/(\S+\s+\S+\s+\S+).*\s(\S+(?:\[\d+\])?): .*[Ss]tarted/);
const stoppedMatch = line.match(/(\S+\s+\S+\s+\S+).*\s(\S+(?:\[\d+\])?): .*[Ss]topped/);
const crashMatch = line.match(/(\S+\s+\S+\s+\S+).*\s(\S+(?:\[\d+\])?): .*(?:[Cc]rash|[Kk]ill|exit.*code [^0])/);
if (crashMatch) return { timestamp: crashMatch[1] ?? "", service: crashMatch[2] ?? "", event: "crashed" as const };
if (startedMatch) return { timestamp: startedMatch[1] ?? "", service: startedMatch[2] ?? "", event: "started" as const };
if (stoppedMatch) return { timestamp: stoppedMatch[1] ?? "", service: stoppedMatch[2] ?? "", event: "stopped" as const };
return { timestamp: "", service: "", event: "unknown" as const };
},
});

// Agent role: Parse system logs to identify service lifecycle events, crash counts, and uptime statistics.
const uptimeLogParser = agent({
model: "small",
instructions: p`Analyze the following system log lines to identify service lifecycle events.
Log lines:
${p.bash("journalctl -n 200 --no-pager 2>/dev/null || tail -n 200 /var/log/syslog 2>/dev/null || echo 'no logs'")}

Use the parseUptimeEvent tool on relevant log lines that mention service starts, stops, or crashes.
Calculate totalUptimeHours as a rough estimate based on timestamps found.
Set hasCrashLoop to true if crashCount >= 3.
Return the structured output.`,
output: s.object({
uptimeEvents: s.array(s.object({
timestamp: s.string,
service: s.string,
event: s.enum("started", "stopped", "crashed", "unknown"),
})),
crashCount: s.int,
totalUptimeHours: s.number,
hasCrashLoop: s.boolean,
}),
tools: [parseUptimeEvent],
addons: [steering(), repair()],
});

export default uptimeLogParser;
```
41 changes: 41 additions & 0 deletions skills/rig/samples/301-css-variable-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 301 - CSS Variable Extractor

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

const parseCssVar = defineTool("parseCssVar", {
description: "Parse a CSS line to extract custom property (CSS variable) declarations",
parameters: s.object({ line: s.string }),
handler({ line }) {
const match = line.match(/^\s*(--[\w-]+)\s*:\s*(.+?)\s*;?\s*$/);
if (!match) return { name: "", value: "", found: false };
return { name: match[1] ?? "", value: match[2] ?? "", found: true };
},
});

// Agent role: Scan CSS files to extract CSS custom property declarations and detect unused variables.
const cssVariableExtractor = agent({
model: "small",
instructions: p`Scan CSS files and extract all custom property (CSS variable) declarations.

CSS file content (declarations and usages):
${p.bash("find . -name '*.css' -not -path '*/node_modules/*' | head -20 | xargs grep -h -- '--' 2>/dev/null | head -100 || echo 'no css files'")}

Use the parseCssVar tool on each line that may contain a CSS variable declaration (--variable: value).
For each variable found, count how many times it appears as a usage (var(--name)).
A variable is unused if it is declared but never used via var().
Return the structured output.`,
output: s.object({
variables: s.record(s.object({
value: s.string,
usageCount: s.int,
})),
unusedVars: s.array(s.string),
totalVars: s.int,
}),
tools: [parseCssVar],
addons: [steering(), repair()],
});

export default cssVariableExtractor;
```
51 changes: 51 additions & 0 deletions skills/rig/samples/302-openapi-route-coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 302 - OpenAPI Route Coverage

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

const extractRoutes = defineTool("extractRoutes", {
description: "Extract HTTP route patterns from router handler code",
parameters: s.object({ content: s.string }),
handler({ content }) {
const routes: Array<{ path: string; method: string }> = [];
const pattern = /router\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi;

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] extractRoutes only matches Express-style router.METHOD() calls and returns them directly as the tool's return value, but the instructions pass the raw grep output (not the parsed code) to the tool. The tool receives the entire concatenated grep lines as content, which includes line numbers and file prefixes (file.ts:42:router.get(...)). The regex router\. will work on this string, but the file-prefixed format isn't called out — a reader adapting this sample may get silent mismatches.

💡 Suggestion

Either document in a comment that grep -h strips the file prefix (which the bash command already uses), or adjust the regex to anchor more carefully. A small clarifying comment near the p.bash line would close the gap for readers:

// -h suppresses filename prefix so each line starts with the code directly
${p.bash("grep -rhn 'router\.' ...")}

let m: RegExpExecArray | null;
while ((m = pattern.exec(content)) !== null) {
routes.push({ method: (m[1] ?? "get").toUpperCase(), path: m[2] ?? "/" });
}
return routes;
},
});

// Agent role: Compare OpenAPI spec routes against implemented router handlers to measure coverage.
const openapiRouteCoverage = agent({
model: "small",
instructions: p`Compare OpenAPI spec routes against implemented router handlers to determine coverage.

OpenAPI spec:
${p.readOptional("openapi.json", "{}")}

Router handler code:
${p.bash("grep -rn 'router\\.' --include='*.ts' --include='*.js' . 2>/dev/null | head -50 || echo 'no routes'")}

Use the extractRoutes tool on the router handler code to discover implemented routes.
Cross-reference with paths defined in the OpenAPI spec.
Return routes array showing which are defined (in spec) vs implemented (in code).
Calculate coveragePercent as (totalImplemented / totalDefined * 100) or 0 if no spec.`,
output: s.object({
routes: s.array(s.object({
path: s.string,
method: s.string,
defined: s.boolean,
implemented: s.boolean,
})),
coveragePercent: s.number,
totalDefined: s.int,
totalImplemented: s.int,
}),
tools: [extractRoutes],
addons: [repair()],
});

export default openapiRouteCoverage;
```
44 changes: 44 additions & 0 deletions skills/rig/samples/303-import-style-checker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 303 - Import Style Checker

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

const classifyStyle = defineTool("classifyStyle", {
description: "Classify a file's module style based on require vs import counts",
parameters: s.object({ file: s.string, requireCount: s.int, importCount: s.int }),
handler({ requireCount, importCount }) {
if (requireCount === 0 && importCount === 0) return "unknown" as const;
if (requireCount > 0 && importCount > 0) return "mixed" as const;
if (requireCount > 0) return "cjs" as const;
return "esm" as const;
},
});

// Agent role: Classify each JavaScript file by its module system (ESM vs CJS) based on import/require usage.
const importStyleChecker = agent({
model: "small",
instructions: p`Analyze JavaScript files to classify their module system style.

Files with require/import patterns:
${p.bash("find . -name '*.js' -not -path '*/node_modules/*' | head -10 | xargs grep -hn '^require\\|^import ' 2>/dev/null | head -80 || echo 'no imports'")}

For each file found, count occurrences of require(...) and import statements.
Use the classifyStyle tool to determine the module style per file.
Return counts per file and summary totals.`,
output: s.object({
files: s.record(s.object({
style: s.enum("esm", "cjs", "mixed", "unknown"),
requireCount: s.int,
importCount: s.int,
})),
esmCount: s.int,
cjsCount: s.int,
mixedCount: s.int,
unknownCount: s.int,
}),
tools: [classifyStyle],
addons: [repair()],
});

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

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

const parseSemver = defineTool("parseSemver", {
description: "Parse a git tag to determine if it follows semver and extract components",
parameters: s.object({ tag: s.string }),
handler({ tag }) {
const match = tag.match(/^v?(\d+)\.(\d+)\.(\d+)/);
if (!match) return { valid: false };
return {
valid: true,
major: parseInt(match[1] ?? "0", 10),
minor: parseInt(match[2] ?? "0", 10),
patch: parseInt(match[3] ?? "0", 10),
};
},
});

// Agent role: Validate git tags against semver format and identify the latest valid version.
const gitTagSemverValidator = agent({
model: "small",
instructions: p`Validate git tags to check semver compliance.

Git tags:
${p.bash("git tag --list 2>/dev/null | head -30 || echo 'no tags'")}

Use the parseSemver tool on each tag to validate format and extract major/minor/patch.
Identify invalidCount (tags that don't follow semver).
Set latestValid to the highest valid semver tag, or omit if none.
Return the structured output.`,
output: s.object({
tags: s.array(s.object({
name: s.string,
valid: s.boolean,
parsed: s.optional(s.object({
major: s.int,
minor: s.int,
patch: s.int,
})),
})),
invalidCount: s.int,
latestValid: s.optional(s.string),
}),
tools: [parseSemver],
addons: [steering()],
});

export default gitTagSemverValidator;
```
48 changes: 48 additions & 0 deletions skills/rig/samples/305-shell-script-safety-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 305 - Shell Script Safety Analyzer

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

const analyzeShellScript = defineTool("analyzeShellScript", {
description: "Analyze a shell script file for safety flags (set -e, set -u, set -o pipefail) and shebang",
parameters: s.object({ filePath: s.string }),
async handler({ filePath }) {
const content = await readFile(filePath, "utf8").catch(() => "");
const lines = content.split("\n");
const shebangLine = lines[0] ?? "";
const shebang = shebangLine.startsWith("#!") ? shebangLine.slice(2).trim() : undefined;
const hasSetE = /\bset\b.*-[a-zA-Z]*e/.test(content) || /\bset\b.*-e/.test(content);
const hasSetU = /\bset\b.*-[a-zA-Z]*u/.test(content) || /\bset\b.*-u/.test(content);
const hasPipefail = /set\s+-o\s+pipefail/.test(content);
let safetyLevel: "strict" | "partial" | "unsafe";
if (hasSetE && hasSetU && hasPipefail) safetyLevel = "strict";
else if (hasSetE || hasSetU || hasPipefail) safetyLevel = "partial";
else safetyLevel = "unsafe";
return { shebang, hasSetE, hasSetU, hasPipefail, safetyLevel };
},
});

// Agent role: Scan shell scripts for safety flags and classify each by safety level.
const shellScriptSafetyAnalyzer = agent({
model: "small",
instructions: p`Find all shell scripts and analyze each for safety flags.

Shell script files:
${p.bash("find . -name '*.sh' -not -path '*/node_modules/*' | head -20 2>/dev/null || echo 'no shell scripts'")}

Use the analyzeShellScript tool on each .sh file path listed above.
Return a record keyed by file path with safety analysis for each script.`,
output: s.record(s.object({
shebang: s.optional(s.string),
hasSetE: s.boolean,
hasSetU: s.boolean,
hasPipefail: s.boolean,
safetyLevel: s.enum("strict", "partial", "unsafe"),
})),
tools: [analyzeShellScript],
addons: [steering()],
});

export default shellScriptSafetyAnalyzer;
```
49 changes: 49 additions & 0 deletions skills/rig/samples/306-git-diff-stats-summarizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 306 - Git Diff Stats Summarizer

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

const classifyDiffEntry = defineTool("classifyDiffEntry", {
description: "Classify a diff entry based on addition/deletion counts",
parameters: s.object({ path: s.string, additions: s.int, deletions: s.int }),
handler({ additions, deletions }) {
if (additions > 0 && deletions === 0) return "added" as const;
if (additions === 0 && deletions > 0) return "deleted" as const;
if (additions > 0 && deletions > 0) return "modified" as const;
return "renamed" as const;

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] classifyDiffEntry returns "renamed" as the fallback when both additions === 0 and deletions === 0 — but that is actually an unchanged file in git diff --numstat output. A renamed file in numstat typically shows as 0 0 old => new or {old => new}. The sample teaches an incorrect classification heuristic.

💡 Suggested fix
handler({ additions, deletions }) {
  if (additions > 0 && deletions === 0) return "added" as const;
  if (additions === 0 && deletions > 0) return "deleted" as const;
  if (additions > 0 && deletions > 0) return "modified" as const;
  return "unchanged" as const;  // 0/0 means no net line changes, not a rename
},

Rename detection would require inspecting the file path string (e.g. {old => new} pattern). This is worth a brief comment so readers don't copy the wrong semantic.

},
});

// Agent role: Summarize git diff statistics between the last two commits, classifying each changed file.
const gitDiffStatsSummarizer = agent({
model: "small",
instructions: p`Analyze git diff statistics for the most recent commit.

Diff numstat (additions deletions file):
${p.bash("git diff --numstat HEAD~1 HEAD 2>/dev/null | head -30 || echo 'no diff'")}

Diff summary:
${p.bash("git diff --stat HEAD~1 HEAD 2>/dev/null | tail -5 || echo 'no stat'")}

Parse the numstat output (format: additions<TAB>deletions<TAB>path).
Use the classifyDiffEntry tool on each file entry.
Sum up totalAdditions and totalDeletions.
Set mostChangedFile to the file with the highest combined additions+deletions, or omit if empty.
Return the structured output.`,
output: s.object({
files: s.array(s.object({
path: s.string,
additions: s.int,
deletions: s.int,
change: s.enum("added", "modified", "deleted", "renamed"),
})),
totalAdditions: s.int,
totalDeletions: s.int,
mostChangedFile: s.optional(s.string),
}),
tools: [classifyDiffEntry],
addons: [repair()],
});

export default gitDiffStatsSummarizer;
```
50 changes: 50 additions & 0 deletions skills/rig/samples/307-dotenv-template-generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 307 - Dotenv Template Generator

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

const extractEnvReferences = defineTool("extractEnvReferences", {
description: "Extract all process.env.VAR_NAME references from a TypeScript source file",
parameters: s.object({ filePath: s.string }),
async handler({ filePath }) {
const content = await readFile(filePath, "utf8").catch(() => "");
const pattern = /process\.env\.([A-Z_][A-Z0-9_]*)/g;
const keys = new Set<string>();
let m: RegExpExecArray | null;
while ((m = pattern.exec(content)) !== null) {
if (m[1]) keys.add(m[1]);
}
return Array.from(keys);
},
});

// Agent role: Generate a .env.template file by scanning source files for process.env references and comparing against existing .env keys.
const dotenvTemplateGenerator = agent({
model: "small",
instructions: p`Generate a .env.template file documenting all required environment variables.

Existing .env file:
${p.readOptional(".env", "# no .env file found")}

TypeScript source files to scan:
${p.glob("src/**/*.ts")}

Use the extractEnvReferences tool on each source file to find process.env.VAR references.
Collect all unique env keys found across all files (envKeys).
Compare with keys present in .env — undocumentedKeys are those referenced in code but missing from .env.
Write a .env.template with each key as KEY=<placeholder>.
${p.write(".env.template", "# Generated .env.template\n# Replace <placeholder> with actual values\n")}
Return templatePath as ".env.template", envKeys array, undocumentedKeys array, and templateGenerated as true.`,
output: s.object({
templatePath: s.path,
envKeys: s.array(s.string),
undocumentedKeys: s.array(s.string),
templateGenerated: 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 p.write call in the instructions prompt is a placeholder that tells the LLM what to write — but the output schema already declares templateGenerated: s.boolean and templatePath: s.path. The sample conflates two different approaches: p.write as a prompt intent vs. returning a path for the caller to act on. Only one pattern should be modeled.

💡 Consider

If the intent is to show p.write as a prompt intent that instructs the model to write the file, the output schema fields templateGenerated and templatePath are redundant and should be removed. If the intent is to return structured data for the caller to write the file, remove the p.write from the instructions and keep the output schema. Mixing both patterns in one sample will confuse readers about when to use each approach.

}),
tools: [extractEnvReferences],
addons: [repair()],
});

export default dotenvTemplateGenerator;
```
Loading
Loading