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
52 changes: 52 additions & 0 deletions skills/rig/samples/351-ts-import-depth-analyzer-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 351 - TS Import Depth Analyzer V2

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

const analyzeImportDepth = defineTool("analyzeImportDepth", {
description: "Analyze relative import depth in a TypeScript file by counting '../' occurrences.",
parameters: { filePath: s.path },
handler: async ({ filePath }: { filePath: string }) => {
const content = await readFile(filePath, "utf8");
const importRegex = /from\s+['"]([^'"]+)['"]/g;
const imports: string[] = [];
let match: RegExpExecArray | null;
while ((match = importRegex.exec(content)) !== null) {
imports.push(match[1]);
}
const relativeImports = imports.filter((i: string) => i.startsWith("."));
const depths = relativeImports.map((i: string) => (i.match(/\.\.\//g) ?? []).length);
const maxDepth = depths.length > 0 ? Math.max(...depths) : 0;
const deepImports = relativeImports.filter((i: string) => (i.match(/\.\.\//g) ?? []).length >= 2);
return { maxDepth, deepImports, importCount: imports.length };
},
});

// Agent role: analyze relative import depth across TypeScript files to find overly deep imports.
const tsImportDepthAnalyzer = agent({
model: "small",
instructions: p`Analyze TypeScript import depth across all source files.

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

For each .ts file (excluding node_modules), call analyzeImportDepth to get maxDepth, deepImports, and importCount.
Calculate the average depth across all files (sum of maxDepths / file count).
Identify the file with the highest maxDepth as deepestFile (omit if no files).
Return the full per-file record plus summary stats.`,
output: s.object({
files: s.record(s.object({
maxDepth: s.int,
deepImports: s.array(s.string),
importCount: s.int,
})),
deepestFile: s.optional(s.path),
averageDepth: s.number,
}),
tools: [analyzeImportDepth],
addons: [repair()],
});

export default tsImportDepthAnalyzer;

```
48 changes: 48 additions & 0 deletions skills/rig/samples/352-env-file-completeness-checker-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 352 - Env File Completeness Checker V2

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

const parseEnvKeys = defineTool("parseEnvKeys", {
description: "Parse an env file string and return the list of key names.",
parameters: { content: s.string },
handler: ({ content }: { content: string }) => {
return content
.split("\n")
.map((line: string) => line.trim())
.filter((line: string) => line.length > 0 && !line.startsWith("#"))
.map((line: string) => line.split("=")[0].trim())
.filter(Boolean);
},
});

// Agent role: compare .env.example with .env to check completeness of environment configuration.
const envFileCompletenessChecker = agent({
model: "small",
instructions: p`Compare .env.example with .env to check that all required keys are present.

.env.example content: ${p.readOptional(".env.example")}

.env content: ${p.readOptional(".env")}

Steps:
1. Call parseEnvKeys with the .env.example content to get exampleKeys.
2. Call parseEnvKeys with the .env content to get presentKeys.
3. Compute missingKeys (keys in exampleKeys but not presentKeys) and extraKeys (keys in presentKeys but not exampleKeys).
4. Compute completeness as (exampleKeys.length - missingKeys.length) / exampleKeys.length, or 1.0 if exampleKeys is empty.
5. Set status: "missing-example" if .env.example is absent, "missing-env" if .env is absent, "complete" if missingKeys is empty, "partial" otherwise.`,
output: s.object({
exampleKeys: s.array(s.string),
presentKeys: s.array(s.string),
missingKeys: s.array(s.string),
extraKeys: s.array(s.string),
completeness: s.number,
status: s.enum("complete", "partial", "missing-example", "missing-env"),
}),
tools: [parseEnvKeys],
addons: [repair()],
});

export default envFileCompletenessChecker;

```
49 changes: 49 additions & 0 deletions skills/rig/samples/353-stale-branch-detector-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 353 - Stale Branch Detector V2

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

const classifyBranchAge = defineTool("classifyBranchAge", {
description: "Classify a git branch as fresh, stale, or dead based on its last commit date.",
parameters: { branchName: s.string, lastCommitDate: s.string },
handler: ({ lastCommitDate }: { branchName: string; lastCommitDate: string }) => {
const ageMs = Date.now() - new Date(lastCommitDate).getTime();
const ageDays = ageMs / (1000 * 60 * 60 * 24);
if (ageDays < 30) return "fresh" as const;
if (ageDays < 90) return "stale" as const;
return "dead" as const;
},
});

// Agent role: detect stale and dead local git branches and recommend candidates for deletion.
const staleBranchDetector = agent({
model: "small",
instructions: p`Detect stale and dead local git branches.

Branch list with last commit dates:
${p.bash("git for-each-ref --format='%(refname:short)|%(committerdate:iso8601)' refs/heads 2>/dev/null || echo ''")}

Steps:
1. Parse each line as "branchName|lastCommitDate".
2. For each branch, call classifyBranchAge to get its classification.
3. Build the branches array with name, lastCommit, and classification fields.
4. Count staleCount (classification = "stale") and deadCount (classification = "dead").
5. Set recommendedForDeletion to the names of branches where classification is "dead".`,
output: s.object({
branches: s.array(s.object({
name: s.string,
lastCommit: s.string,
classification: s.enum("fresh", "stale", "dead"),
})),
staleCount: s.int,
deadCount: s.int,
recommendedForDeletion: s.array(s.string),
}),
tools: [classifyBranchAge],
maxTurns: 6,
addons: [repair()],
});

export default staleBranchDetector;

```
58 changes: 58 additions & 0 deletions skills/rig/samples/354-license-header-checker-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 354 - License Header Checker V2

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

const checkLicenseHeader = defineTool("checkLicenseHeader", {
description: "Check whether a TypeScript file starts with the expected license header.",
parameters: { filePath: s.string, expectedHeader: s.string },
handler: async ({ filePath, expectedHeader }: { filePath: string; expectedHeader: string }) => {
try {
const content = await readFile(filePath, "utf-8");
const headerLines = expectedHeader.split("\n");
const fileStart = content.split("\n").slice(0, headerLines.length).join("\n");
const hasHeader = fileStart === expectedHeader;
const status = hasHeader
? ("ok" as const)
: content.includes(headerLines[0])
? ("wrong" as const)
: ("missing" as const);
return { hasHeader, status };
} catch {
return { hasHeader: false, status: "missing" as const };
}
},
});

// Agent role: check that all TypeScript source files contain the expected license header.
const licenseHeaderChecker = agent({
model: "small",
input: s.object({ expectedHeader: s.string }),
instructions: p`Check each TypeScript file for the expected license header provided in the input.

TypeScript files in the workspace:
${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' -not -path '*/.git/*' | head -60")}

Steps:
1. Read expectedHeader from the agent input.
2. For each file path, call checkLicenseHeader with filePath and expectedHeader.
3. Build a files record keyed by file path containing hasHeader and status.
4. Count missingCount as files where status is "missing" or "wrong".
5. Set allCompliant to true only if missingCount is 0.`,
output: s.object({
files: s.record(s.object({
hasHeader: s.boolean,
status: s.enum("ok", "missing", "wrong"),
})),
missingCount: s.int,
allCompliant: s.boolean,
}),
tools: [checkLicenseHeader],
maxTurns: 8,
addons: [repair()],
});

export default licenseHeaderChecker;

```
59 changes: 59 additions & 0 deletions skills/rig/samples/355-yaml-config-diff-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 355 - YAML Config Diff V2

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

const extractYamlKeys = defineTool("extractYamlKeys", {
description: "Extract top-level keys from a YAML string.",
parameters: { content: s.string },
handler: ({ content }: { content: string }) => {
const keys: string[] = [];
for (const line of content.split("\n")) {
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):/);
if (m) keys.push(m[1]);
}
return keys;
},
});

const diffKeys = defineTool("diffKeys", {
description: "Compute added and removed keys between two key arrays.",
parameters: { baseKeys: s.array(s.string), targetKeys: s.array(s.string) },
handler: ({ baseKeys, targetKeys }: { baseKeys: string[]; targetKeys: string[] }) => {
const addedKeys = targetKeys.filter((k: string) => !baseKeys.includes(k));
const removedKeys = baseKeys.filter((k: string) => !targetKeys.includes(k));
return { addedKeys, removedKeys };
},
});

// Agent role: diff top-level YAML keys between two config files and detect breaking changes.
const yamlConfigDiff = agent({
model: "small",
input: s.object({ baseFile: s.path, targetFile: s.path }),
instructions: p`Diff top-level YAML keys between two config files.

Base file content: ${p.readInput("baseFile")}
Target file content: ${p.readInput("targetFile")}

Steps:
1. Call extractYamlKeys on the base file content to get baseKeys.
2. Call extractYamlKeys on the target file content to get targetKeys.
3. Call diffKeys with baseKeys and targetKeys to get addedKeys and removedKeys.
4. changedKeys: keys present in both but with differing values — estimate from content or leave empty.

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 "estimate from content or leave empty" gives the LLM no reliable way to detect value changes — YAML parsing is needed to compare values, not text estimation. Consumers of this agent may assume changedKeys is authoritative when it will frequently be empty or incorrect.

💡 Suggested fix

Since extractYamlKeys only returns keys (not values), the agent cannot reliably detect changed values. Either:

  • Remove changedKeys from the output schema and note the limitation in the description, or
  • Add a extractYamlKeyValues tool that returns a Record<string, string> and diffs values explicitly.

Leaving the instruction as "estimate" teaches a bad pattern for agent samples.

5. totalChanges = addedKeys.length + removedKeys.length + changedKeys.length.
6. hasBreakingChanges = removedKeys.length > 0.`,
output: s.object({
addedKeys: s.array(s.string),
removedKeys: s.array(s.string),
changedKeys: s.array(s.string),
totalChanges: s.int,
hasBreakingChanges: s.boolean,
}),
tools: [extractYamlKeys, diffKeys],
maxTurns: 6,
addons: [repair()],
});

export default yamlConfigDiff;

```
58 changes: 58 additions & 0 deletions skills/rig/samples/356-monorepo-workspace-lister-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 356 - Monorepo Workspace Lister V2

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

const extractPackageInfo = defineTool("extractPackageInfo", {
description: "Read a nested package.json and extract name, version, dependency count, and private flag.",
parameters: { filePath: s.path },
handler: async ({ filePath }: { filePath: string }) => {
try {
const content = await readFile(filePath, "utf-8");
const pkg = JSON.parse(content);
const dependencyCount =
Object.keys(pkg.dependencies ?? {}).length +
Object.keys(pkg.devDependencies ?? {}).length;
return {
name: (pkg.name ?? "(unnamed)") as string,
version: (pkg.version ?? undefined) as string | undefined,
dependencyCount,
hasPrivate: pkg.private === true,
};
} catch {
return { name: "(error)", version: undefined, dependencyCount: 0, hasPrivate: false };
}
},
});

// Agent role: discover all workspace packages in a monorepo and list their metadata.
const monorepoWorkspaceLister = agent({
model: "small",
instructions: p`Discover all nested package.json files in this monorepo and extract package metadata.

Nested package.json paths (excluding node_modules):
${p.bash("find . -name 'package.json' -not -path '*/node_modules/*' -mindepth 2 -maxdepth 4 2>/dev/null")}

Steps:
1. For each file path in the list above, call extractPackageInfo to get its metadata.
2. Assemble the packages array with name, version (optional), dependencyCount, hasPrivate, and path for each.
3. Set totalPackages to the length of the packages array.`,
output: s.object({
packages: s.array(s.object({
name: s.string,
version: s.optional(s.string),
dependencyCount: s.int,
hasPrivate: s.boolean,
path: s.path,
})),
totalPackages: s.int,
}),
tools: [extractPackageInfo],
maxTurns: 8,
addons: [steering()],
});

export default monorepoWorkspaceLister;

```
53 changes: 53 additions & 0 deletions skills/rig/samples/357-ts-decorator-usage-scanner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 357 - 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 usage and return decorator names with their count.",
parameters: { filePath: s.path },
handler: async ({ filePath }: { filePath: string }) => {
try {
const content = await readFile(filePath, "utf-8");
const decoratorRegex = /@([A-Z][a-zA-Z0-9]*)/g;

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 regex @([A-Z][a-zA-Z0-9]*) silently skips lowercase decorators — e.g. @override, @inject, @memoize, @autobind — which are common in Angular, NestJS, and reflection-metadata usage. This produces silent false-negatives with no indication anything was missed.

💡 Suggested fix
const decoratorRegex = /@([a-zA-Z][a-zA-Z0-9]*)/g;

Lowercase decorators (e.g. @inject, @readonly) are a standard TypeScript pattern; the current pattern excludes them entirely without any warning in the output.

const found: string[] = [];
let match: RegExpExecArray | null;
while ((match = decoratorRegex.exec(content)) !== null) {
found.push(match[1]);
}
return { decorators: found, hasDecorators: found.length > 0 };
} catch {
return { decorators: [], hasDecorators: false };
}
},
});

// Agent role: scan TypeScript files for decorator usage and summarize which decorators are most common.
const tsDecoratorUsageScanner = agent({
model: "small",
instructions: p`Scan TypeScript files for decorator usage (e.g. @Injectable, @Component).

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

Steps:
1. For each file path, call scanDecorators to get the list of decorator names used.
2. Aggregate across all files: build a decorators record keyed by decorator name (without @),
with usageCount (total occurrences) and files (list of file paths where it appears).
3. totalDecorated = number of files that had at least one decorator.
4. mostUsedDecorator = decorator name with highest usageCount (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 tsDecoratorUsageScanner;

```
Loading
Loading