ctx-cc 3.5.0 → 4.1.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/README.md +375 -676
- package/agents/ctx-arch-mapper.md +5 -3
- package/agents/ctx-auditor.md +5 -3
- package/agents/ctx-codex-reviewer.md +214 -0
- package/agents/ctx-concerns-mapper.md +5 -3
- package/agents/ctx-criteria-suggester.md +6 -4
- package/agents/ctx-debugger.md +5 -3
- package/agents/ctx-designer.md +488 -114
- package/agents/ctx-discusser.md +5 -3
- package/agents/ctx-executor.md +5 -3
- package/agents/ctx-handoff.md +6 -4
- package/agents/ctx-learner.md +5 -3
- package/agents/ctx-mapper.md +4 -3
- package/agents/ctx-ml-analyst.md +600 -0
- package/agents/ctx-ml-engineer.md +933 -0
- package/agents/ctx-ml-reviewer.md +485 -0
- package/agents/ctx-ml-scientist.md +626 -0
- package/agents/ctx-parallelizer.md +4 -3
- package/agents/ctx-planner.md +5 -3
- package/agents/ctx-predictor.md +4 -3
- package/agents/ctx-qa.md +5 -3
- package/agents/ctx-quality-mapper.md +5 -3
- package/agents/ctx-researcher.md +5 -3
- package/agents/ctx-reviewer.md +6 -4
- package/agents/ctx-team-coordinator.md +5 -3
- package/agents/ctx-tech-mapper.md +5 -3
- package/agents/ctx-verifier.md +5 -3
- package/bin/ctx.js +199 -27
- package/commands/brand.md +309 -0
- package/commands/ctx.md +10 -10
- package/commands/design.md +304 -0
- package/commands/experiment.md +251 -0
- package/commands/help.md +57 -7
- package/commands/init.md +25 -0
- package/commands/metrics.md +1 -1
- package/commands/milestone.md +1 -1
- package/commands/ml-status.md +197 -0
- package/commands/monitor.md +1 -1
- package/commands/train.md +266 -0
- package/commands/visual-qa.md +559 -0
- package/commands/voice.md +1 -1
- package/hooks/post-tool-use.js +39 -0
- package/hooks/pre-tool-use.js +94 -0
- package/hooks/subagent-stop.js +32 -0
- package/package.json +9 -3
- package/plugin.json +46 -0
- package/skills/ctx-design-system/SKILL.md +572 -0
- package/skills/ctx-ml-experiment/SKILL.md +334 -0
- package/skills/ctx-ml-pipeline/SKILL.md +437 -0
- package/skills/ctx-orchestrator/SKILL.md +91 -0
- package/skills/ctx-review-gate/SKILL.md +147 -0
- package/skills/ctx-state/SKILL.md +100 -0
- package/skills/ctx-visual-qa/SKILL.md +587 -0
- package/src/agents.js +109 -0
- package/src/auto.js +287 -0
- package/src/capabilities.js +226 -0
- package/src/commits.js +94 -0
- package/src/config.js +112 -0
- package/src/context.js +241 -0
- package/src/handoff.js +156 -0
- package/src/hooks.js +218 -0
- package/src/install.js +125 -50
- package/src/lifecycle.js +194 -0
- package/src/metrics.js +198 -0
- package/src/pipeline.js +269 -0
- package/src/review-gate.js +338 -0
- package/src/runner.js +120 -0
- package/src/skills.js +143 -0
- package/src/state.js +267 -0
- package/src/worktree.js +244 -0
- package/templates/PRD.json +1 -1
- package/templates/config.json +4 -237
- package/workflows/ctx-router.md +0 -485
- package/workflows/map-codebase.md +0 -329
package/src/state.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const STATE_FILE = 'STATE.json';
|
|
5
|
+
const LOCK_FILE = 'STATE.lock';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Valid phases and their allowed transitions.
|
|
9
|
+
*/
|
|
10
|
+
const TRANSITIONS = {
|
|
11
|
+
init: ['plan'],
|
|
12
|
+
plan: ['execute'],
|
|
13
|
+
execute: ['verify'],
|
|
14
|
+
verify: ['complete', 'execute'], // execute = fix failures
|
|
15
|
+
complete: ['init'], // reset for next story
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Any phase can reset to init
|
|
19
|
+
const UNIVERSAL_TARGETS = ['init'];
|
|
20
|
+
|
|
21
|
+
const VALID_PHASES = Object.keys(TRANSITIONS);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a fresh state object for a new project/story.
|
|
25
|
+
*/
|
|
26
|
+
export function createInitialState(storyId = null, storyTitle = '') {
|
|
27
|
+
return {
|
|
28
|
+
version: '4.0',
|
|
29
|
+
phase: 'init',
|
|
30
|
+
activeStory: storyId,
|
|
31
|
+
storyTitle,
|
|
32
|
+
completedTasks: [],
|
|
33
|
+
agentHistory: [],
|
|
34
|
+
pipeline: null,
|
|
35
|
+
session: {
|
|
36
|
+
startedAt: isoNow(),
|
|
37
|
+
lastActivity: isoNow(),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the .ctx directory for a project.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveCtxDir(projectDir = process.cwd()) {
|
|
46
|
+
return path.join(projectDir, '.ctx');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read STATE.json from .ctx directory. Returns null if not found.
|
|
51
|
+
*/
|
|
52
|
+
export function readState(ctxDir) {
|
|
53
|
+
const statePath = path.join(ctxDir, STATE_FILE);
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Write STATE.json to .ctx directory.
|
|
63
|
+
* Updates session.lastActivity automatically.
|
|
64
|
+
*/
|
|
65
|
+
export function writeState(ctxDir, state) {
|
|
66
|
+
if (!fs.existsSync(ctxDir)) fs.mkdirSync(ctxDir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
state.session = state.session || {};
|
|
69
|
+
state.session.lastActivity = isoNow();
|
|
70
|
+
|
|
71
|
+
const statePath = path.join(ctxDir, STATE_FILE);
|
|
72
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n');
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Initialize state for a new project. Creates .ctx/STATE.json.
|
|
78
|
+
* Returns the new state.
|
|
79
|
+
*/
|
|
80
|
+
export function initState(ctxDir, storyId = null, storyTitle = '') {
|
|
81
|
+
const state = createInitialState(storyId, storyTitle);
|
|
82
|
+
return writeState(ctxDir, state);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate a phase transition. Returns { valid, error }.
|
|
87
|
+
*/
|
|
88
|
+
export function validateTransition(currentPhase, targetPhase) {
|
|
89
|
+
if (!VALID_PHASES.includes(currentPhase)) {
|
|
90
|
+
return { valid: false, error: `Invalid current phase: "${currentPhase}". Valid phases: ${VALID_PHASES.join(', ')}` };
|
|
91
|
+
}
|
|
92
|
+
if (!VALID_PHASES.includes(targetPhase)) {
|
|
93
|
+
return { valid: false, error: `Invalid target phase: "${targetPhase}". Valid phases: ${VALID_PHASES.join(', ')}` };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const allowed = [...(TRANSITIONS[currentPhase] || []), ...UNIVERSAL_TARGETS];
|
|
97
|
+
if (!allowed.includes(targetPhase)) {
|
|
98
|
+
const hint = phaseHint(currentPhase, targetPhase);
|
|
99
|
+
return {
|
|
100
|
+
valid: false,
|
|
101
|
+
error: `Cannot transition from "${currentPhase}" to "${targetPhase}". Allowed: ${allowed.join(', ')}.${hint}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { valid: true, error: null };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Transition to a new phase. Validates and writes state.
|
|
110
|
+
* Returns the updated state or throws on invalid transition.
|
|
111
|
+
*/
|
|
112
|
+
export function transitionPhase(ctxDir, targetPhase) {
|
|
113
|
+
const state = readState(ctxDir);
|
|
114
|
+
if (!state) {
|
|
115
|
+
throw new Error('No STATE.json found. Run /ctx:init first.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { valid, error } = validateTransition(state.phase, targetPhase);
|
|
119
|
+
if (!valid) throw new Error(error);
|
|
120
|
+
|
|
121
|
+
state.phase = targetPhase;
|
|
122
|
+
return writeState(ctxDir, state);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Log an agent invocation to state history.
|
|
127
|
+
*/
|
|
128
|
+
export function logAgentInvocation(ctxDir, agentName, taskSummary) {
|
|
129
|
+
const state = readState(ctxDir);
|
|
130
|
+
if (!state) return null;
|
|
131
|
+
|
|
132
|
+
const entry = {
|
|
133
|
+
agent: agentName,
|
|
134
|
+
invokedAt: isoNow(),
|
|
135
|
+
completedAt: null,
|
|
136
|
+
taskSummary,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
state.agentHistory = state.agentHistory || [];
|
|
140
|
+
state.agentHistory.push(entry);
|
|
141
|
+
writeState(ctxDir, state);
|
|
142
|
+
return entry;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Mark the last invocation of an agent as completed.
|
|
147
|
+
*/
|
|
148
|
+
export function completeAgentInvocation(ctxDir, agentName) {
|
|
149
|
+
const state = readState(ctxDir);
|
|
150
|
+
if (!state || !state.agentHistory) return;
|
|
151
|
+
|
|
152
|
+
// Find last incomplete invocation of this agent
|
|
153
|
+
for (let i = state.agentHistory.length - 1; i >= 0; i--) {
|
|
154
|
+
if (state.agentHistory[i].agent === agentName && !state.agentHistory[i].completedAt) {
|
|
155
|
+
state.agentHistory[i].completedAt = isoNow();
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
writeState(ctxDir, state);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Record a completed task.
|
|
165
|
+
*/
|
|
166
|
+
export function recordCompletedTask(ctxDir, taskId, title, criteriaIds = []) {
|
|
167
|
+
const state = readState(ctxDir);
|
|
168
|
+
if (!state) return;
|
|
169
|
+
|
|
170
|
+
state.completedTasks = state.completedTasks || [];
|
|
171
|
+
state.completedTasks.push({
|
|
172
|
+
taskId,
|
|
173
|
+
title,
|
|
174
|
+
completedAt: isoNow(),
|
|
175
|
+
criteriaSatisfied: criteriaIds,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
writeState(ctxDir, state);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Acquire a simple lockfile. Returns true if acquired, false if already locked.
|
|
183
|
+
*/
|
|
184
|
+
export function acquireLock(ctxDir) {
|
|
185
|
+
const lockPath = path.join(ctxDir, LOCK_FILE);
|
|
186
|
+
try {
|
|
187
|
+
fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, lockedAt: isoNow() }), { flag: 'wx' });
|
|
188
|
+
return true;
|
|
189
|
+
} catch {
|
|
190
|
+
// Check if lock is stale (PID no longer running)
|
|
191
|
+
try {
|
|
192
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
|
|
193
|
+
try {
|
|
194
|
+
process.kill(lock.pid, 0); // Test if process exists
|
|
195
|
+
return false; // Process still running, lock is valid
|
|
196
|
+
} catch {
|
|
197
|
+
// Process dead, steal the lock
|
|
198
|
+
fs.unlinkSync(lockPath);
|
|
199
|
+
return acquireLock(ctxDir);
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Release the lockfile.
|
|
209
|
+
*/
|
|
210
|
+
export function releaseLock(ctxDir) {
|
|
211
|
+
const lockPath = path.join(ctxDir, LOCK_FILE);
|
|
212
|
+
try {
|
|
213
|
+
fs.unlinkSync(lockPath);
|
|
214
|
+
} catch {
|
|
215
|
+
// Already released
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Format state for human-readable display.
|
|
221
|
+
*/
|
|
222
|
+
export function formatStatus(state) {
|
|
223
|
+
if (!state) return ' No CTX state found. Run /ctx:init to initialize.';
|
|
224
|
+
|
|
225
|
+
const lines = [];
|
|
226
|
+
lines.push(` Phase: ${state.phase}`);
|
|
227
|
+
lines.push(` Story: ${state.activeStory || 'none'} ${state.storyTitle ? `— ${state.storyTitle}` : ''}`);
|
|
228
|
+
lines.push(` Tasks done: ${(state.completedTasks || []).length}`);
|
|
229
|
+
lines.push(` Agent calls: ${(state.agentHistory || []).length}`);
|
|
230
|
+
lines.push(` Last active: ${state.session?.lastActivity || 'unknown'}`);
|
|
231
|
+
|
|
232
|
+
const nextAction = NEXT_ACTIONS[state.phase];
|
|
233
|
+
if (nextAction) {
|
|
234
|
+
lines.push('');
|
|
235
|
+
lines.push(` Next action: ${nextAction}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return lines.join('\n');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// --- internal ---
|
|
242
|
+
|
|
243
|
+
const NEXT_ACTIONS = {
|
|
244
|
+
init: '/ctx:plan — create an execution plan',
|
|
245
|
+
plan: '/ctx:execute — start building',
|
|
246
|
+
execute: '/ctx:verify — verify acceptance criteria',
|
|
247
|
+
verify: '/ctx:next — advance to next story or fix failures',
|
|
248
|
+
complete: '/ctx:next — pick next story from PRD',
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
function isoNow() {
|
|
252
|
+
return new Date().toISOString();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Provide helpful hints for common invalid transitions
|
|
256
|
+
function phaseHint(current, target) {
|
|
257
|
+
const hints = {
|
|
258
|
+
'init→execute': ' Run /ctx:plan first to create a plan.',
|
|
259
|
+
'init→verify': ' Run /ctx:plan then /ctx:execute first.',
|
|
260
|
+
'plan→verify': ' Run /ctx:execute first to build the code.',
|
|
261
|
+
'plan→complete': ' Run /ctx:execute then /ctx:verify first.',
|
|
262
|
+
'execute→complete': ' Run /ctx:verify first to check acceptance criteria.',
|
|
263
|
+
};
|
|
264
|
+
return hints[`${current}→${target}`] || '';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export { VALID_PHASES, TRANSITIONS };
|
package/src/worktree.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { readState, writeState } from './state.js';
|
|
5
|
+
import { buildContext } from './context.js';
|
|
6
|
+
import { runAgent } from './runner.js';
|
|
7
|
+
|
|
8
|
+
const WORKTREES_DIR = '.ctx/worktrees';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a dependency graph from the parallelizer agent output.
|
|
12
|
+
* Input format: array of { taskId, title, dependsOn: [taskId] }
|
|
13
|
+
* Returns waves: [ [task, task], [task], ... ]
|
|
14
|
+
*/
|
|
15
|
+
export function buildWaves(tasks) {
|
|
16
|
+
if (!tasks || tasks.length === 0) return [];
|
|
17
|
+
|
|
18
|
+
const completed = new Set();
|
|
19
|
+
const waves = [];
|
|
20
|
+
const remaining = [...tasks];
|
|
21
|
+
let safetyCounter = 0;
|
|
22
|
+
|
|
23
|
+
while (remaining.length > 0 && safetyCounter < 100) {
|
|
24
|
+
safetyCounter++;
|
|
25
|
+
const wave = [];
|
|
26
|
+
|
|
27
|
+
for (let i = remaining.length - 1; i >= 0; i--) {
|
|
28
|
+
const task = remaining[i];
|
|
29
|
+
const deps = task.dependsOn || [];
|
|
30
|
+
const allDepsComplete = deps.every(d => completed.has(d));
|
|
31
|
+
|
|
32
|
+
if (allDepsComplete) {
|
|
33
|
+
wave.push(task);
|
|
34
|
+
remaining.splice(i, 1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (wave.length === 0) {
|
|
39
|
+
// Circular dependency — force remaining into a single wave
|
|
40
|
+
waves.push(remaining.splice(0));
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
waves.push(wave);
|
|
45
|
+
for (const t of wave) completed.add(t.taskId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return waves;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a git worktree for a task.
|
|
53
|
+
* Returns the worktree path.
|
|
54
|
+
*/
|
|
55
|
+
export function createWorktree(projectDir, taskId) {
|
|
56
|
+
const worktreeBase = path.join(projectDir, WORKTREES_DIR);
|
|
57
|
+
if (!fs.existsSync(worktreeBase)) fs.mkdirSync(worktreeBase, { recursive: true });
|
|
58
|
+
|
|
59
|
+
const worktreePath = path.join(worktreeBase, taskId);
|
|
60
|
+
const branchName = `ctx-task-${taskId}`;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Create branch and worktree
|
|
64
|
+
execSync(`git worktree add "${worktreePath}" -b "${branchName}" HEAD`, {
|
|
65
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 10000,
|
|
66
|
+
});
|
|
67
|
+
return worktreePath;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
throw new Error(`Failed to create worktree for ${taskId}: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Merge a worktree branch back to the current branch.
|
|
75
|
+
* Returns { merged: boolean, conflicts: boolean, error: string|null }
|
|
76
|
+
*/
|
|
77
|
+
export function mergeWorktree(projectDir, taskId) {
|
|
78
|
+
const branchName = `ctx-task-${taskId}`;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const result = execSync(`git merge --no-ff "${branchName}" -m "ctx(merge): task ${taskId}"`, {
|
|
82
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 10000,
|
|
83
|
+
});
|
|
84
|
+
return { merged: true, conflicts: false, error: null };
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err.message.includes('CONFLICT') || err.message.includes('Merge conflict')) {
|
|
87
|
+
// Abort the merge
|
|
88
|
+
try { execSync('git merge --abort', { cwd: projectDir, timeout: 5000 }); } catch {}
|
|
89
|
+
return { merged: false, conflicts: true, error: `Merge conflicts in task ${taskId}` };
|
|
90
|
+
}
|
|
91
|
+
return { merged: false, conflicts: false, error: err.message };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Clean up a worktree and its branch.
|
|
97
|
+
*/
|
|
98
|
+
export function cleanupWorktree(projectDir, taskId) {
|
|
99
|
+
const worktreePath = path.join(projectDir, WORKTREES_DIR, taskId);
|
|
100
|
+
const branchName = `ctx-task-${taskId}`;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
execSync(`git worktree remove "${worktreePath}" --force`, {
|
|
104
|
+
cwd: projectDir, timeout: 10000,
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
// Manual cleanup
|
|
108
|
+
if (fs.existsSync(worktreePath)) fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
109
|
+
try { execSync('git worktree prune', { cwd: projectDir, timeout: 5000 }); } catch {}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Delete branch
|
|
113
|
+
try {
|
|
114
|
+
execSync(`git branch -D "${branchName}"`, { cwd: projectDir, timeout: 5000 });
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Execute a wave of tasks in parallel using git worktrees.
|
|
120
|
+
* Each task runs in its own worktree with a separate claude process.
|
|
121
|
+
*
|
|
122
|
+
* Returns { results: [{ taskId, success, error }], conflicts: [taskId] }
|
|
123
|
+
*/
|
|
124
|
+
export async function executeWave({ wave, agentsDir, agentFile, projectDir, ctxDir, streaming = false, timeout = 300000 }) {
|
|
125
|
+
const agentPath = path.join(agentsDir, agentFile);
|
|
126
|
+
|
|
127
|
+
// Create worktrees for all tasks
|
|
128
|
+
const worktrees = [];
|
|
129
|
+
for (const task of wave) {
|
|
130
|
+
try {
|
|
131
|
+
const wtPath = createWorktree(projectDir, task.taskId);
|
|
132
|
+
worktrees.push({ task, worktreePath: wtPath });
|
|
133
|
+
} catch (err) {
|
|
134
|
+
worktrees.push({ task, worktreePath: null, error: err.message });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Run agents in parallel (non-streaming for parallel execution)
|
|
139
|
+
const promises = worktrees.map(async ({ task, worktreePath, error }) => {
|
|
140
|
+
if (error) return { taskId: task.taskId, success: false, error };
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const { context } = buildContext('execute', worktreePath, ctxDir);
|
|
144
|
+
await runAgent({
|
|
145
|
+
agentPath,
|
|
146
|
+
message: `Execute task: ${task.title}\n\n${task.description || ''}`,
|
|
147
|
+
streaming: false, // Must be non-streaming for parallel
|
|
148
|
+
timeout,
|
|
149
|
+
context,
|
|
150
|
+
});
|
|
151
|
+
return { taskId: task.taskId, success: true, error: null };
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return { taskId: task.taskId, success: false, error: err.message };
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const results = await Promise.all(promises);
|
|
158
|
+
|
|
159
|
+
// Merge successful worktrees back
|
|
160
|
+
const conflicts = [];
|
|
161
|
+
for (const result of results) {
|
|
162
|
+
if (result.success) {
|
|
163
|
+
const merge = mergeWorktree(projectDir, result.taskId);
|
|
164
|
+
if (merge.conflicts) {
|
|
165
|
+
conflicts.push(result.taskId);
|
|
166
|
+
result.success = false;
|
|
167
|
+
result.error = merge.error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Cleanup worktree
|
|
171
|
+
cleanupWorktree(projectDir, result.taskId);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { results, conflicts };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Execute all waves sequentially (wave N+1 waits for wave N).
|
|
179
|
+
*
|
|
180
|
+
* Options:
|
|
181
|
+
* waves, agentsDir, agentFile, projectDir, ctxDir, streaming, timeout, onWave
|
|
182
|
+
*
|
|
183
|
+
* Returns { waveResults: [...], conflicts: [...] }
|
|
184
|
+
*/
|
|
185
|
+
export async function executeAllWaves({ waves, agentsDir, agentFile, projectDir, ctxDir, streaming = false, timeout = 300000, onWave = null }) {
|
|
186
|
+
const allResults = [];
|
|
187
|
+
const allConflicts = [];
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < waves.length; i++) {
|
|
190
|
+
const wave = waves[i];
|
|
191
|
+
if (onWave) onWave({ waveIndex: i, total: waves.length, taskCount: wave.length, status: 'starting' });
|
|
192
|
+
|
|
193
|
+
const { results, conflicts } = await executeWave({
|
|
194
|
+
wave, agentsDir, agentFile, projectDir, ctxDir, streaming, timeout,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
allResults.push({ waveIndex: i, results });
|
|
198
|
+
allConflicts.push(...conflicts);
|
|
199
|
+
|
|
200
|
+
if (onWave) onWave({ waveIndex: i, total: waves.length, taskCount: wave.length, status: 'completed', results });
|
|
201
|
+
|
|
202
|
+
// Update state with wave progress
|
|
203
|
+
const state = readState(ctxDir);
|
|
204
|
+
if (state) {
|
|
205
|
+
state.waveProgress = state.waveProgress || [];
|
|
206
|
+
state.waveProgress.push({
|
|
207
|
+
waveIndex: i,
|
|
208
|
+
completedAt: new Date().toISOString(),
|
|
209
|
+
taskResults: results.map(r => ({ taskId: r.taskId, success: r.success })),
|
|
210
|
+
});
|
|
211
|
+
writeState(ctxDir, state);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Re-queue conflicting tasks as sequential
|
|
216
|
+
if (allConflicts.length > 0) {
|
|
217
|
+
const state = readState(ctxDir);
|
|
218
|
+
if (state) {
|
|
219
|
+
state.conflictTasks = allConflicts;
|
|
220
|
+
writeState(ctxDir, state);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { waveResults: allResults, conflicts: allConflicts };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Format wave execution results for display.
|
|
229
|
+
*/
|
|
230
|
+
export function formatWaveResults(waveResults, conflicts) {
|
|
231
|
+
const lines = [];
|
|
232
|
+
for (const wave of waveResults) {
|
|
233
|
+
lines.push(` Wave ${wave.waveIndex + 1}:`);
|
|
234
|
+
for (const r of wave.results) {
|
|
235
|
+
const icon = r.success ? '✓' : '✗';
|
|
236
|
+
lines.push(` ${icon} ${r.taskId}${r.error ? ` — ${r.error}` : ''}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (conflicts.length > 0) {
|
|
240
|
+
lines.push('');
|
|
241
|
+
lines.push(` ⚠ Merge conflicts (re-queued as sequential): ${conflicts.join(', ')}`);
|
|
242
|
+
}
|
|
243
|
+
return lines.join('\n');
|
|
244
|
+
}
|