agentsys 5.0.3 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/.claude-plugin/marketplace.json +21 -14
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +18 -0
  5. package/README.md +7 -6
  6. package/adapters/codex/skills/agnix/SKILL.md +0 -1
  7. package/adapters/codex/skills/audit-project/SKILL.md +0 -1
  8. package/adapters/codex/skills/audit-project-agents/SKILL.md +0 -1
  9. package/adapters/codex/skills/audit-project-github/SKILL.md +0 -1
  10. package/adapters/codex/skills/consult/SKILL.md +132 -57
  11. package/adapters/codex/skills/debate/SKILL.md +214 -0
  12. package/adapters/codex/skills/delivery-approval/SKILL.md +0 -1
  13. package/adapters/codex/skills/deslop/SKILL.md +0 -1
  14. package/adapters/codex/skills/drift-detect/SKILL.md +0 -1
  15. package/adapters/codex/skills/enhance/SKILL.md +0 -1
  16. package/adapters/codex/skills/learn/SKILL.md +0 -1
  17. package/adapters/codex/skills/next-task/SKILL.md +0 -1
  18. package/adapters/codex/skills/perf/SKILL.md +0 -1
  19. package/adapters/codex/skills/repo-map/SKILL.md +0 -1
  20. package/adapters/codex/skills/ship/SKILL.md +0 -1
  21. package/adapters/codex/skills/ship-ci-review-loop/SKILL.md +0 -1
  22. package/adapters/codex/skills/ship-deployment/SKILL.md +0 -1
  23. package/adapters/codex/skills/ship-error-handling/SKILL.md +0 -1
  24. package/adapters/codex/skills/sync-docs/SKILL.md +0 -1
  25. package/adapters/opencode/agents/agent-enhancer.md +0 -1
  26. package/adapters/opencode/agents/agnix-agent.md +0 -1
  27. package/adapters/opencode/agents/ci-fixer.md +0 -1
  28. package/adapters/opencode/agents/ci-monitor.md +0 -1
  29. package/adapters/opencode/agents/claudemd-enhancer.md +0 -1
  30. package/adapters/opencode/agents/consult-agent.md +122 -30
  31. package/adapters/opencode/agents/cross-file-enhancer.md +0 -1
  32. package/adapters/opencode/agents/debate-orchestrator.md +169 -0
  33. package/adapters/opencode/agents/delivery-validator.md +0 -1
  34. package/adapters/opencode/agents/deslop-agent.md +0 -1
  35. package/adapters/opencode/agents/docs-enhancer.md +0 -1
  36. package/adapters/opencode/agents/exploration-agent.md +0 -1
  37. package/adapters/opencode/agents/hooks-enhancer.md +0 -1
  38. package/adapters/opencode/agents/implementation-agent.md +0 -1
  39. package/adapters/opencode/agents/learn-agent.md +0 -1
  40. package/adapters/opencode/agents/map-validator.md +0 -1
  41. package/adapters/opencode/agents/perf-analyzer.md +0 -1
  42. package/adapters/opencode/agents/perf-code-paths.md +0 -1
  43. package/adapters/opencode/agents/perf-investigation-logger.md +0 -1
  44. package/adapters/opencode/agents/perf-orchestrator.md +0 -1
  45. package/adapters/opencode/agents/perf-theory-gatherer.md +0 -1
  46. package/adapters/opencode/agents/perf-theory-tester.md +0 -1
  47. package/adapters/opencode/agents/plan-synthesizer.md +0 -1
  48. package/adapters/opencode/agents/planning-agent.md +0 -1
  49. package/adapters/opencode/agents/plugin-enhancer.md +0 -1
  50. package/adapters/opencode/agents/prompt-enhancer.md +0 -1
  51. package/adapters/opencode/agents/simple-fixer.md +0 -1
  52. package/adapters/opencode/agents/skills-enhancer.md +0 -1
  53. package/adapters/opencode/agents/sync-docs-agent.md +0 -1
  54. package/adapters/opencode/agents/task-discoverer.md +0 -1
  55. package/adapters/opencode/agents/test-coverage-checker.md +0 -1
  56. package/adapters/opencode/agents/worktree-manager.md +0 -1
  57. package/adapters/opencode/commands/agnix.md +0 -1
  58. package/adapters/opencode/commands/audit-project-agents.md +0 -1
  59. package/adapters/opencode/commands/audit-project-github.md +0 -1
  60. package/adapters/opencode/commands/audit-project.md +0 -1
  61. package/adapters/opencode/commands/consult.md +133 -57
  62. package/adapters/opencode/commands/debate.md +224 -0
  63. package/adapters/opencode/commands/delivery-approval.md +0 -1
  64. package/adapters/opencode/commands/deslop.md +0 -1
  65. package/adapters/opencode/commands/drift-detect.md +0 -1
  66. package/adapters/opencode/commands/enhance.md +0 -1
  67. package/adapters/opencode/commands/learn.md +0 -1
  68. package/adapters/opencode/commands/next-task.md +0 -1
  69. package/adapters/opencode/commands/perf.md +0 -1
  70. package/adapters/opencode/commands/repo-map.md +0 -1
  71. package/adapters/opencode/commands/ship-ci-review-loop.md +0 -1
  72. package/adapters/opencode/commands/ship-deployment.md +0 -1
  73. package/adapters/opencode/commands/ship-error-handling.md +0 -1
  74. package/adapters/opencode/commands/ship.md +0 -1
  75. package/adapters/opencode/commands/sync-docs.md +0 -1
  76. package/adapters/opencode/skills/agnix/SKILL.md +1 -2
  77. package/adapters/opencode/skills/consult/SKILL.md +33 -23
  78. package/adapters/opencode/skills/debate/SKILL.md +245 -0
  79. package/adapters/opencode/skills/deslop/SKILL.md +1 -2
  80. package/adapters/opencode/skills/discover-tasks/SKILL.md +1 -2
  81. package/adapters/opencode/skills/drift-analysis/SKILL.md +1 -2
  82. package/adapters/opencode/skills/enhance-agent-prompts/SKILL.md +1 -2
  83. package/adapters/opencode/skills/enhance-claude-memory/SKILL.md +1 -2
  84. package/adapters/opencode/skills/enhance-cross-file/SKILL.md +1 -2
  85. package/adapters/opencode/skills/enhance-docs/SKILL.md +1 -2
  86. package/adapters/opencode/skills/enhance-hooks/SKILL.md +1 -2
  87. package/adapters/opencode/skills/enhance-orchestrator/SKILL.md +1 -2
  88. package/adapters/opencode/skills/enhance-plugins/SKILL.md +1 -2
  89. package/adapters/opencode/skills/enhance-prompts/SKILL.md +1 -2
  90. package/adapters/opencode/skills/enhance-skills/SKILL.md +1 -2
  91. package/adapters/opencode/skills/learn/SKILL.md +1 -2
  92. package/adapters/opencode/skills/orchestrate-review/SKILL.md +0 -1
  93. package/adapters/opencode/skills/perf-analyzer/SKILL.md +1 -2
  94. package/adapters/opencode/skills/perf-baseline-manager/SKILL.md +1 -2
  95. package/adapters/opencode/skills/perf-benchmarker/SKILL.md +1 -2
  96. package/adapters/opencode/skills/perf-code-paths/SKILL.md +1 -2
  97. package/adapters/opencode/skills/perf-investigation-logger/SKILL.md +1 -2
  98. package/adapters/opencode/skills/perf-profiler/SKILL.md +1 -2
  99. package/adapters/opencode/skills/perf-theory-gatherer/SKILL.md +1 -2
  100. package/adapters/opencode/skills/perf-theory-tester/SKILL.md +1 -2
  101. package/adapters/opencode/skills/repo-mapping/SKILL.md +1 -2
  102. package/adapters/opencode/skills/sync-docs/SKILL.md +1 -2
  103. package/adapters/opencode/skills/validate-delivery/SKILL.md +1 -2
  104. package/lib/adapter-transforms.js +24 -4
  105. package/package.json +1 -1
  106. package/plugins/agnix/.claude-plugin/plugin.json +1 -1
  107. package/plugins/agnix/skills/agnix/SKILL.md +1 -1
  108. package/plugins/audit-project/.claude-plugin/plugin.json +1 -1
  109. package/plugins/audit-project/lib/adapter-transforms.js +24 -4
  110. package/plugins/consult/.claude-plugin/plugin.json +1 -1
  111. package/plugins/consult/agents/consult-agent.md +122 -29
  112. package/plugins/consult/commands/consult.md +135 -58
  113. package/plugins/consult/skills/consult/SKILL.md +31 -20
  114. package/plugins/debate/.claude-plugin/plugin.json +21 -0
  115. package/plugins/debate/agents/debate-orchestrator.md +175 -0
  116. package/plugins/debate/commands/debate.md +221 -0
  117. package/plugins/debate/lib/adapter-transforms.js +298 -0
  118. package/plugins/debate/lib/collectors/codebase.js +392 -0
  119. package/plugins/debate/lib/collectors/docs-patterns.js +713 -0
  120. package/plugins/debate/lib/collectors/documentation.js +219 -0
  121. package/plugins/debate/lib/collectors/github.js +330 -0
  122. package/plugins/debate/lib/collectors/index.js +126 -0
  123. package/plugins/debate/lib/config/index.js +14 -0
  124. package/plugins/debate/lib/cross-platform/index.js +539 -0
  125. package/plugins/debate/lib/discovery/index.js +352 -0
  126. package/plugins/debate/lib/drift-detect/collectors.js +37 -0
  127. package/plugins/debate/lib/enhance/agent-analyzer.js +421 -0
  128. package/plugins/debate/lib/enhance/agent-patterns.js +571 -0
  129. package/plugins/debate/lib/enhance/auto-suppression.js +622 -0
  130. package/plugins/debate/lib/enhance/benchmark.js +417 -0
  131. package/plugins/debate/lib/enhance/cross-file-analyzer.js +930 -0
  132. package/plugins/debate/lib/enhance/cross-file-patterns.js +370 -0
  133. package/plugins/debate/lib/enhance/docs-analyzer.js +325 -0
  134. package/plugins/debate/lib/enhance/docs-patterns.js +671 -0
  135. package/plugins/debate/lib/enhance/fixer.js +721 -0
  136. package/plugins/debate/lib/enhance/hook-analyzer.js +135 -0
  137. package/plugins/debate/lib/enhance/hook-patterns.js +40 -0
  138. package/plugins/debate/lib/enhance/index.js +127 -0
  139. package/plugins/debate/lib/enhance/plugin-analyzer.js +402 -0
  140. package/plugins/debate/lib/enhance/plugin-patterns.js +326 -0
  141. package/plugins/debate/lib/enhance/projectmemory-analyzer.js +551 -0
  142. package/plugins/debate/lib/enhance/projectmemory-patterns.js +617 -0
  143. package/plugins/debate/lib/enhance/prompt-analyzer.js +457 -0
  144. package/plugins/debate/lib/enhance/prompt-patterns.js +1484 -0
  145. package/plugins/debate/lib/enhance/reporter.js +1348 -0
  146. package/plugins/debate/lib/enhance/security-patterns.js +284 -0
  147. package/plugins/debate/lib/enhance/skill-analyzer.js +182 -0
  148. package/plugins/debate/lib/enhance/skill-patterns.js +147 -0
  149. package/plugins/debate/lib/enhance/suppression.js +352 -0
  150. package/plugins/debate/lib/enhance/tool-patterns.js +373 -0
  151. package/plugins/debate/lib/index.js +270 -0
  152. package/plugins/debate/lib/patterns/cli-enhancers.js +611 -0
  153. package/plugins/debate/lib/patterns/pipeline.js +948 -0
  154. package/plugins/debate/lib/patterns/review-patterns.js +558 -0
  155. package/plugins/debate/lib/patterns/slop-analyzers.js +2305 -0
  156. package/plugins/debate/lib/patterns/slop-patterns.js +1187 -0
  157. package/plugins/debate/lib/perf/analyzer/index.js +22 -0
  158. package/plugins/debate/lib/perf/argument-parser.js +105 -0
  159. package/plugins/debate/lib/perf/baseline-comparator.js +50 -0
  160. package/plugins/debate/lib/perf/baseline-store.js +127 -0
  161. package/plugins/debate/lib/perf/benchmark-runner.js +404 -0
  162. package/plugins/debate/lib/perf/breaking-point-finder.js +52 -0
  163. package/plugins/debate/lib/perf/breaking-point-runner.js +60 -0
  164. package/plugins/debate/lib/perf/checkpoint.js +123 -0
  165. package/plugins/debate/lib/perf/code-paths.js +86 -0
  166. package/plugins/debate/lib/perf/consolidation.js +37 -0
  167. package/plugins/debate/lib/perf/constraint-runner.js +71 -0
  168. package/plugins/debate/lib/perf/experiment-runner.js +32 -0
  169. package/plugins/debate/lib/perf/index.js +41 -0
  170. package/plugins/debate/lib/perf/investigation-state.js +874 -0
  171. package/plugins/debate/lib/perf/optimization-runner.js +79 -0
  172. package/plugins/debate/lib/perf/profilers/go.js +22 -0
  173. package/plugins/debate/lib/perf/profilers/index.js +46 -0
  174. package/plugins/debate/lib/perf/profilers/java.js +23 -0
  175. package/plugins/debate/lib/perf/profilers/node.js +27 -0
  176. package/plugins/debate/lib/perf/profilers/python.js +23 -0
  177. package/plugins/debate/lib/perf/profilers/rust.js +23 -0
  178. package/plugins/debate/lib/perf/profiling-runner.js +75 -0
  179. package/plugins/debate/lib/perf/schemas.js +140 -0
  180. package/plugins/debate/lib/platform/detect-platform.js +413 -0
  181. package/plugins/debate/lib/platform/detection-configs.js +93 -0
  182. package/plugins/debate/lib/platform/state-dir.js +132 -0
  183. package/plugins/debate/lib/platform/verify-tools.js +182 -0
  184. package/plugins/debate/lib/repo-map/cache.js +152 -0
  185. package/plugins/debate/lib/repo-map/concurrency.js +29 -0
  186. package/plugins/debate/lib/repo-map/index.js +222 -0
  187. package/plugins/debate/lib/repo-map/installer.js +212 -0
  188. package/plugins/debate/lib/repo-map/queries/go.js +27 -0
  189. package/plugins/debate/lib/repo-map/queries/index.js +100 -0
  190. package/plugins/debate/lib/repo-map/queries/java.js +38 -0
  191. package/plugins/debate/lib/repo-map/queries/javascript.js +55 -0
  192. package/plugins/debate/lib/repo-map/queries/python.js +24 -0
  193. package/plugins/debate/lib/repo-map/queries/rust.js +73 -0
  194. package/plugins/debate/lib/repo-map/queries/typescript.js +38 -0
  195. package/plugins/debate/lib/repo-map/runner.js +1364 -0
  196. package/plugins/debate/lib/repo-map/updater.js +562 -0
  197. package/plugins/debate/lib/repo-map/usage-analyzer.js +407 -0
  198. package/plugins/debate/lib/schemas/plugin-manifest.schema.json +57 -0
  199. package/plugins/debate/lib/schemas/validator.js +247 -0
  200. package/plugins/debate/lib/sources/custom-handler.js +199 -0
  201. package/plugins/debate/lib/sources/policy-questions.js +246 -0
  202. package/plugins/debate/lib/sources/source-cache.js +165 -0
  203. package/plugins/debate/lib/state/workflow-state.js +576 -0
  204. package/plugins/debate/lib/types/agent-frontmatter.d.ts +134 -0
  205. package/plugins/debate/lib/types/command-frontmatter.d.ts +107 -0
  206. package/plugins/debate/lib/types/hook-frontmatter.d.ts +115 -0
  207. package/plugins/debate/lib/types/index.d.ts +84 -0
  208. package/plugins/debate/lib/types/plugin-manifest.d.ts +102 -0
  209. package/plugins/debate/lib/types/skill-frontmatter.d.ts +89 -0
  210. package/plugins/debate/lib/utils/atomic-write.js +94 -0
  211. package/plugins/debate/lib/utils/cache-manager.js +159 -0
  212. package/plugins/debate/lib/utils/command-parser.js +0 -0
  213. package/plugins/debate/lib/utils/context-optimizer.js +300 -0
  214. package/plugins/debate/lib/utils/deprecation.js +37 -0
  215. package/plugins/debate/lib/utils/shell-escape.js +88 -0
  216. package/plugins/debate/lib/utils/state-helpers.js +61 -0
  217. package/plugins/debate/skills/debate/SKILL.md +264 -0
  218. package/plugins/deslop/.claude-plugin/plugin.json +1 -1
  219. package/plugins/deslop/lib/adapter-transforms.js +24 -4
  220. package/plugins/deslop/skills/deslop/SKILL.md +1 -1
  221. package/plugins/drift-detect/.claude-plugin/plugin.json +1 -1
  222. package/plugins/drift-detect/lib/adapter-transforms.js +24 -4
  223. package/plugins/drift-detect/skills/drift-analysis/SKILL.md +1 -1
  224. package/plugins/enhance/.claude-plugin/plugin.json +1 -1
  225. package/plugins/enhance/lib/adapter-transforms.js +24 -4
  226. package/plugins/enhance/skills/enhance-agent-prompts/SKILL.md +1 -1
  227. package/plugins/enhance/skills/enhance-claude-memory/SKILL.md +1 -1
  228. package/plugins/enhance/skills/enhance-cross-file/SKILL.md +1 -1
  229. package/plugins/enhance/skills/enhance-docs/SKILL.md +1 -1
  230. package/plugins/enhance/skills/enhance-hooks/SKILL.md +1 -1
  231. package/plugins/enhance/skills/enhance-orchestrator/SKILL.md +1 -1
  232. package/plugins/enhance/skills/enhance-plugins/SKILL.md +1 -1
  233. package/plugins/enhance/skills/enhance-prompts/SKILL.md +1 -1
  234. package/plugins/enhance/skills/enhance-skills/SKILL.md +1 -1
  235. package/plugins/learn/.claude-plugin/plugin.json +1 -1
  236. package/plugins/learn/agents/learn-agent.md +1 -1
  237. package/plugins/learn/lib/adapter-transforms.js +24 -4
  238. package/plugins/learn/skills/learn/SKILL.md +1 -1
  239. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  240. package/plugins/next-task/agents/exploration-agent.md +1 -1
  241. package/plugins/next-task/lib/adapter-transforms.js +24 -4
  242. package/plugins/next-task/skills/discover-tasks/SKILL.md +1 -1
  243. package/plugins/next-task/skills/validate-delivery/SKILL.md +1 -1
  244. package/plugins/perf/.claude-plugin/plugin.json +1 -1
  245. package/plugins/perf/lib/adapter-transforms.js +24 -4
  246. package/plugins/perf/skills/perf-analyzer/SKILL.md +1 -1
  247. package/plugins/perf/skills/perf-baseline-manager/SKILL.md +1 -1
  248. package/plugins/perf/skills/perf-benchmarker/SKILL.md +1 -1
  249. package/plugins/perf/skills/perf-code-paths/SKILL.md +1 -1
  250. package/plugins/perf/skills/perf-investigation-logger/SKILL.md +1 -1
  251. package/plugins/perf/skills/perf-profiler/SKILL.md +1 -1
  252. package/plugins/perf/skills/perf-theory-gatherer/SKILL.md +1 -1
  253. package/plugins/perf/skills/perf-theory-tester/SKILL.md +1 -1
  254. package/plugins/repo-map/.claude-plugin/plugin.json +1 -1
  255. package/plugins/repo-map/lib/adapter-transforms.js +24 -4
  256. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  257. package/plugins/ship/lib/adapter-transforms.js +24 -4
  258. package/plugins/sync-docs/.claude-plugin/plugin.json +1 -1
  259. package/plugins/sync-docs/lib/adapter-transforms.js +24 -4
  260. package/plugins/sync-docs/skills/sync-docs/SKILL.md +1 -1
  261. package/scripts/gen-adapters.js +6 -7
  262. package/scripts/generate-docs.js +4 -2
  263. package/scripts/plugins.txt +1 -0
  264. package/site/content.json +6 -6
@@ -0,0 +1,948 @@
1
+ /**
2
+ * Slop Detection Pipeline
3
+ *
4
+ * 3-phase detection pipeline orchestrator:
5
+ * - Phase 1 (built-in): regex patterns + multi-pass analyzers - always runs
6
+ * - Phase 2 (optional): CLI tools (jscpd, madge, escomplex) - if available
7
+ * - Phase 3 (LLM handoff): certainty-tagged findings for agent review
8
+ *
9
+ * Inherits modes from deslop: report (analyze only) vs apply (fix issues)
10
+ *
11
+ * @module patterns/pipeline
12
+ * @author Avi Fenesh
13
+ * @license MIT
14
+ */
15
+
16
+ const path = require('path');
17
+ const fs = require('fs');
18
+ const fsPromises = require('fs').promises;
19
+ const slopPatterns = require('./slop-patterns');
20
+ const analyzers = require('./slop-analyzers');
21
+
22
+ // Concurrent file reads per batch
23
+ const FILE_READ_BATCH_SIZE = 50;
24
+
25
+ /**
26
+ * Global exclusions - files that should NEVER be flagged
27
+ * These are meta-files that define detection patterns, so they naturally
28
+ * contain the patterns they're detecting (not actual slop)
29
+ */
30
+ const GLOBAL_EXCLUSIONS = [
31
+ '**/slop-patterns.js',
32
+ '**/slop-analyzers.js',
33
+ '**/cli-enhancers.js',
34
+ '**/pipeline.js',
35
+ '**/review-patterns.js',
36
+ '**/detect.js',
37
+ '**/*-patterns.js', // Any pattern definition file
38
+ '**/*-analyzers.js' // Any analyzer definition file
39
+ ];
40
+
41
+ /**
42
+ * Certainty levels for findings
43
+ * HIGH: Single regex match - definitive
44
+ * MEDIUM: Multi-pass analysis - requires context
45
+ * LOW: Heuristic/CLI tool - needs verification
46
+ */
47
+ const CERTAINTY = {
48
+ HIGH: 'HIGH',
49
+ MEDIUM: 'MEDIUM',
50
+ LOW: 'LOW'
51
+ };
52
+
53
+ /**
54
+ * Thoroughness levels
55
+ * quick: Phase 1 regex only - fastest
56
+ * normal: Phase 1 + multi-pass analyzers - balanced
57
+ * deep: Phase 1 + Phase 2 CLI tools (if available) - thorough
58
+ */
59
+ const THOROUGHNESS = {
60
+ QUICK: 'quick',
61
+ NORMAL: 'normal',
62
+ DEEP: 'deep'
63
+ };
64
+
65
+ // Default timeout for pipeline execution (5 minutes)
66
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
67
+
68
+ /**
69
+ * Read multiple files asynchronously in batches
70
+ * @param {string[]} files - Array of file paths
71
+ * @param {number} batchSize - Concurrent reads per batch
72
+ * @returns {Promise<Map<string, {content: string|null, error: Error|null}>>}
73
+ */
74
+ async function batchReadFiles(files, batchSize = FILE_READ_BATCH_SIZE) {
75
+ const results = new Map();
76
+
77
+ for (let i = 0; i < files.length; i += batchSize) {
78
+ const batch = files.slice(i, i + batchSize);
79
+ const batchResults = await Promise.all(
80
+ batch.map(async (file) => {
81
+ try {
82
+ const content = await fsPromises.readFile(file, 'utf8');
83
+ return { file, content, error: null };
84
+ } catch (err) {
85
+ return { file, content: null, error: err };
86
+ }
87
+ })
88
+ );
89
+
90
+ for (const result of batchResults) {
91
+ results.set(result.file, { content: result.content, error: result.error });
92
+ }
93
+ }
94
+
95
+ return results;
96
+ }
97
+
98
+ /**
99
+ * Run the slop detection pipeline
100
+ *
101
+ * @param {string} repoPath - Repository root path
102
+ * @param {Object} options - Pipeline options
103
+ * @param {string} [options.thoroughness='normal'] - quick | normal | deep
104
+ * @param {string[]} [options.targetFiles] - Specific files to analyze (defaults to all source files)
105
+ * @param {string} [options.language] - Filter to specific language
106
+ * @param {string} [options.mode='report'] - report | apply
107
+ * @param {Object} [options.cliTools] - Pre-detected CLI tools (from detectAvailableTools)
108
+ * @param {number} [options.timeout] - Timeout in ms (default: 5 minutes)
109
+ * @returns {Promise<Object>} Pipeline results: { findings, summary, phase3Prompt, missingTools }
110
+ */
111
+ async function runPipeline(repoPath, options = {}) {
112
+ const thoroughness = options.thoroughness || THOROUGHNESS.NORMAL;
113
+ const mode = options.mode || 'report';
114
+ const language = options.language || null;
115
+ const timeout = options.timeout || DEFAULT_TIMEOUT_MS;
116
+ const startTime = Date.now();
117
+
118
+ const findings = [];
119
+ const missingTools = [];
120
+ let cliTools = options.cliTools || null;
121
+ let timedOut = false;
122
+
123
+ // Helper to check if timeout exceeded
124
+ function isTimedOut() {
125
+ return Date.now() - startTime > timeout;
126
+ }
127
+
128
+ // Get target files - limit to 200 to prevent memory exhaustion
129
+ // Users can pass targetFiles explicitly for larger scans
130
+ let targetFiles = options.targetFiles;
131
+ const maxFiles = options.maxFiles || 200;
132
+ if (!targetFiles || targetFiles.length === 0) {
133
+ const result = analyzers.countSourceFiles(repoPath, {
134
+ maxFiles,
135
+ includeTests: false
136
+ });
137
+ targetFiles = result.files;
138
+ }
139
+
140
+ // Pre-load all file contents asynchronously (used by Phase 1 and Phase 1b)
141
+ let fileContents;
142
+ try {
143
+ fileContents = await batchReadFiles(targetFiles);
144
+ } catch (err) {
145
+ console.error('[WARN] File pre-loading failed:', err.message);
146
+ fileContents = new Map();
147
+ }
148
+
149
+ // Phase 1: Built-in regex patterns (always runs)
150
+ // Wrapped in try-catch to prevent crashes on malformed files
151
+ try {
152
+ const phase1Results = runPhase1(repoPath, targetFiles, language, fileContents);
153
+ findings.push(...phase1Results);
154
+ } catch (err) {
155
+ // Log but continue with empty phase1 results rather than crash
156
+ console.error('[WARN] Phase 1 failed:', err.message);
157
+ }
158
+
159
+ // Check timeout after Phase 1
160
+ if (isTimedOut()) {
161
+ console.error('[WARN] Pipeline timeout after Phase 1');
162
+ timedOut = true;
163
+ }
164
+
165
+ // Phase 1b: Multi-pass analyzers (if normal or deep)
166
+ // Wrapped in try-catch as these are expensive and can fail
167
+ if (!timedOut && thoroughness !== THOROUGHNESS.QUICK) {
168
+ try {
169
+ // runMultiPassAnalyzers is async to support non-blocking I/O (PERF-007)
170
+ const multiPassResults = await runMultiPassAnalyzers(repoPath, targetFiles, fileContents);
171
+ findings.push(...multiPassResults);
172
+ } catch (err) {
173
+ // Log but continue with partial results rather than crash
174
+ console.error('[WARN] Phase 1b failed:', err.message);
175
+ }
176
+
177
+ // Check timeout after Phase 1b
178
+ if (isTimedOut()) {
179
+ console.error('[WARN] Pipeline timeout after Phase 1b');
180
+ timedOut = true;
181
+ }
182
+ }
183
+
184
+ // Phase 2: CLI tools (only if deep and tools available)
185
+ // Detect project languages for language-aware tool recommendations
186
+ let detectedLanguages = [];
187
+ if (!timedOut && thoroughness === THOROUGHNESS.DEEP) {
188
+ // Lazy-load CLI enhancers to avoid circular dependencies
189
+ const cliEnhancers = require('./cli-enhancers');
190
+
191
+ // Detect project languages
192
+ detectedLanguages = cliEnhancers.detectProjectLanguages(repoPath);
193
+
194
+ if (!cliTools) {
195
+ // Get tools relevant for detected languages
196
+ cliTools = cliEnhancers.detectAvailableTools(detectedLanguages);
197
+ }
198
+
199
+ // Track missing tools (only those relevant for project languages)
200
+ const relevantTools = cliEnhancers.getToolsForLanguages(detectedLanguages);
201
+ for (const toolName of Object.keys(relevantTools)) {
202
+ if (!cliTools[toolName]) {
203
+ missingTools.push(toolName);
204
+ }
205
+ }
206
+
207
+ const phase2Results = runPhase2(repoPath, cliTools, targetFiles);
208
+ findings.push(...phase2Results);
209
+ }
210
+
211
+ // Build summary
212
+ const summary = buildSummary(findings);
213
+
214
+ // Generate Phase 3 handoff prompt
215
+ const phase3Prompt = formatHandoffPrompt(findings, mode);
216
+
217
+ return {
218
+ findings,
219
+ summary,
220
+ phase3Prompt,
221
+ missingTools,
222
+ detectedLanguages,
223
+ metadata: {
224
+ repoPath,
225
+ thoroughness,
226
+ mode,
227
+ filesAnalyzed: targetFiles.length,
228
+ timestamp: new Date().toISOString(),
229
+ timedOut,
230
+ elapsedMs: Date.now() - startTime
231
+ }
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Phase 1: Run built-in regex patterns against target files
237
+ *
238
+ * @param {string} repoPath - Repository root
239
+ * @param {string[]} targetFiles - Files to analyze
240
+ * @param {string|null} language - Optional language filter
241
+ * @param {Map<string, {content: string|null, error: Error|null}>} [fileContents] - Pre-loaded file contents (optional)
242
+ * @returns {Array} Findings with HIGH certainty
243
+ */
244
+ function runPhase1(repoPath, targetFiles, language, fileContents) {
245
+ const findings = [];
246
+ const contentMap = fileContents || new Map();
247
+
248
+ // Get patterns (filtered by language if specified)
249
+ const patterns = language
250
+ ? slopPatterns.getPatternsForLanguage(language)
251
+ : slopPatterns.slopPatterns;
252
+
253
+ for (const file of targetFiles) {
254
+ // Skip globally excluded files (pattern definition files)
255
+ if (slopPatterns.isFileExcluded(file, GLOBAL_EXCLUSIONS)) {
256
+ continue;
257
+ }
258
+
259
+ // Detect file language once per file
260
+ const fileLanguage = analyzers.detectLanguage(file);
261
+
262
+ // Skip if language filter doesn't match file extension
263
+ if (language) {
264
+ // For JS/TS language filter, accept both 'javascript' and 'js' detection results
265
+ const isJsFamily = (language === 'javascript' || language === 'typescript') && fileLanguage === 'js';
266
+ if (fileLanguage !== language && !isJsFamily) continue;
267
+ }
268
+
269
+ // Get pre-loaded content from map, fallback to synchronous read
270
+ const filePath = path.isAbsolute(file) ? file : path.join(repoPath, file);
271
+ const readResult = contentMap.get(file) || contentMap.get(filePath);
272
+
273
+ let content;
274
+ if (readResult && !readResult.error && readResult.content !== null) {
275
+ content = readResult.content;
276
+ } else {
277
+ // Fallback to synchronous read (for backward compatibility with direct calls)
278
+ try {
279
+ content = fs.readFileSync(filePath, 'utf8');
280
+ } catch {
281
+ continue; // Skip unreadable files
282
+ }
283
+ }
284
+
285
+ const lines = content.split('\n');
286
+
287
+ for (const [patternName, pattern] of Object.entries(patterns)) {
288
+ // Skip multi-pass patterns (handled separately)
289
+ if (pattern.requiresMultiPass) continue;
290
+
291
+ // Skip if no regex pattern
292
+ if (!pattern.pattern) continue;
293
+
294
+ // Skip if file matches exclude patterns
295
+ if (slopPatterns.isFileExcluded(file, pattern.exclude)) continue;
296
+
297
+ // Skip language-specific patterns that don't match file's language
298
+ if (pattern.language) {
299
+ const patternLang = pattern.language;
300
+ // Map detection results to pattern language names
301
+ // Note: 'js' from detectLanguage covers both JavaScript and TypeScript
302
+ const langMatch = (patternLang === 'javascript' && fileLanguage === 'js') ||
303
+ (patternLang === 'python' && fileLanguage === 'python') ||
304
+ (patternLang === 'rust' && fileLanguage === 'rust') ||
305
+ (patternLang === 'go' && fileLanguage === 'go') ||
306
+ (patternLang === 'java' && fileLanguage === 'java') ||
307
+ patternLang === fileLanguage; // Direct match fallback
308
+ if (!langMatch) continue;
309
+ }
310
+
311
+ // Handle patterns requiring consecutive line blocks
312
+ if (pattern.minConsecutiveLines) {
313
+ const minLines = pattern.minConsecutiveLines;
314
+ let consecutiveStart = -1;
315
+ let consecutiveCount = 0;
316
+
317
+ for (let i = 0; i <= lines.length; i++) {
318
+ const line = i < lines.length ? lines[i] : ''; // Empty line at end to flush
319
+ const matches = i < lines.length && pattern.pattern.test(line);
320
+
321
+ if (matches) {
322
+ if (consecutiveStart === -1) {
323
+ consecutiveStart = i;
324
+ }
325
+ consecutiveCount++;
326
+ } else {
327
+ // End of consecutive block - check if it meets threshold
328
+ if (consecutiveCount >= minLines) {
329
+ findings.push({
330
+ file,
331
+ line: consecutiveStart + 1,
332
+ patternName,
333
+ severity: pattern.severity,
334
+ certainty: CERTAINTY.HIGH,
335
+ description: `${pattern.description} (${consecutiveCount} consecutive lines)`,
336
+ autoFix: pattern.autoFix,
337
+ content: `Lines ${consecutiveStart + 1}-${consecutiveStart + consecutiveCount}`,
338
+ phase: 1,
339
+ details: { startLine: consecutiveStart + 1, endLine: consecutiveStart + consecutiveCount, lineCount: consecutiveCount }
340
+ });
341
+ }
342
+ consecutiveStart = -1;
343
+ consecutiveCount = 0;
344
+ }
345
+ }
346
+ } else {
347
+ // Standard per-line matching
348
+ for (let i = 0; i < lines.length; i++) {
349
+ const line = lines[i];
350
+ if (pattern.pattern.test(line)) {
351
+ findings.push({
352
+ file,
353
+ line: i + 1,
354
+ patternName,
355
+ severity: pattern.severity,
356
+ certainty: CERTAINTY.HIGH,
357
+ description: pattern.description,
358
+ autoFix: pattern.autoFix,
359
+ content: line.trim().substring(0, 100),
360
+ phase: 1
361
+ });
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ return findings;
369
+ }
370
+
371
+ /**
372
+ * Run multi-pass analyzers (doc/code ratio, verbosity, etc.)
373
+ *
374
+ * @param {string} repoPath - Repository root
375
+ * @param {string[]} targetFiles - Files to analyze
376
+ * @param {Map<string, {content: string|null, error: Error|null}>} [fileContents] - Pre-loaded file contents (optional)
377
+ * @returns {Promise<Array>} Findings with MEDIUM certainty
378
+ */
379
+ async function runMultiPassAnalyzers(repoPath, targetFiles, fileContents) {
380
+ const findings = [];
381
+ const contentMap = fileContents || new Map();
382
+
383
+ // Skip expensive analyzers for large file sets to prevent memory exhaustion
384
+ const isLargeRepo = targetFiles.length > 100;
385
+
386
+ // Get multi-pass pattern definitions for thresholds
387
+ const multiPassPatterns = slopPatterns.getMultiPassPatterns();
388
+
389
+ // Supported languages for doc/code and verbosity analysis
390
+ const docCodeLangs = /\.(js|jsx|ts|tsx|mjs|cjs|py|rs|java|go)$/i;
391
+
392
+ for (const file of targetFiles) {
393
+ if (!file.match(docCodeLangs)) continue;
394
+ if (analyzers.isTestFile(file)) continue;
395
+ // Skip globally excluded files (pattern definition files)
396
+ if (slopPatterns.isFileExcluded(file, GLOBAL_EXCLUSIONS)) continue;
397
+
398
+ // Get pre-loaded content from map, fallback to synchronous read
399
+ const filePath = path.isAbsolute(file) ? file : path.join(repoPath, file);
400
+ const readResult = contentMap.get(file) || contentMap.get(filePath);
401
+
402
+ let content;
403
+ if (readResult && !readResult.error && readResult.content !== null) {
404
+ content = readResult.content;
405
+ } else {
406
+ // Fallback to synchronous read (for backward compatibility with direct calls)
407
+ try {
408
+ content = fs.readFileSync(filePath, 'utf8');
409
+ } catch {
410
+ continue;
411
+ }
412
+ }
413
+
414
+ // Doc/code ratio analysis (multi-language)
415
+ const docCodePattern = multiPassPatterns.doc_code_ratio_js;
416
+ if (docCodePattern) {
417
+ const docRatioViolations = analyzers.analyzeDocCodeRatio(content, {
418
+ minFunctionLines: docCodePattern.minFunctionLines || 3,
419
+ maxRatio: docCodePattern.maxRatio || 3.0,
420
+ filePath: file
421
+ });
422
+
423
+ for (const v of docRatioViolations) {
424
+ findings.push({
425
+ file,
426
+ line: v.line,
427
+ patternName: 'doc_code_ratio',
428
+ severity: docCodePattern.severity,
429
+ certainty: CERTAINTY.MEDIUM,
430
+ description: `${docCodePattern.description} (${v.docLines} doc lines / ${v.codeLines} code lines = ${v.ratio}x)`,
431
+ autoFix: docCodePattern.autoFix,
432
+ content: v.functionName ? `${v.functionName}()` : `Function at line ${v.line}`,
433
+ phase: 1,
434
+ details: { docLines: v.docLines, codeLines: v.codeLines, ratio: v.ratio, functionName: v.functionName }
435
+ });
436
+ }
437
+ }
438
+
439
+ // Verbosity ratio analysis (multi-language)
440
+ const verbosityPattern = multiPassPatterns.verbosity_ratio;
441
+ if (verbosityPattern) {
442
+ const verbosityViolations = analyzers.analyzeVerbosityRatio(content, {
443
+ minCodeLines: verbosityPattern.minCodeLines || 3,
444
+ maxCommentRatio: verbosityPattern.maxCommentRatio || 2.0,
445
+ filePath: file
446
+ });
447
+
448
+ for (const v of verbosityViolations) {
449
+ findings.push({
450
+ file,
451
+ line: v.line,
452
+ patternName: 'verbosity_ratio',
453
+ severity: verbosityPattern.severity,
454
+ certainty: CERTAINTY.MEDIUM,
455
+ description: `${verbosityPattern.description} (${v.commentLines} comment lines / ${v.codeLines} code lines = ${v.ratio}x)`,
456
+ autoFix: verbosityPattern.autoFix,
457
+ content: `Function at line ${v.line}`,
458
+ phase: 1,
459
+ details: { commentLines: v.commentLines, codeLines: v.codeLines, ratio: v.ratio }
460
+ });
461
+ }
462
+ }
463
+ }
464
+
465
+ // Project-level analyzers (run once, not per-file)
466
+ // Wrap in try-catch to prevent crashes
467
+ const overEngPattern = multiPassPatterns.over_engineering_metrics;
468
+ if (overEngPattern && !isLargeRepo) {
469
+ try {
470
+ // analyzeOverEngineering is async to avoid blocking I/O (PERF-007)
471
+ const overEngResult = await analyzers.analyzeOverEngineering(repoPath, {
472
+ fileRatioThreshold: overEngPattern.fileRatioThreshold || 20,
473
+ linesPerExportThreshold: overEngPattern.linesPerExportThreshold || 500,
474
+ depthThreshold: overEngPattern.depthThreshold || 4
475
+ });
476
+
477
+ for (const v of overEngResult.violations) {
478
+ findings.push({
479
+ file: 'project-level',
480
+ line: 0,
481
+ patternName: 'over_engineering_metrics',
482
+ severity: v.severity,
483
+ certainty: CERTAINTY.MEDIUM,
484
+ description: `Over-engineering: ${v.type} - ${v.value} (threshold: ${v.threshold})`,
485
+ autoFix: 'flag',
486
+ content: v.value,
487
+ phase: 1,
488
+ details: v.details
489
+ });
490
+ }
491
+ } catch (err) {
492
+ // Skip on error - don't crash the pipeline
493
+ }
494
+ }
495
+
496
+ // Buzzword inflation analysis - EXPENSIVE: reads files multiple times
497
+ // Skip for large repos to prevent memory exhaustion
498
+ const buzzwordPattern = multiPassPatterns.buzzword_inflation;
499
+ if (buzzwordPattern && !isLargeRepo) {
500
+ try {
501
+ const buzzwordResult = analyzers.analyzeBuzzwordInflation(repoPath, {
502
+ minEvidenceMatches: buzzwordPattern.minEvidenceMatches || 2
503
+ });
504
+
505
+ for (const v of buzzwordResult.violations) {
506
+ findings.push({
507
+ file: v.file,
508
+ line: v.line,
509
+ patternName: 'buzzword_inflation',
510
+ severity: v.severity,
511
+ certainty: CERTAINTY.MEDIUM,
512
+ description: v.message,
513
+ autoFix: 'flag',
514
+ content: v.claim,
515
+ phase: 1,
516
+ details: { buzzword: v.buzzword, category: v.category, evidenceCount: v.evidenceCount }
517
+ });
518
+ }
519
+ } catch (err) {
520
+ // Skip on error - don't crash the pipeline
521
+ }
522
+ }
523
+
524
+ // Infrastructure without implementation - EXPENSIVE: reads all files twice
525
+ // Skip for large repos to prevent memory exhaustion
526
+ const infraPattern = multiPassPatterns.infrastructure_without_implementation;
527
+ if (infraPattern && !isLargeRepo) {
528
+ try {
529
+ const infraResult = analyzers.analyzeInfrastructureWithoutImplementation(repoPath);
530
+
531
+ for (const v of infraResult.violations) {
532
+ findings.push({
533
+ file: v.file,
534
+ line: v.line,
535
+ patternName: 'infrastructure_without_implementation',
536
+ severity: v.severity,
537
+ certainty: CERTAINTY.MEDIUM,
538
+ description: v.message,
539
+ autoFix: 'flag',
540
+ content: v.content,
541
+ phase: 1,
542
+ details: { varName: v.varName, type: v.type }
543
+ });
544
+ }
545
+ } catch (err) {
546
+ // Skip on error - don't crash the pipeline
547
+ }
548
+ }
549
+
550
+ // Dead code analysis (per-file)
551
+ const deadCodePattern = multiPassPatterns.dead_code;
552
+ if (deadCodePattern) {
553
+ for (const file of targetFiles) {
554
+ // Skip test files
555
+ if (analyzers.isTestFile(file)) continue;
556
+ // Skip globally excluded files (pattern definition files)
557
+ if (slopPatterns.isFileExcluded(file, GLOBAL_EXCLUSIONS)) continue;
558
+
559
+ // Get pre-loaded content from map, fallback to synchronous read
560
+ const filePath = path.isAbsolute(file) ? file : path.join(repoPath, file);
561
+ const readResult = contentMap.get(file) || contentMap.get(filePath);
562
+
563
+ let content;
564
+ if (readResult && !readResult.error && readResult.content !== null) {
565
+ content = readResult.content;
566
+ } else {
567
+ // Fallback to synchronous read (for backward compatibility with direct calls)
568
+ try {
569
+ content = fs.readFileSync(filePath, 'utf8');
570
+ } catch {
571
+ continue;
572
+ }
573
+ }
574
+
575
+ const deadCodeViolations = analyzers.analyzeDeadCode(content, { filePath: file });
576
+
577
+ for (const v of deadCodeViolations) {
578
+ findings.push({
579
+ file,
580
+ line: v.line,
581
+ patternName: 'dead_code',
582
+ severity: deadCodePattern.severity,
583
+ certainty: CERTAINTY.MEDIUM,
584
+ description: `${deadCodePattern.description}: ${v.terminationType} at line ${v.terminationLine}`,
585
+ autoFix: deadCodePattern.autoFix,
586
+ content: v.content,
587
+ phase: 1,
588
+ details: { terminationType: v.terminationType, terminationLine: v.terminationLine }
589
+ });
590
+ }
591
+ }
592
+ }
593
+
594
+ // Stub function analysis (per-file, multi-language)
595
+ const stubPattern = multiPassPatterns.placeholder_stub_returns_js;
596
+ if (stubPattern) {
597
+ // Supported extensions for stub detection
598
+ const stubExtensions = /\.(js|jsx|ts|tsx|mjs|cjs|py|rs|java|go)$/i;
599
+
600
+ for (const file of targetFiles) {
601
+ if (analyzers.isTestFile(file)) continue;
602
+ if (!file.match(stubExtensions)) continue;
603
+ // Honor pattern exclude globs (e.g., *.config.*)
604
+ if (slopPatterns.isFileExcluded(file, stubPattern.exclude)) continue;
605
+
606
+ // Get pre-loaded content from map, fallback to synchronous read
607
+ const filePath = path.isAbsolute(file) ? file : path.join(repoPath, file);
608
+ const readResult = contentMap.get(file) || contentMap.get(filePath);
609
+
610
+ let content;
611
+ if (readResult && !readResult.error && readResult.content !== null) {
612
+ content = readResult.content;
613
+ } else {
614
+ // Fallback to synchronous read (for backward compatibility with direct calls)
615
+ try {
616
+ content = fs.readFileSync(filePath, 'utf8');
617
+ } catch {
618
+ continue;
619
+ }
620
+ }
621
+
622
+ const stubViolations = analyzers.analyzeStubFunctions(content, { filePath: file });
623
+
624
+ for (const v of stubViolations) {
625
+ findings.push({
626
+ file,
627
+ line: v.line,
628
+ patternName: 'placeholder_stub_returns',
629
+ severity: v.hasTodo ? 'high' : stubPattern.severity,
630
+ certainty: v.certainty,
631
+ description: `${stubPattern.description}: ${v.functionName}() returns ${v.returnValue}`,
632
+ autoFix: stubPattern.autoFix,
633
+ content: v.content,
634
+ phase: 1,
635
+ details: { functionName: v.functionName, returnValue: v.returnValue, hasTodo: v.hasTodo }
636
+ });
637
+ }
638
+ }
639
+ }
640
+
641
+ // Shotgun surgery analysis (git history) - can have large buffer issues
642
+ // Skip for large repos to prevent memory exhaustion
643
+ const shotgunPattern = multiPassPatterns.shotgun_surgery;
644
+ if (shotgunPattern && !isLargeRepo) {
645
+ try {
646
+ const shotgunResult = analyzers.analyzeShotgunSurgery(repoPath, {
647
+ commitLimit: Math.min(shotgunPattern.commitLimit || 100, 50), // Reduce commit limit
648
+ clusterThreshold: shotgunPattern.clusterThreshold || 5
649
+ });
650
+
651
+ for (const v of shotgunResult.violations) {
652
+ findings.push({
653
+ file: 'project-level',
654
+ line: 0,
655
+ patternName: 'shotgun_surgery',
656
+ severity: shotgunPattern.severity,
657
+ certainty: CERTAINTY.MEDIUM,
658
+ description: `${shotgunPattern.description}: ${v.files.length} files change together ${v.count} times`,
659
+ autoFix: shotgunPattern.autoFix,
660
+ content: v.files.join(', ').substring(0, 100),
661
+ phase: 1,
662
+ details: { files: v.files, changeCount: v.count }
663
+ });
664
+ }
665
+ } catch {
666
+ // Git not available or not a git repo - skip silently
667
+ }
668
+ }
669
+
670
+ return findings;
671
+ }
672
+
673
+ /**
674
+ * Phase 2: Run CLI tools (if available)
675
+ *
676
+ * @param {string} repoPath - Repository root
677
+ * @param {Object} cliTools - Available CLI tools { jscpd, madge, escomplex }
678
+ * @param {string[]} targetFiles - Files to analyze
679
+ * @returns {Array} Findings with LOW certainty
680
+ */
681
+ function runPhase2(repoPath, cliTools, targetFiles) {
682
+ const findings = [];
683
+ const cliEnhancers = require('./cli-enhancers');
684
+
685
+ // Duplicate detection with jscpd
686
+ if (cliTools.jscpd) {
687
+ const duplicates = cliEnhancers.runDuplicateDetection(repoPath);
688
+ if (duplicates) {
689
+ for (const dup of duplicates) {
690
+ findings.push({
691
+ file: dup.firstFile,
692
+ line: dup.firstLine,
693
+ patternName: 'code_duplication',
694
+ severity: 'medium',
695
+ certainty: CERTAINTY.LOW,
696
+ description: `Code duplication: ${dup.lines} lines duplicated in ${dup.secondFile}:${dup.secondLine}`,
697
+ autoFix: 'flag',
698
+ content: `${dup.lines} lines duplicated`,
699
+ phase: 2,
700
+ details: dup
701
+ });
702
+ }
703
+ }
704
+ }
705
+
706
+ // Circular dependencies with madge
707
+ if (cliTools.madge) {
708
+ const circularDeps = cliEnhancers.runDependencyAnalysis(repoPath);
709
+ if (circularDeps) {
710
+ for (const cycle of circularDeps) {
711
+ findings.push({
712
+ file: cycle[0],
713
+ line: 0,
714
+ patternName: 'circular_dependency',
715
+ severity: 'high',
716
+ certainty: CERTAINTY.LOW,
717
+ description: `Circular dependency: ${cycle.join(' -> ')}`,
718
+ autoFix: 'flag',
719
+ content: cycle.join(' -> '),
720
+ phase: 2,
721
+ details: { cycle }
722
+ });
723
+ }
724
+ }
725
+ }
726
+
727
+ // Complexity analysis with escomplex
728
+ if (cliTools.escomplex) {
729
+ const complexityResults = cliEnhancers.runComplexityAnalysis(repoPath, targetFiles);
730
+ if (complexityResults) {
731
+ for (const result of complexityResults) {
732
+ if (result.complexity > 10) { // High cyclomatic complexity threshold
733
+ findings.push({
734
+ file: result.file,
735
+ line: result.line || 0,
736
+ patternName: 'high_complexity',
737
+ severity: result.complexity > 20 ? 'high' : 'medium',
738
+ certainty: CERTAINTY.LOW,
739
+ description: `High cyclomatic complexity: ${result.complexity} in ${result.name}`,
740
+ autoFix: 'flag',
741
+ content: `${result.name}: complexity ${result.complexity}`,
742
+ phase: 2,
743
+ details: result
744
+ });
745
+ }
746
+ }
747
+ }
748
+ }
749
+
750
+ return findings;
751
+ }
752
+
753
+ /**
754
+ * Build summary statistics from findings
755
+ *
756
+ * @param {Array} findings - All findings
757
+ * @returns {Object} Summary statistics
758
+ */
759
+ function buildSummary(findings) {
760
+ const summary = {
761
+ total: findings.length,
762
+ bySeverity: { critical: 0, high: 0, medium: 0, low: 0 },
763
+ byCertainty: { HIGH: 0, MEDIUM: 0, LOW: 0 },
764
+ byPhase: { 1: 0, 2: 0 },
765
+ byAutoFix: { remove: 0, replace: 0, add_logging: 0, flag: 0, none: 0 },
766
+ topPatterns: {}
767
+ };
768
+
769
+ for (const f of findings) {
770
+ summary.bySeverity[f.severity] = (summary.bySeverity[f.severity] || 0) + 1;
771
+ summary.byCertainty[f.certainty] = (summary.byCertainty[f.certainty] || 0) + 1;
772
+ summary.byPhase[f.phase] = (summary.byPhase[f.phase] || 0) + 1;
773
+ summary.byAutoFix[f.autoFix] = (summary.byAutoFix[f.autoFix] || 0) + 1;
774
+ summary.topPatterns[f.patternName] = (summary.topPatterns[f.patternName] || 0) + 1;
775
+ }
776
+
777
+ return summary;
778
+ }
779
+
780
+ /**
781
+ * Format handoff prompt for LLM (Phase 3)
782
+ *
783
+ * Creates a token-efficient prompt for the agent to review findings.
784
+ * Groups by certainty level with action guidance:
785
+ * - HIGH: Apply directly (if apply mode)
786
+ * - MEDIUM: Verify context before applying
787
+ * - LOW: Use judgment, may be false positive
788
+ *
789
+ * @param {Array} findings - All findings
790
+ * @param {string} mode - report | apply
791
+ * @param {Object} options - Formatting options
792
+ * @param {boolean} options.compact - Use compact table format (60-70% fewer tokens)
793
+ * @param {number} options.maxFindings - Maximum findings to include (default: 50)
794
+ * @returns {string} Formatted prompt
795
+ */
796
+ function formatHandoffPrompt(findings, mode, options = {}) {
797
+ const { compact = false, maxFindings = 50 } = options;
798
+
799
+ if (findings.length === 0) {
800
+ return '## Slop Detection Results\n\nNo issues detected.';
801
+ }
802
+
803
+ // Use compact format if requested
804
+ if (compact) {
805
+ return formatCompactPrompt(findings, mode, maxFindings);
806
+ }
807
+
808
+ // Group findings by certainty
809
+ const byGroup = {
810
+ HIGH: findings.filter(f => f.certainty === CERTAINTY.HIGH),
811
+ MEDIUM: findings.filter(f => f.certainty === CERTAINTY.MEDIUM),
812
+ LOW: findings.filter(f => f.certainty === CERTAINTY.LOW)
813
+ };
814
+
815
+ let prompt = '## Slop Detection Results\n\n';
816
+ prompt += `Mode: **${mode}** | Total: ${findings.length} findings\n\n`;
817
+
818
+ // HIGH certainty - definitive matches
819
+ if (byGroup.HIGH.length > 0) {
820
+ prompt += '### HIGH Certainty (Definitive - trust these)\n\n';
821
+ if (mode === 'apply') {
822
+ prompt += '_Action: Apply fixes directly for autoFix patterns._\n\n';
823
+ }
824
+ prompt += formatFindingsList(byGroup.HIGH);
825
+ prompt += '\n';
826
+ }
827
+
828
+ // MEDIUM certainty - needs context verification
829
+ if (byGroup.MEDIUM.length > 0) {
830
+ prompt += '### MEDIUM Certainty (Verify context)\n\n';
831
+ prompt += '_Action: Review surrounding code before applying._\n\n';
832
+ prompt += formatFindingsList(byGroup.MEDIUM);
833
+ prompt += '\n';
834
+ }
835
+
836
+ // LOW certainty - use judgment
837
+ if (byGroup.LOW.length > 0) {
838
+ prompt += '### LOW Certainty (Use judgment)\n\n';
839
+ prompt += '_Action: May be false positives. Investigate before acting._\n\n';
840
+ prompt += formatFindingsList(byGroup.LOW);
841
+ prompt += '\n';
842
+ }
843
+
844
+ // Action summary
845
+ prompt += '### Action Summary\n\n';
846
+ const autoFixable = findings.filter(f => f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none');
847
+ const needsReview = findings.filter(f => f.autoFix === 'flag' || f.autoFix === 'none');
848
+
849
+ prompt += `- Auto-fixable: ${autoFixable.length}\n`;
850
+ prompt += `- Needs manual review: ${needsReview.length}\n`;
851
+
852
+ return prompt;
853
+ }
854
+
855
+ /**
856
+ * Format findings in compact table format for token efficiency
857
+ *
858
+ * Reduces token usage by ~60-70% compared to verbose format.
859
+ * Best for large finding sets where full descriptions aren't needed.
860
+ *
861
+ * @param {Array} findings - All findings
862
+ * @param {string} mode - report | apply
863
+ * @param {number} maxFindings - Maximum findings to include
864
+ * @returns {string} Compact formatted prompt
865
+ */
866
+ function formatCompactPrompt(findings, mode, maxFindings) {
867
+ // Single pass to count certainty levels and auto-fixable findings
868
+ const { highCount, mediumCount, lowCount, autoFixableCount } = findings.reduce((acc, f) => {
869
+ switch (f.certainty) {
870
+ case CERTAINTY.HIGH: acc.highCount++; break;
871
+ case CERTAINTY.MEDIUM: acc.mediumCount++; break;
872
+ case CERTAINTY.LOW: acc.lowCount++; break;
873
+ }
874
+ if (f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none') {
875
+ acc.autoFixableCount++;
876
+ }
877
+ return acc;
878
+ }, { highCount: 0, mediumCount: 0, lowCount: 0, autoFixableCount: 0 });
879
+
880
+ // Truncate if needed
881
+ const limited = findings.slice(0, maxFindings);
882
+ const truncated = findings.length > maxFindings;
883
+
884
+ // Summary header
885
+ let output = `## Slop: ${mode}|H:${highCount}|M:${mediumCount}|L:${lowCount}\n\n`;
886
+
887
+ // Table format
888
+ output += '|File|L|Pattern|Cert|Fix|\n';
889
+ output += '|---|---|---|---|---|\n';
890
+
891
+ for (const f of limited) {
892
+ const fix = f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none' ? f.autoFix : '-';
893
+ const cert = f.certainty.charAt(0); // H, M, or L
894
+ output += `|${f.file}|${f.line}|${f.patternName}|${cert}|${fix}|\n`;
895
+ }
896
+
897
+ if (truncated) {
898
+ output += `\n_+${findings.length - maxFindings} more findings (truncated)_\n`;
899
+ }
900
+
901
+ // Auto-fix summary
902
+ output += `\n**Auto-fixable: ${autoFixableCount}** | Manual: ${findings.length - autoFixableCount}`;
903
+
904
+ return output;
905
+ }
906
+
907
+ /**
908
+ * Format a list of findings for the prompt
909
+ *
910
+ * @param {Array} findings - Findings to format
911
+ * @returns {string} Formatted list
912
+ */
913
+ function formatFindingsList(findings) {
914
+ // Group by file for compact output
915
+ const byFile = {};
916
+ for (const f of findings) {
917
+ if (!byFile[f.file]) byFile[f.file] = [];
918
+ byFile[f.file].push(f);
919
+ }
920
+
921
+ let output = '';
922
+ for (const [file, fileFindings] of Object.entries(byFile)) {
923
+ output += `**${file}**\n`;
924
+ for (const f of fileFindings) {
925
+ const fixTag = f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none'
926
+ ? ` [${f.autoFix}]`
927
+ : '';
928
+ output += `- L${f.line}: ${f.description}${fixTag}\n`;
929
+ }
930
+ output += '\n';
931
+ }
932
+
933
+ return output;
934
+ }
935
+
936
+ module.exports = {
937
+ runPipeline,
938
+ // Exported for testing
939
+ runPhase1,
940
+ runMultiPassAnalyzers,
941
+ runPhase2,
942
+ buildSummary,
943
+ formatHandoffPrompt,
944
+ formatCompactPrompt,
945
+ // Constants
946
+ CERTAINTY,
947
+ THOROUGHNESS
948
+ };