Skip to content
Draft
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
54 changes: 54 additions & 0 deletions skills/rig/samples/371-git-file-ownership-mapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 371 - Git File Ownership Mapper

```rig
import { agent, p, s, defineTool, steering } from "rig";
import { execSync } from "node:child_process";

const getFileOwner = defineTool("getFileOwner", {
description: "Get git commit history for a file and return ownership info.",
parameters: { filePath: s.path },
handler: ({ filePath }: { filePath: string }) => {
try {
const output = execSync(`git log --format="%ae" -- "${filePath}" 2>/dev/null`, { encoding: "utf-8" }).trim();
const emails = output ? output.split("\n").filter(Boolean) : [];
const counts: Record<string, number> = {};
for (const email of emails) counts[email] = (counts[email] ?? 0) + 1;
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
const primaryOwner = sorted[0]?.[0] ?? "unknown";
const commitCount = emails.length;
const contributors = sorted.map(([email]) => email);
return { primaryOwner, commitCount, contributors };
} catch {
return { primaryOwner: "unknown", commitCount: 0, contributors: [] };
}
},
});

// Agent role: map git file ownership for all TypeScript source files.
const gitFileOwnershipMapper = agent({
model: "small",
instructions: p`Map git file ownership for TypeScript source files.

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

Steps:
1. For each file path listed above, call getFileOwner to retrieve primaryOwner, commitCount, contributors.
2. Build ownership record keyed by file path.
3. Find mostActiveContributor: the email with the highest total commit count across all files.`,
output: s.object({
ownership: s.record(
s.object({
primaryOwner: s.string,
commitCount: s.int,
contributors: s.array(s.string),
})
),
mostActiveContributor: s.string,
}),
tools: [getFileOwner],
addons: [steering()],
});

export default gitFileOwnershipMapper;
```
64 changes: 64 additions & 0 deletions skills/rig/samples/372-workflow-input-validator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# 372 - Workflow Input Validator

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

const validateWorkflowInputs = defineTool("validateWorkflowInputs", {
description: "Parse a GitHub Actions workflow YAML file and extract workflow_dispatch inputs.",
parameters: { filePath: s.path },
handler: async ({ filePath }: { filePath: string }) => {
const content = await readFile(filePath, "utf-8");
const inputsMatch = content.match(/workflow_dispatch:\s*\n(?:\s+.*\n)*?\s+inputs:([\s\S]*?)(?=\n\w|\n\s{0,2}\w|$)/);
if (!inputsMatch) return { inputs: {}, inputCount: 0, hasRequiredWithoutDefault: false };
const inputsBlock = inputsMatch[1];
const inputEntries: Record<string, { type: string; required: boolean; default?: string | undefined }> = {};
const inputPattern = /^\s{4,8}(\w+):\s*\n((?:\s{6,12}.+\n?)*)/gm;
let match: RegExpExecArray | null;
while ((match = inputPattern.exec(inputsBlock)) !== null) {
const name = match[1];
const block = match[2];
const typeM = block.match(/type:\s*(.+)/);
const requiredM = block.match(/required:\s*(true|false)/);
const defaultM = block.match(/default:\s*(.+)/);
inputEntries[name] = {
type: typeM ? typeM[1].trim() : "string",
required: requiredM ? requiredM[1] === "true" : false,
default: defaultM ? defaultM[1].trim() : undefined,
};
}
const inputCount = Object.keys(inputEntries).length;
const hasRequiredWithoutDefault = Object.values(inputEntries).some(
(v) => v.required && v.default === undefined
);
return { inputs: inputEntries, inputCount, hasRequiredWithoutDefault };
},
});

// Agent role: validate GitHub Actions workflow_dispatch inputs across all workflow files.
const workflowInputValidator = agent({
model: "small",
instructions: p`Validate workflow_dispatch inputs in all GitHub Actions workflow files.

Workflow files:
${p.glob(".github/workflows/*.yml")}

Steps:
1. For each file path listed above, call validateWorkflowInputs to extract inputs, inputCount, hasRequiredWithoutDefault.
2. Build workflows record keyed by filename.
3. Set totalWorkflows to the count of workflow files processed.`,
output: s.object({
workflows: s.record(
s.object({
inputCount: s.int,
hasRequiredWithoutDefault: s.boolean,
})
),
totalWorkflows: s.int,
}),
tools: [validateWorkflowInputs],
addons: [repair()],
});

export default workflowInputValidator;
```
59 changes: 59 additions & 0 deletions skills/rig/samples/373-ts-dead-export-finder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 373 - TypeScript Dead Export Finder

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

const findUnusedExports = defineTool("findUnusedExports", {
description: "Find exported symbols in a TypeScript file that are not imported in other files.",
parameters: { filePath: s.path, allFiles: s.array(s.string) },
handler: async ({ filePath, allFiles }: { filePath: string; allFiles: string[] }) => {
const content = await readFile(filePath, "utf-8");
const exportPattern = /export\s+(?:const|function|class|type|interface|enum)\s+(\w+)/g;
const exported: string[] = [];
let m: RegExpExecArray | null;
while ((m = exportPattern.exec(content)) !== null) exported.push(m[1]);
const unused: string[] = [];
for (const sym of exported) {
let found = false;
for (const other of allFiles) {
if (other === filePath) continue;
try {
const otherContent = await readFile(other, "utf-8");
if (otherContent.includes(sym)) { found = true; break; }
} catch { /* skip */ }
}
if (!found) unused.push(sym);
}
return { unused };
},
});

// Agent role: find unused TypeScript exports across the codebase.
const tsDeadExportFinder = agent({
model: "small",
instructions: p`Find TypeScript exported symbols that are never imported elsewhere.

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

All TypeScript files (for cross-reference):
${p.bash("find . -name '*.ts' -not -path '*/node_modules/*' 2>/dev/null | head -100")}

Steps:
1. For each file in the source list, call findUnusedExports with that file and the full allFiles list.
2. Build unusedExports record keyed by file path, value is array of unused symbol names.
3. Omit files with no unused exports.
4. totalUnused = total count of unused symbols across all files.
5. hasDeadCode = totalUnused > 0.`,
output: s.object({
unusedExports: s.record(s.array(s.string)),
totalUnused: s.int,
hasDeadCode: s.boolean,
}),
tools: [findUnusedExports],
addons: [steering(), repair()],
});

export default tsDeadExportFinder;
```
54 changes: 54 additions & 0 deletions skills/rig/samples/374-package-scripts-documenter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 374 - Package Scripts Documenter

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

const inferScriptPurpose = defineTool("inferScriptPurpose", {
description: "Classify a package.json script by its command into a category.",
parameters: { name: s.string, command: s.string },
handler: ({ name, command }: { name: string; command: string }) => {
const cmd = command.toLowerCase();
const nm = name.toLowerCase();
let category: "build" | "test" | "lint" | "release" | "dev" | "other" = "other";
if (/\btest\b|jest|vitest|mocha/.test(cmd) || /\btest/.test(nm)) category = "test";
else if (/\bbuild\b|tsc|webpack|vite|rollup|esbuild/.test(cmd) || /\bbuild/.test(nm)) category = "build";
else if (/\blint\b|eslint|prettier|biome/.test(cmd) || /\blint/.test(nm)) category = "lint";
else if (/\brelease\b|publish|changeset|version/.test(cmd) || /\brelease\b|\bpublish/.test(nm)) category = "release";
else if (/\bdev\b|watch|start\b|nodemon/.test(cmd) || /\bdev\b|\bstart\b|\bwatch/.test(nm)) category = "dev";
const purpose = `Runs ${name}: ${command.slice(0, 60)}`;
return { purpose, category } as const;
},
});

// Agent role: document all package.json scripts with purpose and category, then write SCRIPTS.md.
const packageScriptsDocumenter = agent({
model: "small",
instructions: p`Document all scripts in package.json and write SCRIPTS.md.

package.json contents:
${p.read("package.json")}

Steps:
1. Parse the scripts object from the package.json content above.
2. For each script name and command, call inferScriptPurpose to get purpose and category.
3. Build the scripts record keyed by script name.
4. Write SCRIPTS.md using p.write with a markdown table listing each script, its category, and purpose.
5. documentedCount = number of scripts processed.
6. outputFile = "SCRIPTS.md".`,
output: s.object({
scripts: s.record(
s.object({
purpose: s.string,
category: s.enum("build", "test", "lint", "release", "dev", "other"),
command: s.string,
})
),
documentedCount: s.int,
outputFile: s.path,
}),
tools: [inferScriptPurpose],
addons: [repair()],
});

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

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

const classifyDiffEntry = defineTool("classifyDiffEntry", {
description: "Classify a git diff --numstat entry as added, modified, deleted, or renamed.",
parameters: { additions: s.int, deletions: s.int, path: s.string },
handler: ({ additions, deletions, path }: { additions: number; deletions: number; path: string }) => {
let changeType: "added" | "modified" | "deleted" | "renamed" = "modified";
if (path.includes(" => ") || path.includes("{")) changeType = "renamed";
else if (deletions === 0 && additions > 0) changeType = "added";
else if (additions === 0 && deletions > 0) changeType = "deleted";
return { changeType } as const;
},
});

// Agent role: summarize git diff statistics between HEAD~1 and HEAD.
const gitDiffStatsSummarizer = agent({
model: "small",
instructions: p`Summarize file changes between the last two commits.

Diff numstat output:
${p.bash("git diff --numstat HEAD~1 HEAD 2>/dev/null || echo ''")}

Steps:
1. Parse each line of the numstat output (format: additions TAB deletions TAB path).
2. For each entry, call classifyDiffEntry with additions, deletions, path to get changeType.
3. Build files array with path, additions, deletions, changeType.
4. totalAdditions = sum of all additions.
5. totalDeletions = sum of all deletions.
6. mostChangedFile = path with highest additions+deletions (omit if no files).`,
output: s.object({
files: s.array(
s.object({
path: s.string,
additions: s.int,
deletions: s.int,
changeType: s.enum("added", "modified", "deleted", "renamed"),
})
),
totalAdditions: s.int,
totalDeletions: s.int,
mostChangedFile: s.optional(s.string),
}),
tools: [classifyDiffEntry],
addons: [repair()],
});

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

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

const extractEnvReferences = defineTool("extractEnvReferences", {
description: "Extract process.env.X references from a TypeScript file.",
parameters: { filePath: s.path },
handler: async ({ filePath }: { filePath: string }) => {
try {
const content = await readFile(filePath, "utf-8");
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) keys.add(m[1]);
return { keys: Array.from(keys) };
} catch {
return { keys: [] };
}
},
});

// Agent role: generate a .env.template from process.env references in source files.
const dotenvTemplateGenerator = agent({
model: "small",
instructions: p`Generate a .env.template file from process.env references in TypeScript source files.

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

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

Steps:
1. For each TypeScript file path listed above, call extractEnvReferences to get the list of env keys.
2. Collect all unique keys referenced across all files → envKeys.
3. Parse the existing .env content to find documented keys.
4. undocumentedKeys = envKeys not already in .env.
5. Write .env.template with each key as KEY= (one per line, with a comment header).
6. templatePath = ".env.template", templateGenerated = true.`,
output: s.object({
templatePath: s.path,
envKeys: s.array(s.string),
undocumentedKeys: s.array(s.string),
templateGenerated: s.boolean,
}),
tools: [extractEnvReferences],
addons: [repair()],
});

export default dotenvTemplateGenerator;
```
Loading