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/411-ts-generic-constraint-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 411 - TS Generic Constraint Extractor

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

// Agent role: extract generic type constraints from TypeScript files and summarize per file.
const tsGenericConstraintExtractor = agent({
model: "small",
instructions: p`Extract TypeScript generic type constraints (patterns like \`T extends ...\`) from source files.

TypeScript files: ${p.bash("find src -name '*.ts' 2>/dev/null | head -50 || echo ''")}

For each file path, call extractGenericConstraints. Then produce the declared output.`,
tools: [
defineTool("extractGenericConstraints", {
description: "Extract generic type constraints from a TypeScript file",
parameters: s.object({ filePath: s.path }),
async handler({ filePath }) {
try {
const content = await readFile(filePath, "utf-8");
const regex = /\bextends\s+([^\s,>{=]+(?:\s*[<(][^>)]*[>)])?)/g;
const constraints: string[] = [];
let m: RegExpExecArray | null;
while ((m = regex.exec(content)) !== null) {
constraints.push(m[1].trim());
}
return { filePath, constraints };
} catch {
return { filePath, constraints: [] };
}
},
}),
],
output: s.object({
constraints: s.record(s.array(s.string)),
totalConstraints: s.int,
constrainedCount: s.int,
mostConstrainedFile: s.optional(s.string),
}),
addons: [repair()],
});

export default tsGenericConstraintExtractor;

```
46 changes: 46 additions & 0 deletions skills/rig/samples/412-readme-badge-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 412 - README Badge Analyzer

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

// Agent role: parse README.md badges and classify each by category.
const readmeBadgeAnalyzer = agent({
model: "small",
instructions: p`Analyze badge images in the README and classify each badge.

README content:
${p.readOptional("README.md", "")}

Call parseBadge for each markdown image badge you find (pattern: [![...](imgUrl)](link)).
Then return the declared output.`,
tools: [
defineTool("parseBadge", {
description: "Parse a badge URL and classify it as ci, coverage, version, license, or other",
parameters: s.object({ url: s.string, label: s.string }),
handler({ url, label }: { url: string; label: string }) {
const u = url.toLowerCase();
const l = label.toLowerCase();
let category: "ci" | "coverage" | "version" | "license" | "other" = "other";
if (/github.*action|travis|circleci|appveyor|workflow|build/.test(u + l)) category = "ci";
else if (/coverage|codecov|coveralls/.test(u + l)) category = "coverage";
else if (/version|release|npm|pypi/.test(u + l)) category = "version";
else if (/license|mit|apache|gpl/.test(u + l)) category = "license";
return { url, label, category };
},
}),
],
output: s.object({
badges: s.array(s.object({
url: s.string,
label: s.string,
category: s.enum("ci", "coverage", "version", "license", "other"),
})),
totalBadges: s.int,
hasCiBadge: s.boolean,
}),
addons: [steering()],
});

export default readmeBadgeAnalyzer;

```
54 changes: 54 additions & 0 deletions skills/rig/samples/413-python-requirements-risk-mapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 413 - Python Requirements Risk Mapper

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

// Agent role: classify Python package risk from requirements.txt and pip list output.
const pythonRequirementsRiskMapper = agent({
model: "small",
instructions: p`Analyze Python package risk based on requirements.txt and installed packages.

requirements.txt:
${p.readOptional("requirements.txt", "(no requirements.txt found)")}

Installed packages (pip list):
${p.bash("pip list --format=json 2>/dev/null || echo '[]'")}

For each package, call classifyPackageRisk. Packages that are very old (version < 1.0),
known for past vulnerabilities (e.g. requests<2.20, pyyaml<5.4, pillow<9),
or have no version pinned are considered high risk.
Produce the declared output.`,
tools: [
defineTool("classifyPackageRisk", {
description: "Classify a Python package by risk level based on name and version",
parameters: s.object({ name: s.string, version: s.string }),
handler({ name, version }: { name: string; version: string }) {
const n = name.toLowerCase();
const major = parseInt(version.split(".")[0] ?? "0", 10);
let risk: "low" | "medium" | "high" = "low";
let reason = "stable package";
if (!version || version === "unknown") { risk = "high"; reason = "no version pinned"; }

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] Vulnerability heuristic is inverted: the condition checks major < 2 for packages like requests and urllib3, but requests has had CVEs on major == 2 (e.g. 2.18 → CVE-2018-18074). major < 2 would wrongly mark requests==1.x as high-risk and pass requests==2.x as low. A version-range check (e.g. < 2.20) would be more accurate.

💡 Example improvement
const knownVulnerable = [
  { name: "requests", maxSafe: [2, 20] },
  { name: "pyyaml",   maxSafe: [5, 4] },
  { name: "pillow",   maxSafe: [9, 0] },
];
const entry = knownVulnerable.find(v => n === v.name);
if (entry) {
  const [safeMaj, safeMin] = entry.maxSafe;
  const minor = parseInt(version.split(".")[1] ?? "0", 10);
  if (major < safeMaj || (major === safeMaj && minor < safeMin)) {
    risk = "high";
    reason = "known past vulnerability in old version";
  }
}

This is a sample, so a code comment acknowledging the simplification would also suffice.

else if (major === 0) { risk = "medium"; reason = "pre-1.0 release"; }
if (/(pyyaml|pillow|requests|urllib3|cryptography)/.test(n) && major < 2) {
risk = "high"; reason = "known past vulnerability in old version";
}
return { name, version, risk, reason };
},
}),
],
output: s.object({
packages: s.record(s.object({
version: s.string,
risk: s.enum("low", "medium", "high"),
reason: s.string,
})),
totalPackages: s.int,
riskyCount: s.int,
recommendedAction: s.enum("audit", "review", "ok"),
}),
addons: [steering()],
});

export default pythonRequirementsRiskMapper;

```
50 changes: 50 additions & 0 deletions skills/rig/samples/414-git-merge-complexity-scorer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 414 - Git Merge Complexity Scorer

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

// Agent role: score git merge commits by complexity based on files changed.
const gitMergeComplexityScorer = agent({
model: "small",
instructions: p`Score the complexity of recent merge commits in this repository.

Recent merge commits:
${p.bash("git log --merges --oneline -20 2>/dev/null || echo '(no merges found)'")}

For each merge commit hash, call scoreMergeComplexity. Then produce the declared output.`,
tools: [
defineTool("scoreMergeComplexity", {
description: "Score a merge commit complexity by running git show --stat",
parameters: s.object({ hash: s.string, message: s.string }),
handler({ hash, message }: { hash: string; message: string }) {
const { execSync } = require("node:child_process");

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: hash is interpolated directly into the shell command without quoting. A crafted commit hash (or a hash supplied by the LLM) could execute arbitrary shell code.

💡 Fix: use execFileSync with an args array

Replace the dynamic import and execSync with a top-level import and execFileSync:

import { execFileSync } from "node:child_process";

// inside handler:
const stat = execFileSync("git", ["show", "--stat", hash], { encoding: "utf-8" });

This completely avoids shell parsing. As a bonus, also move the require() call to a top-level import, consistent with all other samples.

try {
const stat = execSync(`git show --stat ${hash} 2>/dev/null`, { encoding: "utf-8" });
const match = stat.match(/(\d+) files? changed/);
const filesChanged = match ? parseInt(match[1], 10) : 0;
const complexity: "simple" | "moderate" | "complex" =
filesChanged <= 3 ? "simple" : filesChanged <= 10 ? "moderate" : "complex";
return { hash, message, filesChanged, complexity };
} catch {
return { hash, message, filesChanged: 0, complexity: "simple" as const };
}
},
}),
],
output: s.object({
merges: s.array(s.object({
hash: s.string,
message: s.string,
filesChanged: s.int,
complexity: s.enum("simple", "moderate", "complex"),
})),
totalMerges: s.int,
complexMergeCount: s.int,
mostComplexMerge: s.optional(s.string),
}),
addons: [repair()],
});

export default gitMergeComplexityScorer;

```
43 changes: 43 additions & 0 deletions skills/rig/samples/415-ts-reexport-chain-tracer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 415 - TS Reexport Chain Tracer

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

// Agent role: trace TypeScript re-export chains from an entry file.
const tsReexportChainTracer = agent({
model: "small",
input: s.object({ entryFile: s.path }),
instructions: p`Trace TypeScript re-export chains starting from the entry file.
Use traceReexports to follow export * from and export { X } from chains.
Detect circular references. Produce the declared output.`,
tools: [
defineTool("traceReexports", {
description: "Read a TypeScript file and extract its re-export paths",
parameters: s.object({ filePath: s.path }),
async handler({ filePath }) {
try {
const content = await readFile(filePath, "utf-8");
const regex = /export\s+(?:\*|\{[^}]*\})\s+from\s+['"]([^'"]+)['"]/g;
const exports: string[] = [];
let m: RegExpExecArray | null;
while ((m = regex.exec(content)) !== null) exports.push(m[1]);
return { filePath, reexports: exports };
} catch {
return { filePath, reexports: [] };

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] Circular detection is declared in the output schema (circularDetected: s.boolean) but the tool never tracks visited paths — the detection is left entirely to the LLM's memory. On deep or large trees the LLM will hallucinate this value.

💡 Implement tracking in the tool handler

Pass a visited set from the workflow level, or have the handler accept a chain parameter and check it:

parameters: s.object({ filePath: s.path, visitedPaths: s.array(s.string) }),
async handler({ filePath, visitedPaths }) {
  if (visitedPaths.includes(filePath)) return { filePath, reexports: [], circular: true };
  // ... read and extract
  return { filePath, reexports: exports, circular: false };
}

Alternatively, document in the sample that circularDetected is a best-effort LLM judgement, not a computed fact.

}
},
}),
],
output: s.object({
chain: s.array(s.object({ file: s.path, symbols: s.array(s.string) })),
chainDepth: s.int,
circularDetected: s.boolean,
}),
maxTurns: 6,
addons: [repair()],
});

export default tsReexportChainTracer;

```
57 changes: 57 additions & 0 deletions skills/rig/samples/416-source-comment-density-reporter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 416 - Source Comment Density Reporter

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

// Agent role: measure comment density across TypeScript source files.
const sourceCommentDensityReporter = agent({
model: "small",
instructions: p`Measure comment density for TypeScript source files.

Source files: ${p.glob("src/**/*.ts")}

For each file path, call measureCommentDensity. Then produce the declared output.`,
tools: [
defineTool("measureCommentDensity", {
description: "Count comment lines vs code lines in a TypeScript file",
parameters: s.object({ filePath: s.path }),
async handler({ filePath }) {
try {
const content = await readFile(filePath, "utf-8");
const lines = content.split("\n");
let commentLines = 0;
let inBlock = false;
for (const line of lines) {
const t = line.trim();
if (inBlock) { commentLines++; if (t.includes("*/")) inBlock = false; }
else if (t.startsWith("/*") || t.startsWith("*")) { commentLines++; if (!t.includes("*/")) inBlock = true; }
else if (t.startsWith("//")) commentLines++;
}

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] False-positive comment detection: t.startsWith("*") matches lines like * 2 or *args in code, not just JSDoc continuation lines. This will over-count comment lines in files that use multiplication or pointer-like patterns.

💡 Suggested fix

Restrict the continuation-line check to lines that start with optional whitespace followed by * as a JSDoc marker:

// Replace:
else if (t.startsWith("/*") || t.startsWith("*")) { ... }

// With:
else if (t.startsWith("/*") || (inBlock && t.startsWith("*"))) { ... }
// or simply:
else if (t.startsWith("/*")) { commentLines++; if (!t.includes("*/")) inBlock = true; }

Counting t.startsWith("*") only when inBlock is true avoids matching stray * characters in real code.

const codeLines = lines.length - commentLines;
const density = lines.length > 0 ? commentLines / lines.length : 0;
return { commentLines, codeLines, density };
} catch {
return { commentLines: 0, codeLines: 0, density: 0 };
}
},
}),
],
output: s.object({
files: s.record(s.object({
commentLines: s.int,
codeLines: s.int,
density: s.number,
})),
overall: s.object({
totalFiles: s.int,
averageDensity: s.number,
highDensityFiles: s.array(s.path),
}),
}),
addons: [repair()],
});

export default sourceCommentDensityReporter;

```
58 changes: 58 additions & 0 deletions skills/rig/samples/417-git-blame-line-age-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 417 - Git Blame Line Age Analyzer

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

// Agent role: analyze git blame line ages for TypeScript source files.
const gitBlameLineAgeAnalyzer = agent({
model: "small",
instructions: p`Analyze the age of lines in TypeScript source files using git blame.

TypeScript files (sample): ${p.bash("find src -name '*.ts' 2>/dev/null | head -10 || echo ''")}

For each file, call analyzeBlameAge to get line age statistics. Then produce the declared output.`,
tools: [
defineTool("analyzeBlameAge", {
description: "Run git blame on a file and compute line age statistics",
parameters: s.object({ filePath: s.path }),
handler({ filePath }: { filePath: string }) {
const { execSync } = require("node:child_process");
try {
const out = execSync(`git blame --line-porcelain "${filePath}" 2>/dev/null`, { encoding: "utf-8" });
const now = Date.now() / 1000;

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: filePath is inserted into the shell command inside double-quotes, but a path containing " or $(...) can still break out and run arbitrary commands.

💡 Fix: use execFileSync with an args array
import { execFileSync } from "node:child_process";

const out = execFileSync("git", ["blame", "--line-porcelain", filePath], { encoding: "utf-8" });

Also move the require() call to a top-level import to match the style of the other samples.

const timestamps: number[] = [];
for (const line of out.split("\n")) {
if (line.startsWith("author-time ")) timestamps.push(parseInt(line.slice(12), 10));
}
if (timestamps.length === 0) return { filePath, avgAgeDays: 0, staleLines: 0, recentLines: 0, totalLines: 0 };
const dayMs = 86400;
let stale = 0, recent = 0;
for (const ts of timestamps) {
const days = (now - ts) / dayMs;
if (days < 30) recent++;
else if (days > 365) stale++;
}
const avgAgeDays = Math.round(timestamps.reduce((a, t) => a + (now - t) / dayMs, 0) / timestamps.length);
return { filePath, avgAgeDays, staleLines: stale, recentLines: recent, totalLines: timestamps.length };
} catch {
return { filePath, avgAgeDays: 0, staleLines: 0, recentLines: 0, totalLines: 0 };
}
},
}),
],
output: s.object({
files: s.record(s.object({
avgAgeDays: s.int,
staleLines: s.int,
recentLines: s.int,
totalLines: s.int,
})),
oldestFile: s.optional(s.string),
newestFile: s.optional(s.string),
}),
addons: [repair()],
});

export default gitBlameLineAgeAnalyzer;

```
Loading
Loading