diff --git a/__tests__/pipeline.test.js b/__tests__/pipeline.test.js index d4040e7f..bc380803 100644 --- a/__tests__/pipeline.test.js +++ b/__tests__/pipeline.test.js @@ -13,6 +13,7 @@ const { runMultiPassAnalyzers, buildSummary, formatHandoffPrompt, + formatCompactPrompt, CERTAINTY, THOROUGHNESS } = require('../lib/patterns/pipeline'); @@ -374,6 +375,142 @@ function process(data) { expect(prompt).not.toContain('[flag]'); }); + + it('should use compact format when option is set', () => { + const findings = [ + { file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'console_debugging', autoFix: 'remove', severity: 'high' }, + { file: 'b.js', line: 2, certainty: 'MEDIUM', patternName: 'old_todos', autoFix: 'flag', severity: 'medium' } + ]; + + const prompt = formatHandoffPrompt(findings, 'report', { compact: true }); + + // Compact format uses table structure + expect(prompt).toContain('|File|L|Pattern|Cert|Fix|'); + expect(prompt).toContain('|---|---|---|---|---|'); + // Should use abbreviated certainty + expect(prompt).toContain('|H|'); + expect(prompt).toContain('|M|'); + }); + }); + + describe('formatCompactPrompt', () => { + it('should format findings in table structure', () => { + const findings = [ + { file: 'app.js', line: 42, certainty: 'HIGH', patternName: 'console_debugging', autoFix: 'remove' } + ]; + + const prompt = formatCompactPrompt(findings, 'report', 50); + + expect(prompt).toContain('|File|L|Pattern|Cert|Fix|'); + expect(prompt).toContain('|---|---|---|---|---|'); + expect(prompt).toContain('|app.js|42|console_debugging|H|remove|'); + }); + + it('should show certainty counts in header', () => { + const findings = [ + { file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'console_debugging', autoFix: 'remove' }, + { file: 'b.js', line: 2, certainty: 'HIGH', patternName: 'debug_import', autoFix: 'remove' }, + { file: 'c.js', line: 3, certainty: 'MEDIUM', patternName: 'old_todos', autoFix: 'flag' }, + { file: 'd.js', line: 4, certainty: 'LOW', patternName: 'magic_numbers', autoFix: 'flag' } + ]; + + const prompt = formatCompactPrompt(findings, 'apply', 50); + + expect(prompt).toContain('## Slop: apply|H:2|M:1|L:1'); + }); + + it('should abbreviate certainty levels', () => { + const findings = [ + { file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'test', autoFix: 'remove' }, + { file: 'b.js', line: 2, certainty: 'MEDIUM', patternName: 'test', autoFix: 'flag' }, + { file: 'c.js', line: 3, certainty: 'LOW', patternName: 'test', autoFix: 'none' } + ]; + + const prompt = formatCompactPrompt(findings, 'report', 50); + + // Should use H, M, L abbreviations in the Cert column + expect(prompt).toMatch(/\|a\.js\|1\|test\|H\|/); + expect(prompt).toMatch(/\|b\.js\|2\|test\|M\|/); + expect(prompt).toMatch(/\|c\.js\|3\|test\|L\|/); + }); + + it('should show dash for non-fixable patterns', () => { + const findings = [ + { file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'test', autoFix: 'flag' }, + { file: 'b.js', line: 2, certainty: 'MEDIUM', patternName: 'test', autoFix: 'none' }, + { file: 'c.js', line: 3, certainty: 'LOW', patternName: 'test', autoFix: null } + ]; + + const prompt = formatCompactPrompt(findings, 'report', 50); + + // Non-fixable should show '-' in Fix column + expect(prompt).toContain('|a.js|1|test|H|-|'); + expect(prompt).toContain('|b.js|2|test|M|-|'); + expect(prompt).toContain('|c.js|3|test|L|-|'); + }); + + it('should truncate findings when exceeding maxFindings', () => { + const findings = []; + for (let i = 1; i <= 10; i++) { + findings.push({ + file: `file${i}.js`, + line: i, + certainty: 'HIGH', + patternName: 'console_debugging', + autoFix: 'remove' + }); + } + + const prompt = formatCompactPrompt(findings, 'report', 5); + + // Should only have 5 rows plus truncation message + expect(prompt).toContain('file1.js'); + expect(prompt).toContain('file5.js'); + expect(prompt).not.toContain('file6.js'); + expect(prompt).toContain('+5 more findings (truncated)'); + }); + + it('should include auto-fixable summary', () => { + const findings = [ + { file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'console_debugging', autoFix: 'remove' }, + { file: 'b.js', line: 2, certainty: 'HIGH', patternName: 'debug_import', autoFix: 'remove' }, + { file: 'c.js', line: 3, certainty: 'MEDIUM', patternName: 'old_todos', autoFix: 'flag' } + ]; + + const prompt = formatCompactPrompt(findings, 'report', 50); + + expect(prompt).toContain('**Auto-fixable: 2**'); + expect(prompt).toContain('Manual: 1'); + }); + + it('should handle empty findings', () => { + const prompt = formatCompactPrompt([], 'report', 50); + + expect(prompt).toContain('## Slop: report|H:0|M:0|L:0'); + expect(prompt).toContain('**Auto-fixable: 0**'); + }); + + it('should not show truncation message when under limit', () => { + const findings = [ + { file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'test', autoFix: 'remove' } + ]; + + const prompt = formatCompactPrompt(findings, 'report', 50); + + expect(prompt).not.toContain('truncated'); + }); + + it('should include mode in header', () => { + const findings = [ + { file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'test', autoFix: 'remove' } + ]; + + const reportPrompt = formatCompactPrompt(findings, 'report', 50); + const applyPrompt = formatCompactPrompt(findings, 'apply', 50); + + expect(reportPrompt).toContain('## Slop: report|'); + expect(applyPrompt).toContain('## Slop: apply|'); + }); }); describe('runPipeline', () => { diff --git a/lib/patterns/pipeline.js b/lib/patterns/pipeline.js index 788d759a..838df50e 100644 --- a/lib/patterns/pipeline.js +++ b/lib/patterns/pipeline.js @@ -468,13 +468,23 @@ function buildSummary(findings) { * * @param {Array} findings - All findings * @param {string} mode - report | apply + * @param {Object} options - Formatting options + * @param {boolean} options.compact - Use compact table format (60-70% fewer tokens) + * @param {number} options.maxFindings - Maximum findings to include (default: 50) * @returns {string} Formatted prompt */ -function formatHandoffPrompt(findings, mode) { +function formatHandoffPrompt(findings, mode, options = {}) { + const { compact = false, maxFindings = 50 } = options; + if (findings.length === 0) { return '## Slop Detection Results\n\nNo issues detected.'; } + // Use compact format if requested + if (compact) { + return formatCompactPrompt(findings, mode, maxFindings); + } + // Group findings by certainty const byGroup = { HIGH: findings.filter(f => f.certainty === CERTAINTY.HIGH), @@ -522,6 +532,58 @@ function formatHandoffPrompt(findings, mode) { return prompt; } +/** + * Format findings in compact table format for token efficiency + * + * Reduces token usage by ~60-70% compared to verbose format. + * Best for large finding sets where full descriptions aren't needed. + * + * @param {Array} findings - All findings + * @param {string} mode - report | apply + * @param {number} maxFindings - Maximum findings to include + * @returns {string} Compact formatted prompt + */ +function formatCompactPrompt(findings, mode, maxFindings) { + // Single pass to count certainty levels and auto-fixable findings + const { highCount, mediumCount, lowCount, autoFixableCount } = findings.reduce((acc, f) => { + switch (f.certainty) { + case CERTAINTY.HIGH: acc.highCount++; break; + case CERTAINTY.MEDIUM: acc.mediumCount++; break; + case CERTAINTY.LOW: acc.lowCount++; break; + } + if (f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none') { + acc.autoFixableCount++; + } + return acc; + }, { highCount: 0, mediumCount: 0, lowCount: 0, autoFixableCount: 0 }); + + // Truncate if needed + const limited = findings.slice(0, maxFindings); + const truncated = findings.length > maxFindings; + + // Summary header + let output = `## Slop: ${mode}|H:${highCount}|M:${mediumCount}|L:${lowCount}\n\n`; + + // Table format + output += '|File|L|Pattern|Cert|Fix|\n'; + output += '|---|---|---|---|---|\n'; + + for (const f of limited) { + const fix = f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none' ? f.autoFix : '-'; + const cert = f.certainty.charAt(0); // H, M, or L + output += `|${f.file}|${f.line}|${f.patternName}|${cert}|${fix}|\n`; + } + + if (truncated) { + output += `\n_+${findings.length - maxFindings} more findings (truncated)_\n`; + } + + // Auto-fix summary + output += `\n**Auto-fixable: ${autoFixableCount}** | Manual: ${findings.length - autoFixableCount}`; + + return output; +} + /** * Format a list of findings for the prompt * @@ -559,6 +621,7 @@ module.exports = { runPhase2, buildSummary, formatHandoffPrompt, + formatCompactPrompt, // Constants CERTAINTY, THOROUGHNESS diff --git a/plugins/deslop-around/commands/deslop-around.md b/plugins/deslop-around/commands/deslop-around.md index e72cf23c..781b2b8d 100644 --- a/plugins/deslop-around/commands/deslop-around.md +++ b/plugins/deslop-around/commands/deslop-around.md @@ -7,10 +7,21 @@ argument-hint: "[report|apply] [scope-path] [max-changes]" You are a senior maintainer doing periodic repo hygiene. Your mission: remove "AI slop" while preserving behavior and minimizing diffs. +## Modes (User Choice) + +This command supports **two scope modes** - you choose: + +| Mode | Scope | Command | +|------|-------|---------| +| **Path-based** | Specific directory/files | `/deslop-around [apply] src/` | +| **Codebase** | Entire repository | `/deslop-around [apply]` | + +For **diff-based cleanup** of new work only, use the `deslop-work` agent via `/next-task`. + ## Arguments - **Mode**: `report` (default) or `apply` -- **Scope**: Path or glob pattern (default: `.`) +- **Scope**: Path or glob pattern (default: `.` = codebase) - **Max changes**: Number of changesets (default: 5) Parse from $ARGUMENTS or use defaults. @@ -71,41 +82,30 @@ git ls-files | wc -l # File count ## AI Slop Definitions -Detect and remove: - -- **Console Debugging**: `console.log()`, `print()`, `println!()`, `dbg!()` -- **Old TODOs**: Comments >90 days old (check line age) -- **Commented Code**: >5 consecutive commented lines -- **Placeholder Text**: "lorem ipsum", "test test", "TODO: implement" -- **Empty Catch**: Empty catch/except blocks without logging -- **Magic Numbers**: Large hardcoded numbers (>1000) -- **Disabled Linters**: eslint-disable, pylint: disable, #noqa -- **Trailing Whitespace**: Whitespace at end of lines -- **Mixed Indentation**: Tabs and spaces mixed -- **Unused Imports**: Imports marked as unused -- **Hardcoded URLs**: URLs that should be config -- **Debug Imports**: `import pdb`, `import ipdb` -- **Placeholder Functions**: `return 0`, `todo!()`, `raise NotImplementedError`, `throw Error("TODO")` -- **Excessive Documentation**: JSDoc >3x function body length -- **Phantom References**: Issue/PR mentions, file path references in comments -- **Generic Naming**: Variables named `data`, `result`, `item`, `temp`, `value` (suggests more specific names) - -### Code Smell Detection - -High-impact code smells that indicate maintainability issues: - -- **Boolean Blindness**: Function calls with 3+ consecutive boolean params (e.g., `process(true, false, true)`) -- **Message Chains**: Long method chains (4+ calls) or deep property access (5+ levels) -- **Mutable Globals**: Module-level mutable state with UPPERCASE names (`let CONFIG = {}`) -- **Dead Code**: Unreachable code after `return`, `throw`, `break`, `continue` -- **Shotgun Surgery**: Files that frequently change together (git history analysis) - -Heuristic patterns (may have false positives, use judgment): - -- **Feature Envy**: Method accessing another object 3+ times (may belong in that class) -- **Speculative Generality**: Underscore-prefixed unused params, empty interfaces - -Reference patterns from `${CLAUDE_PLUGIN_ROOT}/lib/patterns/slop-patterns.js` +Detect and remove patterns from `${CLAUDE_PLUGIN_ROOT}/lib/patterns/slop-patterns.js`. + +**Categories detected:** + +| Category | Examples | +|----------|----------| +| Console debugging | `console.log()`, `print()`, `dbg!()`, `println!()` | +| Old TODOs | Comments with TODO/FIXME >90 days old | +| Placeholder code | `return 0`, `todo!()`, `raise NotImplementedError` | +| Empty catch/except | Empty error handlers without logging | +| Hardcoded secrets | API keys, tokens, credentials | +| Excessive docs | JSDoc >3x function body length | +| Phantom references | Issue/PR mentions in comments | +| Code smells | Boolean blindness, message chains, mutable globals | + +**Certainty levels:** + +| Level | Action | Description | +|-------|--------|-------------| +| **HIGH** | Auto-fix | Direct regex match - definitive slop | +| **MEDIUM** | Verify context | Multi-pass analysis - review before fixing | +| **LOW** | Flag only | Heuristic - may be false positive | + +See pattern library for full regex patterns and language-specific variants. ## Phase A: Map + Diagnose (Always) diff --git a/plugins/deslop-around/lib/patterns/pipeline.js b/plugins/deslop-around/lib/patterns/pipeline.js index 1b630ad7..838df50e 100644 --- a/plugins/deslop-around/lib/patterns/pipeline.js +++ b/plugins/deslop-around/lib/patterns/pipeline.js @@ -84,18 +84,27 @@ function runPipeline(repoPath, options = {}) { } // Phase 2: CLI tools (only if deep and tools available) + // Detect project languages for language-aware tool recommendations + let detectedLanguages = []; if (thoroughness === THOROUGHNESS.DEEP) { // Lazy-load CLI enhancers to avoid circular dependencies const cliEnhancers = require('./cli-enhancers'); + // Detect project languages + detectedLanguages = cliEnhancers.detectProjectLanguages(repoPath); + if (!cliTools) { - cliTools = cliEnhancers.detectAvailableTools(); + // Get tools relevant for detected languages + cliTools = cliEnhancers.detectAvailableTools(detectedLanguages); } - // Track missing tools for user notification - if (!cliTools.jscpd) missingTools.push('jscpd'); - if (!cliTools.madge) missingTools.push('madge'); - if (!cliTools.escomplex) missingTools.push('escomplex'); + // Track missing tools (only those relevant for project languages) + const relevantTools = cliEnhancers.getToolsForLanguages(detectedLanguages); + for (const toolName of Object.keys(relevantTools)) { + if (!cliTools[toolName]) { + missingTools.push(toolName); + } + } const phase2Results = runPhase2(repoPath, cliTools, targetFiles); findings.push(...phase2Results); @@ -112,6 +121,7 @@ function runPipeline(repoPath, options = {}) { summary, phase3Prompt, missingTools, + detectedLanguages, metadata: { repoPath, thoroughness, @@ -142,7 +152,9 @@ function runPhase1(repoPath, targetFiles, language) { // Skip if language filter doesn't match file extension if (language) { const fileLanguage = analyzers.detectLanguage(file); - if (fileLanguage !== language && fileLanguage !== 'js') continue; + // For JS/TS language filter, accept both 'javascript' and 'js' detection results + const isJsFamily = (language === 'javascript' || language === 'typescript') && fileLanguage === 'js'; + if (fileLanguage !== language && !isJsFamily) continue; } const filePath = path.isAbsolute(file) ? file : path.join(repoPath, file); @@ -456,13 +468,23 @@ function buildSummary(findings) { * * @param {Array} findings - All findings * @param {string} mode - report | apply + * @param {Object} options - Formatting options + * @param {boolean} options.compact - Use compact table format (60-70% fewer tokens) + * @param {number} options.maxFindings - Maximum findings to include (default: 50) * @returns {string} Formatted prompt */ -function formatHandoffPrompt(findings, mode) { +function formatHandoffPrompt(findings, mode, options = {}) { + const { compact = false, maxFindings = 50 } = options; + if (findings.length === 0) { return '## Slop Detection Results\n\nNo issues detected.'; } + // Use compact format if requested + if (compact) { + return formatCompactPrompt(findings, mode, maxFindings); + } + // Group findings by certainty const byGroup = { HIGH: findings.filter(f => f.certainty === CERTAINTY.HIGH), @@ -510,6 +532,58 @@ function formatHandoffPrompt(findings, mode) { return prompt; } +/** + * Format findings in compact table format for token efficiency + * + * Reduces token usage by ~60-70% compared to verbose format. + * Best for large finding sets where full descriptions aren't needed. + * + * @param {Array} findings - All findings + * @param {string} mode - report | apply + * @param {number} maxFindings - Maximum findings to include + * @returns {string} Compact formatted prompt + */ +function formatCompactPrompt(findings, mode, maxFindings) { + // Single pass to count certainty levels and auto-fixable findings + const { highCount, mediumCount, lowCount, autoFixableCount } = findings.reduce((acc, f) => { + switch (f.certainty) { + case CERTAINTY.HIGH: acc.highCount++; break; + case CERTAINTY.MEDIUM: acc.mediumCount++; break; + case CERTAINTY.LOW: acc.lowCount++; break; + } + if (f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none') { + acc.autoFixableCount++; + } + return acc; + }, { highCount: 0, mediumCount: 0, lowCount: 0, autoFixableCount: 0 }); + + // Truncate if needed + const limited = findings.slice(0, maxFindings); + const truncated = findings.length > maxFindings; + + // Summary header + let output = `## Slop: ${mode}|H:${highCount}|M:${mediumCount}|L:${lowCount}\n\n`; + + // Table format + output += '|File|L|Pattern|Cert|Fix|\n'; + output += '|---|---|---|---|---|\n'; + + for (const f of limited) { + const fix = f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none' ? f.autoFix : '-'; + const cert = f.certainty.charAt(0); // H, M, or L + output += `|${f.file}|${f.line}|${f.patternName}|${cert}|${fix}|\n`; + } + + if (truncated) { + output += `\n_+${findings.length - maxFindings} more findings (truncated)_\n`; + } + + // Auto-fix summary + output += `\n**Auto-fixable: ${autoFixableCount}** | Manual: ${findings.length - autoFixableCount}`; + + return output; +} + /** * Format a list of findings for the prompt * @@ -547,6 +621,7 @@ module.exports = { runPhase2, buildSummary, formatHandoffPrompt, + formatCompactPrompt, // Constants CERTAINTY, THOROUGHNESS diff --git a/plugins/next-task/agents/deslop-work.md b/plugins/next-task/agents/deslop-work.md index 591eb7fd..116e8009 100644 --- a/plugins/next-task/agents/deslop-work.md +++ b/plugins/next-task/agents/deslop-work.md @@ -1,283 +1,241 @@ --- name: deslop-work description: Clean AI slop from committed but unpushed changes. Use this agent before review and after each review iteration. Only analyzes new work, not entire codebase. -tools: Bash(git:*), Read, Grep, Glob, Task +tools: Bash(git:*), Read, Grep, Glob, Edit, Task model: sonnet --- -# Deslop Work Agent +# Deslop Work Agent (Mode A - Diff Scope) -Clean AI slop specifically from new work (committed but not pushed to remote). -Unlike `/deslop-around` which scans the entire codebase, this agent focuses only -on the diff between the current branch and origin/main. +Clean AI slop from **new work only** (committed but not pushed to remote). -**Architecture**: Pipeline-driven detection with certainty-tagged findings -- Phase 1: Built-in regex + multi-pass analyzers (always runs) -- Phase 2: Optional CLI tools (jscpd, madge, escomplex) - if available -- Phase 3: LLM review with structured handoff +**Scope**: `git diff origin/main..HEAD` - only files changed in current branch. -Certainty levels guide action: -- **HIGH**: Trust these - apply fixes directly (for autoFix patterns) -- **MEDIUM**: Verify context - review surrounding code before applying -- **LOW**: Use judgment - may be false positives, investigate first +This is **Mode A (Diff)** - for path-based or codebase-wide cleanup, users should run `/deslop-around`. -## Scope +## Certainty Levels -Only analyze files in: `git diff --name-only origin/main..HEAD` +| Level | Action | Examples | +|-------|--------|----------| +| **HIGH** | Auto-fix directly | console.log, debug imports, placeholder text | +| **MEDIUM** | Verify context first | TODOs, empty catch blocks | +| **LOW** | Flag for manual review | Complex patterns, heuristics | + +--- ## Phase 1: Get Changed Files +Use **Bash** to get files changed since origin/main: + ```bash -# Get base branch (main or master) BASE_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main") +git diff --name-only origin/${BASE_BRANCH}..HEAD 2>/dev/null || git diff --name-only HEAD~5..HEAD +``` -# Get list of changed files (committed but not pushed) -CHANGED_FILES=$(git diff --name-only origin/${BASE_BRANCH}..HEAD 2>/dev/null || git diff --name-only HEAD~5..HEAD) +If the output is empty, report **"No changes to analyze"** and stop. -if [ -z "$CHANGED_FILES" ]; then - echo "NO_CHANGES=true" -else - echo "CHANGED_COUNT=$(echo "$CHANGED_FILES" | wc -l)" - echo "$CHANGED_FILES" -fi -``` +Store the list of changed files for Phase 2. -## Phase 2: Run Detection Pipeline +--- -Use the pipeline orchestrator with changed files: +## Phase 2: Scan for Slop Patterns -```javascript -const { runPipeline, THOROUGHNESS } = require('${CLAUDE_PLUGIN_ROOT}/lib/patterns/pipeline.js'); +For each changed file, use **Grep** to find slop patterns. -// Determine mode from args (default: apply for deslop-work) -const mode = args.mode || 'apply'; +### HIGH Certainty Patterns (auto-fix) -// Run pipeline on changed files only -const result = runPipeline(repoPath, { - thoroughness: THOROUGHNESS.NORMAL, // Regex + multi-pass analyzers - targetFiles: changedFiles, - mode: mode -}); +Scan for these patterns using **Grep** tool: -console.log(`## Deslop Work Analysis\n`); -console.log(`Files analyzed: ${result.metadata.filesAnalyzed}`); -console.log(`Total findings: ${result.summary.total}`); -console.log(`\nBy Certainty:`); -console.log(`- HIGH: ${result.summary.byCertainty.HIGH}`); -console.log(`- MEDIUM: ${result.summary.byCertainty.MEDIUM}`); -console.log(`- LOW: ${result.summary.byCertainty.LOW}`); +**Console debugging (JavaScript/TypeScript)**: +``` +console\.(log|debug|info|warn)\( ``` -## Phase 3: Process Findings by Certainty - -### HIGH Certainty (Trust and Apply) - -For HIGH certainty findings with autoFix strategies (remove, replace, add_logging): - -```javascript -const highCertaintyFixable = result.findings.filter(f => - f.certainty === 'HIGH' && - f.autoFix && - f.autoFix !== 'flag' && - f.autoFix !== 'none' -); - -if (highCertaintyFixable.length > 0) { - console.log(`\n### Auto-fixing ${highCertaintyFixable.length} HIGH certainty issues`); - - const fixList = { - fixes: highCertaintyFixable.map(f => ({ - file: f.file, - line: f.line, - action: f.autoFix === 'remove' ? 'remove-line' : f.autoFix, - reason: f.description, - content: f.content - })), - commitMessage: 'fix: clean up AI slop (console.log, TODOs, etc.)' - }; - - // Delegate to simple-fixer (haiku) for execution - const fixResult = await Task({ - subagent_type: 'simple-fixer', - prompt: JSON.stringify(fixList), - model: 'haiku' - }); -} +**Debug imports (Python)**: +``` +^import (pdb|ipdb)|^from (pdb|ipdb) ``` -### MEDIUM Certainty (Verify Before Applying) +**Debug macros (Rust)**: +``` +(println!|dbg!|eprintln!)\( +``` -For MEDIUM certainty findings, verify context before deciding: +**Placeholder text**: +``` +(lorem ipsum|test test test|asdf|foo bar baz|placeholder|TODO: implement) +``` -```javascript -const mediumCertainty = result.findings.filter(f => f.certainty === 'MEDIUM'); +**Issue/PR references in comments** (should be in commits, not code): +``` +//.*#\d+|//.*issue\s+#?\d+|//.*PR\s+#?\d+ +``` -if (mediumCertainty.length > 0) { - console.log(`\n### MEDIUM Certainty (${mediumCertainty.length} findings - verify context)`); +### MEDIUM Certainty Patterns (verify context) - for (const finding of mediumCertainty) { - // Read surrounding context - const context = await Read({ - file_path: finding.file, - offset: Math.max(1, finding.line - 5), - limit: 15 - }); +**Old TODOs**: +``` +(TODO|FIXME|HACK|XXX): +``` - console.log(`\n**${finding.file}:${finding.line}**`); - console.log(`Pattern: ${finding.patternName}`); - console.log(`Description: ${finding.description}`); - console.log(`Context:\n\`\`\`\n${context}\n\`\`\``); +**Empty catch blocks (JS)**: +``` +catch\s*\([^)]*\)\s*\{\s*\} +``` - // Make judgment call based on context - // If clearly slop, add to fix list - // If ambiguous, flag for manual review - } -} +**Empty except (Python)**: +``` +except.*:\s*pass\s*$ ``` -### LOW Certainty (Investigate) +**Placeholder functions**: +``` +return\s+(0|true|false|null|undefined|\[\]|\{\})\s*;?\s*$ +``` -For LOW certainty findings (usually from CLI tools), investigate carefully: +### LOW Certainty Patterns (flag only) -```javascript -const lowCertainty = result.findings.filter(f => f.certainty === 'LOW'); +**Magic numbers**: +``` +(? 0) { - console.log(`\n### LOW Certainty (${lowCertainty.length} findings - investigate)`); - console.log('_These may be false positives. Use judgment before acting._\n'); +**Generic variable names**: +``` +\b(const|let|var)\s+(data|result|item|temp|value)\s*[=:] +``` - for (const finding of lowCertainty) { - console.log(`- **${finding.file}:${finding.line}**: ${finding.description}`); - if (finding.details) { - console.log(` Details: ${JSON.stringify(finding.details)}`); - } - } -} +--- + +## Phase 3: Review Findings and Apply Fixes + +### For HIGH Certainty Findings + +1. For each HIGH certainty match, use **Read** to see 3 lines of context +2. If confirmed as slop, use **Edit** to remove or fix the line +3. Track all modified files + +**Example Edit for console.log removal**: +``` +Use Edit tool: +- file_path: +- old_string: " console.log('debug');\n" (include full line with indentation and newline) +- new_string: "" (empty string to delete the line) ``` -## Phase 4: Handle Missing Tools +**Note**: When removing a line, include the trailing newline `\n` in old_string and use empty string for new_string. -If pipeline reports missing CLI tools, notify user at end: +### For MEDIUM Certainty Findings -```javascript -const { getMissingToolsMessage } = require('${CLAUDE_PLUGIN_ROOT}/lib/patterns/cli-enhancers.js'); +1. Use **Read** with offset/limit to get 5 lines before and after the match +2. Analyze context to determine if it's actually slop: + - Is this TODO actively being worked on? → Keep + - Is this catch block intentionally empty (documented)? → Keep + - Is this clearly leftover debugging? → Fix +3. If confirmed as slop, add to fix list +4. If ambiguous, flag for manual review -if (result.missingTools && result.missingTools.length > 0) { - const message = getMissingToolsMessage(result.missingTools); - console.log(message); -} +### For LOW Certainty Findings + +1. Report these in the output +2. Do NOT auto-fix +3. Include file, line, and description for manual review + +--- + +## Phase 4: Commit Changes + +If any fixes were applied: + +1. Stage only the files you modified (use specific file names, not `git add -A`) +2. Commit with descriptive message: + +```bash +git add && git commit -m "fix: clean up AI slop (debugging, placeholders)" ``` +If no fixes were needed, report **"No slop found in changed files"**. + +--- + ## Phase 5: Report Results +Output a summary in this format: + ```markdown ## Deslop Work Report -### Summary -| Category | Count | -|----------|-------| -| HIGH certainty (auto-fixed) | ${highFixed} | -| MEDIUM certainty (reviewed) | ${mediumReviewed} | -| LOW certainty (flagged) | ${lowFlagged} | -| Manual review needed | ${manualCount} | +**Scope**: Diff of feature branch vs origin/main +**Files Analyzed**: ### Fixed Issues (HIGH Certainty) -${fixedIssues.map(i => `- **${i.file}:${i.line}** - ${i.reason}`).join('\n')} - -### Reviewed Issues (MEDIUM Certainty) -${reviewedIssues.map(i => `- **${i.file}:${i.line}** - ${i.description} - ${i.action}`).join('\n')} +- `src/api.js:42` - Removed console.log +- `src/utils.ts:15` - Removed debug import -### Flagged for Investigation (LOW Certainty) -${lowCertaintyIssues.map(i => `- **${i.file}:${i.line}** - ${i.description}`).join('\n')} +### Flagged for Review (MEDIUM/LOW) +- `src/handler.js:88` - TODO comment (verify if still needed) +- `src/config.ts:22` - Magic number 86400 (seconds in day - may be intentional) -### Requires Manual Review -${manualIssues.map(i => `- **${i.file}:${i.line}** - ${i.description}\n \`${i.content}\``).join('\n')} +### Summary +| Category | Count | +|----------|-------| +| Auto-fixed | 5 | +| Flagged | 2 | +| Total findings | 7 | ``` +--- + ## Output Format (JSON) -```json +At the end, output structured JSON between markers: + +``` +=== DESLOP_RESULT_START === { - "scope": "new-work-only", + "scope": "diff", "baseBranch": "origin/main", "filesAnalyzed": 5, - "pipeline": { - "thoroughness": "normal", - "mode": "apply" - }, "summary": { - "total": 12, - "byCertainty": { "HIGH": 8, "MEDIUM": 3, "LOW": 1 }, - "bySeverity": { "critical": 0, "high": 2, "medium": 7, "low": 3 } - }, - "actions": { - "autoFixed": 6, - "manualReview": 4, + "total": 7, + "byCertainty": { "HIGH": 5, "MEDIUM": 1, "LOW": 1 }, + "autoFixed": 5, "flagged": 2 }, - "missingTools": ["jscpd", "escomplex"] + "fixes": [ + { "file": "src/api.js", "line": 42, "pattern": "console_debugging", "action": "removed" } + ], + "flagged": [ + { "file": "src/handler.js", "line": 88, "pattern": "old_todos", "reason": "verify if still needed" } + ] } +=== DESLOP_RESULT_END === ``` +--- + ## Integration Points -This agent is called: +This agent is called by the `/next-task` workflow: 1. **Before first review round** - After implementation-agent completes -2. **After each review iteration** - After review-orchestrator finds issues and fixes are applied - -## Behavior by Certainty Level - -| Certainty | Source | Action | -|-----------|--------|--------| -| HIGH | Phase 1 regex | Auto-fix directly | -| MEDIUM | Multi-pass analyzers | Verify context, then fix or flag | -| LOW | CLI tools (Phase 2) | Investigate, likely flag | - -## Language Detection - -```javascript -function getLanguageFromExtension(ext) { - const map = { - 'js': 'javascript', - 'ts': 'javascript', - 'jsx': 'javascript', - 'tsx': 'javascript', - 'mjs': 'javascript', - 'cjs': 'javascript', - 'py': 'python', - 'rs': 'rust', - 'go': 'go', - 'rb': 'ruby', - 'java': 'java', - 'kt': 'kotlin', - 'swift': 'swift', - 'cpp': 'cpp', - 'c': 'c', - 'cs': 'csharp' - }; - return map[ext] || null; -} -``` +2. **After each review iteration** - After fixes are applied + +--- + +## Important Notes + +- **Only analyze changed files** - Never scan the entire codebase +- **Prefer deletion over modification** - Remove slop, don't refactor it +- **Be conservative with MEDIUM/LOW** - When in doubt, flag for review +- **Commit atomically** - One commit for all slop fixes +- This agent uses **sonnet** because certainty-based decisions require judgment + +--- ## Success Criteria -- Only analyzes files in current branch diff (not entire repo) -- Uses pipeline orchestrator for structured detection -- Respects certainty levels for action decisions -- **HIGH certainty**: Auto-fix via simple-fixer delegation -- **MEDIUM certainty**: Verify context before applying -- **LOW certainty**: Flag for investigation -- Reports missing CLI tools at end (non-blocking) -- Returns structured JSON for orchestrator consumption - -## Architecture Notes - -This agent uses **sonnet** for analysis because: -- Certainty-based decision making requires judgment -- Context verification needs understanding -- Creating fix lists requires reasoning about safety - -**simple-fixer** uses **haiku** because: -- Executing pre-defined edits is mechanical -- No judgment calls needed -- Fast and cost-efficient for batch operations +- ✓ Only analyzes files in current branch diff +- ✓ HIGH certainty patterns auto-fixed +- ✓ MEDIUM/LOW patterns flagged for review +- ✓ Changes committed with descriptive message +- ✓ Structured JSON output for orchestrator diff --git a/plugins/next-task/lib/patterns/pipeline.js b/plugins/next-task/lib/patterns/pipeline.js index 1b630ad7..838df50e 100644 --- a/plugins/next-task/lib/patterns/pipeline.js +++ b/plugins/next-task/lib/patterns/pipeline.js @@ -84,18 +84,27 @@ function runPipeline(repoPath, options = {}) { } // Phase 2: CLI tools (only if deep and tools available) + // Detect project languages for language-aware tool recommendations + let detectedLanguages = []; if (thoroughness === THOROUGHNESS.DEEP) { // Lazy-load CLI enhancers to avoid circular dependencies const cliEnhancers = require('./cli-enhancers'); + // Detect project languages + detectedLanguages = cliEnhancers.detectProjectLanguages(repoPath); + if (!cliTools) { - cliTools = cliEnhancers.detectAvailableTools(); + // Get tools relevant for detected languages + cliTools = cliEnhancers.detectAvailableTools(detectedLanguages); } - // Track missing tools for user notification - if (!cliTools.jscpd) missingTools.push('jscpd'); - if (!cliTools.madge) missingTools.push('madge'); - if (!cliTools.escomplex) missingTools.push('escomplex'); + // Track missing tools (only those relevant for project languages) + const relevantTools = cliEnhancers.getToolsForLanguages(detectedLanguages); + for (const toolName of Object.keys(relevantTools)) { + if (!cliTools[toolName]) { + missingTools.push(toolName); + } + } const phase2Results = runPhase2(repoPath, cliTools, targetFiles); findings.push(...phase2Results); @@ -112,6 +121,7 @@ function runPipeline(repoPath, options = {}) { summary, phase3Prompt, missingTools, + detectedLanguages, metadata: { repoPath, thoroughness, @@ -142,7 +152,9 @@ function runPhase1(repoPath, targetFiles, language) { // Skip if language filter doesn't match file extension if (language) { const fileLanguage = analyzers.detectLanguage(file); - if (fileLanguage !== language && fileLanguage !== 'js') continue; + // For JS/TS language filter, accept both 'javascript' and 'js' detection results + const isJsFamily = (language === 'javascript' || language === 'typescript') && fileLanguage === 'js'; + if (fileLanguage !== language && !isJsFamily) continue; } const filePath = path.isAbsolute(file) ? file : path.join(repoPath, file); @@ -456,13 +468,23 @@ function buildSummary(findings) { * * @param {Array} findings - All findings * @param {string} mode - report | apply + * @param {Object} options - Formatting options + * @param {boolean} options.compact - Use compact table format (60-70% fewer tokens) + * @param {number} options.maxFindings - Maximum findings to include (default: 50) * @returns {string} Formatted prompt */ -function formatHandoffPrompt(findings, mode) { +function formatHandoffPrompt(findings, mode, options = {}) { + const { compact = false, maxFindings = 50 } = options; + if (findings.length === 0) { return '## Slop Detection Results\n\nNo issues detected.'; } + // Use compact format if requested + if (compact) { + return formatCompactPrompt(findings, mode, maxFindings); + } + // Group findings by certainty const byGroup = { HIGH: findings.filter(f => f.certainty === CERTAINTY.HIGH), @@ -510,6 +532,58 @@ function formatHandoffPrompt(findings, mode) { return prompt; } +/** + * Format findings in compact table format for token efficiency + * + * Reduces token usage by ~60-70% compared to verbose format. + * Best for large finding sets where full descriptions aren't needed. + * + * @param {Array} findings - All findings + * @param {string} mode - report | apply + * @param {number} maxFindings - Maximum findings to include + * @returns {string} Compact formatted prompt + */ +function formatCompactPrompt(findings, mode, maxFindings) { + // Single pass to count certainty levels and auto-fixable findings + const { highCount, mediumCount, lowCount, autoFixableCount } = findings.reduce((acc, f) => { + switch (f.certainty) { + case CERTAINTY.HIGH: acc.highCount++; break; + case CERTAINTY.MEDIUM: acc.mediumCount++; break; + case CERTAINTY.LOW: acc.lowCount++; break; + } + if (f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none') { + acc.autoFixableCount++; + } + return acc; + }, { highCount: 0, mediumCount: 0, lowCount: 0, autoFixableCount: 0 }); + + // Truncate if needed + const limited = findings.slice(0, maxFindings); + const truncated = findings.length > maxFindings; + + // Summary header + let output = `## Slop: ${mode}|H:${highCount}|M:${mediumCount}|L:${lowCount}\n\n`; + + // Table format + output += '|File|L|Pattern|Cert|Fix|\n'; + output += '|---|---|---|---|---|\n'; + + for (const f of limited) { + const fix = f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none' ? f.autoFix : '-'; + const cert = f.certainty.charAt(0); // H, M, or L + output += `|${f.file}|${f.line}|${f.patternName}|${cert}|${fix}|\n`; + } + + if (truncated) { + output += `\n_+${findings.length - maxFindings} more findings (truncated)_\n`; + } + + // Auto-fix summary + output += `\n**Auto-fixable: ${autoFixableCount}** | Manual: ${findings.length - autoFixableCount}`; + + return output; +} + /** * Format a list of findings for the prompt * @@ -547,6 +621,7 @@ module.exports = { runPhase2, buildSummary, formatHandoffPrompt, + formatCompactPrompt, // Constants CERTAINTY, THOROUGHNESS diff --git a/plugins/project-review/lib/patterns/pipeline.js b/plugins/project-review/lib/patterns/pipeline.js index 1b630ad7..838df50e 100644 --- a/plugins/project-review/lib/patterns/pipeline.js +++ b/plugins/project-review/lib/patterns/pipeline.js @@ -84,18 +84,27 @@ function runPipeline(repoPath, options = {}) { } // Phase 2: CLI tools (only if deep and tools available) + // Detect project languages for language-aware tool recommendations + let detectedLanguages = []; if (thoroughness === THOROUGHNESS.DEEP) { // Lazy-load CLI enhancers to avoid circular dependencies const cliEnhancers = require('./cli-enhancers'); + // Detect project languages + detectedLanguages = cliEnhancers.detectProjectLanguages(repoPath); + if (!cliTools) { - cliTools = cliEnhancers.detectAvailableTools(); + // Get tools relevant for detected languages + cliTools = cliEnhancers.detectAvailableTools(detectedLanguages); } - // Track missing tools for user notification - if (!cliTools.jscpd) missingTools.push('jscpd'); - if (!cliTools.madge) missingTools.push('madge'); - if (!cliTools.escomplex) missingTools.push('escomplex'); + // Track missing tools (only those relevant for project languages) + const relevantTools = cliEnhancers.getToolsForLanguages(detectedLanguages); + for (const toolName of Object.keys(relevantTools)) { + if (!cliTools[toolName]) { + missingTools.push(toolName); + } + } const phase2Results = runPhase2(repoPath, cliTools, targetFiles); findings.push(...phase2Results); @@ -112,6 +121,7 @@ function runPipeline(repoPath, options = {}) { summary, phase3Prompt, missingTools, + detectedLanguages, metadata: { repoPath, thoroughness, @@ -142,7 +152,9 @@ function runPhase1(repoPath, targetFiles, language) { // Skip if language filter doesn't match file extension if (language) { const fileLanguage = analyzers.detectLanguage(file); - if (fileLanguage !== language && fileLanguage !== 'js') continue; + // For JS/TS language filter, accept both 'javascript' and 'js' detection results + const isJsFamily = (language === 'javascript' || language === 'typescript') && fileLanguage === 'js'; + if (fileLanguage !== language && !isJsFamily) continue; } const filePath = path.isAbsolute(file) ? file : path.join(repoPath, file); @@ -456,13 +468,23 @@ function buildSummary(findings) { * * @param {Array} findings - All findings * @param {string} mode - report | apply + * @param {Object} options - Formatting options + * @param {boolean} options.compact - Use compact table format (60-70% fewer tokens) + * @param {number} options.maxFindings - Maximum findings to include (default: 50) * @returns {string} Formatted prompt */ -function formatHandoffPrompt(findings, mode) { +function formatHandoffPrompt(findings, mode, options = {}) { + const { compact = false, maxFindings = 50 } = options; + if (findings.length === 0) { return '## Slop Detection Results\n\nNo issues detected.'; } + // Use compact format if requested + if (compact) { + return formatCompactPrompt(findings, mode, maxFindings); + } + // Group findings by certainty const byGroup = { HIGH: findings.filter(f => f.certainty === CERTAINTY.HIGH), @@ -510,6 +532,58 @@ function formatHandoffPrompt(findings, mode) { return prompt; } +/** + * Format findings in compact table format for token efficiency + * + * Reduces token usage by ~60-70% compared to verbose format. + * Best for large finding sets where full descriptions aren't needed. + * + * @param {Array} findings - All findings + * @param {string} mode - report | apply + * @param {number} maxFindings - Maximum findings to include + * @returns {string} Compact formatted prompt + */ +function formatCompactPrompt(findings, mode, maxFindings) { + // Single pass to count certainty levels and auto-fixable findings + const { highCount, mediumCount, lowCount, autoFixableCount } = findings.reduce((acc, f) => { + switch (f.certainty) { + case CERTAINTY.HIGH: acc.highCount++; break; + case CERTAINTY.MEDIUM: acc.mediumCount++; break; + case CERTAINTY.LOW: acc.lowCount++; break; + } + if (f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none') { + acc.autoFixableCount++; + } + return acc; + }, { highCount: 0, mediumCount: 0, lowCount: 0, autoFixableCount: 0 }); + + // Truncate if needed + const limited = findings.slice(0, maxFindings); + const truncated = findings.length > maxFindings; + + // Summary header + let output = `## Slop: ${mode}|H:${highCount}|M:${mediumCount}|L:${lowCount}\n\n`; + + // Table format + output += '|File|L|Pattern|Cert|Fix|\n'; + output += '|---|---|---|---|---|\n'; + + for (const f of limited) { + const fix = f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none' ? f.autoFix : '-'; + const cert = f.certainty.charAt(0); // H, M, or L + output += `|${f.file}|${f.line}|${f.patternName}|${cert}|${fix}|\n`; + } + + if (truncated) { + output += `\n_+${findings.length - maxFindings} more findings (truncated)_\n`; + } + + // Auto-fix summary + output += `\n**Auto-fixable: ${autoFixableCount}** | Manual: ${findings.length - autoFixableCount}`; + + return output; +} + /** * Format a list of findings for the prompt * @@ -547,6 +621,7 @@ module.exports = { runPhase2, buildSummary, formatHandoffPrompt, + formatCompactPrompt, // Constants CERTAINTY, THOROUGHNESS diff --git a/plugins/reality-check/lib/patterns/pipeline.js b/plugins/reality-check/lib/patterns/pipeline.js index 1b630ad7..838df50e 100644 --- a/plugins/reality-check/lib/patterns/pipeline.js +++ b/plugins/reality-check/lib/patterns/pipeline.js @@ -84,18 +84,27 @@ function runPipeline(repoPath, options = {}) { } // Phase 2: CLI tools (only if deep and tools available) + // Detect project languages for language-aware tool recommendations + let detectedLanguages = []; if (thoroughness === THOROUGHNESS.DEEP) { // Lazy-load CLI enhancers to avoid circular dependencies const cliEnhancers = require('./cli-enhancers'); + // Detect project languages + detectedLanguages = cliEnhancers.detectProjectLanguages(repoPath); + if (!cliTools) { - cliTools = cliEnhancers.detectAvailableTools(); + // Get tools relevant for detected languages + cliTools = cliEnhancers.detectAvailableTools(detectedLanguages); } - // Track missing tools for user notification - if (!cliTools.jscpd) missingTools.push('jscpd'); - if (!cliTools.madge) missingTools.push('madge'); - if (!cliTools.escomplex) missingTools.push('escomplex'); + // Track missing tools (only those relevant for project languages) + const relevantTools = cliEnhancers.getToolsForLanguages(detectedLanguages); + for (const toolName of Object.keys(relevantTools)) { + if (!cliTools[toolName]) { + missingTools.push(toolName); + } + } const phase2Results = runPhase2(repoPath, cliTools, targetFiles); findings.push(...phase2Results); @@ -112,6 +121,7 @@ function runPipeline(repoPath, options = {}) { summary, phase3Prompt, missingTools, + detectedLanguages, metadata: { repoPath, thoroughness, @@ -142,7 +152,9 @@ function runPhase1(repoPath, targetFiles, language) { // Skip if language filter doesn't match file extension if (language) { const fileLanguage = analyzers.detectLanguage(file); - if (fileLanguage !== language && fileLanguage !== 'js') continue; + // For JS/TS language filter, accept both 'javascript' and 'js' detection results + const isJsFamily = (language === 'javascript' || language === 'typescript') && fileLanguage === 'js'; + if (fileLanguage !== language && !isJsFamily) continue; } const filePath = path.isAbsolute(file) ? file : path.join(repoPath, file); @@ -456,13 +468,23 @@ function buildSummary(findings) { * * @param {Array} findings - All findings * @param {string} mode - report | apply + * @param {Object} options - Formatting options + * @param {boolean} options.compact - Use compact table format (60-70% fewer tokens) + * @param {number} options.maxFindings - Maximum findings to include (default: 50) * @returns {string} Formatted prompt */ -function formatHandoffPrompt(findings, mode) { +function formatHandoffPrompt(findings, mode, options = {}) { + const { compact = false, maxFindings = 50 } = options; + if (findings.length === 0) { return '## Slop Detection Results\n\nNo issues detected.'; } + // Use compact format if requested + if (compact) { + return formatCompactPrompt(findings, mode, maxFindings); + } + // Group findings by certainty const byGroup = { HIGH: findings.filter(f => f.certainty === CERTAINTY.HIGH), @@ -510,6 +532,58 @@ function formatHandoffPrompt(findings, mode) { return prompt; } +/** + * Format findings in compact table format for token efficiency + * + * Reduces token usage by ~60-70% compared to verbose format. + * Best for large finding sets where full descriptions aren't needed. + * + * @param {Array} findings - All findings + * @param {string} mode - report | apply + * @param {number} maxFindings - Maximum findings to include + * @returns {string} Compact formatted prompt + */ +function formatCompactPrompt(findings, mode, maxFindings) { + // Single pass to count certainty levels and auto-fixable findings + const { highCount, mediumCount, lowCount, autoFixableCount } = findings.reduce((acc, f) => { + switch (f.certainty) { + case CERTAINTY.HIGH: acc.highCount++; break; + case CERTAINTY.MEDIUM: acc.mediumCount++; break; + case CERTAINTY.LOW: acc.lowCount++; break; + } + if (f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none') { + acc.autoFixableCount++; + } + return acc; + }, { highCount: 0, mediumCount: 0, lowCount: 0, autoFixableCount: 0 }); + + // Truncate if needed + const limited = findings.slice(0, maxFindings); + const truncated = findings.length > maxFindings; + + // Summary header + let output = `## Slop: ${mode}|H:${highCount}|M:${mediumCount}|L:${lowCount}\n\n`; + + // Table format + output += '|File|L|Pattern|Cert|Fix|\n'; + output += '|---|---|---|---|---|\n'; + + for (const f of limited) { + const fix = f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none' ? f.autoFix : '-'; + const cert = f.certainty.charAt(0); // H, M, or L + output += `|${f.file}|${f.line}|${f.patternName}|${cert}|${fix}|\n`; + } + + if (truncated) { + output += `\n_+${findings.length - maxFindings} more findings (truncated)_\n`; + } + + // Auto-fix summary + output += `\n**Auto-fixable: ${autoFixableCount}** | Manual: ${findings.length - autoFixableCount}`; + + return output; +} + /** * Format a list of findings for the prompt * @@ -547,6 +621,7 @@ module.exports = { runPhase2, buildSummary, formatHandoffPrompt, + formatCompactPrompt, // Constants CERTAINTY, THOROUGHNESS diff --git a/plugins/ship/lib/patterns/pipeline.js b/plugins/ship/lib/patterns/pipeline.js index 1b630ad7..838df50e 100644 --- a/plugins/ship/lib/patterns/pipeline.js +++ b/plugins/ship/lib/patterns/pipeline.js @@ -84,18 +84,27 @@ function runPipeline(repoPath, options = {}) { } // Phase 2: CLI tools (only if deep and tools available) + // Detect project languages for language-aware tool recommendations + let detectedLanguages = []; if (thoroughness === THOROUGHNESS.DEEP) { // Lazy-load CLI enhancers to avoid circular dependencies const cliEnhancers = require('./cli-enhancers'); + // Detect project languages + detectedLanguages = cliEnhancers.detectProjectLanguages(repoPath); + if (!cliTools) { - cliTools = cliEnhancers.detectAvailableTools(); + // Get tools relevant for detected languages + cliTools = cliEnhancers.detectAvailableTools(detectedLanguages); } - // Track missing tools for user notification - if (!cliTools.jscpd) missingTools.push('jscpd'); - if (!cliTools.madge) missingTools.push('madge'); - if (!cliTools.escomplex) missingTools.push('escomplex'); + // Track missing tools (only those relevant for project languages) + const relevantTools = cliEnhancers.getToolsForLanguages(detectedLanguages); + for (const toolName of Object.keys(relevantTools)) { + if (!cliTools[toolName]) { + missingTools.push(toolName); + } + } const phase2Results = runPhase2(repoPath, cliTools, targetFiles); findings.push(...phase2Results); @@ -112,6 +121,7 @@ function runPipeline(repoPath, options = {}) { summary, phase3Prompt, missingTools, + detectedLanguages, metadata: { repoPath, thoroughness, @@ -142,7 +152,9 @@ function runPhase1(repoPath, targetFiles, language) { // Skip if language filter doesn't match file extension if (language) { const fileLanguage = analyzers.detectLanguage(file); - if (fileLanguage !== language && fileLanguage !== 'js') continue; + // For JS/TS language filter, accept both 'javascript' and 'js' detection results + const isJsFamily = (language === 'javascript' || language === 'typescript') && fileLanguage === 'js'; + if (fileLanguage !== language && !isJsFamily) continue; } const filePath = path.isAbsolute(file) ? file : path.join(repoPath, file); @@ -456,13 +468,23 @@ function buildSummary(findings) { * * @param {Array} findings - All findings * @param {string} mode - report | apply + * @param {Object} options - Formatting options + * @param {boolean} options.compact - Use compact table format (60-70% fewer tokens) + * @param {number} options.maxFindings - Maximum findings to include (default: 50) * @returns {string} Formatted prompt */ -function formatHandoffPrompt(findings, mode) { +function formatHandoffPrompt(findings, mode, options = {}) { + const { compact = false, maxFindings = 50 } = options; + if (findings.length === 0) { return '## Slop Detection Results\n\nNo issues detected.'; } + // Use compact format if requested + if (compact) { + return formatCompactPrompt(findings, mode, maxFindings); + } + // Group findings by certainty const byGroup = { HIGH: findings.filter(f => f.certainty === CERTAINTY.HIGH), @@ -510,6 +532,58 @@ function formatHandoffPrompt(findings, mode) { return prompt; } +/** + * Format findings in compact table format for token efficiency + * + * Reduces token usage by ~60-70% compared to verbose format. + * Best for large finding sets where full descriptions aren't needed. + * + * @param {Array} findings - All findings + * @param {string} mode - report | apply + * @param {number} maxFindings - Maximum findings to include + * @returns {string} Compact formatted prompt + */ +function formatCompactPrompt(findings, mode, maxFindings) { + // Single pass to count certainty levels and auto-fixable findings + const { highCount, mediumCount, lowCount, autoFixableCount } = findings.reduce((acc, f) => { + switch (f.certainty) { + case CERTAINTY.HIGH: acc.highCount++; break; + case CERTAINTY.MEDIUM: acc.mediumCount++; break; + case CERTAINTY.LOW: acc.lowCount++; break; + } + if (f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none') { + acc.autoFixableCount++; + } + return acc; + }, { highCount: 0, mediumCount: 0, lowCount: 0, autoFixableCount: 0 }); + + // Truncate if needed + const limited = findings.slice(0, maxFindings); + const truncated = findings.length > maxFindings; + + // Summary header + let output = `## Slop: ${mode}|H:${highCount}|M:${mediumCount}|L:${lowCount}\n\n`; + + // Table format + output += '|File|L|Pattern|Cert|Fix|\n'; + output += '|---|---|---|---|---|\n'; + + for (const f of limited) { + const fix = f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none' ? f.autoFix : '-'; + const cert = f.certainty.charAt(0); // H, M, or L + output += `|${f.file}|${f.line}|${f.patternName}|${cert}|${fix}|\n`; + } + + if (truncated) { + output += `\n_+${findings.length - maxFindings} more findings (truncated)_\n`; + } + + // Auto-fix summary + output += `\n**Auto-fixable: ${autoFixableCount}** | Manual: ${findings.length - autoFixableCount}`; + + return output; +} + /** * Format a list of findings for the prompt * @@ -547,6 +621,7 @@ module.exports = { runPhase2, buildSummary, formatHandoffPrompt, + formatCompactPrompt, // Constants CERTAINTY, THOROUGHNESS