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.
- package/CHANGELOG.md +5 -0
- package/README.md +4 -4
- package/package.json +1 -1
- package/scripts/agileflow-welcome.js +79 -0
- package/scripts/claude-tmux.sh +12 -36
- package/scripts/lib/ac-test-matcher.js +452 -0
- package/scripts/lib/audit-registry.js +58 -2
- package/scripts/lib/configure-features.js +35 -0
- package/scripts/lib/model-profiles.js +25 -5
- package/scripts/lib/quality-gates.js +163 -0
- package/scripts/lib/signal-detectors.js +43 -0
- package/scripts/lib/status-writer.js +255 -0
- package/scripts/lib/story-claiming.js +128 -45
- package/scripts/lib/task-sync.js +32 -38
- package/scripts/lib/tmux-audit-monitor.js +611 -0
- package/scripts/lib/tool-registry.yaml +241 -0
- package/scripts/lib/tool-shed.js +441 -0
- package/scripts/native-team-observer.js +219 -0
- package/scripts/obtain-context.js +14 -0
- package/scripts/ralph-loop.js +30 -5
- package/scripts/smart-detect.js +21 -0
- package/scripts/spawn-audit-sessions.js +372 -44
- package/scripts/team-manager.js +19 -0
- package/src/core/agents/a11y-analyzer-aria.md +155 -0
- package/src/core/agents/a11y-analyzer-forms.md +162 -0
- package/src/core/agents/a11y-analyzer-keyboard.md +175 -0
- package/src/core/agents/a11y-analyzer-semantic.md +153 -0
- package/src/core/agents/a11y-analyzer-visual.md +158 -0
- package/src/core/agents/a11y-consensus.md +248 -0
- package/src/core/agents/ads-consensus.md +74 -0
- package/src/core/agents/ads-generate.md +145 -0
- package/src/core/agents/ads-performance-tracker.md +197 -0
- package/src/core/agents/api-quality-analyzer-conventions.md +148 -0
- package/src/core/agents/api-quality-analyzer-docs.md +176 -0
- package/src/core/agents/api-quality-analyzer-errors.md +183 -0
- package/src/core/agents/api-quality-analyzer-pagination.md +171 -0
- package/src/core/agents/api-quality-analyzer-versioning.md +143 -0
- package/src/core/agents/api-quality-consensus.md +214 -0
- package/src/core/agents/arch-analyzer-circular.md +148 -0
- package/src/core/agents/arch-analyzer-complexity.md +171 -0
- package/src/core/agents/arch-analyzer-coupling.md +146 -0
- package/src/core/agents/arch-analyzer-layering.md +151 -0
- package/src/core/agents/arch-analyzer-patterns.md +162 -0
- package/src/core/agents/arch-consensus.md +227 -0
- package/src/core/commands/adr.md +1 -0
- package/src/core/commands/ads/generate.md +238 -0
- package/src/core/commands/ads/health.md +327 -0
- package/src/core/commands/ads/test-plan.md +317 -0
- package/src/core/commands/ads/track.md +288 -0
- package/src/core/commands/ads.md +28 -16
- package/src/core/commands/assign.md +1 -0
- package/src/core/commands/audit.md +43 -6
- package/src/core/commands/babysit.md +90 -6
- package/src/core/commands/baseline.md +1 -0
- package/src/core/commands/blockers.md +1 -0
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/changelog.md +1 -0
- package/src/core/commands/choose.md +1 -0
- package/src/core/commands/ci.md +1 -0
- package/src/core/commands/code/accessibility.md +347 -0
- package/src/core/commands/code/api.md +297 -0
- package/src/core/commands/code/architecture.md +297 -0
- package/src/core/commands/code/completeness.md +43 -6
- package/src/core/commands/code/legal.md +43 -6
- package/src/core/commands/code/logic.md +43 -6
- package/src/core/commands/code/performance.md +43 -6
- package/src/core/commands/code/security.md +43 -6
- package/src/core/commands/code/test.md +43 -6
- package/src/core/commands/configure.md +1 -0
- package/src/core/commands/council.md +1 -0
- package/src/core/commands/deploy.md +1 -0
- package/src/core/commands/diagnose.md +1 -0
- package/src/core/commands/docs.md +1 -0
- package/src/core/commands/epic/edit.md +213 -0
- package/src/core/commands/epic.md +1 -0
- package/src/core/commands/export.md +238 -0
- package/src/core/commands/help.md +16 -1
- package/src/core/commands/ideate/discover.md +7 -3
- package/src/core/commands/ideate/features.md +65 -4
- package/src/core/commands/ideate/new.md +158 -124
- package/src/core/commands/impact.md +1 -0
- package/src/core/commands/learn/explain.md +118 -0
- package/src/core/commands/learn/glossary.md +135 -0
- package/src/core/commands/learn/patterns.md +138 -0
- package/src/core/commands/learn/tour.md +126 -0
- package/src/core/commands/migrate/codemods.md +151 -0
- package/src/core/commands/migrate/plan.md +131 -0
- package/src/core/commands/migrate/scan.md +114 -0
- package/src/core/commands/migrate/validate.md +119 -0
- package/src/core/commands/multi-expert.md +1 -0
- package/src/core/commands/pr.md +1 -0
- package/src/core/commands/review.md +1 -0
- package/src/core/commands/sprint.md +1 -0
- package/src/core/commands/status/undo.md +191 -0
- package/src/core/commands/status.md +1 -0
- package/src/core/commands/story/edit.md +204 -0
- package/src/core/commands/story/view.md +29 -7
- package/src/core/commands/story-validate.md +1 -0
- package/src/core/commands/story.md +1 -0
- package/src/core/commands/tdd.md +1 -0
- package/src/core/commands/team/start.md +10 -6
- package/src/core/commands/tests.md +1 -0
- package/src/core/commands/verify.md +27 -1
- package/src/core/commands/workflow.md +2 -0
- package/src/core/teams/backend.json +41 -0
- package/src/core/teams/frontend.json +41 -0
- package/src/core/teams/qa.json +41 -0
- package/src/core/teams/solo.json +35 -0
- package/src/core/templates/agileflow-metadata.json +5 -0
- package/tools/cli/commands/setup.js +85 -3
- package/tools/cli/commands/update.js +42 -0
- 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
|
+
};
|