diff --git a/skills/rig/samples/220-git-rename-tracker-v2.md b/skills/rig/samples/220-git-rename-tracker-v2.md new file mode 100644 index 0000000..f554641 --- /dev/null +++ b/skills/rig/samples/220-git-rename-tracker-v2.md @@ -0,0 +1,25 @@ +# 220 - Git Rename Tracker V2 + +```rig +import { agent, p, s, repair } from "rig"; + +// Agent role: track file renames in git history and produce a structured rename log. +const gitRenameTrackerV2 = agent({ + model: "small", + instructions: p`Retrieve git file rename history: ${p.bash("git log --diff-filter=R --summary --pretty=format:'%h %ai' | head -60 2>/dev/null || echo 'No renames found'")}. Parse each rename entry (lines matching "rename ... => ...") and associate it with the nearest commit hash and date. Return all renames sorted newest first, the total count, and the path that appears most often as either old or new.`, + output: s.object({ + renames: s.array(s.object({ + hash: s.string, + date: s.string, + oldPath: s.path, + newPath: s.path, + })), + totalRenames: s.int, + mostRenamedFile: s.optional(s.path), + }), + maxTurns: 4, + addons: repair(), +}); + +export default gitRenameTrackerV2; +``` diff --git a/skills/rig/samples/221-yaml-workflow-linter.md b/skills/rig/samples/221-yaml-workflow-linter.md new file mode 100644 index 0000000..e80c605 --- /dev/null +++ b/skills/rig/samples/221-yaml-workflow-linter.md @@ -0,0 +1,45 @@ +# 221 - Yaml Workflow Linter + +```rig +import { agent, p, s, defineTool } from "rig"; +import { readFileSync } from "node:fs"; + +const validateYaml = defineTool("validateYaml", { + description: "Validate a YAML file for structural issues and required top-level keys", + parameters: s.object({ filePath: s.path }), + handler({ filePath }) { + let content: string; + try { + content = readFileSync(filePath, "utf-8"); + } catch { + return { issues: ["File not readable"], lineCount: 0, status: "fail" as const }; + } + const issues: string[] = []; + const lines = content.split("\n"); + const lineCount = lines.length; + const isWorkflow = filePath.includes(".github/workflows"); + if (isWorkflow) { + if (!content.includes("name:")) issues.push("Missing 'name' key"); + if (!content.includes("on:") && !content.includes('"on":')) issues.push("Missing 'on' trigger"); + if (!content.includes("jobs:")) issues.push("Missing 'jobs' key"); + } + if (content.includes("\t")) issues.push("Contains tab characters (use spaces in YAML)"); + const status = issues.length === 0 ? "pass" : issues.some(i => i.startsWith("Missing")) ? "fail" : "warn"; + return { issues, lineCount, status } as { issues: string[]; lineCount: number; status: "pass" | "warn" | "fail" }; + }, +}); + +// Agent role: lint YAML files and report structural issues per file. +const yamlWorkflowLinter = agent({ + model: "small", + instructions: p`Find YAML files: ${p.bash("find . \\( -name '*.yml' -o -name '*.yaml' \\) | grep -v node_modules | grep -v .git | head -20")}. For each file path, call validateYaml and collect the result. Return a record keyed by file path.`, + output: s.record(s.object({ + issues: s.array(s.string), + lineCount: s.int, + status: s.enum("pass", "warn", "fail"), + })), + tools: [validateYaml], +}); + +export default yamlWorkflowLinter; +``` diff --git a/skills/rig/samples/222-two-phase-complexity-review-v2.md b/skills/rig/samples/222-two-phase-complexity-review-v2.md new file mode 100644 index 0000000..0344f6c --- /dev/null +++ b/skills/rig/samples/222-two-phase-complexity-review-v2.md @@ -0,0 +1,37 @@ +# 222 - Two Phase Complexity Review V2 + +```rig +import { agent, p, s } from "rig"; + +// Agent role: extract TypeScript function names and approximate line counts from source files. +const extractor = agent({ + name: "extractor", + model: "small", + instructions: p`Scan TypeScript source files for function definitions: ${p.bash("grep -rn --include='*.ts' 'function \\|=> {\\|async ' . | grep -v node_modules | grep -v '.test.' | head -60 2>/dev/null || echo 'no ts files'")}. For each distinct function name found, estimate its line count from context. Return an array of objects with functionName, lineCount, and file.`, + output: s.array(s.object({ + functionName: s.string, + lineCount: s.int, + file: s.path, + })), +}); + +// Agent role: rate each extracted function for complexity and produce a summary. +const reviewer = agent({ + name: "reviewer", + model: "small", + instructions: p`You will receive a list of functions with their line counts. Classify each as: simple (<10 lines), moderate (10-29), complex (30-59), or critical (≥60). Return one rating object per function plus a summary with total and complex+critical count.`, + output: s.object({ + analysis: s.record(s.object({ + lineCount: s.int, + complexity: s.enum("simple", "moderate", "complex", "critical"), + })), + summary: s.object({ + totalFunctions: s.int, + complexCount: s.int, + }), + }), + agents: { extractor }, +}); + +export default reviewer; +``` diff --git a/skills/rig/samples/223-barrel-file-generator-v2.md b/skills/rig/samples/223-barrel-file-generator-v2.md new file mode 100644 index 0000000..307c98d --- /dev/null +++ b/skills/rig/samples/223-barrel-file-generator-v2.md @@ -0,0 +1,35 @@ +# 223 - Barrel File Generator V2 + +```rig +import { agent, defineTool, p, s } from "rig"; + +const detectExports = defineTool("detectExports", { + description: "Detect export declarations in a TypeScript source file", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }) { + const { readFile } = await import("node:fs/promises"); + try { + const content = await readFile(filePath, "utf-8"); + const exports = (content.match(/^export\s+(default\s+)?(function|class|const|let|var|type|interface|enum)\s+\w+/gm) ?? []); + return { filePath, exports, exportCount: exports.length }; + } catch { + return { filePath, exports: [], exportCount: 0 }; + } + }, +}); + +// Agent role: generate barrel index.ts content for TypeScript source directories. +const barrelFileGenerator = agent({ + model: "small", + instructions: p`Find TypeScript source files: ${p.bash("find src -name '*.ts' ! -name 'index.ts' ! -name '*.test.ts' ! -name '*.spec.ts' 2>/dev/null | head -30 || find . -name '*.ts' ! -name 'index.ts' ! -name '*.test.ts' ! -path '*/node_modules/*' | head -20")}. Use detectExports for each file. Tally filesScanned, barrelFilesWritten (directories that have at least one export), exportCount (total exports found), and directories (unique directory paths). Return only the declared output.`, + tools: [detectExports], + output: s.object({ + filesScanned: s.int, + barrelFilesWritten: s.int, + exportCount: s.int, + directories: s.array(s.string), + }), +}); + +export default barrelFileGenerator; +``` diff --git a/skills/rig/samples/224-git-hook-inventory-v2.md b/skills/rig/samples/224-git-hook-inventory-v2.md new file mode 100644 index 0000000..9c9f49f --- /dev/null +++ b/skills/rig/samples/224-git-hook-inventory-v2.md @@ -0,0 +1,34 @@ +# 224 - Git Hook Inventory V2 + +```rig +import { agent, defineTool, p, s } from "rig"; + +const classifyHook = defineTool("classifyHook", { + description: "Classify a git hook by inspecting its content", + parameters: s.object({ name: s.string, content: s.string }), + handler({ content }) { + if (!content || content.trim() === "missing") { + return { status: "missing" as const, isAsync: false, summary: "Hook file not present" }; + } + const isSample = content.includes("sample") || content.trim() === "#!/bin/sh" || content.trim() === "#!/bin/bash"; + const isAsync = content.includes(" &") || content.includes("async "); + const status = isSample ? "stub" : "active"; + const summary = isSample ? "Sample/placeholder hook — not active" : `Active hook (${content.split("\n").length} lines)`; + return { status, isAsync, summary } as { status: "active" | "stub" | "missing"; isAsync: boolean; summary: string }; + }, +}); + +// Agent role: inventory git hooks and classify each as active, stub, or missing. +const gitHookInventory = agent({ + model: "small", + instructions: p`List and inspect git hooks: ${p.bash("ls .git/hooks/ 2>/dev/null || echo 'no hooks directory'")}. ${p.bash("for f in pre-commit commit-msg pre-push post-commit prepare-commit-msg pre-rebase; do echo \"=== $f ===\"; cat \".git/hooks/$f\" 2>/dev/null || echo 'missing'; done")}. Use classifyHook for each hook name with its content. Return a record keyed by hook name.`, + tools: [classifyHook], + output: s.record(s.object({ + summary: s.string, + status: s.enum("active", "stub", "missing"), + isAsync: s.boolean, + })), +}); + +export default gitHookInventory; +``` diff --git a/skills/rig/samples/225-pr-review-checklist-v2.md b/skills/rig/samples/225-pr-review-checklist-v2.md new file mode 100644 index 0000000..b25f33b --- /dev/null +++ b/skills/rig/samples/225-pr-review-checklist-v2.md @@ -0,0 +1,32 @@ +# 225 - Pr Review Checklist V2 + +```rig +import { agent, p, s, repair } from "rig"; + +// Agent role: generate a PR review checklist from the current git diff. +const prReviewChecklist = agent({ + model: "small", + maxTurns: 6, + addons: repair(), + instructions: p`Generate a PR review checklist based on these changes. + +Changed files: +${p.bash("git diff --stat HEAD~1 2>/dev/null || git diff --stat HEAD 2>/dev/null || echo 'no diff available'")} + +Diff: +${p.bash("git diff HEAD~1 -- . 2>/dev/null | head -300 || git diff HEAD -- . 2>/dev/null | head -300 || echo 'no diff'")} + +For each concern, produce a checklist item with a clear description, category (security/performance/correctness/style/testing), and priority (must/should/nice-to-have). List changed files and indicate if the PR is ready for review.`, + output: s.object({ + checklist: s.array(s.object({ + item: s.string, + category: s.enum("security", "performance", "correctness", "style", "testing"), + priority: s.enum("must", "should", "nice-to-have"), + })), + changedFiles: s.array(s.path), + ready: s.boolean, + }), +}); + +export default prReviewChecklist; +``` diff --git a/skills/rig/samples/226-js-ast-node-counter.md b/skills/rig/samples/226-js-ast-node-counter.md new file mode 100644 index 0000000..9a82d72 --- /dev/null +++ b/skills/rig/samples/226-js-ast-node-counter.md @@ -0,0 +1,44 @@ +# 226 - Js Ast Node Counter + +```rig +import { agent, defineTool, p, s } from "rig"; + +const countAstNodes = defineTool("countAstNodes", { + description: "Count syntax node patterns in a JavaScript/TypeScript file using regex heuristics", + parameters: s.object({ filePath: s.path }), + async handler({ filePath }) { + const { readFile } = await import("node:fs/promises"); + try { + const content = await readFile(filePath, "utf-8"); + const functionCount = (content.match(/\bfunction\s+\w+\s*\(/g) ?? []).length; + const arrowFunctionCount = (content.match(/=>\s*[{(]/g) ?? []).length; + const classCount = (content.match(/\bclass\s+\w+/g) ?? []).length; + const importCount = (content.match(/^\s*import\s+/gm) ?? []).length; + const exportCount = (content.match(/^\s*export\s+/gm) ?? []).length; + return { filePath, functionCount, arrowFunctionCount, classCount, importCount, exportCount }; + } catch { + return { filePath, functionCount: 0, arrowFunctionCount: 0, classCount: 0, importCount: 0, exportCount: 0 }; + } + }, +}); + +// Agent role: count AST-like syntax node patterns in JavaScript files. +const jsAstNodeCounter = agent({ + model: "small", + instructions: p`Find JavaScript files: ${p.bash("find . -name '*.js' ! -path '*/node_modules/*' ! -path '*/.git/*' | head -10 2>/dev/null || echo 'no js files found'")}. For each file, call countAstNodes and collect results. Tally totalFunctions across all files and identify mostComplexFile (highest combined function + arrowFunction count). Return only the declared output.`, + tools: [countAstNodes], + output: s.object({ + files: s.record(s.object({ + functionCount: s.int, + arrowFunctionCount: s.int, + classCount: s.int, + importCount: s.int, + exportCount: s.int, + })), + totalFunctions: s.int, + mostComplexFile: s.optional(s.path), + }), +}); + +export default jsAstNodeCounter; +``` diff --git a/skills/rig/samples/227-git-tag-date-mapper.md b/skills/rig/samples/227-git-tag-date-mapper.md new file mode 100644 index 0000000..21a4d70 --- /dev/null +++ b/skills/rig/samples/227-git-tag-date-mapper.md @@ -0,0 +1,48 @@ +# 227 - Git Tag Date Mapper + +```rig +import { agent, defineTool, p, s, steering } from "rig"; + +const classifyTagAge = defineTool("classifyTagAge", { + description: "Classify a tag's age based on its ISO date string", + parameters: s.object({ dateStr: s.string }), + handler({ dateStr }) { + const date = new Date(dateStr); + const now = new Date(); + const daysDiff = (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24); + let ageClass: "recent" | "stable" | "old" | "ancient"; + if (daysDiff < 30) ageClass = "recent"; + else if (daysDiff < 180) ageClass = "stable"; + else if (daysDiff < 730) ageClass = "old"; + else ageClass = "ancient"; + return { ageClass }; + }, +}); + +// Agent role: map git tags to their commit dates and classify their age. +const gitTagDateMapper = agent({ + model: "small", + addons: steering(), + instructions: p`Inspect git tags and their dates. + +Tags: +${p.bash("git tag -l --sort=-version:refname | head -20 2>/dev/null || echo 'no tags'")} + +Tag log: +${p.bash("git log --tags --simplify-by-decoration --pretty='%D|%ai|%s' | head -30 2>/dev/null || echo 'no tag log'")} + +For each tag, extract its ISO date and call classifyTagAge. Return a record keyed by tag name with date, subject, and ageClass. Include totalTags and latestTag.`, + tools: [classifyTagAge], + output: s.object({ + tags: s.record(s.object({ + date: s.string, + subject: s.string, + ageClass: s.enum("recent", "stable", "old", "ancient"), + })), + totalTags: s.int, + latestTag: s.optional(s.string), + }), +}); + +export default gitTagDateMapper; +``` diff --git a/skills/rig/samples/228-circular-import-detector.md b/skills/rig/samples/228-circular-import-detector.md new file mode 100644 index 0000000..da6b5ce --- /dev/null +++ b/skills/rig/samples/228-circular-import-detector.md @@ -0,0 +1,53 @@ +# 228 - Circular Import Detector + +```rig +import { agent, defineTool, p, s, steering } from "rig"; + +const buildImportGraph = defineTool("buildImportGraph", { + description: "Build an import graph from TypeScript files in a directory by parsing relative import statements", + parameters: s.object({ directory: s.path }), + async handler({ directory }) { + const { readdir, readFile } = await import("node:fs/promises"); + const { join } = await import("node:path"); + const graph: Record = {}; + try { + const entries = await readdir(directory, { recursive: true, encoding: "utf-8" }); + const tsFiles = entries.filter(f => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.endsWith(".d.ts")); + for (const file of tsFiles.slice(0, 20)) { + const fullPath = join(directory, file); + try { + const content = await readFile(fullPath, "utf-8"); + const imports = [...content.matchAll(/from\s+['"](\.[^'"]+)['"]/g)].map(m => m[1]); + graph[file] = imports; + } catch { + graph[file] = []; + } + } + } catch { + // directory not found + } + return { graph }; + }, +}); + +// Agent role: detect circular imports in TypeScript source files. +const circularImportDetector = agent({ + model: "small", + addons: steering(), + instructions: p`Detect circular imports in TypeScript files. + +Import overview: +${p.bash("grep -rn \"from '\\./\\|from \\\"\\./\" src/ --include='*.ts' 2>/dev/null | head -40 || grep -rn \"from '\\./\" . --include='*.ts' | grep -v node_modules | head -40 || echo 'no relative imports found'")} + +Call buildImportGraph on the source directory (try "src" first, then "."). Analyse the graph to find cycles (A imports B imports A, etc.). Return cycles as arrays of file paths, hasCycles, totalFiles, and cycleCount.`, + tools: [buildImportGraph], + output: s.object({ + cycles: s.array(s.array(s.string)), + hasCycles: s.boolean, + totalFiles: s.int, + cycleCount: s.int, + }), +}); + +export default circularImportDetector; +``` diff --git a/skills/rig/samples/229-package-json-scorer.md b/skills/rig/samples/229-package-json-scorer.md new file mode 100644 index 0000000..be81e62 --- /dev/null +++ b/skills/rig/samples/229-package-json-scorer.md @@ -0,0 +1,50 @@ +# 229 - Package Json Scorer + +```rig +import { agent, defineTool, p, s, repair } from "rig"; + +const scoreField = defineTool("scoreField", { + description: "Score a package.json field for presence, completeness, and quality", + parameters: s.object({ + fieldName: s.string, + value: s.unknown, + importance: s.enum("required", "recommended", "optional"), + }), + handler({ fieldName, value, importance }) { + const present = value !== undefined && value !== null && value !== ""; + const nonEmpty = present && (typeof value !== "object" || Object.keys(value as object).length > 0); + const score = !present ? 0 : !nonEmpty ? 30 : importance === "required" ? 100 : importance === "recommended" ? 80 : 60; + const note = !present + ? `Missing ${importance} field` + : !nonEmpty + ? `Field '${fieldName}' is empty` + : `Field '${fieldName}' is present`; + return { present, score, note }; + }, +}); + +// Agent role: score a package.json manifest for completeness and produce a grade. +const packageJsonScorer = agent({ + model: "small", + addons: repair(), + instructions: p`Score this package.json for completeness: ${p.read("package.json")}. Use scoreField for each of these fields: +- required: name, version, description, main +- recommended: license, author, repository, keywords, bugs, homepage +- optional: engines, files, exports +Compute an overall score (0-100) as weighted average and assign a grade: A(90+), B(75+), C(60+), D(40+), F(<40). List missingRequired and missingRecommended fields.`, + tools: [scoreField], + output: s.object({ + score: s.int, + grade: s.enum("A", "B", "C", "D", "F"), + missingRequired: s.array(s.string), + missingRecommended: s.array(s.string), + details: s.record(s.object({ + present: s.boolean, + score: s.int, + note: s.string, + })), + }), +}); + +export default packageJsonScorer; +```