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
|
@@ -70,8 +70,11 @@ function parseArgs() {
|
|
|
70
70
|
traceId: null,
|
|
71
71
|
timeout: 30,
|
|
72
72
|
dryRun: false,
|
|
73
|
+
json: false,
|
|
73
74
|
stagger: null,
|
|
74
75
|
concurrency: null,
|
|
76
|
+
depth: null,
|
|
77
|
+
partitions: null,
|
|
75
78
|
};
|
|
76
79
|
|
|
77
80
|
for (const arg of args) {
|
|
@@ -89,7 +92,11 @@ function parseArgs() {
|
|
|
89
92
|
} else if (arg.startsWith('--concurrency=')) {
|
|
90
93
|
const parsed = parseInt(arg.split('=')[1], 10);
|
|
91
94
|
options.concurrency = isNaN(parsed) ? null : parsed;
|
|
92
|
-
} else if (arg
|
|
95
|
+
} else if (arg.startsWith('--depth=')) options.depth = arg.split('=')[1];
|
|
96
|
+
else if (arg.startsWith('--partitions='))
|
|
97
|
+
options.partitions = arg.split('=')[1].split(',').filter(Boolean);
|
|
98
|
+
else if (arg === '--dry-run') options.dryRun = true;
|
|
99
|
+
else if (arg === '--json') options.json = true;
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
if (!options.traceId) {
|
|
@@ -133,7 +140,7 @@ function createSentinelDir(rootDir, traceId) {
|
|
|
133
140
|
* @param {number} [staggerMs] - Stagger delay in milliseconds
|
|
134
141
|
* @param {number} [maxConcurrent] - Max concurrent sessions (0 = unlimited)
|
|
135
142
|
*/
|
|
136
|
-
function writeStatusFile(sentinelDir, auditType, analyzers, staggerMs, maxConcurrent) {
|
|
143
|
+
function writeStatusFile(sentinelDir, auditType, analyzers, staggerMs, maxConcurrent, extra) {
|
|
137
144
|
const status = {
|
|
138
145
|
started_at: new Date().toISOString(),
|
|
139
146
|
audit_type: auditType,
|
|
@@ -143,6 +150,12 @@ function writeStatusFile(sentinelDir, auditType, analyzers, staggerMs, maxConcur
|
|
|
143
150
|
stagger_ms: staggerMs != null ? staggerMs : null,
|
|
144
151
|
max_concurrent: maxConcurrent || null,
|
|
145
152
|
};
|
|
153
|
+
// Store extra fields for retry support
|
|
154
|
+
if (extra) {
|
|
155
|
+
if (extra.target != null) status.target = extra.target;
|
|
156
|
+
if (extra.model != null) status.model = extra.model;
|
|
157
|
+
if (extra.timeout_minutes != null) status.timeout_minutes = extra.timeout_minutes;
|
|
158
|
+
}
|
|
146
159
|
fs.writeFileSync(path.join(sentinelDir, '_status.json'), JSON.stringify(status, null, 2) + '\n');
|
|
147
160
|
}
|
|
148
161
|
|
|
@@ -153,32 +166,47 @@ function writeStatusFile(sentinelDir, auditType, analyzers, staggerMs, maxConcur
|
|
|
153
166
|
* @param {string} traceId - Trace ID
|
|
154
167
|
* @param {string} sentinelDir - Sentinel directory for output
|
|
155
168
|
* @param {string} auditType - Audit type key
|
|
169
|
+
* @param {string} [model] - Resolved model name for sub-agent
|
|
156
170
|
* @returns {string} Prompt text
|
|
157
171
|
*/
|
|
158
|
-
function buildAnalyzerPrompt(analyzer, target, traceId, sentinelDir, auditType) {
|
|
172
|
+
function buildAnalyzerPrompt(analyzer, target, traceId, sentinelDir, auditType, model) {
|
|
159
173
|
const findingsFile = path.join(sentinelDir, `${analyzer.key}.findings.json`);
|
|
160
174
|
|
|
161
|
-
|
|
175
|
+
// Sanitize fields that get interpolated into double-quoted prompt sections
|
|
176
|
+
const safeLabel = String(analyzer.label || '').replace(/["\\]/g, '');
|
|
177
|
+
const safeTarget = String(target || '').replace(/["\\]/g, '');
|
|
178
|
+
const safeSubagentType = String(analyzer.subagent_type || '').replace(/["\\]/g, '');
|
|
179
|
+
|
|
180
|
+
return `You are an ULTRADEEP audit session coordinator.
|
|
181
|
+
|
|
182
|
+
## Task
|
|
162
183
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
ANALYZER: ${analyzer.key} (${analyzer.label})
|
|
184
|
+
1. Use the Agent tool to spawn a sub-agent for analysis
|
|
185
|
+
2. After the sub-agent completes, parse its output and write findings as JSON to the sentinel file
|
|
166
186
|
|
|
167
|
-
##
|
|
187
|
+
## Agent Configuration
|
|
168
188
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
189
|
+
Use the Agent tool with these parameters:
|
|
190
|
+
- subagent_type: "${safeSubagentType}"
|
|
191
|
+
- description: "${safeLabel} analysis of ${safeTarget}"${model ? `\n- model: "${model}"` : ''}
|
|
192
|
+
- prompt: |
|
|
193
|
+
Analyze the target path ${safeTarget} thoroughly for ${safeLabel}-related issues.
|
|
194
|
+
Search all relevant files recursively. Be thorough.
|
|
195
|
+
Return a JSON object with this structure:
|
|
196
|
+
{"findings": [{"id": "${analyzer.key}-NNN", "severity": "P0|P1|P2|P3", "title": "Short description", "file": "path/to/file.js", "line": 42, "description": "Detailed explanation", "evidence": "Code snippet or reasoning", "recommendation": "How to fix"}], "summary": {"files_scanned": 0, "total_findings": 0, "by_severity": {"P0": 0, "P1": 0, "P2": 0, "P3": 0}}}
|
|
197
|
+
TRACE_ID: ${traceId}
|
|
198
|
+
ANALYZER: ${analyzer.key}
|
|
173
199
|
|
|
174
|
-
## Output
|
|
200
|
+
## Output
|
|
175
201
|
|
|
176
|
-
|
|
202
|
+
After the Agent tool returns its analysis, write a JSON file to: ${findingsFile}
|
|
203
|
+
|
|
204
|
+
Use this structure:
|
|
177
205
|
{
|
|
178
206
|
"analyzer": "${analyzer.key}",
|
|
179
207
|
"audit_type": "${auditType}",
|
|
180
208
|
"trace_id": "${traceId}",
|
|
181
|
-
"target": "${
|
|
209
|
+
"target": "${safeTarget}",
|
|
182
210
|
"completed_at": "<ISO timestamp>",
|
|
183
211
|
"findings": [
|
|
184
212
|
{
|
|
@@ -200,7 +228,107 @@ Write a JSON file with this structure:
|
|
|
200
228
|
}
|
|
201
229
|
|
|
202
230
|
IMPORTANT: You MUST write the findings JSON file when complete. This is how the orchestrator knows you're done.
|
|
203
|
-
|
|
231
|
+
If the Agent tool is unavailable or returns an error, perform the analysis directly using Read, Glob, and Grep tools.
|
|
232
|
+
Start by spawning the Agent now.`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create a slug from a partition path for use in window names and filenames.
|
|
237
|
+
* Only allows alphanumeric, hyphens, and underscores — safe for tmux window
|
|
238
|
+
* names, shell interpolation, and filesystem paths.
|
|
239
|
+
* @param {string} partition - Partition path (e.g. 'src/auth')
|
|
240
|
+
* @returns {string} Slug (e.g. 'src-auth')
|
|
241
|
+
*/
|
|
242
|
+
function partitionSlug(partition) {
|
|
243
|
+
return (
|
|
244
|
+
partition
|
|
245
|
+
.replace(/^\.?\/?/, '')
|
|
246
|
+
.replace(/\/+$/g, '')
|
|
247
|
+
.replace(/[/\\]/g, '-')
|
|
248
|
+
.replace(/[^a-zA-Z0-9_-]/g, '_') || 'root'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Build the prompt for an extreme-mode partition coordinator session.
|
|
254
|
+
* This coordinator runs ALL analyzers on a single partition using the Agent tool.
|
|
255
|
+
* @param {string} partition - Partition path to analyze
|
|
256
|
+
* @param {Array<{ key: string, subagent_type: string, label: string }>} analyzers - All analyzers to run
|
|
257
|
+
* @param {string} traceId - Trace ID
|
|
258
|
+
* @param {string} sentinelDir - Sentinel directory for output
|
|
259
|
+
* @param {string} auditType - Audit type key
|
|
260
|
+
* @param {string} [model] - Resolved model name for sub-agents
|
|
261
|
+
* @returns {string} Prompt text
|
|
262
|
+
*/
|
|
263
|
+
function buildExtremePrompt(partition, analyzers, traceId, sentinelDir, auditType, model) {
|
|
264
|
+
const slug = partitionSlug(partition);
|
|
265
|
+
const findingsFile = path.join(sentinelDir, `${slug}.findings.json`);
|
|
266
|
+
|
|
267
|
+
const safePartition = String(partition || '').replace(/["\\]/g, '');
|
|
268
|
+
|
|
269
|
+
const analyzerList = analyzers
|
|
270
|
+
.map((a, i) => `${i + 1}. subagent_type: "${a.subagent_type}" — ${a.label} (key: ${a.key})`)
|
|
271
|
+
.join('\n');
|
|
272
|
+
|
|
273
|
+
const modelLine = model ? `\n- model: "${model}"` : '';
|
|
274
|
+
|
|
275
|
+
return `You are an EXTREME audit session coordinator for partition: ${safePartition}
|
|
276
|
+
|
|
277
|
+
## Task
|
|
278
|
+
|
|
279
|
+
Run ALL of the following analyzers on your partition using the Agent tool.
|
|
280
|
+
Deploy them in parallel (multiple Agent calls in one message) where possible.
|
|
281
|
+
|
|
282
|
+
## Analyzers to Run
|
|
283
|
+
${analyzerList}
|
|
284
|
+
|
|
285
|
+
## Agent Configuration for Each Analyzer
|
|
286
|
+
|
|
287
|
+
Use the Agent tool with these parameters for each analyzer:
|
|
288
|
+
- subagent_type: (from the list above)
|
|
289
|
+
- description: "[Analyzer label] analysis of ${safePartition}"${modelLine}
|
|
290
|
+
- prompt: |
|
|
291
|
+
Analyze the target path ${safePartition} thoroughly for issues relevant to your analyzer domain.
|
|
292
|
+
Search all relevant files recursively. Be thorough.
|
|
293
|
+
Use the analyzer key from your subagent_type as the ID prefix (e.g., for "security-analyzer-injection" use "injection").
|
|
294
|
+
Return a JSON object with this structure:
|
|
295
|
+
{"findings": [{"id": "<analyzer-key>-NNN", "severity": "P0|P1|P2|P3", "title": "Short description", "file": "path/to/file.js", "line": 42, "description": "Detailed explanation", "evidence": "Code snippet or reasoning", "recommendation": "How to fix"}], "summary": {"files_scanned": 0, "total_findings": 0, "by_severity": {"P0": 0, "P1": 0, "P2": 0, "P3": 0}}}
|
|
296
|
+
TRACE_ID: ${traceId}
|
|
297
|
+
|
|
298
|
+
## After All Analyzers Complete
|
|
299
|
+
|
|
300
|
+
Combine ALL findings from ALL analyzers into a single JSON file: ${findingsFile}
|
|
301
|
+
|
|
302
|
+
Use this structure:
|
|
303
|
+
{
|
|
304
|
+
"partition": "${safePartition}",
|
|
305
|
+
"audit_type": "${auditType}",
|
|
306
|
+
"trace_id": "${traceId}",
|
|
307
|
+
"completed_at": "<ISO timestamp>",
|
|
308
|
+
"analyzer_count": ${analyzers.length},
|
|
309
|
+
"analyzers_run": [${analyzers.map(a => `"${a.key}"`).join(', ')}],
|
|
310
|
+
"findings": [
|
|
311
|
+
{
|
|
312
|
+
"id": "<analyzer_key>-001",
|
|
313
|
+
"analyzer": "<analyzer_key>",
|
|
314
|
+
"severity": "P0|P1|P2|P3",
|
|
315
|
+
"title": "Short description",
|
|
316
|
+
"file": "path/to/file.js",
|
|
317
|
+
"line": 42,
|
|
318
|
+
"description": "Detailed explanation",
|
|
319
|
+
"evidence": "Code snippet or reasoning",
|
|
320
|
+
"recommendation": "How to fix"
|
|
321
|
+
}
|
|
322
|
+
],
|
|
323
|
+
"summary": {
|
|
324
|
+
"files_scanned": 0,
|
|
325
|
+
"total_findings": 0,
|
|
326
|
+
"by_severity": { "P0": 0, "P1": 0, "P2": 0, "P3": 0 }
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
IMPORTANT: You MUST write the findings JSON file when complete. This is how the orchestrator knows you're done.
|
|
331
|
+
Start by spawning ALL ${analyzers.length} analyzers now, in parallel.`;
|
|
204
332
|
}
|
|
205
333
|
|
|
206
334
|
/**
|
|
@@ -225,7 +353,8 @@ function spawnOneSession({
|
|
|
225
353
|
options.target,
|
|
226
354
|
options.traceId,
|
|
227
355
|
sentinelDir,
|
|
228
|
-
options.audit
|
|
356
|
+
options.audit,
|
|
357
|
+
model
|
|
229
358
|
);
|
|
230
359
|
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
231
360
|
|
|
@@ -248,7 +377,7 @@ function spawnOneSession({
|
|
|
248
377
|
{ stdio: 'pipe' }
|
|
249
378
|
);
|
|
250
379
|
|
|
251
|
-
const claudeCmd = `echo '${escapedPrompt}' | claude --model ${model} --allowedTools 'Read Glob Grep Write' 2>&1; echo "AUDIT_COMPLETE: ${analyzer.key}"`;
|
|
380
|
+
const claudeCmd = `echo '${escapedPrompt}' | claude --model ${model} --allowedTools 'Read Glob Grep Write Agent' 2>&1; echo "AUDIT_COMPLETE: ${analyzer.key}"`;
|
|
252
381
|
execFileSync('tmux', ['send-keys', '-t', `${sessionName}:${windowName}`, claudeCmd, 'Enter'], {
|
|
253
382
|
stdio: 'pipe',
|
|
254
383
|
});
|
|
@@ -295,44 +424,202 @@ async function spawnAuditInTmux(options) {
|
|
|
295
424
|
process.exit(1);
|
|
296
425
|
}
|
|
297
426
|
|
|
298
|
-
const
|
|
427
|
+
const isExtreme = options.depth === 'extreme';
|
|
428
|
+
const depthForRegistry = isExtreme ? 'extreme' : 'ultradeep';
|
|
429
|
+
const result = getAnalyzersForAudit(options.audit, depthForRegistry, options.focus);
|
|
299
430
|
if (!result || result.analyzers.length === 0) {
|
|
300
431
|
console.error(`No analyzers found for ${options.audit} with focus: ${options.focus.join(',')}`);
|
|
301
432
|
process.exit(1);
|
|
302
433
|
}
|
|
303
434
|
|
|
435
|
+
const sentinelDir = createSentinelDir(rootDir, options.traceId);
|
|
436
|
+
|
|
437
|
+
// Use stderr for human output when --json mode is active
|
|
438
|
+
const log = options.json ? console.error : console.log;
|
|
439
|
+
|
|
440
|
+
// --- EXTREME MODE: partition-based multi-agent ---
|
|
441
|
+
if (isExtreme) {
|
|
442
|
+
if (!options.partitions || options.partitions.length === 0) {
|
|
443
|
+
console.error('EXTREME mode requires --partitions=dir1,dir2,...');
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const partitions = options.partitions;
|
|
448
|
+
const partitionSlugs = partitions.map(p => partitionSlug(p));
|
|
449
|
+
|
|
450
|
+
// Write status file with partition info
|
|
451
|
+
const statusData = {
|
|
452
|
+
started_at: new Date().toISOString(),
|
|
453
|
+
audit_type: options.audit,
|
|
454
|
+
mode: 'extreme',
|
|
455
|
+
partitions: partitions,
|
|
456
|
+
analyzers: partitionSlugs,
|
|
457
|
+
analyzers_per_partition: result.analyzers.map(a => a.key),
|
|
458
|
+
completed: [],
|
|
459
|
+
failed: [],
|
|
460
|
+
target: options.target,
|
|
461
|
+
model: options.model,
|
|
462
|
+
timeout_minutes: options.timeout,
|
|
463
|
+
};
|
|
464
|
+
fs.writeFileSync(
|
|
465
|
+
path.join(sentinelDir, '_status.json'),
|
|
466
|
+
JSON.stringify(statusData, null, 2) + '\n'
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const groupColor = getColorForAudit(options.audit);
|
|
470
|
+
const totalSessions = partitions.length * result.analyzers.length;
|
|
471
|
+
|
|
472
|
+
if (options.dryRun) {
|
|
473
|
+
log(`\nDry run - EXTREME mode would spawn ${partitions.length} partition coordinators:`);
|
|
474
|
+
log(` Partitions: ${partitions.join(', ')}`);
|
|
475
|
+
log(` Analyzers per partition: ${result.analyzers.length}`);
|
|
476
|
+
log(` Total agent sessions: ${totalSessions}`);
|
|
477
|
+
const model = resolveModel(options.model, 'haiku');
|
|
478
|
+
for (const p of partitions) {
|
|
479
|
+
const slug = partitionSlug(p);
|
|
480
|
+
log(
|
|
481
|
+
` ${auditType.prefix}:${slug} (${model}) → ALL ${result.analyzers.length} analyzers on ${p}`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
log(`\nSentinel dir: ${sentinelDir}`);
|
|
485
|
+
log(`Group color: ${groupColor}`);
|
|
486
|
+
return {
|
|
487
|
+
ok: true,
|
|
488
|
+
traceId: options.traceId,
|
|
489
|
+
sentinelDir,
|
|
490
|
+
sessions: [],
|
|
491
|
+
dryRun: true,
|
|
492
|
+
mode: 'extreme',
|
|
493
|
+
partitions,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const tmux = checkTmux();
|
|
498
|
+
if (!tmux.available) {
|
|
499
|
+
console.error('tmux is not available. EXTREME mode requires tmux.');
|
|
500
|
+
console.error('Falling back to DEPTH=deep mode.');
|
|
501
|
+
return { ok: false, traceId: options.traceId, sentinelDir, sessions: [], fallback: 'deep' };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const sessionName = `audit-${options.audit}-${options.traceId.slice(0, 8)}`;
|
|
505
|
+
const sessions = [];
|
|
506
|
+
const config = getUltradeepConfig();
|
|
507
|
+
const staggerMs =
|
|
508
|
+
((options.stagger != null ? options.stagger : config.stagger_seconds) || 0) * 1000;
|
|
509
|
+
|
|
510
|
+
for (let i = 0; i < partitions.length; i++) {
|
|
511
|
+
if (i > 0 && staggerMs > 0) await sleep(staggerMs);
|
|
512
|
+
|
|
513
|
+
const partition = partitions[i];
|
|
514
|
+
const slug = partitionSlug(partition);
|
|
515
|
+
const windowName = `${auditType.prefix}:${slug}`;
|
|
516
|
+
const model = resolveModel(options.model, 'haiku');
|
|
517
|
+
const prompt = buildExtremePrompt(
|
|
518
|
+
partition,
|
|
519
|
+
result.analyzers,
|
|
520
|
+
options.traceId,
|
|
521
|
+
sentinelDir,
|
|
522
|
+
options.audit,
|
|
523
|
+
model
|
|
524
|
+
);
|
|
525
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
if (i === 0) {
|
|
529
|
+
execFileSync(
|
|
530
|
+
'tmux',
|
|
531
|
+
['new-session', '-d', '-s', sessionName, '-n', windowName, '-c', rootDir],
|
|
532
|
+
{ stdio: 'pipe' }
|
|
533
|
+
);
|
|
534
|
+
} else {
|
|
535
|
+
execFileSync('tmux', ['new-window', '-t', sessionName, '-n', windowName, '-c', rootDir], {
|
|
536
|
+
stdio: 'pipe',
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
execFileSync(
|
|
541
|
+
'tmux',
|
|
542
|
+
['set-option', '-w', '-t', `${sessionName}:${windowName}`, '@group_color', groupColor],
|
|
543
|
+
{ stdio: 'pipe' }
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const claudeCmd = `echo '${escapedPrompt}' | claude --model ${model} --allowedTools 'Read Glob Grep Write Agent' 2>&1; echo "AUDIT_COMPLETE: ${slug}"`;
|
|
547
|
+
execFileSync(
|
|
548
|
+
'tmux',
|
|
549
|
+
['send-keys', '-t', `${sessionName}:${windowName}`, claudeCmd, 'Enter'],
|
|
550
|
+
{
|
|
551
|
+
stdio: 'pipe',
|
|
552
|
+
}
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
sessions.push(windowName);
|
|
556
|
+
} catch (err) {
|
|
557
|
+
console.error(`Failed to spawn ${windowName}: ${err.message}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Apply status bar theme
|
|
562
|
+
try {
|
|
563
|
+
const tmuxScript = path.join(__dirname, 'claude-tmux.sh');
|
|
564
|
+
execFileSync(tmuxScript, [`--configure-session=${sessionName}`], { stdio: 'pipe' });
|
|
565
|
+
} catch (_) {
|
|
566
|
+
// Non-critical
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
log(`\nSpawned ${sessions.length} partition coordinators in tmux session: ${sessionName}`);
|
|
570
|
+
log(` Partitions: ${partitions.join(', ')}`);
|
|
571
|
+
log(` Analyzers per partition: ${result.analyzers.length}`);
|
|
572
|
+
log(` Total agent sessions: ${totalSessions}`);
|
|
573
|
+
log(`Sentinel dir: ${sentinelDir}`);
|
|
574
|
+
log(`Attach with: tmux attach -t ${sessionName}`);
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
ok: true,
|
|
578
|
+
traceId: options.traceId,
|
|
579
|
+
sentinelDir,
|
|
580
|
+
sessions,
|
|
581
|
+
sessionName,
|
|
582
|
+
mode: 'extreme',
|
|
583
|
+
partitions,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// --- ULTRADEEP MODE (existing behavior) ---
|
|
588
|
+
|
|
304
589
|
// Enforce session limit
|
|
305
590
|
if (result.analyzers.length > 20) {
|
|
306
591
|
console.error(`Too many analyzers (${result.analyzers.length}). Maximum is 20.`);
|
|
307
592
|
process.exit(1);
|
|
308
593
|
}
|
|
309
594
|
|
|
310
|
-
const sentinelDir = createSentinelDir(rootDir, options.traceId);
|
|
311
|
-
|
|
312
595
|
// Resolve stagger and concurrency from CLI flags or config
|
|
313
596
|
const config = getUltradeepConfig();
|
|
314
597
|
const staggerMs =
|
|
315
598
|
((options.stagger != null ? options.stagger : config.stagger_seconds) || 0) * 1000;
|
|
316
599
|
const maxConcurrent = options.concurrency != null ? options.concurrency : config.max_concurrent;
|
|
317
600
|
|
|
318
|
-
writeStatusFile(sentinelDir, options.audit, result.analyzers, staggerMs, maxConcurrent
|
|
601
|
+
writeStatusFile(sentinelDir, options.audit, result.analyzers, staggerMs, maxConcurrent, {
|
|
602
|
+
target: options.target,
|
|
603
|
+
model: options.model,
|
|
604
|
+
timeout_minutes: options.timeout,
|
|
605
|
+
});
|
|
319
606
|
|
|
320
607
|
const groupColor = getColorForAudit(options.audit);
|
|
321
608
|
const sessions = [];
|
|
322
609
|
|
|
323
610
|
if (options.dryRun) {
|
|
324
|
-
|
|
325
|
-
|
|
611
|
+
log(`\nDry run - would spawn ${result.analyzers.length} sessions:`);
|
|
612
|
+
log(` Stagger: ${staggerMs / 1000}s between launches`);
|
|
326
613
|
if (maxConcurrent > 0) {
|
|
327
614
|
const waveCount = Math.ceil(result.analyzers.length / maxConcurrent);
|
|
328
|
-
|
|
615
|
+
log(` Concurrency: ${maxConcurrent}/wave (${waveCount} waves)`);
|
|
329
616
|
}
|
|
330
617
|
for (const analyzer of result.analyzers) {
|
|
331
618
|
const model = resolveModel(options.model, 'haiku');
|
|
332
|
-
|
|
619
|
+
log(` ${auditType.prefix}:${analyzer.key} (${model}) → ${analyzer.label}`);
|
|
333
620
|
}
|
|
334
|
-
|
|
335
|
-
|
|
621
|
+
log(`\nSentinel dir: ${sentinelDir}`);
|
|
622
|
+
log(`Group color: ${groupColor}`);
|
|
336
623
|
return { ok: true, traceId: options.traceId, sentinelDir, sessions: [], dryRun: true };
|
|
337
624
|
}
|
|
338
625
|
|
|
@@ -400,9 +687,9 @@ async function spawnAuditInTmux(options) {
|
|
|
400
687
|
// Non-critical styling failure — audit session still works with default theme
|
|
401
688
|
}
|
|
402
689
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
690
|
+
log(`\nSpawned ${sessions.length} analyzer sessions in tmux session: ${sessionName}`);
|
|
691
|
+
log(`Sentinel dir: ${sentinelDir}`);
|
|
692
|
+
log(`Attach with: tmux attach -t ${sessionName}`);
|
|
406
693
|
|
|
407
694
|
return { ok: true, traceId: options.traceId, sentinelDir, sessions, sessionName };
|
|
408
695
|
}
|
|
@@ -460,7 +747,9 @@ async function pollForCompletion(sentinelDir, expected, timeoutMinutes) {
|
|
|
460
747
|
/**
|
|
461
748
|
* Collect all findings from sentinel directory.
|
|
462
749
|
* @param {string} sentinelDir - Sentinel directory path
|
|
463
|
-
* @param {string[]} expected - Expected analyzer keys
|
|
750
|
+
* @param {string[]} expected - Expected keys. In ultradeep mode these are analyzer keys
|
|
751
|
+
* (e.g. 'injection', 'auth'). In extreme mode these are partition slugs
|
|
752
|
+
* (e.g. 'src-auth', 'src-api') — matches the `analyzers` array in `_status.json`.
|
|
464
753
|
* @returns {object[]} Array of parsed findings
|
|
465
754
|
*/
|
|
466
755
|
function collectResults(sentinelDir, expected) {
|
|
@@ -490,18 +779,34 @@ function collectResults(sentinelDir, expected) {
|
|
|
490
779
|
* @param {string} auditType - Audit type key
|
|
491
780
|
* @param {number} analyzerCount - Number of analyzers to spawn
|
|
492
781
|
* @param {string} [model] - Explicit model override
|
|
782
|
+
* @param {object} [opts] - Options
|
|
783
|
+
* @param {boolean} [opts.json] - If true, route output to stderr to keep stdout clean for JSON
|
|
784
|
+
* @param {number} [opts.partitions] - Number of partitions (extreme mode)
|
|
493
785
|
*/
|
|
494
|
-
function showCostEstimate(auditType, analyzerCount, model) {
|
|
786
|
+
function showCostEstimate(auditType, analyzerCount, model, opts) {
|
|
495
787
|
const resolved = resolveModel(model, 'haiku');
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
console.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
788
|
+
const partCount =
|
|
789
|
+
opts && typeof opts.partitions === 'number' && opts.partitions > 1 ? opts.partitions : 1;
|
|
790
|
+
const estimate = estimateCost(resolved, analyzerCount, partCount);
|
|
791
|
+
const log = opts && opts.json ? console.error : console.log;
|
|
792
|
+
|
|
793
|
+
if (partCount > 1) {
|
|
794
|
+
log(`\nCost estimate for EXTREME ${auditType} audit:`);
|
|
795
|
+
log(` Model: ${estimate.model}`);
|
|
796
|
+
log(` Partitions: ${partCount}`);
|
|
797
|
+
log(` Analyzers per partition: ${analyzerCount}`);
|
|
798
|
+
log(` Total agent sessions: ${estimate.totalSessions}`);
|
|
799
|
+
log(` Per-agent estimate: ${estimate.perAnalyzerCost}`);
|
|
800
|
+
log(` Total estimate: ${estimate.totalEstimate}`);
|
|
801
|
+
} else {
|
|
802
|
+
log(`\nCost estimate for ULTRADEEP ${auditType} audit:`);
|
|
803
|
+
log(` Model: ${estimate.model}`);
|
|
804
|
+
log(` Analyzers: ${analyzerCount}`);
|
|
805
|
+
log(` Cost multiplier vs haiku: ${estimate.multiplier}x`);
|
|
806
|
+
log(` Per-analyzer estimate: ${estimate.perAnalyzerCost}`);
|
|
807
|
+
log(` Total estimate: ${estimate.totalEstimate}`);
|
|
808
|
+
log(` Each analyzer runs as a full Claude Code session`);
|
|
809
|
+
}
|
|
505
810
|
}
|
|
506
811
|
|
|
507
812
|
// Main
|
|
@@ -517,12 +822,33 @@ if (require.main === module) {
|
|
|
517
822
|
process.exit(1);
|
|
518
823
|
}
|
|
519
824
|
|
|
520
|
-
const
|
|
825
|
+
const isExtreme = options.depth === 'extreme';
|
|
826
|
+
const depthForCost = isExtreme ? 'extreme' : 'ultradeep';
|
|
827
|
+
const result = getAnalyzersForAudit(options.audit, depthForCost, options.focus);
|
|
521
828
|
if (result) {
|
|
522
|
-
showCostEstimate(options.audit, result.analyzers.length, options.model
|
|
829
|
+
showCostEstimate(options.audit, result.analyzers.length, options.model, {
|
|
830
|
+
json: options.json,
|
|
831
|
+
partitions: isExtreme && options.partitions ? options.partitions.length : 1,
|
|
832
|
+
});
|
|
523
833
|
}
|
|
524
834
|
|
|
525
835
|
const spawnResult = await spawnAuditInTmux(options);
|
|
836
|
+
|
|
837
|
+
if (options.json) {
|
|
838
|
+
const jsonOut = {
|
|
839
|
+
ok: spawnResult.ok,
|
|
840
|
+
traceId: spawnResult.traceId,
|
|
841
|
+
sentinelDir: spawnResult.sentinelDir,
|
|
842
|
+
sessionName: spawnResult.sessionName || null,
|
|
843
|
+
sessions: spawnResult.sessions,
|
|
844
|
+
dryRun: spawnResult.dryRun || false,
|
|
845
|
+
fallback: spawnResult.fallback || null,
|
|
846
|
+
mode: spawnResult.mode || 'ultradeep',
|
|
847
|
+
partitions: spawnResult.partitions || null,
|
|
848
|
+
};
|
|
849
|
+
console.log(JSON.stringify(jsonOut));
|
|
850
|
+
}
|
|
851
|
+
|
|
526
852
|
if (!spawnResult.ok && spawnResult.fallback) {
|
|
527
853
|
process.exit(2); // Signal fallback to caller
|
|
528
854
|
}
|
|
@@ -538,6 +864,8 @@ module.exports = {
|
|
|
538
864
|
createSentinelDir,
|
|
539
865
|
writeStatusFile,
|
|
540
866
|
buildAnalyzerPrompt,
|
|
867
|
+
buildExtremePrompt,
|
|
868
|
+
partitionSlug,
|
|
541
869
|
spawnOneSession,
|
|
542
870
|
spawnAuditInTmux,
|
|
543
871
|
pollForCompletion,
|
package/scripts/team-manager.js
CHANGED
|
@@ -492,6 +492,25 @@ function stopTeam(rootDir) {
|
|
|
492
492
|
// Non-critical - metrics aggregation is best-effort
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
+
// Reconcile teammate final states to status.json (AC2/AC3)
|
|
496
|
+
try {
|
|
497
|
+
const taskSync = require('./lib/task-sync');
|
|
498
|
+
const nativeTasks = (team.teammates || [])
|
|
499
|
+
.filter(t => t.status === 'completed' || t.status === 'done')
|
|
500
|
+
.map(t => ({
|
|
501
|
+
id: t.agent,
|
|
502
|
+
status: 'completed',
|
|
503
|
+
metadata: { story_id: t.story_id },
|
|
504
|
+
}))
|
|
505
|
+
.filter(t => t.metadata.story_id);
|
|
506
|
+
|
|
507
|
+
if (nativeTasks.length > 0) {
|
|
508
|
+
taskSync.reconcile(rootDir, nativeTasks);
|
|
509
|
+
}
|
|
510
|
+
} catch (e) {
|
|
511
|
+
// Non-critical - reconciliation is best-effort
|
|
512
|
+
}
|
|
513
|
+
|
|
495
514
|
return {
|
|
496
515
|
ok: true,
|
|
497
516
|
template: team.template,
|