diff --git a/skills/rig/samples/471-git-hook-scanner.md b/skills/rig/samples/471-git-hook-scanner.md new file mode 100644 index 0000000..3353f19 --- /dev/null +++ b/skills/rig/samples/471-git-hook-scanner.md @@ -0,0 +1,44 @@ +# 471 - Git Hook Scanner + +```rig +import { agent, defineTool, p, repair, s } from "rig"; + +const analyzeHookFile = defineTool("analyzeHookFile", { + description: "Read a git hook file and determine its shebang line and executability.", + parameters: s.object({ hookPath: s.path }), + handler: async ({ hookPath }) => { + const { readFile, stat } = await import("node:fs/promises"); + const info = await stat(hookPath); + const isExecutable = (info.mode & 0o111) !== 0; + let shebang: string | undefined; + try { + const content = await readFile(hookPath, "utf-8"); + const firstLine = content.split("\n")[0] ?? ""; + if (firstLine.startsWith("#!")) shebang = firstLine; + } catch { + // unreadable + } + return { isExecutable, shebang }; + }, +}); + +// Agent role: scan .git/hooks/ and classify each hook file by type. +const gitHookScanner = agent({ + model: "small", + instructions: p`List all files in ${p.bash("ls -la .git/hooks/ 2>/dev/null || echo ''")}. For each hook file (skip .sample files and README), call analyzeHookFile with its full path (.git/hooks/). Classify hookType as one of: pre-commit, commit-msg, post-commit, pre-push, pre-receive, other. Return hooks as a record keyed by hook name, plus activeCount (executable hooks) and totalHooks.`, + output: s.object({ + hooks: s.record(s.object({ + hookType: s.enum("pre-commit", "commit-msg", "post-commit", "pre-push", "pre-receive", "other"), + shebang: s.optional(s.string), + isExecutable: s.boolean, + })), + activeCount: s.int, + totalHooks: s.int, + }), + tools: [analyzeHookFile], + maxTurns: 6, + addons: repair(), +}); + +export default gitHookScanner; +``` diff --git a/skills/rig/samples/472-merge-strategy-selector.md b/skills/rig/samples/472-merge-strategy-selector.md new file mode 100644 index 0000000..cc46628 --- /dev/null +++ b/skills/rig/samples/472-merge-strategy-selector.md @@ -0,0 +1,41 @@ +# 472 - Merge Strategy Selector + +```rig +import { agent, p, s } from "rig"; + +// Agent role: analyze git diff --stat to determine the dominant changed area. +const branchDiffAgent = agent({ + model: "small", + instructions: p`Analyze the output of ${p.bash("git diff --stat HEAD~1 2>/dev/null || git diff --stat HEAD 2>/dev/null || echo 'no diff'")}. Determine the dominant area of changes: src (source code), test (test files), config (config files), docs (documentation), or mixed (multiple areas equally).`, + output: s.object({ + dominantArea: s.enum("src", "test", "config", "docs", "mixed"), + changedFiles: s.int, + }), +}); + +// Agent role: check for conflicts in the working tree. +const conflictRiskAgent = agent({ + model: "small", + instructions: p`Analyze ${p.bash("git status --short 2>/dev/null || echo ''")} and count lines containing conflict markers (<<<<<<, >>>>>>). Return conflictCount and hasConflicts.`, + output: s.object({ + conflictCount: s.int, + hasConflicts: s.boolean, + }), +}); + +// Agent role: recommend a merge strategy based on diff area and conflict risk. +const mergeSelectorCoordinator = agent({ + model: "small", + agents: { branchDiffAgent, conflictRiskAgent }, + instructions: "Call branchDiffAgent first to get the dominant area, then call conflictRiskAgent to assess conflict risk. Based on results, choose mergeRecommendation: fast-forward (no conflicts, few files), squash (many small changes, no conflicts), merge (mixed areas or some conflicts), rebase (single area, no conflicts, clean history). Return all combined fields plus a one-sentence rationale.", + output: s.object({ + mergeRecommendation: s.enum("fast-forward", "squash", "merge", "rebase"), + dominantArea: s.enum("src", "test", "config", "docs", "mixed"), + conflictCount: s.int, + hasConflicts: s.boolean, + rationale: s.string, + }), +}); + +export default mergeSelectorCoordinator; +``` diff --git a/skills/rig/samples/473-git-tag-message-extractor.md b/skills/rig/samples/473-git-tag-message-extractor.md new file mode 100644 index 0000000..8fd7e2f --- /dev/null +++ b/skills/rig/samples/473-git-tag-message-extractor.md @@ -0,0 +1,42 @@ +# 473 - Git Tag Message Extractor + +```rig +import { agent, defineTool, p, repair, s } from "rig"; + +const fetchTagMessage = defineTool("fetchTagMessage", { + description: "Fetch the annotation message for a git tag. Returns empty string if lightweight.", + parameters: s.object({ tagName: s.string }), + handler: async ({ tagName }) => { + const { execSync } = await import("node:child_process"); + try { + const output = execSync(`git cat-file tag ${tagName} 2>/dev/null`, { encoding: "utf8" }); + const lines = output.split("\n"); + const msgStart = lines.findIndex((line: string) => line === "") + 1; + const message = msgStart > 0 ? lines.slice(msgStart).join("\n").trim() : ""; + return { isAnnotated: true, message }; + } catch { + return { isAnnotated: false, message: "" }; + } + }, +}); + +// Agent role: list all git tags and extract their annotation messages. +const gitTagMessageExtractor = agent({ + model: "small", + instructions: p`Get the list of tags from ${p.bash("git tag -l 2>/dev/null || echo ''")}. For each tag, call fetchTagMessage to determine if it is annotated and get its message. Return tags as an array with name, optional message, and isAnnotated. Include totalTags and annotatedCount.`, + output: s.object({ + tags: s.array(s.object({ + name: s.string, + message: s.optional(s.string), + isAnnotated: s.boolean, + })), + totalTags: s.int, + annotatedCount: s.int, + }), + tools: [fetchTagMessage], + maxTurns: 6, + addons: repair(), +}); + +export default gitTagMessageExtractor; +``` diff --git a/skills/rig/samples/474-ts-import-alias-resolver.md b/skills/rig/samples/474-ts-import-alias-resolver.md new file mode 100644 index 0000000..5a9f86f --- /dev/null +++ b/skills/rig/samples/474-ts-import-alias-resolver.md @@ -0,0 +1,47 @@ +# 474 - TS Import Alias Resolver + +```rig +import { agent, defineTool, p, s, steering, repair } from "rig"; + +const resolveAlias = defineTool("resolveAlias", { + description: "Resolve a TypeScript import alias prefix to its real path using tsconfig paths config.", + parameters: s.object({ + aliasPrefix: s.string, + tsconfigContent: s.string, + }), + handler: async ({ aliasPrefix, tsconfigContent }) => { + try { + const tsconfig = JSON.parse(tsconfigContent); + const paths: Record = tsconfig?.compilerOptions?.paths ?? {}; + const key = Object.keys(paths).find((k: string) => k.startsWith(aliasPrefix.replace(/\*$/, ""))); + if (key) { + const targets = paths[key] ?? []; + return { realPath: targets[0]?.replace(/\*$/, "") ?? null, found: true }; + } + return { realPath: null, found: false }; + } catch { + return { realPath: null, found: false }; + } + }, +}); + +// Agent role: resolve TypeScript import aliases by reading tsconfig paths and grepping src/ for usages. +const tsImportAliasResolver = agent({ + model: "small", + instructions: p`Read the project tsconfig: ${p.readOptional("tsconfig.json", "{}")}. Then check grep output: ${p.bash("grep -rn \"from '@\" src/ --include='*.ts' 2>/dev/null | head -100 || echo ''")}. For each unique alias prefix found (e.g. @utils/, @lib/), call resolveAlias with the prefix and the tsconfig content. Count usages per alias and list the files. Return aliases as a record keyed by prefix, plus totalAliasUsages and unmappedAliases (aliases not found in tsconfig).`, + output: s.object({ + aliases: s.record(s.object({ + realPath: s.optional(s.string), + usageCount: s.int, + files: s.array(s.string), + })), + totalAliasUsages: s.int, + unmappedAliases: s.array(s.string), + }), + tools: [resolveAlias], + maxTurns: 6, + addons: [steering(), repair()], +}); + +export default tsImportAliasResolver; +``` diff --git a/skills/rig/samples/475-json-schema-property-counter.md b/skills/rig/samples/475-json-schema-property-counter.md new file mode 100644 index 0000000..dc6ed5c --- /dev/null +++ b/skills/rig/samples/475-json-schema-property-counter.md @@ -0,0 +1,53 @@ +# 475 - JSON Schema Property Counter + +```rig +import { agent, defineTool, p, repair, s } from "rig"; + +const countPropertiesAtLevel = defineTool("countPropertiesAtLevel", { + description: "Recursively count JSON Schema properties at each depth level.", + parameters: s.object({ schemaJson: s.string }), + handler: async ({ schemaJson }) => { + const schema = JSON.parse(schemaJson); + const levels: Record = {}; + let total = 0; + let maxDepth = 0; + let hasRequired = false; + + function walk(node: unknown, depth: number): void { + if (!node || typeof node !== "object") return; + const obj = node as Record; + if (obj["required"]) hasRequired = true; + if (obj["properties"] && typeof obj["properties"] === "object") { + const props = obj["properties"] as Record; + const count = Object.keys(props).length; + levels[String(depth)] = (levels[String(depth)] ?? 0) + count; + total += count; + if (depth > maxDepth) maxDepth = depth; + for (const val of Object.values(props)) walk(val, depth + 1); + } + if (obj["items"]) walk(obj["items"], depth); + } + + walk(schema, 0); + return { levels, totalProperties: total, maxDepth, hasRequired }; + }, +}); + +// Agent role: count JSON schema properties at each nesting depth given a schema file path. +const jsonSchemaPropertyCounter = agent({ + model: "small", + input: s.object({ schemaFile: s.string }), + instructions: p`Read the JSON schema file at the path provided in input.schemaFile: ${p.readInput("schemaFile")}. Pass its full content to countPropertiesAtLevel. Return levels (depth->count), totalProperties, maxDepth, and hasRequired.`, + output: s.object({ + levels: s.record(s.int), + totalProperties: s.int, + maxDepth: s.int, + hasRequired: s.boolean, + }), + tools: [countPropertiesAtLevel], + maxTurns: 4, + addons: repair(), +}); + +export default jsonSchemaPropertyCounter; +``` diff --git a/skills/rig/samples/476-changelog-entry-workflow.md b/skills/rig/samples/476-changelog-entry-workflow.md new file mode 100644 index 0000000..f824504 --- /dev/null +++ b/skills/rig/samples/476-changelog-entry-workflow.md @@ -0,0 +1,62 @@ +# 476 - Changelog Entry Workflow + +```rig +import { agent, p, s } from "rig"; + +// Agent role: collect recent git commits as raw log lines. +const commitCollector = agent({ + model: "small", + instructions: p`Run ${p.bash("git log --oneline -20 2>/dev/null || echo ''")} and return each line as a commit entry with hash and message.`, + output: s.object({ + commits: s.array(s.object({ hash: s.string, message: s.string })), + }), +}); + +// Agent role: classify each commit by conventional commit category. +const commitClassifier = agent({ + model: "small", + input: s.object({ commits: s.array(s.object({ hash: s.string, message: s.string })) }), + instructions: "For each commit in input.commits, classify it as feat, fix, chore, docs, or other based on the commit message prefix. Return classified commits array and a categories count record.", + output: s.object({ + classified: s.array(s.object({ + hash: s.string, + message: s.string, + category: s.enum("feat", "fix", "chore", "docs", "other"), + })), + categories: s.record(s.int), + }), +}); + +// Agent role: format classified commits into a markdown changelog entry. +const changelogFormatter = agent({ + model: "small", + input: s.object({ + classified: s.array(s.object({ + hash: s.string, + message: s.string, + category: s.enum("feat", "fix", "chore", "docs", "other"), + })), + categories: s.record(s.int), + }), + instructions: "Format input.classified into a markdown changelog entry grouped by category. Return changelogEntry (markdown string), commitCount, and categories (the input categories record).", + output: s.object({ + changelogEntry: s.string, + commitCount: s.int, + categories: s.record(s.int), + }), +}); + +// Agent role: orchestrate changelog generation by running collector, classifier, then formatter. +const changelogCoordinator = agent({ + model: "small", + agents: { commitCollector, commitClassifier, changelogFormatter }, + instructions: "Step 1: call commitCollector to get recent commits. Step 2: pass the commits array to commitClassifier. Step 3: pass classified and categories to changelogFormatter. Return changelogFormatter's output.", + output: s.object({ + changelogEntry: s.string, + commitCount: s.int, + categories: s.record(s.int), + }), +}); + +export default changelogCoordinator; +``` diff --git a/skills/rig/samples/477-node-module-path-resolver.md b/skills/rig/samples/477-node-module-path-resolver.md new file mode 100644 index 0000000..f9594ab --- /dev/null +++ b/skills/rig/samples/477-node-module-path-resolver.md @@ -0,0 +1,44 @@ +# 477 - Node Module Path Resolver + +```rig +import { agent, defineTool, p, repair, s } from "rig"; + +const resolveModulePath = defineTool("resolveModulePath", { + description: "Resolve the filesystem path of an installed npm package using node:module.", + parameters: s.object({ moduleName: s.string }), + handler: async ({ moduleName }) => { + const { createRequire } = await import("node:module"); + const { isBuiltin } = await import("node:module"); + if (isBuiltin(moduleName)) { + return { resolvedPath: null, isBuiltin: true }; + } + try { + const req = createRequire(process.cwd() + "/index.js"); + const resolved = req.resolve(moduleName); + return { resolvedPath: resolved, isBuiltin: false }; + } catch { + return { resolvedPath: null, isBuiltin: false }; + } + }, +}); + +// Agent role: resolve an npm module path and extract its package.json metadata. +const nodeModulePathResolver = agent({ + model: "small", + input: s.object({ moduleName: s.string }), + instructions: p`The module to resolve is provided in input.moduleName. Call resolveModulePath with it. If resolvedPath is returned, find and read the package.json near that path: ${p.readOptional("package.json", "{}")}. Extract name, version, main, and types fields. Return resolvedPath, packageName, version, main, types, and isBuiltin.`, + output: s.object({ + resolvedPath: s.string, + packageName: s.string, + version: s.string, + main: s.optional(s.string), + types: s.optional(s.string), + isBuiltin: s.boolean, + }), + tools: [resolveModulePath], + maxTurns: 4, + addons: repair(), +}); + +export default nodeModulePathResolver; +``` diff --git a/skills/rig/samples/478-source-file-checksum-reporter.md b/skills/rig/samples/478-source-file-checksum-reporter.md new file mode 100644 index 0000000..fd6d48d --- /dev/null +++ b/skills/rig/samples/478-source-file-checksum-reporter.md @@ -0,0 +1,34 @@ +# 478 - Source File Checksum Reporter + +```rig +import { agent, defineTool, p, s, steering, repair } from "rig"; + +const computeChecksum = defineTool("computeChecksum", { + description: "Compute the MD5 checksum and byte size of a file.", + parameters: s.object({ filePath: s.path }), + handler: async ({ filePath }) => { + const { createHash } = await import("node:crypto"); + const { readFile } = await import("node:fs/promises"); + const buf = await readFile(filePath); + const checksum = createHash("md5").update(buf).digest("hex"); + return { checksum, sizeBytes: buf.length }; + }, +}); + +// Agent role: compute MD5 checksums for all TypeScript source files and report totals. +const sourceFileChecksumReporter = agent({ + model: "small", + instructions: p`Find TypeScript source files with ${p.bash("find src -name '*.ts' -not -path '*/node_modules/*' 2>/dev/null | head -50 || echo ''")}. For each file path, call computeChecksum. Return files as a record keyed by path with checksum and sizeBytes, plus totalFiles, totalBytes, and largestFile (the path with the most bytes, or omit if none).`, + output: s.object({ + files: s.record(s.object({ checksum: s.string, sizeBytes: s.int })), + totalFiles: s.int, + totalBytes: s.int, + largestFile: s.optional(s.string), + }), + tools: [computeChecksum], + maxTurns: 8, + addons: [steering(), repair()], +}); + +export default sourceFileChecksumReporter; +``` diff --git a/skills/rig/samples/479-three-way-project-health.md b/skills/rig/samples/479-three-way-project-health.md new file mode 100644 index 0000000..ce0c912 --- /dev/null +++ b/skills/rig/samples/479-three-way-project-health.md @@ -0,0 +1,51 @@ +# 479 - Three Way Project Health + +```rig +import { agent, p, s } from "rig"; + +// Agent role: run npm audit and count vulnerabilities. +const dependencyAuditor = agent({ + model: "small", + instructions: p`Run ${p.bash("npm audit --json 2>/dev/null | head -5000 || echo '{\"metadata\":{\"vulnerabilities\":{\"total\":0,\"critical\":0}}}'")}. Parse the JSON and extract total vulnerabilities and critical count. If parsing fails, return 0 for both.`, + output: s.object({ + vulnerabilities: s.int, + critical: s.int, + }), +}); + +// Agent role: check test coverage from coverage-summary.json. +const testCoverageChecker = agent({ + model: "small", + instructions: p`Look for coverage data: ${p.bash("find . -name coverage-summary.json -maxdepth 5 2>/dev/null | head -1 | xargs cat 2>/dev/null || echo '{}'")}. Extract the statements coverage percentage. If not found, return 0.`, + output: s.object({ + coveragePct: s.number, + }), +}); + +// Agent role: run TypeScript type-check and count errors. +const typeCheckAgent = agent({ + model: "small", + instructions: p`Run ${p.bash("npx tsc --noEmit 2>&1 | tail -20 || true")}. Count the number of error lines (lines containing 'error TS'). Return hasErrors and errorCount.`, + output: s.object({ + hasTypeErrors: s.boolean, + typeErrorCount: s.int, + }), +}); + +// Agent role: coordinate three-way project health checks and compute overall health. +const projectHealthCoordinator = agent({ + model: "small", + agents: { dependencyAuditor, testCoverageChecker, typeCheckAgent }, + instructions: "Call all three subagents (dependencyAuditor, testCoverageChecker, typeCheckAgent) and collect their results. Then compute overallHealth: critical (critical vulnerabilities > 0 or typeErrorCount > 10), warning (vulnerabilities > 0, typeErrorCount > 0, or coveragePct < 50), healthy (otherwise). Return all combined fields.", + output: s.object({ + vulnerabilities: s.int, + critical: s.int, + coveragePct: s.number, + hasTypeErrors: s.boolean, + typeErrorCount: s.int, + overallHealth: s.enum("healthy", "warning", "critical"), + }), +}); + +export default projectHealthCoordinator; +``` diff --git a/skills/rig/samples/480-ts-conditional-type-extractor.md b/skills/rig/samples/480-ts-conditional-type-extractor.md new file mode 100644 index 0000000..402929e --- /dev/null +++ b/skills/rig/samples/480-ts-conditional-type-extractor.md @@ -0,0 +1,46 @@ +# 480 - TS Conditional Type Extractor + +```rig +import { agent, defineTool, p, repair, s } from "rig"; + +const extractConditionalTypes = defineTool("extractConditionalTypes", { + description: "Extract TypeScript conditional type patterns from a source file.", + parameters: s.object({ filePath: s.path }), + handler: async ({ filePath }) => { + const { readFile } = await import("node:fs/promises"); + const content = await readFile(filePath, "utf-8"); + const conditionalPattern = /\w+\s+extends\s+[^?]+\?\s*[^:]+:/g; + const inferPattern = /infer\s+\w+/g; + const matches = content.match(conditionalPattern) ?? []; + const inferMatches = content.match(inferPattern) ?? []; + const hasInfer = inferMatches.length > 0; + const distributiveCount = matches.filter((m: string) => /^[A-Z]\s+extends/.test(m)).length; + return { + conditionalCount: matches.length, + hasInfer, + distributiveCount, + }; + }, +}); + +// Agent role: scan TypeScript source files for conditional type patterns and summarize usage. +const tsConditionalTypeExtractor = agent({ + model: "small", + instructions: p`Find TypeScript files using ${p.glob("src/**/*.ts")}. For each file path, call extractConditionalTypes. Return files as a record keyed by path with conditionalCount, hasInfer, and distributiveCount. Include totalConditionals, totalFiles, and mostComplexFile (path with highest conditionalCount, or omit if none have any).`, + output: s.object({ + files: s.record(s.object({ + conditionalCount: s.int, + hasInfer: s.boolean, + distributiveCount: s.int, + })), + totalConditionals: s.int, + totalFiles: s.int, + mostComplexFile: s.optional(s.string), + }), + tools: [extractConditionalTypes], + maxTurns: 8, + addons: repair(), +}); + +export default tsConditionalTypeExtractor; +```