agileflow 3.4.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 (112) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +4 -4
  3. package/package.json +1 -1
  4. package/scripts/agileflow-welcome.js +79 -0
  5. package/scripts/claude-tmux.sh +12 -36
  6. package/scripts/lib/ac-test-matcher.js +452 -0
  7. package/scripts/lib/audit-registry.js +58 -2
  8. package/scripts/lib/configure-features.js +35 -0
  9. package/scripts/lib/model-profiles.js +25 -5
  10. package/scripts/lib/quality-gates.js +163 -0
  11. package/scripts/lib/signal-detectors.js +43 -0
  12. package/scripts/lib/status-writer.js +255 -0
  13. package/scripts/lib/story-claiming.js +128 -45
  14. package/scripts/lib/task-sync.js +32 -38
  15. package/scripts/lib/tmux-audit-monitor.js +611 -0
  16. package/scripts/lib/tool-registry.yaml +241 -0
  17. package/scripts/lib/tool-shed.js +441 -0
  18. package/scripts/native-team-observer.js +219 -0
  19. package/scripts/obtain-context.js +14 -0
  20. package/scripts/ralph-loop.js +30 -5
  21. package/scripts/smart-detect.js +21 -0
  22. package/scripts/spawn-audit-sessions.js +372 -44
  23. package/scripts/team-manager.js +19 -0
  24. package/src/core/agents/a11y-analyzer-aria.md +155 -0
  25. package/src/core/agents/a11y-analyzer-forms.md +162 -0
  26. package/src/core/agents/a11y-analyzer-keyboard.md +175 -0
  27. package/src/core/agents/a11y-analyzer-semantic.md +153 -0
  28. package/src/core/agents/a11y-analyzer-visual.md +158 -0
  29. package/src/core/agents/a11y-consensus.md +248 -0
  30. package/src/core/agents/ads-consensus.md +74 -0
  31. package/src/core/agents/ads-generate.md +145 -0
  32. package/src/core/agents/ads-performance-tracker.md +197 -0
  33. package/src/core/agents/api-quality-analyzer-conventions.md +148 -0
  34. package/src/core/agents/api-quality-analyzer-docs.md +176 -0
  35. package/src/core/agents/api-quality-analyzer-errors.md +183 -0
  36. package/src/core/agents/api-quality-analyzer-pagination.md +171 -0
  37. package/src/core/agents/api-quality-analyzer-versioning.md +143 -0
  38. package/src/core/agents/api-quality-consensus.md +214 -0
  39. package/src/core/agents/arch-analyzer-circular.md +148 -0
  40. package/src/core/agents/arch-analyzer-complexity.md +171 -0
  41. package/src/core/agents/arch-analyzer-coupling.md +146 -0
  42. package/src/core/agents/arch-analyzer-layering.md +151 -0
  43. package/src/core/agents/arch-analyzer-patterns.md +162 -0
  44. package/src/core/agents/arch-consensus.md +227 -0
  45. package/src/core/commands/adr.md +1 -0
  46. package/src/core/commands/ads/generate.md +238 -0
  47. package/src/core/commands/ads/health.md +327 -0
  48. package/src/core/commands/ads/test-plan.md +317 -0
  49. package/src/core/commands/ads/track.md +288 -0
  50. package/src/core/commands/ads.md +28 -16
  51. package/src/core/commands/assign.md +1 -0
  52. package/src/core/commands/audit.md +43 -6
  53. package/src/core/commands/babysit.md +90 -6
  54. package/src/core/commands/baseline.md +1 -0
  55. package/src/core/commands/blockers.md +1 -0
  56. package/src/core/commands/board.md +1 -0
  57. package/src/core/commands/changelog.md +1 -0
  58. package/src/core/commands/choose.md +1 -0
  59. package/src/core/commands/ci.md +1 -0
  60. package/src/core/commands/code/accessibility.md +347 -0
  61. package/src/core/commands/code/api.md +297 -0
  62. package/src/core/commands/code/architecture.md +297 -0
  63. package/src/core/commands/code/completeness.md +43 -6
  64. package/src/core/commands/code/legal.md +43 -6
  65. package/src/core/commands/code/logic.md +43 -6
  66. package/src/core/commands/code/performance.md +43 -6
  67. package/src/core/commands/code/security.md +43 -6
  68. package/src/core/commands/code/test.md +43 -6
  69. package/src/core/commands/configure.md +1 -0
  70. package/src/core/commands/council.md +1 -0
  71. package/src/core/commands/deploy.md +1 -0
  72. package/src/core/commands/diagnose.md +1 -0
  73. package/src/core/commands/docs.md +1 -0
  74. package/src/core/commands/epic/edit.md +213 -0
  75. package/src/core/commands/epic.md +1 -0
  76. package/src/core/commands/export.md +238 -0
  77. package/src/core/commands/help.md +16 -1
  78. package/src/core/commands/ideate/discover.md +7 -3
  79. package/src/core/commands/ideate/features.md +65 -4
  80. package/src/core/commands/ideate/new.md +158 -124
  81. package/src/core/commands/impact.md +1 -0
  82. package/src/core/commands/learn/explain.md +118 -0
  83. package/src/core/commands/learn/glossary.md +135 -0
  84. package/src/core/commands/learn/patterns.md +138 -0
  85. package/src/core/commands/learn/tour.md +126 -0
  86. package/src/core/commands/migrate/codemods.md +151 -0
  87. package/src/core/commands/migrate/plan.md +131 -0
  88. package/src/core/commands/migrate/scan.md +114 -0
  89. package/src/core/commands/migrate/validate.md +119 -0
  90. package/src/core/commands/multi-expert.md +1 -0
  91. package/src/core/commands/pr.md +1 -0
  92. package/src/core/commands/review.md +1 -0
  93. package/src/core/commands/sprint.md +1 -0
  94. package/src/core/commands/status/undo.md +191 -0
  95. package/src/core/commands/status.md +1 -0
  96. package/src/core/commands/story/edit.md +204 -0
  97. package/src/core/commands/story/view.md +29 -7
  98. package/src/core/commands/story-validate.md +1 -0
  99. package/src/core/commands/story.md +1 -0
  100. package/src/core/commands/tdd.md +1 -0
  101. package/src/core/commands/team/start.md +10 -6
  102. package/src/core/commands/tests.md +1 -0
  103. package/src/core/commands/verify.md +27 -1
  104. package/src/core/commands/workflow.md +2 -0
  105. package/src/core/teams/backend.json +41 -0
  106. package/src/core/teams/frontend.json +41 -0
  107. package/src/core/teams/qa.json +41 -0
  108. package/src/core/teams/solo.json +35 -0
  109. package/src/core/templates/agileflow-metadata.json +5 -0
  110. package/tools/cli/commands/setup.js +85 -3
  111. package/tools/cli/commands/update.js +42 -0
  112. package/tools/cli/installers/ide/claude-code.js +68 -0
@@ -0,0 +1,611 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * tmux-audit-monitor.js - Monitor and manage ULTRADEEP audit sessions
5
+ *
6
+ * Provides 6 subcommands for the AI to call during ultradeep audits:
7
+ * status <trace_id> - One-shot state check
8
+ * wait <trace_id> [--timeout=1800] - Block until complete or timeout
9
+ * collect <trace_id> - Collect whatever results are done
10
+ * retry <trace_id> [--analyzer=key] - Re-spawn stalled analyzers
11
+ * kill <trace_id> [--keep-files] - Clean shutdown
12
+ * list - Discover all active traces
13
+ *
14
+ * All output is JSON to stdout. Progress goes to stderr.
15
+ */
16
+
17
+ const { execFileSync } = require('child_process');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ // --- Helpers ---
22
+
23
+ function jsonOut(obj) {
24
+ console.log(JSON.stringify(obj));
25
+ }
26
+
27
+ function progress(msg) {
28
+ process.stderr.write(msg + '\n');
29
+ }
30
+
31
+ function getSentinelDir(rootDir, traceId) {
32
+ return path.join(rootDir, 'docs', '09-agents', 'ultradeep', traceId);
33
+ }
34
+
35
+ function readStatusFile(sentinelDir) {
36
+ const statusPath = path.join(sentinelDir, '_status.json');
37
+ if (!fs.existsSync(statusPath)) return null;
38
+ try {
39
+ return JSON.parse(fs.readFileSync(statusPath, 'utf8'));
40
+ } catch (_) {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Determine the state of a single analyzer.
47
+ * @param {string} key - Analyzer key
48
+ * @param {string} sentinelDir - Sentinel directory path
49
+ * @param {string} sessionName - tmux session name
50
+ * @param {string} prefix - Audit type prefix (e.g. 'Logic', 'Sec')
51
+ * @returns {'done'|'running'|'stalled'}
52
+ */
53
+ function getAnalyzerState(key, sentinelDir, sessionName, prefix) {
54
+ if (fs.existsSync(path.join(sentinelDir, `${key}.findings.json`))) return 'done';
55
+ try {
56
+ const windows = execFileSync(
57
+ 'tmux',
58
+ ['list-windows', '-t', sessionName, '-F', '#{window_name}'],
59
+ {
60
+ encoding: 'utf8',
61
+ stdio: ['pipe', 'pipe', 'pipe'],
62
+ }
63
+ )
64
+ .trim()
65
+ .split('\n');
66
+ return windows.includes(`${prefix}:${key}`) ? 'running' : 'stalled';
67
+ } catch (_) {
68
+ return 'stalled';
69
+ }
70
+ }
71
+
72
+ function readFindings(sentinelDir, key) {
73
+ const findingsFile = path.join(sentinelDir, `${key}.findings.json`);
74
+ try {
75
+ if (fs.existsSync(findingsFile)) {
76
+ return JSON.parse(fs.readFileSync(findingsFile, 'utf8'));
77
+ }
78
+ } catch (err) {
79
+ return { analyzer: key, error: `Failed to parse: ${err.message}`, findings: [] };
80
+ }
81
+ return null;
82
+ }
83
+
84
+ function collectResults(sentinelDir, analyzerKeys) {
85
+ const results = [];
86
+ for (const key of analyzerKeys) {
87
+ const data = readFindings(sentinelDir, key);
88
+ if (data) results.push(data);
89
+ }
90
+ return results;
91
+ }
92
+
93
+ function deriveSessionName(status, traceId) {
94
+ const auditType = status.audit_type || 'unknown';
95
+ return `audit-${auditType}-${traceId.slice(0, 8)}`;
96
+ }
97
+
98
+ function getAuditPrefix(auditType) {
99
+ try {
100
+ const { getAuditType: getType } = require('./audit-registry');
101
+ const typeConfig = getType(auditType);
102
+ if (typeConfig && typeConfig.prefix) return typeConfig.prefix;
103
+ } catch (_) {
104
+ // Fallback if audit-registry not available
105
+ }
106
+ return auditType;
107
+ }
108
+
109
+ function sleep(ms) {
110
+ return new Promise(resolve => setTimeout(resolve, ms));
111
+ }
112
+
113
+ // --- Subcommands ---
114
+
115
+ /**
116
+ * status <trace_id> - One-shot state check
117
+ */
118
+ function cmdStatus(rootDir, traceId) {
119
+ const sentinelDir = getSentinelDir(rootDir, traceId);
120
+ const status = readStatusFile(sentinelDir);
121
+
122
+ if (!status) {
123
+ jsonOut({ ok: false, error: `No trace found: ${traceId}`, traceId });
124
+ return;
125
+ }
126
+
127
+ const sessionName = deriveSessionName(status, traceId);
128
+ const prefix = getAuditPrefix(status.audit_type);
129
+ const startedAt = new Date(status.started_at).getTime();
130
+ const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000);
131
+
132
+ const analyzers = [];
133
+ let doneCount = 0;
134
+ let runningCount = 0;
135
+ let stalledCount = 0;
136
+
137
+ for (const key of status.analyzers) {
138
+ const state = getAnalyzerState(key, sentinelDir, sessionName, prefix);
139
+ const entry = { key, state };
140
+ if (state === 'done') {
141
+ doneCount++;
142
+ const findings = readFindings(sentinelDir, key);
143
+ if (findings && findings.findings) {
144
+ entry.findingsCount = findings.findings.length;
145
+ }
146
+ } else if (state === 'running') {
147
+ runningCount++;
148
+ } else {
149
+ stalledCount++;
150
+ }
151
+ analyzers.push(entry);
152
+ }
153
+
154
+ jsonOut({
155
+ ok: true,
156
+ traceId,
157
+ auditType: status.audit_type,
158
+ elapsedSeconds,
159
+ progress: {
160
+ total: status.analyzers.length,
161
+ done: doneCount,
162
+ running: runningCount,
163
+ stalled: stalledCount,
164
+ },
165
+ analyzers,
166
+ });
167
+ }
168
+
169
+ /**
170
+ * wait <trace_id> [--timeout=1800] [--poll=5] - Block until complete or timeout
171
+ */
172
+ async function cmdWait(rootDir, traceId, timeoutSeconds, pollSeconds) {
173
+ const sentinelDir = getSentinelDir(rootDir, traceId);
174
+ const status = readStatusFile(sentinelDir);
175
+
176
+ if (!status) {
177
+ jsonOut({
178
+ ok: false,
179
+ complete: false,
180
+ error: `No trace found: ${traceId}`,
181
+ traceId,
182
+ elapsedSeconds: 0,
183
+ results: [],
184
+ missing: [],
185
+ });
186
+ process.exitCode = 1;
187
+ return;
188
+ }
189
+
190
+ const sessionName = deriveSessionName(status, traceId);
191
+ const prefix = getAuditPrefix(status.audit_type);
192
+ const expected = status.analyzers;
193
+ const startTime = Date.now();
194
+ const timeoutMs = timeoutSeconds * 1000;
195
+ const pollMs = pollSeconds * 1000;
196
+
197
+ while (Date.now() - startTime < timeoutMs) {
198
+ const done = [];
199
+ const missing = [];
200
+ const stalled = [];
201
+
202
+ for (const key of expected) {
203
+ const state = getAnalyzerState(key, sentinelDir, sessionName, prefix);
204
+ if (state === 'done') done.push(key);
205
+ else if (state === 'stalled') stalled.push(key);
206
+ else missing.push(key);
207
+ }
208
+
209
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
210
+ progress(
211
+ `[${elapsed}s] ${done.length}/${expected.length} done, ${missing.length} running, ${stalled.length} stalled`
212
+ );
213
+
214
+ if (done.length === expected.length) {
215
+ const results = collectResults(sentinelDir, expected);
216
+ jsonOut({ ok: true, complete: true, traceId, elapsedSeconds: elapsed, results, missing: [] });
217
+ return;
218
+ }
219
+
220
+ // If all remaining are stalled (no running), no point waiting
221
+ if (missing.length === 0 && stalled.length > 0) {
222
+ progress(`All remaining analyzers stalled: ${stalled.join(', ')}`);
223
+ const results = collectResults(sentinelDir, expected);
224
+ jsonOut({
225
+ ok: false,
226
+ complete: false,
227
+ traceId,
228
+ elapsedSeconds: elapsed,
229
+ results,
230
+ missing: [],
231
+ stalled,
232
+ });
233
+ process.exitCode = 1;
234
+ return;
235
+ }
236
+
237
+ await sleep(pollMs);
238
+ }
239
+
240
+ // Timeout
241
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
242
+ const results = collectResults(sentinelDir, expected);
243
+ const completedKeys = results.map(r => r.analyzer);
244
+ const missing = expected.filter(k => !completedKeys.includes(k));
245
+ progress(`Timeout after ${elapsed}s. ${results.length}/${expected.length} completed.`);
246
+ jsonOut({ ok: false, complete: false, traceId, elapsedSeconds: elapsed, results, missing });
247
+ process.exitCode = 1;
248
+ }
249
+
250
+ /**
251
+ * collect <trace_id> - One-shot collection
252
+ */
253
+ function cmdCollect(rootDir, traceId) {
254
+ const sentinelDir = getSentinelDir(rootDir, traceId);
255
+ const status = readStatusFile(sentinelDir);
256
+
257
+ if (!status) {
258
+ jsonOut({
259
+ ok: false,
260
+ error: `No trace found: ${traceId}`,
261
+ traceId,
262
+ complete: false,
263
+ found: 0,
264
+ expected: 0,
265
+ results: [],
266
+ missing: [],
267
+ });
268
+ return;
269
+ }
270
+
271
+ const expected = status.analyzers;
272
+ const results = collectResults(sentinelDir, expected);
273
+ const foundKeys = results.map(r => r.analyzer).filter(Boolean);
274
+ const missing = expected.filter(k => !foundKeys.includes(k));
275
+
276
+ jsonOut({
277
+ ok: true,
278
+ traceId,
279
+ complete: missing.length === 0,
280
+ found: results.length,
281
+ expected: expected.length,
282
+ results,
283
+ missing,
284
+ });
285
+ }
286
+
287
+ /**
288
+ * retry <trace_id> [--analyzer=key] [--model=M] - Re-spawn failed/stalled analyzers
289
+ */
290
+ function cmdRetry(rootDir, traceId, analyzerFilter, modelOverride) {
291
+ const sentinelDir = getSentinelDir(rootDir, traceId);
292
+ const status = readStatusFile(sentinelDir);
293
+
294
+ if (!status) {
295
+ jsonOut({ ok: false, error: `No trace found: ${traceId}`, traceId, retried: [], errors: [] });
296
+ return;
297
+ }
298
+
299
+ // Determine which analyzers to retry
300
+ const sessionName = deriveSessionName(status, traceId);
301
+ const prefix = getAuditPrefix(status.audit_type);
302
+ const toRetry = [];
303
+
304
+ for (const key of status.analyzers) {
305
+ if (analyzerFilter && analyzerFilter !== key) continue;
306
+ const state = getAnalyzerState(key, sentinelDir, sessionName, prefix);
307
+ if (state !== 'done') {
308
+ toRetry.push(key);
309
+ }
310
+ }
311
+
312
+ if (toRetry.length === 0) {
313
+ jsonOut({
314
+ ok: true,
315
+ traceId,
316
+ retried: [],
317
+ errors: [],
318
+ message: 'Nothing to retry - all analyzers complete',
319
+ });
320
+ return;
321
+ }
322
+
323
+ // Load audit registry for analyzer configs
324
+ let getAuditType, spawnOneSession, resolveModel, getColorForAudit;
325
+ try {
326
+ ({ getAuditType } = require('./audit-registry'));
327
+ ({ spawnOneSession } = require('../spawn-audit-sessions'));
328
+ ({ resolveModel } = require('./model-profiles'));
329
+ ({ getColorForAudit } = require('./tmux-group-colors'));
330
+ } catch (err) {
331
+ jsonOut({
332
+ ok: false,
333
+ error: `Failed to load dependencies: ${err.message}`,
334
+ traceId,
335
+ retried: [],
336
+ errors: [err.message],
337
+ });
338
+ return;
339
+ }
340
+
341
+ const auditType = getAuditType(status.audit_type);
342
+ if (!auditType) {
343
+ jsonOut({
344
+ ok: false,
345
+ error: `Unknown audit type: ${status.audit_type}`,
346
+ traceId,
347
+ retried: [],
348
+ errors: [],
349
+ });
350
+ return;
351
+ }
352
+
353
+ const model = modelOverride || status.model || 'haiku';
354
+ const target = status.target || '.';
355
+ const groupColor = getColorForAudit(status.audit_type);
356
+ const retried = [];
357
+ const errors = [];
358
+
359
+ // Count existing windows to determine spawn index
360
+ let existingWindows = 0;
361
+ try {
362
+ const output = execFileSync(
363
+ 'tmux',
364
+ ['list-windows', '-t', sessionName, '-F', '#{window_name}'],
365
+ {
366
+ encoding: 'utf8',
367
+ stdio: ['pipe', 'pipe', 'pipe'],
368
+ }
369
+ ).trim();
370
+ existingWindows = output ? output.split('\n').length : 0;
371
+ } catch (_) {
372
+ // Session may not exist; first spawn will create it
373
+ }
374
+
375
+ for (const key of toRetry) {
376
+ const analyzerConfig = auditType.analyzers[key];
377
+ if (!analyzerConfig) {
378
+ errors.push(`Unknown analyzer: ${key}`);
379
+ continue;
380
+ }
381
+
382
+ const analyzer = {
383
+ key,
384
+ subagent_type: analyzerConfig.subagent_type,
385
+ label: analyzerConfig.label,
386
+ };
387
+ try {
388
+ const windowName = spawnOneSession({
389
+ analyzer,
390
+ index: existingWindows,
391
+ sessionName,
392
+ rootDir,
393
+ options: { audit: status.audit_type, target, model, traceId },
394
+ sentinelDir,
395
+ auditType,
396
+ groupColor,
397
+ });
398
+ if (windowName) {
399
+ retried.push(key);
400
+ existingWindows++;
401
+ } else {
402
+ errors.push(`Failed to spawn window for ${key}`);
403
+ }
404
+ } catch (err) {
405
+ errors.push(`${key}: ${err.message}`);
406
+ }
407
+ }
408
+
409
+ jsonOut({ ok: errors.length === 0, traceId, retried, errors });
410
+ }
411
+
412
+ /**
413
+ * kill <trace_id> [--keep-files] - Clean shutdown
414
+ */
415
+ function cmdKill(rootDir, traceId, keepFiles) {
416
+ const sentinelDir = getSentinelDir(rootDir, traceId);
417
+ const status = readStatusFile(sentinelDir);
418
+
419
+ if (!status) {
420
+ jsonOut({
421
+ ok: false,
422
+ error: `No trace found: ${traceId}`,
423
+ traceId,
424
+ sessionKilled: false,
425
+ filesRemoved: false,
426
+ });
427
+ return;
428
+ }
429
+
430
+ const sessionName = deriveSessionName(status, traceId);
431
+
432
+ // Kill tmux session
433
+ let sessionKilled = false;
434
+ try {
435
+ execFileSync('tmux', ['kill-session', '-t', sessionName], { stdio: 'pipe' });
436
+ sessionKilled = true;
437
+ } catch (_) {
438
+ // Session may already be dead
439
+ }
440
+
441
+ // Remove files
442
+ let filesRemoved = false;
443
+ if (!keepFiles) {
444
+ try {
445
+ fs.rmSync(sentinelDir, { recursive: true, force: true });
446
+ filesRemoved = true;
447
+ } catch (_) {
448
+ // Non-critical
449
+ }
450
+ }
451
+
452
+ jsonOut({ ok: sessionKilled || filesRemoved || keepFiles, traceId, sessionKilled, filesRemoved });
453
+ }
454
+
455
+ /**
456
+ * list - Discover all active traces
457
+ */
458
+ function cmdList(rootDir) {
459
+ const ultradeepDir = path.join(rootDir, 'docs', '09-agents', 'ultradeep');
460
+
461
+ if (!fs.existsSync(ultradeepDir)) {
462
+ jsonOut({ ok: true, traces: [] });
463
+ return;
464
+ }
465
+
466
+ const traces = [];
467
+ try {
468
+ const entries = fs.readdirSync(ultradeepDir, { withFileTypes: true });
469
+ for (const entry of entries) {
470
+ if (!entry.isDirectory()) continue;
471
+
472
+ const traceDir = path.join(ultradeepDir, entry.name);
473
+ const status = readStatusFile(traceDir);
474
+ if (!status) continue;
475
+
476
+ const traceId = entry.name;
477
+ const sessionName = deriveSessionName(status, traceId);
478
+
479
+ // Check how many are done
480
+ const doneCount = status.analyzers.filter(key =>
481
+ fs.existsSync(path.join(traceDir, `${key}.findings.json`))
482
+ ).length;
483
+
484
+ // Check if tmux session is alive
485
+ let sessionActive = false;
486
+ try {
487
+ execFileSync('tmux', ['has-session', '-t', sessionName], { stdio: 'pipe' });
488
+ sessionActive = true;
489
+ } catch (_) {
490
+ // Session not found
491
+ }
492
+
493
+ traces.push({
494
+ traceId,
495
+ auditType: status.audit_type,
496
+ progress: { total: status.analyzers.length, done: doneCount },
497
+ sessionActive,
498
+ });
499
+ }
500
+ } catch (err) {
501
+ jsonOut({ ok: false, error: `Failed to read ultradeep dir: ${err.message}`, traces });
502
+ return;
503
+ }
504
+
505
+ jsonOut({ ok: true, traces });
506
+ }
507
+
508
+ // --- Arg parsing and dispatch ---
509
+
510
+ function parseSubcommandArgs(args) {
511
+ const parsed = { timeout: 1800, poll: 5, analyzer: null, model: null, keepFiles: false };
512
+ for (const arg of args) {
513
+ if (arg.startsWith('--timeout=')) {
514
+ const v = parseInt(arg.split('=')[1], 10);
515
+ parsed.timeout = isNaN(v) ? 1800 : v;
516
+ } else if (arg.startsWith('--poll=')) {
517
+ const v = parseInt(arg.split('=')[1], 10);
518
+ parsed.poll = isNaN(v) ? 5 : v;
519
+ } else if (arg.startsWith('--analyzer=')) {
520
+ const val = arg.split('=')[1];
521
+ if (val) parsed.analyzer = val;
522
+ } else if (arg.startsWith('--model=')) {
523
+ parsed.model = arg.split('=')[1];
524
+ } else if (arg === '--keep-files') {
525
+ parsed.keepFiles = true;
526
+ }
527
+ }
528
+ return parsed;
529
+ }
530
+
531
+ if (require.main === module) {
532
+ const args = process.argv.slice(2);
533
+ const subcommand = args[0];
534
+ const traceId = args[1];
535
+ const restArgs = args.slice(2);
536
+ const rootDir = process.cwd();
537
+
538
+ if (!subcommand || subcommand === '--help') {
539
+ console.error('Usage: tmux-audit-monitor.js <subcommand> [trace_id] [options]');
540
+ console.error('Subcommands: status, wait, collect, retry, kill, list');
541
+ process.exit(1);
542
+ }
543
+
544
+ const opts = parseSubcommandArgs(restArgs);
545
+
546
+ switch (subcommand) {
547
+ case 'status':
548
+ if (!traceId) {
549
+ jsonOut({ ok: false, error: 'trace_id required' });
550
+ process.exit(1);
551
+ }
552
+ cmdStatus(rootDir, traceId);
553
+ break;
554
+
555
+ case 'wait':
556
+ if (!traceId) {
557
+ jsonOut({ ok: false, error: 'trace_id required' });
558
+ process.exit(1);
559
+ }
560
+ cmdWait(rootDir, traceId, opts.timeout, opts.poll).catch(err => {
561
+ jsonOut({ ok: false, error: err.message });
562
+ process.exit(1);
563
+ });
564
+ break;
565
+
566
+ case 'collect':
567
+ if (!traceId) {
568
+ jsonOut({ ok: false, error: 'trace_id required' });
569
+ process.exit(1);
570
+ }
571
+ cmdCollect(rootDir, traceId);
572
+ break;
573
+
574
+ case 'retry':
575
+ if (!traceId) {
576
+ jsonOut({ ok: false, error: 'trace_id required' });
577
+ process.exit(1);
578
+ }
579
+ cmdRetry(rootDir, traceId, opts.analyzer, opts.model);
580
+ break;
581
+
582
+ case 'kill':
583
+ if (!traceId) {
584
+ jsonOut({ ok: false, error: 'trace_id required' });
585
+ process.exit(1);
586
+ }
587
+ cmdKill(rootDir, traceId, opts.keepFiles);
588
+ break;
589
+
590
+ case 'list':
591
+ cmdList(rootDir);
592
+ break;
593
+
594
+ default:
595
+ jsonOut({ ok: false, error: `Unknown subcommand: ${subcommand}` });
596
+ process.exit(1);
597
+ }
598
+ }
599
+
600
+ module.exports = {
601
+ getAnalyzerState,
602
+ readStatusFile,
603
+ collectResults,
604
+ parseSubcommandArgs,
605
+ cmdStatus,
606
+ cmdWait,
607
+ cmdCollect,
608
+ cmdRetry,
609
+ cmdKill,
610
+ cmdList,
611
+ };