diff --git a/skills/rig/samples/240-git-commit-annotator.md b/skills/rig/samples/240-git-commit-annotator.md new file mode 100644 index 0000000..4df5dd5 --- /dev/null +++ b/skills/rig/samples/240-git-commit-annotator.md @@ -0,0 +1,40 @@ +# 240 - Git Commit Annotator + +```rig +import { agent, p, s } from "rig"; + +// Agent role: annotate each of the last 20 git commits with a category and impact level. +const commitSummarizer = agent({ + name: "commitSummarizer", + model: "small", + instructions: p`Classify the following git commit line into a category and impact level. +Commit: ${p.readInput("commit")}`, + input: s.object({ commit: s.string }), + output: s.object({ + category: s.enum("feat", "fix", "chore", "docs", "refactor", "test", "perf"), + impact: s.enum("low", "medium", "high"), + }), +}); + +// Agent role: fetch the last 20 git commits and annotate each with category and impact using the commitSummarizer subagent. +const gitCommitAnnotator = agent({ + model: "small", + instructions: p`You are a git commit annotator. Here are the last 20 commits: +${p.bash("git log --oneline -20")} + +For each commit line, call the commitSummarizer subagent with the full commit line. +Then assemble the annotations array and a short prose report summarizing the distribution of categories and impacts.`, + output: s.object({ + annotations: s.array(s.object({ + hash: s.string, + message: s.string, + category: s.enum("feat", "fix", "chore", "docs", "refactor", "test", "perf"), + impact: s.enum("low", "medium", "high"), + })), + report: s.string, + }), + agents: { commitSummarizer }, +}); + +export default gitCommitAnnotator; +``` diff --git a/skills/rig/samples/241-npm-script-dep-mapper.md b/skills/rig/samples/241-npm-script-dep-mapper.md new file mode 100644 index 0000000..569b917 --- /dev/null +++ b/skills/rig/samples/241-npm-script-dep-mapper.md @@ -0,0 +1,58 @@ +# 241 - Npm Script Dep Mapper + +```rig +import { agent, p, s, defineTool, repair } from "rig"; + +const parseScriptDeps = defineTool("parseScriptDeps", { + description: "Parse npm scripts JSON and extract per-script dependency metadata.", + parameters: { scriptsJson: s.string }, + handler: ({ scriptsJson }: { scriptsJson: string }) => { + const scripts: Record = JSON.parse(scriptsJson); + const result: Record = {}; + for (const [name, command] of Object.entries(scripts)) { + const refs = [...command.matchAll(/npm run ([a-z0-9:_-]+)/gi)].map((m: RegExpMatchArray) => m[1]); + const hasPreHook = (`pre${name}`) in scripts; + const hasPostHook = (`post${name}`) in scripts; + let category = "util"; + if (/build|compile|bundle/.test(name)) category = "build"; + else if (/test|spec|coverage/.test(name)) category = "test"; + else if (/lint|format|check/.test(name)) category = "lint"; + else if (/dev|start|watch/.test(name)) category = "dev"; + else if (/deploy|publish|release/.test(name)) category = "deploy"; + else if (/^pre|^post/.test(name)) category = "hook"; + result[name] = { command, dependsOn: refs, category, hasPreHook, hasPostHook }; + } + return result; + }, +}); + +// Agent role: map npm script dependencies in the workspace package.json. +const npmScriptDepMapper = agent({ + model: "small", + instructions: p`Analyze the npm scripts in the workspace package.json. +package.json content: ${p.read("package.json")} + +Steps: +1. Extract the "scripts" field from the package.json content as a JSON string. +2. Call parseScriptDeps with that JSON string. +3. Return the result directly as the output record.`, + output: s.record(s.object({ + command: s.string, + dependsOn: s.array(s.string), + category: s.enum("build", "test", "lint", "dev", "deploy", "util", "hook", "other"), + hasPreHook: s.boolean, + hasPostHook: s.boolean, + })), + tools: [parseScriptDeps], + maxTurns: 6, + addons: [repair()], +}); + +export default npmScriptDepMapper; +``` diff --git a/skills/rig/samples/242-markdown-link-checker.md b/skills/rig/samples/242-markdown-link-checker.md new file mode 100644 index 0000000..b8f94e1 --- /dev/null +++ b/skills/rig/samples/242-markdown-link-checker.md @@ -0,0 +1,59 @@ +# 242 - Markdown Link Checker + +```rig +import { agent, p, s, defineTool } from "rig"; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; + +const execAsync = promisify(exec); + +const checkUrl = defineTool("checkUrl", { + description: "Check an HTTP URL with curl and return its status.", + parameters: { url: s.string }, + handler: async ({ url }: { url: string }) => { + try { + const { stdout } = await execAsync( + `curl -s -o /dev/null -w "%{http_code}" --max-time 5 --location "${url}"`, + ); + const code = parseInt(stdout.trim(), 10); + let status: string; + if (code >= 200 && code < 300) status = "ok"; + else if (code >= 300 && code < 400) status = "redirect"; + else if (code === 0) status = "timeout"; + else status = "broken"; + return { httpCode: code, status }; + } catch { + return { httpCode: 0, status: "timeout" }; + } + }, +}); + +// Agent role: check all HTTP/HTTPS links found in markdown files in the workspace. +const markdownLinkChecker = agent({ + model: "small", + instructions: p`Check all HTTP/HTTPS links found in markdown files. + +Markdown files: ${p.bash("find . -name '*.md' -not -path '*/node_modules/*' | head -30")} +Extracted URLs: ${p.bash("grep -rh 'https\\?://[^ )>\"]*' --include='*.md' --no-filename . 2>/dev/null | grep -oE 'https?://[^ )>\"]+' | sort -u | head -50")} + +Steps: +1. For each unique URL extracted, call checkUrl. +2. Build the links array with url, status, httpCode, and foundInFile (leave as null if not determinable). +3. Count brokenCount (status = "broken" or "timeout"). +4. Set allValid to true only if brokenCount is 0.`, + output: s.object({ + links: s.array(s.object({ + url: s.string, + status: s.enum("ok", "broken", "redirect", "timeout", "skipped"), + httpCode: s.optional(s.number), + foundInFile: s.optional(s.string), + })), + brokenCount: s.number, + allValid: s.boolean, + }), + tools: [checkUrl], + maxTurns: 8, +}); + +export default markdownLinkChecker; +``` diff --git a/skills/rig/samples/243-package-version-drift.md b/skills/rig/samples/243-package-version-drift.md new file mode 100644 index 0000000..14f5fff --- /dev/null +++ b/skills/rig/samples/243-package-version-drift.md @@ -0,0 +1,35 @@ +# 243 - Package Version Drift + +```rig +import { agent, p, s } from "rig"; + +// Agent role: report package version drift by comparing package.json with npm outdated results. +const packageVersionDrift = agent({ + model: "small", + instructions: p`Analyze package version drift for this workspace. + +package.json: ${p.read("package.json")} +npm outdated (JSON, may be empty if all current): ${p.bash("npm outdated --json 2>/dev/null || echo '{}'")} + +Steps: +1. Parse the npm outdated JSON — each key is a package name with current/wanted/latest fields. +2. For each outdated package determine driftLevel: + - "ok" if current === latest + - "patch" if only patch differs + - "minor" if minor version differs + - "major" if major version differs +3. Write a markdown drift report to drift-report.md summarizing the table. +4. Set reportWritten to true after writing.`, + output: s.object({ + packages: s.array(s.object({ + name: s.string, + current: s.string, + latest: s.string, + driftLevel: s.enum("ok", "patch", "minor", "major"), + })), + reportWritten: s.boolean, + }), +}); + +export default packageVersionDrift; +``` diff --git a/skills/rig/samples/244-git-author-stats.md b/skills/rig/samples/244-git-author-stats.md new file mode 100644 index 0000000..bcaf899 --- /dev/null +++ b/skills/rig/samples/244-git-author-stats.md @@ -0,0 +1,32 @@ +# 244 - Git Author Stats + +```rig +import { agent, p, s } from "rig"; + +// Agent role: aggregate git author statistics including commit counts and first/last commit dates. +const gitAuthorStats = agent({ + model: "small", + instructions: p`Aggregate git commit statistics by author. + +Commit counts per author: ${p.bash("git shortlog -sne --all 2>/dev/null || echo ''")} +Per-commit author and date: ${p.bash("git log --format='%ae|%ai' --all 2>/dev/null | head -500")} + +Steps: +1. Parse the shortlog output to extract each author email and commit count. +2. From the per-commit log, compute firstCommit (earliest date) and lastCommit (latest date) per author email. +3. Build the authors record keyed by email with commitCount, firstCommit, lastCommit. +4. Set topAuthor to the email with the highest commitCount. +5. Set totalCommits to the sum of all commitCounts.`, + output: s.object({ + authors: s.record(s.object({ + commitCount: s.number, + firstCommit: s.string, + lastCommit: s.string, + })), + topAuthor: s.string, + totalCommits: s.number, + }), +}); + +export default gitAuthorStats; +``` diff --git a/skills/rig/samples/245-ts-path-alias-validator.md b/skills/rig/samples/245-ts-path-alias-validator.md new file mode 100644 index 0000000..a976453 --- /dev/null +++ b/skills/rig/samples/245-ts-path-alias-validator.md @@ -0,0 +1,43 @@ +# 245 - Ts Path Alias Validator + +```rig +import { agent, p, s, defineTool } from "rig"; +import { existsSync } from "node:fs"; + +const checkAlias = defineTool("checkAlias", { + description: "Check whether a TypeScript path alias target directory exists on disk.", + parameters: { alias: s.string, target: s.string }, + handler: ({ target }: { alias: string; target: string }) => { + const resolved = target.replace(/\/\*$/, ""); + return existsSync(resolved) ? "valid" : "broken"; + }, +}); + +// Agent role: validate all TypeScript path aliases defined in tsconfig.json against the workspace. +const tsPathAliasValidator = agent({ + model: "small", + instructions: p`Validate TypeScript path aliases defined in tsconfig.json. + +tsconfig.json: ${p.read("tsconfig.json")} +Import statements in .ts files: ${p.bash("grep -rh --include='*.ts' 'from \"' . 2>/dev/null | grep -v node_modules | sort -u | head -100")} + +Steps: +1. Parse the compilerOptions.paths from tsconfig.json — each key is an alias pattern, values are target arrays. +2. For each alias, call checkAlias with the alias key and its first target path. +3. Check whether each alias is actually used in the import statements. +4. Set status: "broken" if target doesn't exist, "unused" if never imported, "valid" otherwise. +5. Count brokenCount as the number of broken aliases.`, + output: s.object({ + aliases: s.record(s.object({ + target: s.string, + status: s.enum("valid", "broken", "unused"), + })), + totalAliases: s.number, + brokenCount: s.number, + }), + tools: [checkAlias], + maxTurns: 6, +}); + +export default tsPathAliasValidator; +``` diff --git a/skills/rig/samples/246-stale-branch-detector.md b/skills/rig/samples/246-stale-branch-detector.md new file mode 100644 index 0000000..3ce0e63 --- /dev/null +++ b/skills/rig/samples/246-stale-branch-detector.md @@ -0,0 +1,47 @@ +# 246 - Stale Branch Detector + +```rig +import { agent, p, s, defineTool } from "rig"; + +const classifyBranchAge = defineTool("classifyBranchAge", { + description: "Classify a 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"; + if (ageDays < 90) return "stale"; + return "dead"; + }, +}); + +// 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 age category. +3. Build the branches array with name, lastCommit, and age. +4. Count staleCount (age = "stale") and deadCount (age = "dead"). +5. Set recommendedForDeletion to branch names where age is "dead".`, + output: s.object({ + branches: s.array(s.object({ + name: s.string, + lastCommit: s.string, + age: s.enum("fresh", "stale", "dead"), + })), + staleCount: s.number, + deadCount: s.number, + recommendedForDeletion: s.array(s.string), + }), + tools: [classifyBranchAge], + maxTurns: 6, +}); + +export default staleBranchDetector; +``` diff --git a/skills/rig/samples/247-license-header-checker.md b/skills/rig/samples/247-license-header-checker.md new file mode 100644 index 0000000..8baf872 --- /dev/null +++ b/skills/rig/samples/247-license-header-checker.md @@ -0,0 +1,54 @@ +# 247 - License Header Checker + +```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 lines = content.split("\n"); + const headerLines = expectedHeader.split("\n"); + const fileStart = lines.slice(0, headerLines.length).join("\n"); + if (fileStart === expectedHeader) return "ok"; + if (content.includes(expectedHeader.split("\n")[0])) return "wrong"; + return "missing"; + } catch { + return "missing"; + } + }, +}); + +// 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. + +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 input. +2. For each file path, call checkLicenseHeader with the filePath and expectedHeader. +3. Build a files record keyed by file path with hasHeader and status. +4. Count missingCount (status = "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.number, + allCompliant: s.boolean, + }), + tools: [checkLicenseHeader], + maxTurns: 8, + addons: [repair()], +}); + +export default licenseHeaderChecker; +``` diff --git a/skills/rig/samples/248-yaml-config-diff.md b/skills/rig/samples/248-yaml-config-diff.md new file mode 100644 index 0000000..a21a74b --- /dev/null +++ b/skills/rig/samples/248-yaml-config-diff.md @@ -0,0 +1,57 @@ +# 248 - Yaml Config Diff + +```rig +import { agent, p, s, defineTool } 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 keys between two YAML config files and detect breaking changes. +const yamlConfigDiff = agent({ + model: "small", + input: s.object({ baseFile: s.string, targetFile: s.string }), + 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 are keys present in both where values differ (estimate from context if possible, else leave empty). +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.number, + hasBreakingChanges: s.boolean, + }), + tools: [extractYamlKeys, diffKeys], + maxTurns: 6, +}); + +export default yamlConfigDiff; +``` diff --git a/skills/rig/samples/249-monorepo-workspace-lister.md b/skills/rig/samples/249-monorepo-workspace-lister.md new file mode 100644 index 0000000..d2ca5c9 --- /dev/null +++ b/skills/rig/samples/249-monorepo-workspace-lister.md @@ -0,0 +1,57 @@ +# 249 - Monorepo Workspace Lister + +```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, and dependency names.", + parameters: { filePath: s.string }, + handler: async ({ filePath }: { filePath: string }) => { + try { + const content = await readFile(filePath, "utf-8"); + const pkg = JSON.parse(content); + const deps = [ + ...Object.keys(pkg.dependencies ?? {}), + ...Object.keys(pkg.devDependencies ?? {}), + ]; + return { + name: pkg.name ?? "(unnamed)", + version: pkg.version ?? "0.0.0", + dependencies: deps, + hasPrivate: pkg.private === true, + }; + } catch { + return { name: "(error)", version: "0.0.0", dependencies: [], 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 files (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, dependencies, and hasPrivate 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.string, + dependencies: s.array(s.string), + hasPrivate: s.boolean, + })), + totalPackages: s.number, + }), + tools: [extractPackageInfo], + maxTurns: 8, + addons: [steering()], +}); + +export default monorepoWorkspaceLister; +```