agileflow 3.3.0 → 3.4.1

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 (210) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/skill-loader.js +0 -1
  4. package/package.json +1 -1
  5. package/scripts/agileflow-statusline.sh +81 -0
  6. package/scripts/agileflow-welcome.js +79 -0
  7. package/scripts/claude-tmux.sh +90 -23
  8. package/scripts/claude-watchdog.sh +225 -0
  9. package/scripts/generators/agent-registry.js +14 -1
  10. package/scripts/generators/inject-babysit.js +22 -9
  11. package/scripts/generators/inject-help.js +19 -9
  12. package/scripts/lib/ac-test-matcher.js +452 -0
  13. package/scripts/lib/audit-cleanup.js +250 -0
  14. package/scripts/lib/audit-registry.js +304 -0
  15. package/scripts/lib/configure-features.js +35 -0
  16. package/scripts/lib/feature-catalog.js +3 -3
  17. package/scripts/lib/gate-enforcer.js +295 -0
  18. package/scripts/lib/model-profiles.js +118 -0
  19. package/scripts/lib/quality-gates.js +163 -0
  20. package/scripts/lib/signal-detectors.js +44 -1
  21. package/scripts/lib/skill-catalog.js +557 -0
  22. package/scripts/lib/skill-recommender.js +311 -0
  23. package/scripts/lib/status-writer.js +255 -0
  24. package/scripts/lib/story-claiming.js +128 -45
  25. package/scripts/lib/task-sync.js +32 -38
  26. package/scripts/lib/tdd-phase-manager.js +455 -0
  27. package/scripts/lib/team-events.js +34 -3
  28. package/scripts/lib/tmux-audit-monitor.js +611 -0
  29. package/scripts/lib/tmux-group-colors.js +113 -0
  30. package/scripts/lib/tool-registry.yaml +241 -0
  31. package/scripts/lib/tool-shed.js +441 -0
  32. package/scripts/messaging-bridge.js +209 -1
  33. package/scripts/native-team-observer.js +219 -0
  34. package/scripts/obtain-context.js +14 -0
  35. package/scripts/ralph-loop.js +30 -5
  36. package/scripts/smart-detect.js +21 -0
  37. package/scripts/spawn-audit-sessions.js +877 -0
  38. package/scripts/team-manager.js +56 -16
  39. package/scripts/tmux-close-windows.sh +180 -0
  40. package/src/core/agents/a11y-analyzer-aria.md +155 -0
  41. package/src/core/agents/a11y-analyzer-forms.md +162 -0
  42. package/src/core/agents/a11y-analyzer-keyboard.md +175 -0
  43. package/src/core/agents/a11y-analyzer-semantic.md +153 -0
  44. package/src/core/agents/a11y-analyzer-visual.md +158 -0
  45. package/src/core/agents/a11y-consensus.md +248 -0
  46. package/src/core/agents/ads-audit-budget.md +181 -0
  47. package/src/core/agents/ads-audit-compliance.md +169 -0
  48. package/src/core/agents/ads-audit-creative.md +164 -0
  49. package/src/core/agents/ads-audit-google.md +226 -0
  50. package/src/core/agents/ads-audit-meta.md +183 -0
  51. package/src/core/agents/ads-audit-tracking.md +197 -0
  52. package/src/core/agents/ads-consensus.md +396 -0
  53. package/src/core/agents/ads-generate.md +145 -0
  54. package/src/core/agents/ads-performance-tracker.md +197 -0
  55. package/src/core/agents/api-quality-analyzer-conventions.md +148 -0
  56. package/src/core/agents/api-quality-analyzer-docs.md +176 -0
  57. package/src/core/agents/api-quality-analyzer-errors.md +183 -0
  58. package/src/core/agents/api-quality-analyzer-pagination.md +171 -0
  59. package/src/core/agents/api-quality-analyzer-versioning.md +143 -0
  60. package/src/core/agents/api-quality-consensus.md +214 -0
  61. package/src/core/agents/arch-analyzer-circular.md +148 -0
  62. package/src/core/agents/arch-analyzer-complexity.md +171 -0
  63. package/src/core/agents/arch-analyzer-coupling.md +146 -0
  64. package/src/core/agents/arch-analyzer-layering.md +151 -0
  65. package/src/core/agents/arch-analyzer-patterns.md +162 -0
  66. package/src/core/agents/arch-consensus.md +227 -0
  67. package/src/core/agents/brainstorm-analyzer-features.md +169 -0
  68. package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
  69. package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
  70. package/src/core/agents/brainstorm-analyzer-market.md +147 -0
  71. package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
  72. package/src/core/agents/brainstorm-consensus.md +237 -0
  73. package/src/core/agents/completeness-consensus.md +5 -5
  74. package/src/core/agents/perf-consensus.md +2 -2
  75. package/src/core/agents/security-consensus.md +2 -2
  76. package/src/core/agents/seo-analyzer-content.md +167 -0
  77. package/src/core/agents/seo-analyzer-images.md +187 -0
  78. package/src/core/agents/seo-analyzer-performance.md +206 -0
  79. package/src/core/agents/seo-analyzer-schema.md +176 -0
  80. package/src/core/agents/seo-analyzer-sitemap.md +172 -0
  81. package/src/core/agents/seo-analyzer-technical.md +144 -0
  82. package/src/core/agents/seo-consensus.md +289 -0
  83. package/src/core/agents/test-consensus.md +2 -2
  84. package/src/core/commands/adr.md +1 -0
  85. package/src/core/commands/ads/audit.md +375 -0
  86. package/src/core/commands/ads/budget.md +97 -0
  87. package/src/core/commands/ads/competitor.md +112 -0
  88. package/src/core/commands/ads/creative.md +85 -0
  89. package/src/core/commands/ads/generate.md +238 -0
  90. package/src/core/commands/ads/google.md +112 -0
  91. package/src/core/commands/ads/health.md +327 -0
  92. package/src/core/commands/ads/landing.md +119 -0
  93. package/src/core/commands/ads/linkedin.md +112 -0
  94. package/src/core/commands/ads/meta.md +91 -0
  95. package/src/core/commands/ads/microsoft.md +115 -0
  96. package/src/core/commands/ads/plan.md +321 -0
  97. package/src/core/commands/ads/test-plan.md +317 -0
  98. package/src/core/commands/ads/tiktok.md +129 -0
  99. package/src/core/commands/ads/track.md +288 -0
  100. package/src/core/commands/ads/youtube.md +124 -0
  101. package/src/core/commands/ads.md +140 -0
  102. package/src/core/commands/assign.md +1 -0
  103. package/src/core/commands/audit.md +43 -6
  104. package/src/core/commands/babysit.md +315 -1266
  105. package/src/core/commands/baseline.md +1 -0
  106. package/src/core/commands/blockers.md +1 -0
  107. package/src/core/commands/board.md +1 -0
  108. package/src/core/commands/changelog.md +1 -0
  109. package/src/core/commands/choose.md +1 -0
  110. package/src/core/commands/ci.md +1 -0
  111. package/src/core/commands/code/accessibility.md +347 -0
  112. package/src/core/commands/code/api.md +297 -0
  113. package/src/core/commands/code/architecture.md +297 -0
  114. package/src/core/commands/{audit → code}/completeness.md +72 -25
  115. package/src/core/commands/{audit → code}/legal.md +63 -16
  116. package/src/core/commands/{audit → code}/logic.md +64 -16
  117. package/src/core/commands/{audit → code}/performance.md +67 -20
  118. package/src/core/commands/{audit → code}/security.md +69 -19
  119. package/src/core/commands/{audit → code}/test.md +67 -20
  120. package/src/core/commands/configure.md +1 -0
  121. package/src/core/commands/council.md +1 -0
  122. package/src/core/commands/deploy.md +1 -0
  123. package/src/core/commands/diagnose.md +1 -0
  124. package/src/core/commands/docs.md +1 -0
  125. package/src/core/commands/epic/edit.md +213 -0
  126. package/src/core/commands/epic.md +1 -0
  127. package/src/core/commands/export.md +238 -0
  128. package/src/core/commands/help.md +16 -1
  129. package/src/core/commands/{discovery → ideate}/brief.md +12 -12
  130. package/src/core/commands/{discovery/new.md → ideate/discover.md} +20 -16
  131. package/src/core/commands/ideate/features.md +496 -0
  132. package/src/core/commands/ideate/new.md +158 -124
  133. package/src/core/commands/impact.md +1 -0
  134. package/src/core/commands/learn/explain.md +118 -0
  135. package/src/core/commands/learn/glossary.md +135 -0
  136. package/src/core/commands/learn/patterns.md +138 -0
  137. package/src/core/commands/learn/tour.md +126 -0
  138. package/src/core/commands/migrate/codemods.md +151 -0
  139. package/src/core/commands/migrate/plan.md +131 -0
  140. package/src/core/commands/migrate/scan.md +114 -0
  141. package/src/core/commands/migrate/validate.md +119 -0
  142. package/src/core/commands/multi-expert.md +1 -0
  143. package/src/core/commands/pr.md +1 -0
  144. package/src/core/commands/review.md +1 -0
  145. package/src/core/commands/seo/audit.md +373 -0
  146. package/src/core/commands/seo/competitor.md +174 -0
  147. package/src/core/commands/seo/content.md +107 -0
  148. package/src/core/commands/seo/geo.md +229 -0
  149. package/src/core/commands/seo/hreflang.md +140 -0
  150. package/src/core/commands/seo/images.md +96 -0
  151. package/src/core/commands/seo/page.md +198 -0
  152. package/src/core/commands/seo/plan.md +163 -0
  153. package/src/core/commands/seo/programmatic.md +131 -0
  154. package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
  155. package/src/core/commands/seo/references/eeat-framework.md +110 -0
  156. package/src/core/commands/seo/references/quality-gates.md +91 -0
  157. package/src/core/commands/seo/references/schema-types.md +102 -0
  158. package/src/core/commands/seo/schema.md +183 -0
  159. package/src/core/commands/seo/sitemap.md +97 -0
  160. package/src/core/commands/seo/technical.md +100 -0
  161. package/src/core/commands/seo.md +107 -0
  162. package/src/core/commands/skill/list.md +68 -212
  163. package/src/core/commands/skill/recommend.md +216 -0
  164. package/src/core/commands/sprint.md +1 -0
  165. package/src/core/commands/status/undo.md +191 -0
  166. package/src/core/commands/status.md +1 -0
  167. package/src/core/commands/story/edit.md +204 -0
  168. package/src/core/commands/story/view.md +29 -7
  169. package/src/core/commands/story-validate.md +1 -0
  170. package/src/core/commands/story.md +1 -0
  171. package/src/core/commands/tdd-next.md +238 -0
  172. package/src/core/commands/tdd.md +211 -0
  173. package/src/core/commands/team/start.md +10 -6
  174. package/src/core/commands/tests.md +1 -0
  175. package/src/core/commands/verify.md +27 -1
  176. package/src/core/commands/workflow.md +2 -0
  177. package/src/core/experts/_core-expertise.yaml +105 -0
  178. package/src/core/experts/analytics/expertise.yaml +5 -99
  179. package/src/core/experts/codebase-query/expertise.yaml +3 -72
  180. package/src/core/experts/compliance/expertise.yaml +6 -72
  181. package/src/core/experts/database/expertise.yaml +9 -52
  182. package/src/core/experts/documentation/expertise.yaml +7 -140
  183. package/src/core/experts/integrations/expertise.yaml +7 -127
  184. package/src/core/experts/mentor/expertise.yaml +8 -35
  185. package/src/core/experts/monitoring/expertise.yaml +7 -49
  186. package/src/core/experts/performance/expertise.yaml +1 -26
  187. package/src/core/experts/security/expertise.yaml +9 -34
  188. package/src/core/experts/ui/expertise.yaml +6 -36
  189. package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
  190. package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
  191. package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
  192. package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
  193. package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
  194. package/src/core/teams/backend.json +41 -0
  195. package/src/core/teams/frontend.json +41 -0
  196. package/src/core/teams/qa.json +41 -0
  197. package/src/core/teams/solo.json +35 -0
  198. package/src/core/templates/agileflow-metadata.json +20 -1
  199. package/tools/cli/commands/setup.js +85 -3
  200. package/tools/cli/commands/update.js +42 -0
  201. package/tools/cli/installers/ide/_base-ide.js +42 -5
  202. package/tools/cli/installers/ide/claude-code.js +71 -3
  203. package/tools/cli/lib/content-injector.js +160 -12
  204. package/tools/cli/lib/docs-setup.js +1 -1
  205. package/src/core/commands/skill/create.md +0 -698
  206. package/src/core/commands/skill/delete.md +0 -316
  207. package/src/core/commands/skill/edit.md +0 -359
  208. package/src/core/commands/skill/test.md +0 -394
  209. package/src/core/commands/skill/upgrade.md +0 -552
  210. package/src/core/templates/skill-template.md +0 -117
@@ -0,0 +1,877 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * spawn-audit-sessions.js - Spawn ULTRADEEP audit analyzer sessions in tmux
5
+ *
6
+ * Spawns each analyzer as a separate Claude Code session in tmux,
7
+ * with sentinel files for coordination and tab grouping for visual organization.
8
+ *
9
+ * Key differences from spawn-parallel.js:
10
+ * - No worktrees: audits are read-only, all sessions share the same repo
11
+ * - Piped prompts: each session gets analyzer-specific prompt via echo | claude
12
+ * - Sentinel files: each writes findings to docs/09-agents/ultradeep/{trace_id}/
13
+ * - Tab grouping: applies colors from tmux-group-colors.js
14
+ *
15
+ * Usage:
16
+ * node scripts/spawn-audit-sessions.js --audit=security --target=src/ --focus=all --trace-id=abc123
17
+ * node scripts/spawn-audit-sessions.js --audit=logic --target=. --depth=ultradeep
18
+ *
19
+ * Options:
20
+ * --audit=TYPE Audit type: logic|security|performance|test|completeness|legal
21
+ * --target=PATH Target file or directory to analyze
22
+ * --focus=AREAS Comma-separated focus areas, or 'all'
23
+ * --trace-id=ID Unique trace ID (auto-generated if not provided)
24
+ * --timeout=MINUTES Completion timeout (default: 30)
25
+ * --dry-run Show what would be spawned without executing
26
+ */
27
+
28
+ const { execFileSync } = require('child_process');
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+ const crypto = require('crypto');
32
+
33
+ const { getAuditType, getAnalyzersForAudit } = require('./lib/audit-registry');
34
+ const { getColorForAudit } = require('./lib/tmux-group-colors');
35
+ const { resolveModel, estimateCost } = require('./lib/model-profiles');
36
+
37
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
38
+
39
+ /**
40
+ * Read ultradeep config from agileflow-metadata.json.
41
+ * @returns {{ stagger_seconds: number, max_concurrent: number }}
42
+ */
43
+ function getUltradeepConfig() {
44
+ const defaults = { stagger_seconds: 3, max_concurrent: 0 };
45
+ try {
46
+ const metaPath = path.join(process.cwd(), 'docs', '00-meta', 'agileflow-metadata.json');
47
+ if (fs.existsSync(metaPath)) {
48
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
49
+ const ud = meta.ultradeep || {};
50
+ if (typeof ud.stagger_seconds === 'number') defaults.stagger_seconds = ud.stagger_seconds;
51
+ if (typeof ud.max_concurrent === 'number') defaults.max_concurrent = ud.max_concurrent;
52
+ }
53
+ } catch (_) {
54
+ /* use defaults */
55
+ }
56
+ return defaults;
57
+ }
58
+
59
+ /**
60
+ * Parse CLI arguments.
61
+ * @returns {object} Parsed options
62
+ */
63
+ function parseArgs() {
64
+ const args = process.argv.slice(2);
65
+ const options = {
66
+ audit: null,
67
+ target: '.',
68
+ focus: ['all'],
69
+ model: null,
70
+ traceId: null,
71
+ timeout: 30,
72
+ dryRun: false,
73
+ json: false,
74
+ stagger: null,
75
+ concurrency: null,
76
+ depth: null,
77
+ partitions: null,
78
+ };
79
+
80
+ for (const arg of args) {
81
+ if (arg.startsWith('--audit=')) options.audit = arg.split('=')[1];
82
+ else if (arg.startsWith('--target=')) options.target = arg.split('=')[1];
83
+ else if (arg.startsWith('--focus=')) options.focus = arg.split('=')[1].split(',');
84
+ else if (arg.startsWith('--model=')) options.model = arg.split('=')[1];
85
+ else if (arg.startsWith('--trace-id=')) options.traceId = arg.split('=')[1];
86
+ else if (arg.startsWith('--timeout=')) {
87
+ const parsed = parseInt(arg.split('=')[1], 10);
88
+ options.timeout = isNaN(parsed) ? 30 : parsed;
89
+ } else if (arg.startsWith('--stagger=')) {
90
+ const parsed = parseFloat(arg.split('=')[1]);
91
+ options.stagger = isNaN(parsed) ? null : parsed;
92
+ } else if (arg.startsWith('--concurrency=')) {
93
+ const parsed = parseInt(arg.split('=')[1], 10);
94
+ options.concurrency = isNaN(parsed) ? null : parsed;
95
+ } else if (arg.startsWith('--depth=')) options.depth = arg.split('=')[1];
96
+ else if (arg.startsWith('--partitions='))
97
+ options.partitions = arg.split('=')[1].split(',').filter(Boolean);
98
+ else if (arg === '--dry-run') options.dryRun = true;
99
+ else if (arg === '--json') options.json = true;
100
+ }
101
+
102
+ if (!options.traceId) {
103
+ options.traceId = crypto.randomBytes(6).toString('hex');
104
+ }
105
+
106
+ return options;
107
+ }
108
+
109
+ /**
110
+ * Check if tmux is available and we're in a tmux session.
111
+ * @returns {{ available: boolean, inSession: boolean }}
112
+ */
113
+ function checkTmux() {
114
+ try {
115
+ execFileSync('which', ['tmux'], { stdio: 'pipe' });
116
+ const inSession = !!process.env.TMUX;
117
+ return { available: true, inSession };
118
+ } catch (_) {
119
+ return { available: false, inSession: false };
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Create the sentinel directory for a trace.
125
+ * @param {string} rootDir - Project root
126
+ * @param {string} traceId - Unique trace ID
127
+ * @returns {string} Path to sentinel directory
128
+ */
129
+ function createSentinelDir(rootDir, traceId) {
130
+ const sentinelDir = path.join(rootDir, 'docs', '09-agents', 'ultradeep', traceId);
131
+ fs.mkdirSync(sentinelDir, { recursive: true });
132
+ return sentinelDir;
133
+ }
134
+
135
+ /**
136
+ * Write initial status file for the trace.
137
+ * @param {string} sentinelDir - Sentinel directory path
138
+ * @param {string} auditType - Audit type key
139
+ * @param {Array} analyzers - Array of analyzer configs
140
+ * @param {number} [staggerMs] - Stagger delay in milliseconds
141
+ * @param {number} [maxConcurrent] - Max concurrent sessions (0 = unlimited)
142
+ */
143
+ function writeStatusFile(sentinelDir, auditType, analyzers, staggerMs, maxConcurrent, extra) {
144
+ const status = {
145
+ started_at: new Date().toISOString(),
146
+ audit_type: auditType,
147
+ analyzers: analyzers.map(a => a.key),
148
+ completed: [],
149
+ failed: [],
150
+ stagger_ms: staggerMs != null ? staggerMs : null,
151
+ max_concurrent: maxConcurrent || null,
152
+ };
153
+ // Store extra fields for retry support
154
+ if (extra) {
155
+ if (extra.target != null) status.target = extra.target;
156
+ if (extra.model != null) status.model = extra.model;
157
+ if (extra.timeout_minutes != null) status.timeout_minutes = extra.timeout_minutes;
158
+ }
159
+ fs.writeFileSync(path.join(sentinelDir, '_status.json'), JSON.stringify(status, null, 2) + '\n');
160
+ }
161
+
162
+ /**
163
+ * Build the prompt for an individual analyzer session.
164
+ * @param {object} analyzer - Analyzer config { key, subagent_type, label }
165
+ * @param {string} target - Target path to analyze
166
+ * @param {string} traceId - Trace ID
167
+ * @param {string} sentinelDir - Sentinel directory for output
168
+ * @param {string} auditType - Audit type key
169
+ * @param {string} [model] - Resolved model name for sub-agent
170
+ * @returns {string} Prompt text
171
+ */
172
+ function buildAnalyzerPrompt(analyzer, target, traceId, sentinelDir, auditType, model) {
173
+ const findingsFile = path.join(sentinelDir, `${analyzer.key}.findings.json`);
174
+
175
+ // Sanitize fields that get interpolated into double-quoted prompt sections
176
+ const safeLabel = String(analyzer.label || '').replace(/["\\]/g, '');
177
+ const safeTarget = String(target || '').replace(/["\\]/g, '');
178
+ const safeSubagentType = String(analyzer.subagent_type || '').replace(/["\\]/g, '');
179
+
180
+ return `You are an ULTRADEEP audit session coordinator.
181
+
182
+ ## Task
183
+
184
+ 1. Use the Agent tool to spawn a sub-agent for analysis
185
+ 2. After the sub-agent completes, parse its output and write findings as JSON to the sentinel file
186
+
187
+ ## Agent Configuration
188
+
189
+ Use the Agent tool with these parameters:
190
+ - subagent_type: "${safeSubagentType}"
191
+ - description: "${safeLabel} analysis of ${safeTarget}"${model ? `\n- model: "${model}"` : ''}
192
+ - prompt: |
193
+ Analyze the target path ${safeTarget} thoroughly for ${safeLabel}-related issues.
194
+ Search all relevant files recursively. Be thorough.
195
+ Return a JSON object with this structure:
196
+ {"findings": [{"id": "${analyzer.key}-NNN", "severity": "P0|P1|P2|P3", "title": "Short description", "file": "path/to/file.js", "line": 42, "description": "Detailed explanation", "evidence": "Code snippet or reasoning", "recommendation": "How to fix"}], "summary": {"files_scanned": 0, "total_findings": 0, "by_severity": {"P0": 0, "P1": 0, "P2": 0, "P3": 0}}}
197
+ TRACE_ID: ${traceId}
198
+ ANALYZER: ${analyzer.key}
199
+
200
+ ## Output
201
+
202
+ After the Agent tool returns its analysis, write a JSON file to: ${findingsFile}
203
+
204
+ Use this structure:
205
+ {
206
+ "analyzer": "${analyzer.key}",
207
+ "audit_type": "${auditType}",
208
+ "trace_id": "${traceId}",
209
+ "target": "${safeTarget}",
210
+ "completed_at": "<ISO timestamp>",
211
+ "findings": [
212
+ {
213
+ "id": "${analyzer.key}-001",
214
+ "severity": "P0|P1|P2|P3",
215
+ "title": "Short description",
216
+ "file": "path/to/file.js",
217
+ "line": 42,
218
+ "description": "Detailed explanation",
219
+ "evidence": "Code snippet or reasoning",
220
+ "recommendation": "How to fix"
221
+ }
222
+ ],
223
+ "summary": {
224
+ "files_scanned": 0,
225
+ "total_findings": 0,
226
+ "by_severity": { "P0": 0, "P1": 0, "P2": 0, "P3": 0 }
227
+ }
228
+ }
229
+
230
+ IMPORTANT: You MUST write the findings JSON file when complete. This is how the orchestrator knows you're done.
231
+ If the Agent tool is unavailable or returns an error, perform the analysis directly using Read, Glob, and Grep tools.
232
+ Start by spawning the Agent now.`;
233
+ }
234
+
235
+ /**
236
+ * Create a slug from a partition path for use in window names and filenames.
237
+ * Only allows alphanumeric, hyphens, and underscores — safe for tmux window
238
+ * names, shell interpolation, and filesystem paths.
239
+ * @param {string} partition - Partition path (e.g. 'src/auth')
240
+ * @returns {string} Slug (e.g. 'src-auth')
241
+ */
242
+ function partitionSlug(partition) {
243
+ return (
244
+ partition
245
+ .replace(/^\.?\/?/, '')
246
+ .replace(/\/+$/g, '')
247
+ .replace(/[/\\]/g, '-')
248
+ .replace(/[^a-zA-Z0-9_-]/g, '_') || 'root'
249
+ );
250
+ }
251
+
252
+ /**
253
+ * Build the prompt for an extreme-mode partition coordinator session.
254
+ * This coordinator runs ALL analyzers on a single partition using the Agent tool.
255
+ * @param {string} partition - Partition path to analyze
256
+ * @param {Array<{ key: string, subagent_type: string, label: string }>} analyzers - All analyzers to run
257
+ * @param {string} traceId - Trace ID
258
+ * @param {string} sentinelDir - Sentinel directory for output
259
+ * @param {string} auditType - Audit type key
260
+ * @param {string} [model] - Resolved model name for sub-agents
261
+ * @returns {string} Prompt text
262
+ */
263
+ function buildExtremePrompt(partition, analyzers, traceId, sentinelDir, auditType, model) {
264
+ const slug = partitionSlug(partition);
265
+ const findingsFile = path.join(sentinelDir, `${slug}.findings.json`);
266
+
267
+ const safePartition = String(partition || '').replace(/["\\]/g, '');
268
+
269
+ const analyzerList = analyzers
270
+ .map((a, i) => `${i + 1}. subagent_type: "${a.subagent_type}" — ${a.label} (key: ${a.key})`)
271
+ .join('\n');
272
+
273
+ const modelLine = model ? `\n- model: "${model}"` : '';
274
+
275
+ return `You are an EXTREME audit session coordinator for partition: ${safePartition}
276
+
277
+ ## Task
278
+
279
+ Run ALL of the following analyzers on your partition using the Agent tool.
280
+ Deploy them in parallel (multiple Agent calls in one message) where possible.
281
+
282
+ ## Analyzers to Run
283
+ ${analyzerList}
284
+
285
+ ## Agent Configuration for Each Analyzer
286
+
287
+ Use the Agent tool with these parameters for each analyzer:
288
+ - subagent_type: (from the list above)
289
+ - description: "[Analyzer label] analysis of ${safePartition}"${modelLine}
290
+ - prompt: |
291
+ Analyze the target path ${safePartition} thoroughly for issues relevant to your analyzer domain.
292
+ Search all relevant files recursively. Be thorough.
293
+ Use the analyzer key from your subagent_type as the ID prefix (e.g., for "security-analyzer-injection" use "injection").
294
+ Return a JSON object with this structure:
295
+ {"findings": [{"id": "<analyzer-key>-NNN", "severity": "P0|P1|P2|P3", "title": "Short description", "file": "path/to/file.js", "line": 42, "description": "Detailed explanation", "evidence": "Code snippet or reasoning", "recommendation": "How to fix"}], "summary": {"files_scanned": 0, "total_findings": 0, "by_severity": {"P0": 0, "P1": 0, "P2": 0, "P3": 0}}}
296
+ TRACE_ID: ${traceId}
297
+
298
+ ## After All Analyzers Complete
299
+
300
+ Combine ALL findings from ALL analyzers into a single JSON file: ${findingsFile}
301
+
302
+ Use this structure:
303
+ {
304
+ "partition": "${safePartition}",
305
+ "audit_type": "${auditType}",
306
+ "trace_id": "${traceId}",
307
+ "completed_at": "<ISO timestamp>",
308
+ "analyzer_count": ${analyzers.length},
309
+ "analyzers_run": [${analyzers.map(a => `"${a.key}"`).join(', ')}],
310
+ "findings": [
311
+ {
312
+ "id": "<analyzer_key>-001",
313
+ "analyzer": "<analyzer_key>",
314
+ "severity": "P0|P1|P2|P3",
315
+ "title": "Short description",
316
+ "file": "path/to/file.js",
317
+ "line": 42,
318
+ "description": "Detailed explanation",
319
+ "evidence": "Code snippet or reasoning",
320
+ "recommendation": "How to fix"
321
+ }
322
+ ],
323
+ "summary": {
324
+ "files_scanned": 0,
325
+ "total_findings": 0,
326
+ "by_severity": { "P0": 0, "P1": 0, "P2": 0, "P3": 0 }
327
+ }
328
+ }
329
+
330
+ IMPORTANT: You MUST write the findings JSON file when complete. This is how the orchestrator knows you're done.
331
+ Start by spawning ALL ${analyzers.length} analyzers now, in parallel.`;
332
+ }
333
+
334
+ /**
335
+ * Spawn a single analyzer session in tmux.
336
+ * @param {object} params - Session parameters
337
+ * @returns {string|null} Window name if successful, null on failure
338
+ */
339
+ function spawnOneSession({
340
+ analyzer,
341
+ index,
342
+ sessionName,
343
+ rootDir,
344
+ options,
345
+ sentinelDir,
346
+ auditType,
347
+ groupColor,
348
+ }) {
349
+ const windowName = `${auditType.prefix}:${analyzer.key}`;
350
+ const model = resolveModel(options.model, 'haiku');
351
+ const prompt = buildAnalyzerPrompt(
352
+ analyzer,
353
+ options.target,
354
+ options.traceId,
355
+ sentinelDir,
356
+ options.audit,
357
+ model
358
+ );
359
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
360
+
361
+ try {
362
+ if (index === 0) {
363
+ execFileSync(
364
+ 'tmux',
365
+ ['new-session', '-d', '-s', sessionName, '-n', windowName, '-c', rootDir],
366
+ { stdio: 'pipe' }
367
+ );
368
+ } else {
369
+ execFileSync('tmux', ['new-window', '-t', sessionName, '-n', windowName, '-c', rootDir], {
370
+ stdio: 'pipe',
371
+ });
372
+ }
373
+
374
+ execFileSync(
375
+ 'tmux',
376
+ ['set-option', '-w', '-t', `${sessionName}:${windowName}`, '@group_color', groupColor],
377
+ { stdio: 'pipe' }
378
+ );
379
+
380
+ const claudeCmd = `echo '${escapedPrompt}' | claude --model ${model} --allowedTools 'Read Glob Grep Write Agent' 2>&1; echo "AUDIT_COMPLETE: ${analyzer.key}"`;
381
+ execFileSync('tmux', ['send-keys', '-t', `${sessionName}:${windowName}`, claudeCmd, 'Enter'], {
382
+ stdio: 'pipe',
383
+ });
384
+
385
+ return windowName;
386
+ } catch (err) {
387
+ console.error(`Failed to spawn ${windowName}: ${err.message}`);
388
+ return null;
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Poll sentinel directory for wave completion.
394
+ * @param {string} sentinelDir - Sentinel directory path
395
+ * @param {string[]} keys - Analyzer keys to wait for
396
+ * @param {number} timeoutMinutes - Timeout in minutes
397
+ * @returns {Promise<boolean>} true if all completed, false on timeout
398
+ */
399
+ async function pollWaveCompletion(sentinelDir, keys, timeoutMinutes) {
400
+ const timeoutMs = (timeoutMinutes || 30) * 60 * 1000;
401
+ const startTime = Date.now();
402
+ while (Date.now() - startTime < timeoutMs) {
403
+ const allDone = keys.every(key =>
404
+ fs.existsSync(path.join(sentinelDir, `${key}.findings.json`))
405
+ );
406
+ if (allDone) return true;
407
+ await sleep(3000);
408
+ }
409
+ return false;
410
+ }
411
+
412
+ /**
413
+ * Spawn audit analyzer sessions in tmux with staggered launching.
414
+ * @param {object} options - Parsed CLI options
415
+ * @returns {Promise<{ ok: boolean, traceId: string, sentinelDir: string, sessions: string[] }>}
416
+ */
417
+ async function spawnAuditInTmux(options) {
418
+ const rootDir = process.cwd();
419
+ const auditType = getAuditType(options.audit);
420
+
421
+ if (!auditType) {
422
+ console.error(`Unknown audit type: ${options.audit}`);
423
+ console.error(`Valid types: logic, security, performance, test, completeness, legal`);
424
+ process.exit(1);
425
+ }
426
+
427
+ const isExtreme = options.depth === 'extreme';
428
+ const depthForRegistry = isExtreme ? 'extreme' : 'ultradeep';
429
+ const result = getAnalyzersForAudit(options.audit, depthForRegistry, options.focus);
430
+ if (!result || result.analyzers.length === 0) {
431
+ console.error(`No analyzers found for ${options.audit} with focus: ${options.focus.join(',')}`);
432
+ process.exit(1);
433
+ }
434
+
435
+ const sentinelDir = createSentinelDir(rootDir, options.traceId);
436
+
437
+ // Use stderr for human output when --json mode is active
438
+ const log = options.json ? console.error : console.log;
439
+
440
+ // --- EXTREME MODE: partition-based multi-agent ---
441
+ if (isExtreme) {
442
+ if (!options.partitions || options.partitions.length === 0) {
443
+ console.error('EXTREME mode requires --partitions=dir1,dir2,...');
444
+ process.exit(1);
445
+ }
446
+
447
+ const partitions = options.partitions;
448
+ const partitionSlugs = partitions.map(p => partitionSlug(p));
449
+
450
+ // Write status file with partition info
451
+ const statusData = {
452
+ started_at: new Date().toISOString(),
453
+ audit_type: options.audit,
454
+ mode: 'extreme',
455
+ partitions: partitions,
456
+ analyzers: partitionSlugs,
457
+ analyzers_per_partition: result.analyzers.map(a => a.key),
458
+ completed: [],
459
+ failed: [],
460
+ target: options.target,
461
+ model: options.model,
462
+ timeout_minutes: options.timeout,
463
+ };
464
+ fs.writeFileSync(
465
+ path.join(sentinelDir, '_status.json'),
466
+ JSON.stringify(statusData, null, 2) + '\n'
467
+ );
468
+
469
+ const groupColor = getColorForAudit(options.audit);
470
+ const totalSessions = partitions.length * result.analyzers.length;
471
+
472
+ if (options.dryRun) {
473
+ log(`\nDry run - EXTREME mode would spawn ${partitions.length} partition coordinators:`);
474
+ log(` Partitions: ${partitions.join(', ')}`);
475
+ log(` Analyzers per partition: ${result.analyzers.length}`);
476
+ log(` Total agent sessions: ${totalSessions}`);
477
+ const model = resolveModel(options.model, 'haiku');
478
+ for (const p of partitions) {
479
+ const slug = partitionSlug(p);
480
+ log(
481
+ ` ${auditType.prefix}:${slug} (${model}) → ALL ${result.analyzers.length} analyzers on ${p}`
482
+ );
483
+ }
484
+ log(`\nSentinel dir: ${sentinelDir}`);
485
+ log(`Group color: ${groupColor}`);
486
+ return {
487
+ ok: true,
488
+ traceId: options.traceId,
489
+ sentinelDir,
490
+ sessions: [],
491
+ dryRun: true,
492
+ mode: 'extreme',
493
+ partitions,
494
+ };
495
+ }
496
+
497
+ const tmux = checkTmux();
498
+ if (!tmux.available) {
499
+ console.error('tmux is not available. EXTREME mode requires tmux.');
500
+ console.error('Falling back to DEPTH=deep mode.');
501
+ return { ok: false, traceId: options.traceId, sentinelDir, sessions: [], fallback: 'deep' };
502
+ }
503
+
504
+ const sessionName = `audit-${options.audit}-${options.traceId.slice(0, 8)}`;
505
+ const sessions = [];
506
+ const config = getUltradeepConfig();
507
+ const staggerMs =
508
+ ((options.stagger != null ? options.stagger : config.stagger_seconds) || 0) * 1000;
509
+
510
+ for (let i = 0; i < partitions.length; i++) {
511
+ if (i > 0 && staggerMs > 0) await sleep(staggerMs);
512
+
513
+ const partition = partitions[i];
514
+ const slug = partitionSlug(partition);
515
+ const windowName = `${auditType.prefix}:${slug}`;
516
+ const model = resolveModel(options.model, 'haiku');
517
+ const prompt = buildExtremePrompt(
518
+ partition,
519
+ result.analyzers,
520
+ options.traceId,
521
+ sentinelDir,
522
+ options.audit,
523
+ model
524
+ );
525
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
526
+
527
+ try {
528
+ if (i === 0) {
529
+ execFileSync(
530
+ 'tmux',
531
+ ['new-session', '-d', '-s', sessionName, '-n', windowName, '-c', rootDir],
532
+ { stdio: 'pipe' }
533
+ );
534
+ } else {
535
+ execFileSync('tmux', ['new-window', '-t', sessionName, '-n', windowName, '-c', rootDir], {
536
+ stdio: 'pipe',
537
+ });
538
+ }
539
+
540
+ execFileSync(
541
+ 'tmux',
542
+ ['set-option', '-w', '-t', `${sessionName}:${windowName}`, '@group_color', groupColor],
543
+ { stdio: 'pipe' }
544
+ );
545
+
546
+ const claudeCmd = `echo '${escapedPrompt}' | claude --model ${model} --allowedTools 'Read Glob Grep Write Agent' 2>&1; echo "AUDIT_COMPLETE: ${slug}"`;
547
+ execFileSync(
548
+ 'tmux',
549
+ ['send-keys', '-t', `${sessionName}:${windowName}`, claudeCmd, 'Enter'],
550
+ {
551
+ stdio: 'pipe',
552
+ }
553
+ );
554
+
555
+ sessions.push(windowName);
556
+ } catch (err) {
557
+ console.error(`Failed to spawn ${windowName}: ${err.message}`);
558
+ }
559
+ }
560
+
561
+ // Apply status bar theme
562
+ try {
563
+ const tmuxScript = path.join(__dirname, 'claude-tmux.sh');
564
+ execFileSync(tmuxScript, [`--configure-session=${sessionName}`], { stdio: 'pipe' });
565
+ } catch (_) {
566
+ // Non-critical
567
+ }
568
+
569
+ log(`\nSpawned ${sessions.length} partition coordinators in tmux session: ${sessionName}`);
570
+ log(` Partitions: ${partitions.join(', ')}`);
571
+ log(` Analyzers per partition: ${result.analyzers.length}`);
572
+ log(` Total agent sessions: ${totalSessions}`);
573
+ log(`Sentinel dir: ${sentinelDir}`);
574
+ log(`Attach with: tmux attach -t ${sessionName}`);
575
+
576
+ return {
577
+ ok: true,
578
+ traceId: options.traceId,
579
+ sentinelDir,
580
+ sessions,
581
+ sessionName,
582
+ mode: 'extreme',
583
+ partitions,
584
+ };
585
+ }
586
+
587
+ // --- ULTRADEEP MODE (existing behavior) ---
588
+
589
+ // Enforce session limit
590
+ if (result.analyzers.length > 20) {
591
+ console.error(`Too many analyzers (${result.analyzers.length}). Maximum is 20.`);
592
+ process.exit(1);
593
+ }
594
+
595
+ // Resolve stagger and concurrency from CLI flags or config
596
+ const config = getUltradeepConfig();
597
+ const staggerMs =
598
+ ((options.stagger != null ? options.stagger : config.stagger_seconds) || 0) * 1000;
599
+ const maxConcurrent = options.concurrency != null ? options.concurrency : config.max_concurrent;
600
+
601
+ writeStatusFile(sentinelDir, options.audit, result.analyzers, staggerMs, maxConcurrent, {
602
+ target: options.target,
603
+ model: options.model,
604
+ timeout_minutes: options.timeout,
605
+ });
606
+
607
+ const groupColor = getColorForAudit(options.audit);
608
+ const sessions = [];
609
+
610
+ if (options.dryRun) {
611
+ log(`\nDry run - would spawn ${result.analyzers.length} sessions:`);
612
+ log(` Stagger: ${staggerMs / 1000}s between launches`);
613
+ if (maxConcurrent > 0) {
614
+ const waveCount = Math.ceil(result.analyzers.length / maxConcurrent);
615
+ log(` Concurrency: ${maxConcurrent}/wave (${waveCount} waves)`);
616
+ }
617
+ for (const analyzer of result.analyzers) {
618
+ const model = resolveModel(options.model, 'haiku');
619
+ log(` ${auditType.prefix}:${analyzer.key} (${model}) → ${analyzer.label}`);
620
+ }
621
+ log(`\nSentinel dir: ${sentinelDir}`);
622
+ log(`Group color: ${groupColor}`);
623
+ return { ok: true, traceId: options.traceId, sentinelDir, sessions: [], dryRun: true };
624
+ }
625
+
626
+ const tmux = checkTmux();
627
+ if (!tmux.available) {
628
+ console.error('tmux is not available. ULTRADEEP mode requires tmux.');
629
+ console.error('Falling back to DEPTH=deep mode.');
630
+ return { ok: false, traceId: options.traceId, sentinelDir, sessions: [], fallback: 'deep' };
631
+ }
632
+
633
+ // Create dedicated tmux session for this audit
634
+ const sessionName = `audit-${options.audit}-${options.traceId.slice(0, 8)}`;
635
+ let sessionIndex = 0;
636
+
637
+ if (maxConcurrent > 0 && result.analyzers.length > maxConcurrent) {
638
+ // Wave-based spawning
639
+ const waves = [];
640
+ for (let i = 0; i < result.analyzers.length; i += maxConcurrent) {
641
+ waves.push(result.analyzers.slice(i, i + maxConcurrent));
642
+ }
643
+ for (let w = 0; w < waves.length; w++) {
644
+ if (w > 0) {
645
+ const prevKeys = waves[w - 1].map(a => a.key);
646
+ await pollWaveCompletion(sentinelDir, prevKeys, options.timeout);
647
+ }
648
+ for (let i = 0; i < waves[w].length; i++) {
649
+ if (sessionIndex > 0 && staggerMs > 0) await sleep(staggerMs);
650
+ const name = spawnOneSession({
651
+ analyzer: waves[w][i],
652
+ index: sessionIndex,
653
+ sessionName,
654
+ rootDir,
655
+ options,
656
+ sentinelDir,
657
+ auditType,
658
+ groupColor,
659
+ });
660
+ if (name) sessions.push(name);
661
+ sessionIndex++;
662
+ }
663
+ }
664
+ } else {
665
+ // Simple staggered spawning (no wave limit)
666
+ for (let i = 0; i < result.analyzers.length; i++) {
667
+ if (i > 0 && staggerMs > 0) await sleep(staggerMs);
668
+ const name = spawnOneSession({
669
+ analyzer: result.analyzers[i],
670
+ index: i,
671
+ sessionName,
672
+ rootDir,
673
+ options,
674
+ sentinelDir,
675
+ auditType,
676
+ groupColor,
677
+ });
678
+ if (name) sessions.push(name);
679
+ }
680
+ }
681
+
682
+ // Apply the same status bar theme as normal Claude sessions
683
+ try {
684
+ const tmuxScript = path.join(__dirname, 'claude-tmux.sh');
685
+ execFileSync(tmuxScript, [`--configure-session=${sessionName}`], { stdio: 'pipe' });
686
+ } catch (_) {
687
+ // Non-critical styling failure — audit session still works with default theme
688
+ }
689
+
690
+ log(`\nSpawned ${sessions.length} analyzer sessions in tmux session: ${sessionName}`);
691
+ log(`Sentinel dir: ${sentinelDir}`);
692
+ log(`Attach with: tmux attach -t ${sessionName}`);
693
+
694
+ return { ok: true, traceId: options.traceId, sentinelDir, sessions, sessionName };
695
+ }
696
+
697
+ /**
698
+ * Poll sentinel directory for completion.
699
+ * @param {string} sentinelDir - Sentinel directory path
700
+ * @param {string[]} expected - Expected analyzer keys
701
+ * @param {number} timeoutMinutes - Timeout in minutes
702
+ * @returns {Promise<{ complete: boolean, results: object[], missing: string[] }>}
703
+ */
704
+ async function pollForCompletion(sentinelDir, expected, timeoutMinutes) {
705
+ const timeoutMs = timeoutMinutes * 60 * 1000;
706
+ const startTime = Date.now();
707
+ const pollIntervalMs = 5000;
708
+
709
+ while (Date.now() - startTime < timeoutMs) {
710
+ const completed = [];
711
+ const missing = [];
712
+
713
+ for (const key of expected) {
714
+ const findingsFile = path.join(sentinelDir, `${key}.findings.json`);
715
+ if (fs.existsSync(findingsFile)) {
716
+ completed.push(key);
717
+ } else {
718
+ missing.push(key);
719
+ }
720
+ }
721
+
722
+ // Update status file
723
+ try {
724
+ const statusPath = path.join(sentinelDir, '_status.json');
725
+ const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
726
+ status.completed = completed;
727
+ status.last_checked = new Date().toISOString();
728
+ fs.writeFileSync(statusPath, JSON.stringify(status, null, 2) + '\n');
729
+ } catch (_) {
730
+ // Non-critical
731
+ }
732
+
733
+ if (missing.length === 0) {
734
+ return { complete: true, results: collectResults(sentinelDir, expected), missing: [] };
735
+ }
736
+
737
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
738
+ }
739
+
740
+ // Timeout
741
+ const results = collectResults(sentinelDir, expected);
742
+ const completedKeys = results.map(r => r.analyzer);
743
+ const missing = expected.filter(k => !completedKeys.includes(k));
744
+ return { complete: false, results, missing };
745
+ }
746
+
747
+ /**
748
+ * Collect all findings from sentinel directory.
749
+ * @param {string} sentinelDir - Sentinel directory path
750
+ * @param {string[]} expected - Expected keys. In ultradeep mode these are analyzer keys
751
+ * (e.g. 'injection', 'auth'). In extreme mode these are partition slugs
752
+ * (e.g. 'src-auth', 'src-api') — matches the `analyzers` array in `_status.json`.
753
+ * @returns {object[]} Array of parsed findings
754
+ */
755
+ function collectResults(sentinelDir, expected) {
756
+ const results = [];
757
+
758
+ for (const key of expected) {
759
+ const findingsFile = path.join(sentinelDir, `${key}.findings.json`);
760
+ try {
761
+ if (fs.existsSync(findingsFile)) {
762
+ const data = JSON.parse(fs.readFileSync(findingsFile, 'utf8'));
763
+ results.push(data);
764
+ }
765
+ } catch (err) {
766
+ results.push({
767
+ analyzer: key,
768
+ error: `Failed to parse findings: ${err.message}`,
769
+ findings: [],
770
+ });
771
+ }
772
+ }
773
+
774
+ return results;
775
+ }
776
+
777
+ /**
778
+ * Show cost estimation before launching.
779
+ * @param {string} auditType - Audit type key
780
+ * @param {number} analyzerCount - Number of analyzers to spawn
781
+ * @param {string} [model] - Explicit model override
782
+ * @param {object} [opts] - Options
783
+ * @param {boolean} [opts.json] - If true, route output to stderr to keep stdout clean for JSON
784
+ * @param {number} [opts.partitions] - Number of partitions (extreme mode)
785
+ */
786
+ function showCostEstimate(auditType, analyzerCount, model, opts) {
787
+ const resolved = resolveModel(model, 'haiku');
788
+ const partCount =
789
+ opts && typeof opts.partitions === 'number' && opts.partitions > 1 ? opts.partitions : 1;
790
+ const estimate = estimateCost(resolved, analyzerCount, partCount);
791
+ const log = opts && opts.json ? console.error : console.log;
792
+
793
+ if (partCount > 1) {
794
+ log(`\nCost estimate for EXTREME ${auditType} audit:`);
795
+ log(` Model: ${estimate.model}`);
796
+ log(` Partitions: ${partCount}`);
797
+ log(` Analyzers per partition: ${analyzerCount}`);
798
+ log(` Total agent sessions: ${estimate.totalSessions}`);
799
+ log(` Per-agent estimate: ${estimate.perAnalyzerCost}`);
800
+ log(` Total estimate: ${estimate.totalEstimate}`);
801
+ } else {
802
+ log(`\nCost estimate for ULTRADEEP ${auditType} audit:`);
803
+ log(` Model: ${estimate.model}`);
804
+ log(` Analyzers: ${analyzerCount}`);
805
+ log(` Cost multiplier vs haiku: ${estimate.multiplier}x`);
806
+ log(` Per-analyzer estimate: ${estimate.perAnalyzerCost}`);
807
+ log(` Total estimate: ${estimate.totalEstimate}`);
808
+ log(` Each analyzer runs as a full Claude Code session`);
809
+ }
810
+ }
811
+
812
+ // Main
813
+ if (require.main === module) {
814
+ (async () => {
815
+ const options = parseArgs();
816
+
817
+ if (!options.audit) {
818
+ console.error(
819
+ 'Usage: node spawn-audit-sessions.js --audit=TYPE --target=PATH [--focus=AREAS] [--model=MODEL] [--trace-id=ID] [--stagger=SECONDS] [--concurrency=N]'
820
+ );
821
+ console.error('Types: logic, security, performance, test, completeness, legal');
822
+ process.exit(1);
823
+ }
824
+
825
+ const isExtreme = options.depth === 'extreme';
826
+ const depthForCost = isExtreme ? 'extreme' : 'ultradeep';
827
+ const result = getAnalyzersForAudit(options.audit, depthForCost, options.focus);
828
+ if (result) {
829
+ showCostEstimate(options.audit, result.analyzers.length, options.model, {
830
+ json: options.json,
831
+ partitions: isExtreme && options.partitions ? options.partitions.length : 1,
832
+ });
833
+ }
834
+
835
+ const spawnResult = await spawnAuditInTmux(options);
836
+
837
+ if (options.json) {
838
+ const jsonOut = {
839
+ ok: spawnResult.ok,
840
+ traceId: spawnResult.traceId,
841
+ sentinelDir: spawnResult.sentinelDir,
842
+ sessionName: spawnResult.sessionName || null,
843
+ sessions: spawnResult.sessions,
844
+ dryRun: spawnResult.dryRun || false,
845
+ fallback: spawnResult.fallback || null,
846
+ mode: spawnResult.mode || 'ultradeep',
847
+ partitions: spawnResult.partitions || null,
848
+ };
849
+ console.log(JSON.stringify(jsonOut));
850
+ }
851
+
852
+ if (!spawnResult.ok && spawnResult.fallback) {
853
+ process.exit(2); // Signal fallback to caller
854
+ }
855
+ })().catch(err => {
856
+ console.error(err.message);
857
+ process.exit(1);
858
+ });
859
+ }
860
+
861
+ module.exports = {
862
+ parseArgs,
863
+ checkTmux,
864
+ createSentinelDir,
865
+ writeStatusFile,
866
+ buildAnalyzerPrompt,
867
+ buildExtremePrompt,
868
+ partitionSlug,
869
+ spawnOneSession,
870
+ spawnAuditInTmux,
871
+ pollForCompletion,
872
+ pollWaveCompletion,
873
+ collectResults,
874
+ showCostEstimate,
875
+ getUltradeepConfig,
876
+ sleep,
877
+ };