UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

595 lines (497 loc) 17.3 kB
#!/usr/bin/env node /** * Agent Responsibility Linter * * Validates agent definitions against the 10 Golden Rules from the Agent Design Bible. * * Research Foundation: * - REF-001: Bandara et al. (2024) - Production-grade best practices * - REF-002: Roig (2025) - Failure archetype prevention * * Usage: * node tools/linters/agent-linter.mjs [options] [paths...] * * aiwg lint agents [--fix] [--json] [paths...] * * Options: * --fix Attempt to auto-fix simple issues * --json Output results as JSON * --strict Treat warnings as errors * --verbose Show detailed findings * --help Show help */ import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs'; import { join, basename, dirname, relative } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Default agent directories to lint const DEFAULT_PATHS = [ 'agentic/code/frameworks/sdlc-complete/agents', 'agentic/code/frameworks/media-marketing-kit/agents', '.claude/agents' ]; // Rule definitions based on Agent Design Bible const RULES = { 'rule-1-single-responsibility': { name: 'Single Responsibility', severity: 'error', description: 'Agent should have ONE clear purpose', check: checkSingleResponsibility }, 'rule-2-minimal-tools': { name: 'Minimal Tools', severity: 'warning', description: 'Agent should have 0-3 tools', check: checkMinimalTools }, 'rule-3-explicit-io': { name: 'Explicit I/O', severity: 'error', description: 'Agent should define inputs and outputs', check: checkExplicitIO }, 'rule-4-grounding': { name: 'Grounding Step', severity: 'warning', description: 'Agent should verify before acting (Archetype 1 prevention)', check: checkGrounding }, 'rule-5-uncertainty': { name: 'Uncertainty Escalation', severity: 'warning', description: 'Agent should escalate ambiguity (Archetype 2 prevention)', check: checkUncertainty }, 'rule-6-context-scope': { name: 'Context Scoping', severity: 'info', description: 'Agent should scope relevant context (Archetype 3 prevention)', check: checkContextScope }, 'rule-7-recovery': { name: 'Recovery Protocol', severity: 'warning', description: 'Agent should define error recovery (Archetype 4 prevention)', check: checkRecovery }, 'rule-8-model-tier': { name: 'Appropriate Model Tier', severity: 'info', description: 'Model tier should match task complexity', check: checkModelTier }, 'rule-9-parallel-ready': { name: 'Parallel Ready', severity: 'info', description: 'Agent should be designed for concurrent execution', check: checkParallelReady }, 'rule-10-observable': { name: 'Observable Execution', severity: 'info', description: 'Agent should produce traceable output', check: checkObservable } }; // Frontmatter parser function parseFrontmatter(content) { const match = content.match(/^---\n([\s\S]*?)\n---/); if (!match) return { frontmatter: {}, body: content }; const frontmatterText = match[1]; const frontmatter = {}; const body = content.slice(match[0].length).trim(); for (const line of frontmatterText.split('\n')) { const colonIndex = line.indexOf(':'); if (colonIndex > 0) { const key = line.slice(0, colonIndex).trim(); const value = line.slice(colonIndex + 1).trim(); frontmatter[key] = value; } } return { frontmatter, body }; } // Rule checker functions function checkSingleResponsibility(frontmatter, body, filepath) { const findings = []; const description = frontmatter.description || ''; // Check for multiple responsibilities (multiple "and" conjunctions) const andCount = (description.match(/\band\b/gi) || []).length; if (andCount > 1) { findings.push({ message: `Description contains ${andCount} "and" conjunctions - may indicate multiple responsibilities`, line: 1, suggestion: 'Consider splitting into focused agents with single purposes' }); } // Check for vague descriptions const vagueTerms = ['helps', 'assists', 'various', 'multiple', 'different']; for (const term of vagueTerms) { if (description.toLowerCase().includes(term)) { findings.push({ message: `Description uses vague term "${term}" - be more specific`, line: 1, suggestion: 'Replace with precise action verbs (validates, generates, reviews, etc.)' }); } } // Check description length if (description.length > 150) { findings.push({ message: 'Description is too long - may indicate multiple responsibilities', line: 1, suggestion: 'Keep description under 150 characters with single clear purpose' }); } return findings; } function checkMinimalTools(frontmatter, body, filepath) { const findings = []; const tools = frontmatter.tools || ''; const toolList = tools.split(',').map(t => t.trim()).filter(t => t); if (toolList.length > 5) { findings.push({ message: `Agent has ${toolList.length} tools - exceeds recommended maximum of 3`, line: 1, severity: 'error', suggestion: 'Reduce to 0-3 focused tools; consider splitting agent if more needed' }); } else if (toolList.length > 3) { findings.push({ message: `Agent has ${toolList.length} tools - consider reducing to 3 or fewer`, line: 1, suggestion: 'Each tool increases decision space; fewer tools = more predictable behavior' }); } return findings; } function checkExplicitIO(frontmatter, body, filepath) { const findings = []; const lowerBody = body.toLowerCase(); // Check for input definition const hasInputs = lowerBody.includes('## inputs') || lowerBody.includes('### inputs') || lowerBody.includes('**inputs**') || lowerBody.includes('- **required**'); if (!hasInputs) { findings.push({ message: 'Missing explicit input definition', suggestion: 'Add "## Inputs" section with Required/Optional/Context subsections' }); } // Check for output definition const hasOutputs = lowerBody.includes('## outputs') || lowerBody.includes('### outputs') || lowerBody.includes('**outputs**') || lowerBody.includes('## output format') || lowerBody.includes('### output format'); if (!hasOutputs) { findings.push({ message: 'Missing explicit output definition', suggestion: 'Add "## Outputs" section with Primary/Format subsections' }); } return findings; } function checkGrounding(frontmatter, body, filepath) { const findings = []; const lowerBody = body.toLowerCase(); // Keywords indicating grounding behavior const groundingKeywords = [ 'verify', 'confirm', 'check', 'inspect', 'validate', 'read first', 'before action', 'grounding', 'ground' ]; const hasGrounding = groundingKeywords.some(kw => lowerBody.includes(kw)); if (!hasGrounding) { findings.push({ message: 'No grounding/verification step detected (Archetype 1 risk)', suggestion: 'Add step to verify assumptions before modifying external state' }); } return findings; } function checkUncertainty(frontmatter, body, filepath) { const findings = []; const lowerBody = body.toLowerCase(); // Keywords indicating uncertainty handling const uncertaintyKeywords = [ 'uncertainty', 'ambiguous', 'unclear', 'escalate', 'clarification', 'ask', 'confirm with', 'if unsure' ]; const hasUncertainty = uncertaintyKeywords.some(kw => lowerBody.includes(kw)); if (!hasUncertainty) { findings.push({ message: 'No uncertainty escalation defined (Archetype 2 risk)', suggestion: 'Add section on how to handle ambiguous or missing information' }); } return findings; } function checkContextScope(frontmatter, body, filepath) { const findings = []; const lowerBody = body.toLowerCase(); // Keywords indicating context scoping const scopeKeywords = [ 'relevant', 'scope', 'focus on', 'ignore', 'distractor', 'peripheral', 'filter', 'classify' ]; const hasScoping = scopeKeywords.some(kw => lowerBody.includes(kw)); if (!hasScoping) { findings.push({ message: 'No context scoping guidance (Archetype 3 risk)', suggestion: 'Add guidance on identifying relevant vs. irrelevant context' }); } return findings; } function checkRecovery(frontmatter, body, filepath) { const findings = []; const lowerBody = body.toLowerCase(); // Keywords indicating recovery handling const recoveryKeywords = [ 'error', 'recovery', 'retry', 'fail', 'exception', 'fallback', 'escalate', 'if fails', 'when error' ]; const hasRecovery = recoveryKeywords.some(kw => lowerBody.includes(kw)); if (!hasRecovery) { findings.push({ message: 'No error recovery protocol defined (Archetype 4 risk)', suggestion: 'Add error handling section with PAUSE-DIAGNOSE-ADAPT-RETRY-ESCALATE protocol' }); } return findings; } function checkModelTier(frontmatter, body, filepath) { const findings = []; const model = (frontmatter.model || '').toLowerCase(); const description = (frontmatter.description || '').toLowerCase(); const tools = (frontmatter.tools || '').toLowerCase(); if (!model) { findings.push({ message: 'No model tier specified', suggestion: 'Add model: haiku|sonnet|opus based on task complexity' }); return findings; } // Heuristics for model appropriateness const complexTerms = ['architecture', 'design', 'security', 'threat', 'strategy', 'complex']; const simpleTerms = ['validate', 'format', 'check', 'simple', 'utility']; const isComplex = complexTerms.some(t => description.includes(t)); const isSimple = simpleTerms.some(t => description.includes(t)); if (model === 'opus' && isSimple && !isComplex) { findings.push({ message: 'Using opus for potentially simple task', suggestion: 'Consider haiku or sonnet for validation/formatting tasks' }); } if (model === 'haiku' && isComplex) { findings.push({ message: 'Using haiku for potentially complex task', suggestion: 'Consider sonnet or opus for architecture/security analysis' }); } // Check if Task tool used (orchestrator) should be opus if (tools.includes('task') && model !== 'opus') { findings.push({ message: 'Orchestrator agents (using Task tool) typically need opus tier', suggestion: 'Consider model: opus for multi-agent coordination' }); } return findings; } function checkParallelReady(frontmatter, body, filepath) { const findings = []; const lowerBody = body.toLowerCase(); // Check for parallel execution notes const parallelKeywords = [ 'parallel', 'concurrent', 'independent', 'simultaneously', 'alongside', 'can run with' ]; const hasParallelNotes = parallelKeywords.some(kw => lowerBody.includes(kw)); // This is informational - not having it isn't necessarily bad if (!hasParallelNotes) { findings.push({ message: 'No parallel execution guidance', suggestion: 'Consider adding notes on whether this agent can run concurrently with others' }); } return findings; } function checkObservable(frontmatter, body, filepath) { const findings = []; const lowerBody = body.toLowerCase(); // Keywords indicating observability const observableKeywords = [ 'trace', 'log', 'output', 'report', 'summary', 'timestamp', 'duration', 'metrics' ]; const hasObservability = observableKeywords.some(kw => lowerBody.includes(kw)); if (!hasObservability) { findings.push({ message: 'No observability/trace output defined', suggestion: 'Add guidance on what the agent should log for debugging' }); } return findings; } // File discovery function findAgentFiles(paths, cwd) { const files = []; for (const p of paths) { const fullPath = p.startsWith('/') ? p : join(cwd, p); try { const stat = statSync(fullPath); if (stat.isDirectory()) { const entries = readdirSync(fullPath); for (const entry of entries) { if (entry.endsWith('.md') && !entry.toLowerCase().includes('readme')) { files.push(join(fullPath, entry)); } } } else if (stat.isFile() && fullPath.endsWith('.md')) { files.push(fullPath); } } catch (e) { // Path doesn't exist, skip } } return files; } // Main linting function function lintAgent(filepath) { const content = readFileSync(filepath, 'utf-8'); const { frontmatter, body } = parseFrontmatter(content); const results = { file: filepath, name: frontmatter.name || basename(filepath, '.md'), findings: [], passed: 0, failed: 0, warnings: 0, info: 0 }; for (const [ruleId, rule] of Object.entries(RULES)) { const ruleFindings = rule.check(frontmatter, body, filepath); if (ruleFindings.length === 0) { results.passed++; } else { for (const finding of ruleFindings) { const severity = finding.severity || rule.severity; results.findings.push({ rule: ruleId, ruleName: rule.name, severity, ...finding }); if (severity === 'error') results.failed++; else if (severity === 'warning') results.warnings++; else results.info++; } } } return results; } // Output formatting function formatResults(results, options = {}) { if (options.json) { return JSON.stringify(results, null, 2); } let output = ''; let totalPassed = 0; let totalFailed = 0; let totalWarnings = 0; for (const result of results) { const relativePath = relative(process.cwd(), result.file); output += `\n${relativePath} (${result.name})\n`; if (result.findings.length === 0) { output += ` ✓ All ${result.passed} rules passed\n`; } else { for (const finding of result.findings) { const icon = finding.severity === 'error' ? '✗' : finding.severity === 'warning' ? '⚠' : 'ℹ'; output += ` ${icon} [${finding.ruleName}] ${finding.message}\n`; if (options.verbose && finding.suggestion) { output += ` → ${finding.suggestion}\n`; } } } totalPassed += result.passed; totalFailed += result.failed; totalWarnings += result.warnings; } output += `\n${'─'.repeat(60)}\n`; output += `Summary: ${results.length} agents, ${totalPassed} passed, `; output += `${totalFailed} errors, ${totalWarnings} warnings\n`; if (totalFailed > 0) { output += `\nFailed. Fix errors to pass lint check.\n`; } else if (totalWarnings > 0) { output += `\nPassed with warnings. Consider addressing them.\n`; } else { output += `\nAll checks passed!\n`; } return output; } // CLI function main() { const args = process.argv.slice(2); const options = { json: args.includes('--json'), strict: args.includes('--strict'), verbose: args.includes('--verbose'), fix: args.includes('--fix'), help: args.includes('--help') || args.includes('-h') }; if (options.help) { console.log(` Agent Responsibility Linter Validates agent definitions against the 10 Golden Rules from the Agent Design Bible. Usage: node tools/linters/agent-linter.mjs [options] [paths...] Options: --json Output results as JSON --strict Treat warnings as errors --verbose Show suggestions for each finding --fix Attempt auto-fixes (not yet implemented) --help Show this help Paths: Specify directories or files to lint. Defaults to: - agentic/code/frameworks/sdlc-complete/agents - agentic/code/frameworks/media-marketing-kit/agents - .claude/agents Rules: 1. Single Responsibility - One clear purpose 2. Minimal Tools - 0-3 tools recommended 3. Explicit I/O - Define inputs and outputs 4. Grounding - Verify before acting (Archetype 1) 5. Uncertainty - Escalate ambiguity (Archetype 2) 6. Context Scope - Filter distractors (Archetype 3) 7. Recovery - Handle errors (Archetype 4) 8. Model Tier - Match complexity 9. Parallel Ready - Design for concurrency 10. Observable - Traceable execution Research Foundation: - REF-001: Bandara et al. (2024) Production-Grade Agentic AI Workflows - REF-002: Roig (2025) How Do LLMs Fail In Agentic Scenarios? `); process.exit(0); } // Filter out options from paths const paths = args.filter(a => !a.startsWith('--')); const targetPaths = paths.length > 0 ? paths : DEFAULT_PATHS; const cwd = process.cwd(); const files = findAgentFiles(targetPaths, cwd); if (files.length === 0) { console.log('No agent files found to lint.'); process.exit(0); } const results = files.map(f => lintAgent(f)); console.log(formatResults(results, options)); // Exit code const hasErrors = results.some(r => r.failed > 0); const hasWarnings = results.some(r => r.warnings > 0); if (hasErrors || (options.strict && hasWarnings)) { process.exit(1); } process.exit(0); } main();