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.
- package/CHANGELOG.md +5 -0
- package/README.md +6 -6
- package/lib/skill-loader.js +0 -1
- package/package.json +1 -1
- package/scripts/agileflow-statusline.sh +81 -0
- package/scripts/claude-tmux.sh +113 -22
- package/scripts/claude-watchdog.sh +225 -0
- package/scripts/generators/agent-registry.js +14 -1
- package/scripts/generators/inject-babysit.js +22 -9
- package/scripts/generators/inject-help.js +19 -9
- package/scripts/lib/audit-cleanup.js +250 -0
- package/scripts/lib/audit-registry.js +248 -0
- package/scripts/lib/feature-catalog.js +3 -3
- package/scripts/lib/gate-enforcer.js +295 -0
- package/scripts/lib/model-profiles.js +98 -0
- package/scripts/lib/signal-detectors.js +1 -1
- package/scripts/lib/skill-catalog.js +557 -0
- package/scripts/lib/skill-recommender.js +311 -0
- package/scripts/lib/tdd-phase-manager.js +455 -0
- package/scripts/lib/team-events.js +34 -3
- package/scripts/lib/tmux-group-colors.js +113 -0
- package/scripts/messaging-bridge.js +209 -1
- package/scripts/spawn-audit-sessions.js +549 -0
- package/scripts/team-manager.js +37 -16
- package/scripts/tmux-close-windows.sh +180 -0
- package/src/core/agents/ads-audit-budget.md +181 -0
- package/src/core/agents/ads-audit-compliance.md +169 -0
- package/src/core/agents/ads-audit-creative.md +164 -0
- package/src/core/agents/ads-audit-google.md +226 -0
- package/src/core/agents/ads-audit-meta.md +183 -0
- package/src/core/agents/ads-audit-tracking.md +197 -0
- package/src/core/agents/ads-consensus.md +322 -0
- package/src/core/agents/brainstorm-analyzer-features.md +169 -0
- package/src/core/agents/brainstorm-analyzer-growth.md +161 -0
- package/src/core/agents/brainstorm-analyzer-integration.md +172 -0
- package/src/core/agents/brainstorm-analyzer-market.md +147 -0
- package/src/core/agents/brainstorm-analyzer-ux.md +167 -0
- package/src/core/agents/brainstorm-consensus.md +237 -0
- package/src/core/agents/completeness-consensus.md +5 -5
- package/src/core/agents/perf-consensus.md +2 -2
- package/src/core/agents/security-consensus.md +2 -2
- package/src/core/agents/seo-analyzer-content.md +167 -0
- package/src/core/agents/seo-analyzer-images.md +187 -0
- package/src/core/agents/seo-analyzer-performance.md +206 -0
- package/src/core/agents/seo-analyzer-schema.md +176 -0
- package/src/core/agents/seo-analyzer-sitemap.md +172 -0
- package/src/core/agents/seo-analyzer-technical.md +144 -0
- package/src/core/agents/seo-consensus.md +289 -0
- package/src/core/agents/test-consensus.md +2 -2
- package/src/core/commands/ads/audit.md +375 -0
- package/src/core/commands/ads/budget.md +97 -0
- package/src/core/commands/ads/competitor.md +112 -0
- package/src/core/commands/ads/creative.md +85 -0
- package/src/core/commands/ads/google.md +112 -0
- package/src/core/commands/ads/landing.md +119 -0
- package/src/core/commands/ads/linkedin.md +112 -0
- package/src/core/commands/ads/meta.md +91 -0
- package/src/core/commands/ads/microsoft.md +115 -0
- package/src/core/commands/ads/plan.md +321 -0
- package/src/core/commands/ads/tiktok.md +129 -0
- package/src/core/commands/ads/youtube.md +124 -0
- package/src/core/commands/ads.md +128 -0
- package/src/core/commands/babysit.md +249 -1284
- package/src/core/commands/{audit → code}/completeness.md +35 -25
- package/src/core/commands/{audit → code}/legal.md +26 -16
- package/src/core/commands/{audit → code}/logic.md +27 -16
- package/src/core/commands/{audit → code}/performance.md +30 -20
- package/src/core/commands/{audit → code}/security.md +32 -19
- package/src/core/commands/{audit → code}/test.md +30 -20
- package/src/core/commands/{discovery → ideate}/brief.md +12 -12
- package/src/core/commands/{discovery/new.md → ideate/discover.md} +13 -13
- package/src/core/commands/ideate/features.md +435 -0
- package/src/core/commands/seo/audit.md +373 -0
- package/src/core/commands/seo/competitor.md +174 -0
- package/src/core/commands/seo/content.md +107 -0
- package/src/core/commands/seo/geo.md +229 -0
- package/src/core/commands/seo/hreflang.md +140 -0
- package/src/core/commands/seo/images.md +96 -0
- package/src/core/commands/seo/page.md +198 -0
- package/src/core/commands/seo/plan.md +163 -0
- package/src/core/commands/seo/programmatic.md +131 -0
- package/src/core/commands/seo/references/cwv-thresholds.md +64 -0
- package/src/core/commands/seo/references/eeat-framework.md +110 -0
- package/src/core/commands/seo/references/quality-gates.md +91 -0
- package/src/core/commands/seo/references/schema-types.md +102 -0
- package/src/core/commands/seo/schema.md +183 -0
- package/src/core/commands/seo/sitemap.md +97 -0
- package/src/core/commands/seo/technical.md +100 -0
- package/src/core/commands/seo.md +107 -0
- package/src/core/commands/skill/list.md +68 -212
- package/src/core/commands/skill/recommend.md +216 -0
- package/src/core/commands/tdd-next.md +238 -0
- package/src/core/commands/tdd.md +210 -0
- package/src/core/experts/_core-expertise.yaml +105 -0
- package/src/core/experts/analytics/expertise.yaml +5 -99
- package/src/core/experts/codebase-query/expertise.yaml +3 -72
- package/src/core/experts/compliance/expertise.yaml +6 -72
- package/src/core/experts/database/expertise.yaml +9 -52
- package/src/core/experts/documentation/expertise.yaml +7 -140
- package/src/core/experts/integrations/expertise.yaml +7 -127
- package/src/core/experts/mentor/expertise.yaml +8 -35
- package/src/core/experts/monitoring/expertise.yaml +7 -49
- package/src/core/experts/performance/expertise.yaml +1 -26
- package/src/core/experts/security/expertise.yaml +9 -34
- package/src/core/experts/ui/expertise.yaml +6 -36
- package/src/core/knowledge/ads/ad-audit-checklist-scoring.md +424 -0
- package/src/core/knowledge/ads/ad-optimization-logic.md +590 -0
- package/src/core/knowledge/ads/ad-technical-specifications.md +385 -0
- package/src/core/knowledge/ads/definitive-advertising-reference-2026.md +506 -0
- package/src/core/knowledge/ads/paid-advertising-research-2026.md +445 -0
- package/src/core/templates/agileflow-metadata.json +15 -1
- package/tools/cli/installers/ide/_base-ide.js +42 -5
- package/tools/cli/installers/ide/claude-code.js +3 -3
- package/tools/cli/lib/content-injector.js +160 -12
- package/tools/cli/lib/docs-setup.js +1 -1
- package/src/core/commands/skill/create.md +0 -698
- package/src/core/commands/skill/delete.md +0 -316
- package/src/core/commands/skill/edit.md +0 -359
- package/src/core/commands/skill/test.md +0 -394
- package/src/core/commands/skill/upgrade.md +0 -552
- 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
|
+
};
|
package/scripts/team-manager.js
CHANGED
|
@@ -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
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
445
|
-
if (
|
|
446
|
-
|
|
447
|
-
|
|
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) {
|