agileflow 3.3.0 → 3.4.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 (121) hide show
  1. package/CHANGELOG.md +5 -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/claude-tmux.sh +113 -22
  7. package/scripts/claude-watchdog.sh +225 -0
  8. package/scripts/generators/agent-registry.js +14 -1
  9. package/scripts/generators/inject-babysit.js +22 -9
  10. package/scripts/generators/inject-help.js +19 -9
  11. package/scripts/lib/audit-cleanup.js +250 -0
  12. package/scripts/lib/audit-registry.js +248 -0
  13. package/scripts/lib/feature-catalog.js +3 -3
  14. package/scripts/lib/gate-enforcer.js +295 -0
  15. package/scripts/lib/model-profiles.js +98 -0
  16. package/scripts/lib/signal-detectors.js +1 -1
  17. package/scripts/lib/skill-catalog.js +557 -0
  18. package/scripts/lib/skill-recommender.js +311 -0
  19. package/scripts/lib/tdd-phase-manager.js +455 -0
  20. package/scripts/lib/team-events.js +34 -3
  21. package/scripts/lib/tmux-group-colors.js +113 -0
  22. package/scripts/messaging-bridge.js +209 -1
  23. package/scripts/spawn-audit-sessions.js +549 -0
  24. package/scripts/team-manager.js +37 -16
  25. package/scripts/tmux-close-windows.sh +180 -0
  26. package/src/core/agents/ads-audit-budget.md +181 -0
  27. package/src/core/agents/ads-audit-compliance.md +169 -0
  28. package/src/core/agents/ads-audit-creative.md +164 -0
  29. package/src/core/agents/ads-audit-google.md +226 -0
  30. package/src/core/agents/ads-audit-meta.md +183 -0
  31. package/src/core/agents/ads-audit-tracking.md +197 -0
  32. package/src/core/agents/ads-consensus.md +322 -0
  33. package/src/core/agents/brainstorm-analyzer-features.md +169 -0
  34. package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
  35. package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
  36. package/src/core/agents/brainstorm-analyzer-market.md +147 -0
  37. package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
  38. package/src/core/agents/brainstorm-consensus.md +237 -0
  39. package/src/core/agents/completeness-consensus.md +5 -5
  40. package/src/core/agents/perf-consensus.md +2 -2
  41. package/src/core/agents/security-consensus.md +2 -2
  42. package/src/core/agents/seo-analyzer-content.md +167 -0
  43. package/src/core/agents/seo-analyzer-images.md +187 -0
  44. package/src/core/agents/seo-analyzer-performance.md +206 -0
  45. package/src/core/agents/seo-analyzer-schema.md +176 -0
  46. package/src/core/agents/seo-analyzer-sitemap.md +172 -0
  47. package/src/core/agents/seo-analyzer-technical.md +144 -0
  48. package/src/core/agents/seo-consensus.md +289 -0
  49. package/src/core/agents/test-consensus.md +2 -2
  50. package/src/core/commands/ads/audit.md +375 -0
  51. package/src/core/commands/ads/budget.md +97 -0
  52. package/src/core/commands/ads/competitor.md +112 -0
  53. package/src/core/commands/ads/creative.md +85 -0
  54. package/src/core/commands/ads/google.md +112 -0
  55. package/src/core/commands/ads/landing.md +119 -0
  56. package/src/core/commands/ads/linkedin.md +112 -0
  57. package/src/core/commands/ads/meta.md +91 -0
  58. package/src/core/commands/ads/microsoft.md +115 -0
  59. package/src/core/commands/ads/plan.md +321 -0
  60. package/src/core/commands/ads/tiktok.md +129 -0
  61. package/src/core/commands/ads/youtube.md +124 -0
  62. package/src/core/commands/ads.md +128 -0
  63. package/src/core/commands/babysit.md +249 -1284
  64. package/src/core/commands/{audit → code}/completeness.md +35 -25
  65. package/src/core/commands/{audit → code}/legal.md +26 -16
  66. package/src/core/commands/{audit → code}/logic.md +27 -16
  67. package/src/core/commands/{audit → code}/performance.md +30 -20
  68. package/src/core/commands/{audit → code}/security.md +32 -19
  69. package/src/core/commands/{audit → code}/test.md +30 -20
  70. package/src/core/commands/{discovery → ideate}/brief.md +12 -12
  71. package/src/core/commands/{discovery/new.md → ideate/discover.md} +13 -13
  72. package/src/core/commands/ideate/features.md +435 -0
  73. package/src/core/commands/seo/audit.md +373 -0
  74. package/src/core/commands/seo/competitor.md +174 -0
  75. package/src/core/commands/seo/content.md +107 -0
  76. package/src/core/commands/seo/geo.md +229 -0
  77. package/src/core/commands/seo/hreflang.md +140 -0
  78. package/src/core/commands/seo/images.md +96 -0
  79. package/src/core/commands/seo/page.md +198 -0
  80. package/src/core/commands/seo/plan.md +163 -0
  81. package/src/core/commands/seo/programmatic.md +131 -0
  82. package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
  83. package/src/core/commands/seo/references/eeat-framework.md +110 -0
  84. package/src/core/commands/seo/references/quality-gates.md +91 -0
  85. package/src/core/commands/seo/references/schema-types.md +102 -0
  86. package/src/core/commands/seo/schema.md +183 -0
  87. package/src/core/commands/seo/sitemap.md +97 -0
  88. package/src/core/commands/seo/technical.md +100 -0
  89. package/src/core/commands/seo.md +107 -0
  90. package/src/core/commands/skill/list.md +68 -212
  91. package/src/core/commands/skill/recommend.md +216 -0
  92. package/src/core/commands/tdd-next.md +238 -0
  93. package/src/core/commands/tdd.md +210 -0
  94. package/src/core/experts/_core-expertise.yaml +105 -0
  95. package/src/core/experts/analytics/expertise.yaml +5 -99
  96. package/src/core/experts/codebase-query/expertise.yaml +3 -72
  97. package/src/core/experts/compliance/expertise.yaml +6 -72
  98. package/src/core/experts/database/expertise.yaml +9 -52
  99. package/src/core/experts/documentation/expertise.yaml +7 -140
  100. package/src/core/experts/integrations/expertise.yaml +7 -127
  101. package/src/core/experts/mentor/expertise.yaml +8 -35
  102. package/src/core/experts/monitoring/expertise.yaml +7 -49
  103. package/src/core/experts/performance/expertise.yaml +1 -26
  104. package/src/core/experts/security/expertise.yaml +9 -34
  105. package/src/core/experts/ui/expertise.yaml +6 -36
  106. package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
  107. package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
  108. package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
  109. package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
  110. package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
  111. package/src/core/templates/agileflow-metadata.json +15 -1
  112. package/tools/cli/installers/ide/_base-ide.js +42 -5
  113. package/tools/cli/installers/ide/claude-code.js +3 -3
  114. package/tools/cli/lib/content-injector.js +160 -12
  115. package/tools/cli/lib/docs-setup.js +1 -1
  116. package/src/core/commands/skill/create.md +0 -698
  117. package/src/core/commands/skill/delete.md +0 -316
  118. package/src/core/commands/skill/edit.md +0 -359
  119. package/src/core/commands/skill/test.md +0 -394
  120. package/src/core/commands/skill/upgrade.md +0 -552
  121. package/src/core/templates/skill-template.md +0 -117
@@ -0,0 +1,549 @@
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
+ stagger: null,
74
+ concurrency: null,
75
+ };
76
+
77
+ for (const arg of args) {
78
+ if (arg.startsWith('--audit=')) options.audit = arg.split('=')[1];
79
+ else if (arg.startsWith('--target=')) options.target = arg.split('=')[1];
80
+ else if (arg.startsWith('--focus=')) options.focus = arg.split('=')[1].split(',');
81
+ else if (arg.startsWith('--model=')) options.model = arg.split('=')[1];
82
+ else if (arg.startsWith('--trace-id=')) options.traceId = arg.split('=')[1];
83
+ else if (arg.startsWith('--timeout=')) {
84
+ const parsed = parseInt(arg.split('=')[1], 10);
85
+ options.timeout = isNaN(parsed) ? 30 : parsed;
86
+ } else if (arg.startsWith('--stagger=')) {
87
+ const parsed = parseFloat(arg.split('=')[1]);
88
+ options.stagger = isNaN(parsed) ? null : parsed;
89
+ } else if (arg.startsWith('--concurrency=')) {
90
+ const parsed = parseInt(arg.split('=')[1], 10);
91
+ options.concurrency = isNaN(parsed) ? null : parsed;
92
+ } else if (arg === '--dry-run') options.dryRun = true;
93
+ }
94
+
95
+ if (!options.traceId) {
96
+ options.traceId = crypto.randomBytes(6).toString('hex');
97
+ }
98
+
99
+ return options;
100
+ }
101
+
102
+ /**
103
+ * Check if tmux is available and we're in a tmux session.
104
+ * @returns {{ available: boolean, inSession: boolean }}
105
+ */
106
+ function checkTmux() {
107
+ try {
108
+ execFileSync('which', ['tmux'], { stdio: 'pipe' });
109
+ const inSession = !!process.env.TMUX;
110
+ return { available: true, inSession };
111
+ } catch (_) {
112
+ return { available: false, inSession: false };
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Create the sentinel directory for a trace.
118
+ * @param {string} rootDir - Project root
119
+ * @param {string} traceId - Unique trace ID
120
+ * @returns {string} Path to sentinel directory
121
+ */
122
+ function createSentinelDir(rootDir, traceId) {
123
+ const sentinelDir = path.join(rootDir, 'docs', '09-agents', 'ultradeep', traceId);
124
+ fs.mkdirSync(sentinelDir, { recursive: true });
125
+ return sentinelDir;
126
+ }
127
+
128
+ /**
129
+ * Write initial status file for the trace.
130
+ * @param {string} sentinelDir - Sentinel directory path
131
+ * @param {string} auditType - Audit type key
132
+ * @param {Array} analyzers - Array of analyzer configs
133
+ * @param {number} [staggerMs] - Stagger delay in milliseconds
134
+ * @param {number} [maxConcurrent] - Max concurrent sessions (0 = unlimited)
135
+ */
136
+ function writeStatusFile(sentinelDir, auditType, analyzers, staggerMs, maxConcurrent) {
137
+ const status = {
138
+ started_at: new Date().toISOString(),
139
+ audit_type: auditType,
140
+ analyzers: analyzers.map(a => a.key),
141
+ completed: [],
142
+ failed: [],
143
+ stagger_ms: staggerMs != null ? staggerMs : null,
144
+ max_concurrent: maxConcurrent || null,
145
+ };
146
+ fs.writeFileSync(path.join(sentinelDir, '_status.json'), JSON.stringify(status, null, 2) + '\n');
147
+ }
148
+
149
+ /**
150
+ * Build the prompt for an individual analyzer session.
151
+ * @param {object} analyzer - Analyzer config { key, subagent_type, label }
152
+ * @param {string} target - Target path to analyze
153
+ * @param {string} traceId - Trace ID
154
+ * @param {string} sentinelDir - Sentinel directory for output
155
+ * @param {string} auditType - Audit type key
156
+ * @returns {string} Prompt text
157
+ */
158
+ function buildAnalyzerPrompt(analyzer, target, traceId, sentinelDir, auditType) {
159
+ const findingsFile = path.join(sentinelDir, `${analyzer.key}.findings.json`);
160
+
161
+ return `You are the ${analyzer.label} analyzer for an ULTRADEEP ${auditType} audit.
162
+
163
+ TARGET: ${target}
164
+ TRACE_ID: ${traceId}
165
+ ANALYZER: ${analyzer.key} (${analyzer.label})
166
+
167
+ ## Instructions
168
+
169
+ 1. Analyze the target path thoroughly for ${analyzer.label}-related issues
170
+ 2. Search all relevant files recursively
171
+ 3. Document every finding with: file path, line number, severity (P0-P3), description, and evidence
172
+ 4. When complete, write your findings as JSON to: ${findingsFile}
173
+
174
+ ## Output Format
175
+
176
+ Write a JSON file with this structure:
177
+ {
178
+ "analyzer": "${analyzer.key}",
179
+ "audit_type": "${auditType}",
180
+ "trace_id": "${traceId}",
181
+ "target": "${target}",
182
+ "completed_at": "<ISO timestamp>",
183
+ "findings": [
184
+ {
185
+ "id": "${analyzer.key}-001",
186
+ "severity": "P0|P1|P2|P3",
187
+ "title": "Short description",
188
+ "file": "path/to/file.js",
189
+ "line": 42,
190
+ "description": "Detailed explanation",
191
+ "evidence": "Code snippet or reasoning",
192
+ "recommendation": "How to fix"
193
+ }
194
+ ],
195
+ "summary": {
196
+ "files_scanned": 0,
197
+ "total_findings": 0,
198
+ "by_severity": { "P0": 0, "P1": 0, "P2": 0, "P3": 0 }
199
+ }
200
+ }
201
+
202
+ IMPORTANT: You MUST write the findings JSON file when complete. This is how the orchestrator knows you're done.
203
+ Start analyzing now.`;
204
+ }
205
+
206
+ /**
207
+ * Spawn a single analyzer session in tmux.
208
+ * @param {object} params - Session parameters
209
+ * @returns {string|null} Window name if successful, null on failure
210
+ */
211
+ function spawnOneSession({
212
+ analyzer,
213
+ index,
214
+ sessionName,
215
+ rootDir,
216
+ options,
217
+ sentinelDir,
218
+ auditType,
219
+ groupColor,
220
+ }) {
221
+ const windowName = `${auditType.prefix}:${analyzer.key}`;
222
+ const model = resolveModel(options.model, 'haiku');
223
+ const prompt = buildAnalyzerPrompt(
224
+ analyzer,
225
+ options.target,
226
+ options.traceId,
227
+ sentinelDir,
228
+ options.audit
229
+ );
230
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
231
+
232
+ try {
233
+ if (index === 0) {
234
+ execFileSync(
235
+ 'tmux',
236
+ ['new-session', '-d', '-s', sessionName, '-n', windowName, '-c', rootDir],
237
+ { stdio: 'pipe' }
238
+ );
239
+ } else {
240
+ execFileSync('tmux', ['new-window', '-t', sessionName, '-n', windowName, '-c', rootDir], {
241
+ stdio: 'pipe',
242
+ });
243
+ }
244
+
245
+ execFileSync(
246
+ 'tmux',
247
+ ['set-option', '-w', '-t', `${sessionName}:${windowName}`, '@group_color', groupColor],
248
+ { stdio: 'pipe' }
249
+ );
250
+
251
+ const claudeCmd = `echo '${escapedPrompt}' | claude --model ${model} --allowedTools 'Read Glob Grep Write' 2>&1; echo "AUDIT_COMPLETE: ${analyzer.key}"`;
252
+ execFileSync('tmux', ['send-keys', '-t', `${sessionName}:${windowName}`, claudeCmd, 'Enter'], {
253
+ stdio: 'pipe',
254
+ });
255
+
256
+ return windowName;
257
+ } catch (err) {
258
+ console.error(`Failed to spawn ${windowName}: ${err.message}`);
259
+ return null;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Poll sentinel directory for wave completion.
265
+ * @param {string} sentinelDir - Sentinel directory path
266
+ * @param {string[]} keys - Analyzer keys to wait for
267
+ * @param {number} timeoutMinutes - Timeout in minutes
268
+ * @returns {Promise<boolean>} true if all completed, false on timeout
269
+ */
270
+ async function pollWaveCompletion(sentinelDir, keys, timeoutMinutes) {
271
+ const timeoutMs = (timeoutMinutes || 30) * 60 * 1000;
272
+ const startTime = Date.now();
273
+ while (Date.now() - startTime < timeoutMs) {
274
+ const allDone = keys.every(key =>
275
+ fs.existsSync(path.join(sentinelDir, `${key}.findings.json`))
276
+ );
277
+ if (allDone) return true;
278
+ await sleep(3000);
279
+ }
280
+ return false;
281
+ }
282
+
283
+ /**
284
+ * Spawn audit analyzer sessions in tmux with staggered launching.
285
+ * @param {object} options - Parsed CLI options
286
+ * @returns {Promise<{ ok: boolean, traceId: string, sentinelDir: string, sessions: string[] }>}
287
+ */
288
+ async function spawnAuditInTmux(options) {
289
+ const rootDir = process.cwd();
290
+ const auditType = getAuditType(options.audit);
291
+
292
+ if (!auditType) {
293
+ console.error(`Unknown audit type: ${options.audit}`);
294
+ console.error(`Valid types: logic, security, performance, test, completeness, legal`);
295
+ process.exit(1);
296
+ }
297
+
298
+ const result = getAnalyzersForAudit(options.audit, 'ultradeep', options.focus);
299
+ if (!result || result.analyzers.length === 0) {
300
+ console.error(`No analyzers found for ${options.audit} with focus: ${options.focus.join(',')}`);
301
+ process.exit(1);
302
+ }
303
+
304
+ // Enforce session limit
305
+ if (result.analyzers.length > 20) {
306
+ console.error(`Too many analyzers (${result.analyzers.length}). Maximum is 20.`);
307
+ process.exit(1);
308
+ }
309
+
310
+ const sentinelDir = createSentinelDir(rootDir, options.traceId);
311
+
312
+ // Resolve stagger and concurrency from CLI flags or config
313
+ const config = getUltradeepConfig();
314
+ const staggerMs =
315
+ ((options.stagger != null ? options.stagger : config.stagger_seconds) || 0) * 1000;
316
+ const maxConcurrent = options.concurrency != null ? options.concurrency : config.max_concurrent;
317
+
318
+ writeStatusFile(sentinelDir, options.audit, result.analyzers, staggerMs, maxConcurrent);
319
+
320
+ const groupColor = getColorForAudit(options.audit);
321
+ const sessions = [];
322
+
323
+ if (options.dryRun) {
324
+ console.log(`\nDry run - would spawn ${result.analyzers.length} sessions:`);
325
+ console.log(` Stagger: ${staggerMs / 1000}s between launches`);
326
+ if (maxConcurrent > 0) {
327
+ const waveCount = Math.ceil(result.analyzers.length / maxConcurrent);
328
+ console.log(` Concurrency: ${maxConcurrent}/wave (${waveCount} waves)`);
329
+ }
330
+ for (const analyzer of result.analyzers) {
331
+ const model = resolveModel(options.model, 'haiku');
332
+ console.log(` ${auditType.prefix}:${analyzer.key} (${model}) → ${analyzer.label}`);
333
+ }
334
+ console.log(`\nSentinel dir: ${sentinelDir}`);
335
+ console.log(`Group color: ${groupColor}`);
336
+ return { ok: true, traceId: options.traceId, sentinelDir, sessions: [], dryRun: true };
337
+ }
338
+
339
+ const tmux = checkTmux();
340
+ if (!tmux.available) {
341
+ console.error('tmux is not available. ULTRADEEP mode requires tmux.');
342
+ console.error('Falling back to DEPTH=deep mode.');
343
+ return { ok: false, traceId: options.traceId, sentinelDir, sessions: [], fallback: 'deep' };
344
+ }
345
+
346
+ // Create dedicated tmux session for this audit
347
+ const sessionName = `audit-${options.audit}-${options.traceId.slice(0, 8)}`;
348
+ let sessionIndex = 0;
349
+
350
+ if (maxConcurrent > 0 && result.analyzers.length > maxConcurrent) {
351
+ // Wave-based spawning
352
+ const waves = [];
353
+ for (let i = 0; i < result.analyzers.length; i += maxConcurrent) {
354
+ waves.push(result.analyzers.slice(i, i + maxConcurrent));
355
+ }
356
+ for (let w = 0; w < waves.length; w++) {
357
+ if (w > 0) {
358
+ const prevKeys = waves[w - 1].map(a => a.key);
359
+ await pollWaveCompletion(sentinelDir, prevKeys, options.timeout);
360
+ }
361
+ for (let i = 0; i < waves[w].length; i++) {
362
+ if (sessionIndex > 0 && staggerMs > 0) await sleep(staggerMs);
363
+ const name = spawnOneSession({
364
+ analyzer: waves[w][i],
365
+ index: sessionIndex,
366
+ sessionName,
367
+ rootDir,
368
+ options,
369
+ sentinelDir,
370
+ auditType,
371
+ groupColor,
372
+ });
373
+ if (name) sessions.push(name);
374
+ sessionIndex++;
375
+ }
376
+ }
377
+ } else {
378
+ // Simple staggered spawning (no wave limit)
379
+ for (let i = 0; i < result.analyzers.length; i++) {
380
+ if (i > 0 && staggerMs > 0) await sleep(staggerMs);
381
+ const name = spawnOneSession({
382
+ analyzer: result.analyzers[i],
383
+ index: i,
384
+ sessionName,
385
+ rootDir,
386
+ options,
387
+ sentinelDir,
388
+ auditType,
389
+ groupColor,
390
+ });
391
+ if (name) sessions.push(name);
392
+ }
393
+ }
394
+
395
+ // Apply the same status bar theme as normal Claude sessions
396
+ try {
397
+ const tmuxScript = path.join(__dirname, 'claude-tmux.sh');
398
+ execFileSync(tmuxScript, [`--configure-session=${sessionName}`], { stdio: 'pipe' });
399
+ } catch (_) {
400
+ // Non-critical styling failure — audit session still works with default theme
401
+ }
402
+
403
+ console.log(`\nSpawned ${sessions.length} analyzer sessions in tmux session: ${sessionName}`);
404
+ console.log(`Sentinel dir: ${sentinelDir}`);
405
+ console.log(`Attach with: tmux attach -t ${sessionName}`);
406
+
407
+ return { ok: true, traceId: options.traceId, sentinelDir, sessions, sessionName };
408
+ }
409
+
410
+ /**
411
+ * Poll sentinel directory for completion.
412
+ * @param {string} sentinelDir - Sentinel directory path
413
+ * @param {string[]} expected - Expected analyzer keys
414
+ * @param {number} timeoutMinutes - Timeout in minutes
415
+ * @returns {Promise<{ complete: boolean, results: object[], missing: string[] }>}
416
+ */
417
+ async function pollForCompletion(sentinelDir, expected, timeoutMinutes) {
418
+ const timeoutMs = timeoutMinutes * 60 * 1000;
419
+ const startTime = Date.now();
420
+ const pollIntervalMs = 5000;
421
+
422
+ while (Date.now() - startTime < timeoutMs) {
423
+ const completed = [];
424
+ const missing = [];
425
+
426
+ for (const key of expected) {
427
+ const findingsFile = path.join(sentinelDir, `${key}.findings.json`);
428
+ if (fs.existsSync(findingsFile)) {
429
+ completed.push(key);
430
+ } else {
431
+ missing.push(key);
432
+ }
433
+ }
434
+
435
+ // Update status file
436
+ try {
437
+ const statusPath = path.join(sentinelDir, '_status.json');
438
+ const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
439
+ status.completed = completed;
440
+ status.last_checked = new Date().toISOString();
441
+ fs.writeFileSync(statusPath, JSON.stringify(status, null, 2) + '\n');
442
+ } catch (_) {
443
+ // Non-critical
444
+ }
445
+
446
+ if (missing.length === 0) {
447
+ return { complete: true, results: collectResults(sentinelDir, expected), missing: [] };
448
+ }
449
+
450
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
451
+ }
452
+
453
+ // Timeout
454
+ const results = collectResults(sentinelDir, expected);
455
+ const completedKeys = results.map(r => r.analyzer);
456
+ const missing = expected.filter(k => !completedKeys.includes(k));
457
+ return { complete: false, results, missing };
458
+ }
459
+
460
+ /**
461
+ * Collect all findings from sentinel directory.
462
+ * @param {string} sentinelDir - Sentinel directory path
463
+ * @param {string[]} expected - Expected analyzer keys
464
+ * @returns {object[]} Array of parsed findings
465
+ */
466
+ function collectResults(sentinelDir, expected) {
467
+ const results = [];
468
+
469
+ for (const key of expected) {
470
+ const findingsFile = path.join(sentinelDir, `${key}.findings.json`);
471
+ try {
472
+ if (fs.existsSync(findingsFile)) {
473
+ const data = JSON.parse(fs.readFileSync(findingsFile, 'utf8'));
474
+ results.push(data);
475
+ }
476
+ } catch (err) {
477
+ results.push({
478
+ analyzer: key,
479
+ error: `Failed to parse findings: ${err.message}`,
480
+ findings: [],
481
+ });
482
+ }
483
+ }
484
+
485
+ return results;
486
+ }
487
+
488
+ /**
489
+ * Show cost estimation before launching.
490
+ * @param {string} auditType - Audit type key
491
+ * @param {number} analyzerCount - Number of analyzers to spawn
492
+ * @param {string} [model] - Explicit model override
493
+ */
494
+ function showCostEstimate(auditType, analyzerCount, model) {
495
+ const resolved = resolveModel(model, 'haiku');
496
+ const estimate = estimateCost(resolved, analyzerCount);
497
+
498
+ console.log(`\nCost estimate for ULTRADEEP ${auditType} audit:`);
499
+ console.log(` Model: ${estimate.model}`);
500
+ console.log(` Analyzers: ${analyzerCount}`);
501
+ console.log(` Cost multiplier vs haiku: ${estimate.multiplier}x`);
502
+ console.log(` Per-analyzer estimate: ${estimate.perAnalyzerCost}`);
503
+ console.log(` Total estimate: ${estimate.totalEstimate}`);
504
+ console.log(` Each analyzer runs as a full Claude Code session`);
505
+ }
506
+
507
+ // Main
508
+ if (require.main === module) {
509
+ (async () => {
510
+ const options = parseArgs();
511
+
512
+ if (!options.audit) {
513
+ console.error(
514
+ 'Usage: node spawn-audit-sessions.js --audit=TYPE --target=PATH [--focus=AREAS] [--model=MODEL] [--trace-id=ID] [--stagger=SECONDS] [--concurrency=N]'
515
+ );
516
+ console.error('Types: logic, security, performance, test, completeness, legal');
517
+ process.exit(1);
518
+ }
519
+
520
+ const result = getAnalyzersForAudit(options.audit, 'ultradeep', options.focus);
521
+ if (result) {
522
+ showCostEstimate(options.audit, result.analyzers.length, options.model);
523
+ }
524
+
525
+ const spawnResult = await spawnAuditInTmux(options);
526
+ if (!spawnResult.ok && spawnResult.fallback) {
527
+ process.exit(2); // Signal fallback to caller
528
+ }
529
+ })().catch(err => {
530
+ console.error(err.message);
531
+ process.exit(1);
532
+ });
533
+ }
534
+
535
+ module.exports = {
536
+ parseArgs,
537
+ checkTmux,
538
+ createSentinelDir,
539
+ writeStatusFile,
540
+ buildAnalyzerPrompt,
541
+ spawnOneSession,
542
+ spawnAuditInTmux,
543
+ pollForCompletion,
544
+ pollWaveCompletion,
545
+ collectResults,
546
+ showCostEstimate,
547
+ getUltradeepConfig,
548
+ sleep,
549
+ };
@@ -81,6 +81,19 @@ function getMessagingBridge() {
81
81
  return _messagingBridge;
82
82
  }
83
83
 
84
+ // Lazy-load team events for dual-write (session-state + JSONL bus)
85
+ let _teamEvents;
86
+ function getTeamEvents() {
87
+ if (!_teamEvents) {
88
+ try {
89
+ _teamEvents = require('./lib/team-events');
90
+ } catch (e) {
91
+ _teamEvents = null;
92
+ }
93
+ }
94
+ return _teamEvents;
95
+ }
96
+
84
97
  /**
85
98
  * Find the teams directory
86
99
  */
@@ -344,17 +357,14 @@ function startTeam(rootDir, templateName) {
344
357
  // Non-critical - team can still function without state tracking
345
358
  }
346
359
 
347
- // Log team_created event to bus
360
+ // Log team_created event to session-state + JSONL bus (dual-write)
348
361
  try {
349
- const bridge = getMessagingBridge();
350
- if (bridge) {
351
- bridge.sendMessage(rootDir, {
352
- from: 'team-manager',
353
- to: 'team-lead',
354
- type: 'team_created',
362
+ const teamEvents = getTeamEvents();
363
+ if (teamEvents) {
364
+ teamEvents.trackEvent(rootDir, 'team_created', {
365
+ trace_id: traceId,
355
366
  template: templateName,
356
367
  mode,
357
- trace_id: traceId,
358
368
  teammate_count: template.teammates.length,
359
369
  });
360
370
  }
@@ -439,17 +449,14 @@ function stopTeam(rootDir) {
439
449
  fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
440
450
  }
441
451
 
442
- // Log team_stopped event
452
+ // Log team_stopped event to session-state + JSONL bus (dual-write)
443
453
  try {
444
- const bridge = getMessagingBridge();
445
- if (bridge) {
446
- bridge.sendMessage(rootDir, {
447
- from: 'team-manager',
448
- to: 'system',
449
- type: 'team_stopped',
454
+ const teamEvents = getTeamEvents();
455
+ if (teamEvents) {
456
+ teamEvents.trackEvent(rootDir, 'team_stopped', {
457
+ trace_id: team.trace_id,
450
458
  template: team.template,
451
459
  mode: team.mode,
452
- trace_id: team.trace_id,
453
460
  duration_ms: duration,
454
461
  tasks_completed: state.team_metrics ? state.team_metrics.tasks_completed : 0,
455
462
  });
@@ -458,6 +465,20 @@ function stopTeam(rootDir) {
458
465
  // Non-critical
459
466
  }
460
467
 
468
+ // Log team_completed event for observability parity (AC3)
469
+ try {
470
+ const teamEvents = require('./lib/team-events');
471
+ teamEvents.trackEvent(rootDir, 'team_completed', {
472
+ trace_id: team.trace_id,
473
+ template: team.template,
474
+ mode: team.mode,
475
+ duration_ms: duration,
476
+ tasks_completed: state.team_metrics ? state.team_metrics.tasks_completed : 0,
477
+ });
478
+ } catch (e) {
479
+ // Non-critical
480
+ }
481
+
461
482
  // Aggregate and save team metrics by trace_id
462
483
  try {
463
484
  if (team.trace_id) {