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,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
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * tmux-group-colors.js - Color palette for tmux tab groups
3
+ *
4
+ * Provides a curated palette of 8 colors optimized for dark terminal
5
+ * backgrounds (Tokyo Night compatible). Supports color assignment by
6
+ * audit type and random selection with avoidance of in-use colors.
7
+ *
8
+ * Usage:
9
+ * const { getColorForAudit, pickGroupColor } = require('./tmux-group-colors');
10
+ * const color = getColorForAudit('security'); // '#f7768e'
11
+ * const random = pickGroupColor(['#f7768e']); // random excluding coral
12
+ */
13
+
14
+ /**
15
+ * Curated palette for dark backgrounds.
16
+ * Each color has sufficient contrast against #1a1b26 (Tokyo Night bg)
17
+ * and #2d2f3a (tab bg).
18
+ */
19
+ const GROUP_PALETTE = [
20
+ { name: 'coral', hex: '#f7768e', audit: 'security' },
21
+ { name: 'sky', hex: '#7aa2f7', audit: 'logic' },
22
+ { name: 'mint', hex: '#73daca', audit: 'performance' },
23
+ { name: 'amber', hex: '#e0af68', audit: 'test' },
24
+ { name: 'violet', hex: '#bb9af7', audit: 'completeness' },
25
+ { name: 'lime', hex: '#9ece6a', audit: 'legal' },
26
+ { name: 'rose', hex: '#ff9e64', audit: null },
27
+ { name: 'ice', hex: '#89ddff', audit: null },
28
+ ];
29
+
30
+ /**
31
+ * Map audit type to its assigned color.
32
+ */
33
+ const AUDIT_COLOR_MAP = {};
34
+ for (const entry of GROUP_PALETTE) {
35
+ if (entry.audit) {
36
+ AUDIT_COLOR_MAP[entry.audit] = entry.hex;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get the assigned color for an audit type.
42
+ *
43
+ * @param {string} auditType - Audit type key (security, logic, etc.)
44
+ * @returns {string} Hex color string
45
+ */
46
+ function getColorForAudit(auditType) {
47
+ return AUDIT_COLOR_MAP[auditType] || pickGroupColor([]);
48
+ }
49
+
50
+ /**
51
+ * Pick a group color, avoiding colors currently in use.
52
+ *
53
+ * @param {string[]} [inUseColors] - Array of hex colors currently in use
54
+ * @returns {string} Hex color string not in the in-use list
55
+ */
56
+ function pickGroupColor(inUseColors) {
57
+ const avoid = new Set((inUseColors || []).map(c => c.toLowerCase()));
58
+ const available = GROUP_PALETTE.filter(entry => !avoid.has(entry.hex.toLowerCase()));
59
+
60
+ if (available.length === 0) {
61
+ // All colors in use, pick first from full palette
62
+ return GROUP_PALETTE[0].hex;
63
+ }
64
+
65
+ // Deterministic pick: first available for consistency
66
+ return available[0].hex;
67
+ }
68
+
69
+ /**
70
+ * Get a palette entry by name.
71
+ *
72
+ * @param {string} name - Color name (e.g. 'coral', 'sky')
73
+ * @returns {object|null} Palette entry or null
74
+ */
75
+ function getColorByName(name) {
76
+ return GROUP_PALETTE.find(entry => entry.name === name) || null;
77
+ }
78
+
79
+ /**
80
+ * Build tmux window format string with group color.
81
+ *
82
+ * @param {string} groupColor - Hex color for the group
83
+ * @param {string} prefix - Short prefix (e.g. 'Sec', 'Logic')
84
+ * @param {boolean} [isActive=false] - Whether this is the active window
85
+ * @returns {string} tmux format string
86
+ */
87
+ function buildGroupWindowFormat(groupColor, prefix, isActive) {
88
+ if (isActive) {
89
+ // Active: colored bg for index, dark bg for name
90
+ return `#[fg=#1a1b26 bg=${groupColor} bold] #I #[fg=${groupColor} bg=#2d2f3a]#[fg=#e0e0e0] ${prefix}:#{window_name} #[bg=#1a1b26 fg=#2d2f3a]`;
91
+ }
92
+ // Inactive: colored dot prefix + gray text
93
+ return `#[fg=${groupColor}]#[fg=#8a8a8a] #I:${prefix}:#{window_name} `;
94
+ }
95
+
96
+ /**
97
+ * Get all palette colors.
98
+ *
99
+ * @returns {Array<{ name: string, hex: string, audit: string|null }>}
100
+ */
101
+ function getAllColors() {
102
+ return [...GROUP_PALETTE];
103
+ }
104
+
105
+ module.exports = {
106
+ GROUP_PALETTE,
107
+ AUDIT_COLOR_MAP,
108
+ getColorForAudit,
109
+ pickGroupColor,
110
+ getColorByName,
111
+ buildGroupWindowFormat,
112
+ getAllColors,
113
+ };