aicodeman 0.4.5 → 0.4.7
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/dist/orchestrator-loop.d.ts +124 -0
- package/dist/orchestrator-loop.d.ts.map +1 -0
- package/dist/orchestrator-loop.js +853 -0
- package/dist/orchestrator-loop.js.map +1 -0
- package/dist/orchestrator-planner.d.ts +58 -0
- package/dist/orchestrator-planner.d.ts.map +1 -0
- package/dist/orchestrator-planner.js +342 -0
- package/dist/orchestrator-planner.js.map +1 -0
- package/dist/orchestrator-verifier.d.ts +52 -0
- package/dist/orchestrator-verifier.d.ts.map +1 -0
- package/dist/orchestrator-verifier.js +234 -0
- package/dist/orchestrator-verifier.js.map +1 -0
- package/dist/prompts/index.d.ts +1 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +1 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/orchestrator.d.ts +65 -0
- package/dist/prompts/orchestrator.d.ts.map +1 -0
- package/dist/prompts/orchestrator.js +112 -0
- package/dist/prompts/orchestrator.js.map +1 -0
- package/dist/state-store.d.ts +6 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +21 -0
- package/dist/state-store.js.map +1 -1
- package/dist/types/app-state.d.ts +2 -0
- package/dist/types/app-state.d.ts.map +1 -1
- package/dist/types/app-state.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/orchestrator.d.ts +211 -0
- package/dist/types/orchestrator.d.ts.map +1 -0
- package/dist/types/orchestrator.js +58 -0
- package/dist/types/orchestrator.js.map +1 -0
- package/dist/web/ports/index.d.ts +1 -0
- package/dist/web/ports/index.d.ts.map +1 -1
- package/dist/web/ports/orchestrator-port.d.ts +10 -0
- package/dist/web/ports/orchestrator-port.d.ts.map +1 -0
- package/dist/web/ports/orchestrator-port.js +6 -0
- package/dist/web/ports/orchestrator-port.js.map +1 -0
- package/dist/web/public/api-client.3adebdc2.js.gz +0 -0
- package/dist/web/public/app.cbf6e9e8.js +26 -0
- package/dist/web/public/app.cbf6e9e8.js.br +0 -0
- package/dist/web/public/app.cbf6e9e8.js.gz +0 -0
- package/dist/web/public/{constants.cd61abbc.js → constants.64161167.js} +14 -0
- package/dist/web/public/constants.64161167.js.br +0 -0
- package/dist/web/public/constants.64161167.js.gz +0 -0
- package/dist/web/public/index.html +23 -9
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.92544c51.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.9fb81db6.js.gz +0 -0
- package/dist/web/public/{mobile-handlers.65e5638d.js → mobile-handlers.1e2a8ef8.js} +86 -22
- package/dist/web/public/mobile-handlers.1e2a8ef8.js.br +0 -0
- package/dist/web/public/mobile-handlers.1e2a8ef8.js.gz +0 -0
- package/dist/web/public/mobile.fdd28a54.css +1 -0
- package/dist/web/public/mobile.fdd28a54.css.br +0 -0
- package/dist/web/public/mobile.fdd28a54.css.gz +0 -0
- package/dist/web/public/notification-manager.2d5ea8ec.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js +475 -0
- package/dist/web/public/orchestrator-panel.js.br +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.d7f6be08.js → panels-ui.3dd2e29b.js} +25 -25
- package/dist/web/public/panels-ui.3dd2e29b.js.br +0 -0
- package/dist/web/public/panels-ui.3dd2e29b.js.gz +0 -0
- package/dist/web/public/ralph-panel.7b014f16.js.gz +0 -0
- package/dist/web/public/ralph-wizard.f31ab90e.js.gz +0 -0
- package/dist/web/public/respawn-ui.372c6ea7.js.gz +0 -0
- package/dist/web/public/session-ui.0a07c3b7.js.gz +0 -0
- package/dist/web/public/settings-ui.94c57184.js.gz +0 -0
- package/dist/web/public/styles.8e110d27.css +1 -0
- package/dist/web/public/styles.8e110d27.css.br +0 -0
- package/dist/web/public/styles.8e110d27.css.gz +0 -0
- package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.9b40798a.js +3 -0
- package/dist/web/public/terminal-ui.9b40798a.js.br +0 -0
- package/dist/web/public/terminal-ui.9b40798a.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
- package/dist/web/routes/index.d.ts +1 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +1 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/orchestrator-routes.d.ts +21 -0
- package/dist/web/routes/orchestrator-routes.d.ts.map +1 -0
- package/dist/web/routes/orchestrator-routes.js +230 -0
- package/dist/web/routes/orchestrator-routes.js.map +1 -0
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +62 -9
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +25 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +22 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +23 -1
- package/dist/web/server.js.map +1 -1
- package/dist/web/sse-events.d.ts +37 -1
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +39 -1
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/app.9dab4ee2.js +0 -26
- package/dist/web/public/app.9dab4ee2.js.br +0 -0
- package/dist/web/public/app.9dab4ee2.js.gz +0 -0
- package/dist/web/public/constants.cd61abbc.js.br +0 -0
- package/dist/web/public/constants.cd61abbc.js.gz +0 -0
- package/dist/web/public/mobile-handlers.65e5638d.js.br +0 -0
- package/dist/web/public/mobile-handlers.65e5638d.js.gz +0 -0
- package/dist/web/public/mobile.b09497a4.css +0 -1
- package/dist/web/public/mobile.b09497a4.css.br +0 -0
- package/dist/web/public/mobile.b09497a4.css.gz +0 -0
- package/dist/web/public/panels-ui.d7f6be08.js.br +0 -0
- package/dist/web/public/panels-ui.d7f6be08.js.gz +0 -0
- package/dist/web/public/styles.b8ec2f5a.css +0 -1
- package/dist/web/public/styles.b8ec2f5a.css.br +0 -0
- package/dist/web/public/styles.b8ec2f5a.css.gz +0 -0
- package/dist/web/public/terminal-ui.e4565c7b.js +0 -3
- package/dist/web/public/terminal-ui.e4565c7b.js.br +0 -0
- package/dist/web/public/terminal-ui.e4565c7b.js.gz +0 -0
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Orchestrator Loop — phased plan execution with team agents.
|
|
3
|
+
*
|
|
4
|
+
* State machine that generates plans from user goals, executes them
|
|
5
|
+
* phase-by-phase with verification gates, and adapts on failure.
|
|
6
|
+
*
|
|
7
|
+
* States: idle → planning → approval → executing → verifying → (replanning) → completed/failed
|
|
8
|
+
*
|
|
9
|
+
* Key exports:
|
|
10
|
+
* - `OrchestratorLoop` class — main engine, extends EventEmitter
|
|
11
|
+
* - `OrchestratorLoopEvents` interface — typed event map
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle: `start(goal)` → plan → approve → execute phases → verify → complete
|
|
14
|
+
*
|
|
15
|
+
* @dependencies orchestrator-planner (plan generation), orchestrator-verifier (phase verification),
|
|
16
|
+
* session-manager (sessions), task-queue (task execution), state-store (persistence),
|
|
17
|
+
* prompts/orchestrator (prompt templates)
|
|
18
|
+
* @consumedby web/server (orchestrator routes, SSE)
|
|
19
|
+
* @emits stateChanged, planReady, phaseStarted, phaseCompleted, phaseFailed,
|
|
20
|
+
* taskAssigned, taskCompleted, taskFailed, verificationResult, completed, error
|
|
21
|
+
* @persistence Orchestrator state saved to `~/.codeman/state.json` (orchestrator key)
|
|
22
|
+
*
|
|
23
|
+
* @module orchestrator-loop
|
|
24
|
+
*/
|
|
25
|
+
import { EventEmitter } from 'node:events';
|
|
26
|
+
import { getSessionManager } from './session-manager.js';
|
|
27
|
+
import { getTaskQueue } from './task-queue.js';
|
|
28
|
+
import { getStore } from './state-store.js';
|
|
29
|
+
import { OrchestratorPlanner } from './orchestrator-planner.js';
|
|
30
|
+
import { OrchestratorVerifier } from './orchestrator-verifier.js';
|
|
31
|
+
import { PHASE_EXECUTION_PROMPT, REPLAN_PROMPT, SINGLE_TASK_PROMPT, TEAM_LEAD_PROMPT } from './prompts/index.js';
|
|
32
|
+
import { DEFAULT_ORCHESTRATOR_CONFIG, createInitialOrchestratorStats, getErrorMessage, } from './types.js';
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════
|
|
34
|
+
// Constants
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════
|
|
36
|
+
/** Poll interval for checking task completion within a phase (2 seconds) */
|
|
37
|
+
const PHASE_POLL_INTERVAL_MS = 2000;
|
|
38
|
+
/** Delay between phase completion and verification (1 second) */
|
|
39
|
+
const POST_PHASE_DELAY_MS = 1000;
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════
|
|
41
|
+
// OrchestratorLoop
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════
|
|
43
|
+
export class OrchestratorLoop extends EventEmitter {
|
|
44
|
+
_state = 'idle';
|
|
45
|
+
plan = null;
|
|
46
|
+
currentPhaseIndex = 0;
|
|
47
|
+
config;
|
|
48
|
+
stats;
|
|
49
|
+
startedAt = null;
|
|
50
|
+
completedAt = null;
|
|
51
|
+
workingDir;
|
|
52
|
+
planner;
|
|
53
|
+
verifier;
|
|
54
|
+
sessionManager;
|
|
55
|
+
taskQueue;
|
|
56
|
+
store;
|
|
57
|
+
/** State before pause (to resume to correct state) */
|
|
58
|
+
pausedState = null;
|
|
59
|
+
/** Phase poll timer for checking task completion */
|
|
60
|
+
phasePollTimer = null;
|
|
61
|
+
/** Phase-level timeout timer */
|
|
62
|
+
phaseTimeoutTimer = null;
|
|
63
|
+
/** Post-phase delay timer before verification */
|
|
64
|
+
postPhaseTimer = null;
|
|
65
|
+
/** Session completion listener (bound for cleanup) */
|
|
66
|
+
sessionCompletionListener = null;
|
|
67
|
+
/** Active sessions assigned to current phase */
|
|
68
|
+
phaseSessionIds = new Set();
|
|
69
|
+
constructor(mux, workingDir, config) {
|
|
70
|
+
super();
|
|
71
|
+
this.workingDir = workingDir;
|
|
72
|
+
this.config = { ...DEFAULT_ORCHESTRATOR_CONFIG, ...config };
|
|
73
|
+
this.stats = createInitialOrchestratorStats();
|
|
74
|
+
this.sessionManager = getSessionManager();
|
|
75
|
+
this.taskQueue = getTaskQueue();
|
|
76
|
+
this.store = getStore();
|
|
77
|
+
this.planner = new OrchestratorPlanner(mux, workingDir, this.config);
|
|
78
|
+
this.verifier = new OrchestratorVerifier(this.config);
|
|
79
|
+
// Restore state if crashed while running
|
|
80
|
+
this.restore();
|
|
81
|
+
}
|
|
82
|
+
// ═══════════════════════════════════════════════════════════════
|
|
83
|
+
// Public API — Lifecycle
|
|
84
|
+
// ═══════════════════════════════════════════════════════════════
|
|
85
|
+
/** Start orchestration with a goal. Transitions: idle → planning */
|
|
86
|
+
async start(goal) {
|
|
87
|
+
if (this._state !== 'idle' && this._state !== 'failed' && this._state !== 'completed') {
|
|
88
|
+
throw new Error(`Cannot start from state "${this._state}"`);
|
|
89
|
+
}
|
|
90
|
+
this.reset();
|
|
91
|
+
this.startedAt = Date.now();
|
|
92
|
+
this.setState('planning');
|
|
93
|
+
try {
|
|
94
|
+
const plan = await this.planner.generatePlan(goal, (phase, detail) => {
|
|
95
|
+
this.emit('planProgress', phase, detail);
|
|
96
|
+
});
|
|
97
|
+
if (this.currentState() !== 'planning') {
|
|
98
|
+
// Cancelled during planning
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.plan = plan;
|
|
102
|
+
this.persist();
|
|
103
|
+
if (this.config.autoApprove) {
|
|
104
|
+
this.setState('executing');
|
|
105
|
+
await this.executeCurrentPhase();
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
this.setState('approval');
|
|
109
|
+
this.emit('planReady', plan);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
this.handleError(err);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Approve the generated plan. Transitions: approval → executing */
|
|
117
|
+
async approve() {
|
|
118
|
+
this.requireState('approval');
|
|
119
|
+
if (!this.plan) {
|
|
120
|
+
throw new Error('No plan to approve');
|
|
121
|
+
}
|
|
122
|
+
this.setState('executing');
|
|
123
|
+
await this.executeCurrentPhase();
|
|
124
|
+
}
|
|
125
|
+
/** Reject plan with feedback. Transitions: approval → planning (regenerate) */
|
|
126
|
+
async reject(feedback) {
|
|
127
|
+
this.requireState('approval');
|
|
128
|
+
if (!this.plan) {
|
|
129
|
+
throw new Error('No plan to reject');
|
|
130
|
+
}
|
|
131
|
+
const goal = this.plan.goal + '\n\nFeedback on previous plan: ' + feedback;
|
|
132
|
+
this.plan = null;
|
|
133
|
+
this.setState('planning');
|
|
134
|
+
try {
|
|
135
|
+
const plan = await this.planner.generatePlan(goal);
|
|
136
|
+
if (this._state !== 'planning')
|
|
137
|
+
return;
|
|
138
|
+
this.plan = plan;
|
|
139
|
+
this.persist();
|
|
140
|
+
this.setState('approval');
|
|
141
|
+
this.emit('planReady', plan);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
this.handleError(err);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Pause execution. Saves current state. */
|
|
148
|
+
pause() {
|
|
149
|
+
if (this._state === 'idle' || this._state === 'paused' || this._state === 'completed' || this._state === 'failed') {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.pausedState = this._state;
|
|
153
|
+
this.clearPhasePoll();
|
|
154
|
+
this.cleanupTaskHandlers();
|
|
155
|
+
this.setState('paused');
|
|
156
|
+
}
|
|
157
|
+
/** Resume from pause. */
|
|
158
|
+
async resume() {
|
|
159
|
+
if (this._state !== 'paused' || !this.pausedState) {
|
|
160
|
+
throw new Error('Not paused');
|
|
161
|
+
}
|
|
162
|
+
const resumeTo = this.pausedState;
|
|
163
|
+
this.pausedState = null;
|
|
164
|
+
this.setState(resumeTo);
|
|
165
|
+
// Re-enter the appropriate phase of execution
|
|
166
|
+
if (resumeTo === 'executing') {
|
|
167
|
+
await this.executeCurrentPhase();
|
|
168
|
+
}
|
|
169
|
+
else if (resumeTo === 'verifying') {
|
|
170
|
+
await this.verifyCurrentPhase();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Stop everything and clean up. */
|
|
174
|
+
async stop() {
|
|
175
|
+
this.clearPhasePoll();
|
|
176
|
+
this.cleanupTaskHandlers();
|
|
177
|
+
await this.planner.cancel();
|
|
178
|
+
this.setState('idle');
|
|
179
|
+
this.store.clearOrchestratorState();
|
|
180
|
+
}
|
|
181
|
+
/** Skip a specific phase. */
|
|
182
|
+
async skipPhase(phaseId) {
|
|
183
|
+
if (!this.plan)
|
|
184
|
+
return;
|
|
185
|
+
const phase = this.plan.phases.find((p) => p.id === phaseId);
|
|
186
|
+
if (!phase)
|
|
187
|
+
throw new Error(`Phase "${phaseId}" not found`);
|
|
188
|
+
phase.status = 'skipped';
|
|
189
|
+
phase.completedAt = Date.now();
|
|
190
|
+
this.persist();
|
|
191
|
+
// If this is the current phase, advance
|
|
192
|
+
if (this.plan.phases[this.currentPhaseIndex]?.id === phaseId) {
|
|
193
|
+
await this.advanceToNextPhase();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Retry a failed phase. */
|
|
197
|
+
async retryPhase(phaseId) {
|
|
198
|
+
if (!this.plan)
|
|
199
|
+
return;
|
|
200
|
+
if (this._state !== 'executing' && this._state !== 'failed') {
|
|
201
|
+
throw new Error(`Cannot retry from state "${this._state}"`);
|
|
202
|
+
}
|
|
203
|
+
const phaseIndex = this.plan.phases.findIndex((p) => p.id === phaseId);
|
|
204
|
+
if (phaseIndex === -1)
|
|
205
|
+
throw new Error(`Phase "${phaseId}" not found`);
|
|
206
|
+
const phase = this.plan.phases[phaseIndex];
|
|
207
|
+
phase.status = 'pending';
|
|
208
|
+
phase.attempts = 0;
|
|
209
|
+
for (const task of phase.tasks) {
|
|
210
|
+
task.status = 'pending';
|
|
211
|
+
task.error = null;
|
|
212
|
+
task.assignedSessionId = null;
|
|
213
|
+
task.queueTaskId = null;
|
|
214
|
+
}
|
|
215
|
+
this.currentPhaseIndex = phaseIndex;
|
|
216
|
+
this.setState('executing');
|
|
217
|
+
await this.executeCurrentPhase();
|
|
218
|
+
}
|
|
219
|
+
// ═══════════════════════════════════════════════════════════════
|
|
220
|
+
// Public API — Getters
|
|
221
|
+
// ═══════════════════════════════════════════════════════════════
|
|
222
|
+
get state() {
|
|
223
|
+
return this._state;
|
|
224
|
+
}
|
|
225
|
+
getPlan() {
|
|
226
|
+
return this.plan;
|
|
227
|
+
}
|
|
228
|
+
getCurrentPhase() {
|
|
229
|
+
if (!this.plan)
|
|
230
|
+
return null;
|
|
231
|
+
return this.plan.phases[this.currentPhaseIndex] ?? null;
|
|
232
|
+
}
|
|
233
|
+
getStats() {
|
|
234
|
+
return { ...this.stats };
|
|
235
|
+
}
|
|
236
|
+
getStatus() {
|
|
237
|
+
return {
|
|
238
|
+
state: this._state,
|
|
239
|
+
plan: this.plan,
|
|
240
|
+
currentPhaseIndex: this.currentPhaseIndex,
|
|
241
|
+
startedAt: this.startedAt,
|
|
242
|
+
completedAt: this.completedAt,
|
|
243
|
+
config: this.config,
|
|
244
|
+
stats: this.stats,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
isRunning() {
|
|
248
|
+
return this._state !== 'idle' && this._state !== 'completed' && this._state !== 'failed';
|
|
249
|
+
}
|
|
250
|
+
// ═══════════════════════════════════════════════════════════════
|
|
251
|
+
// Internal — Phase Execution
|
|
252
|
+
// ═══════════════════════════════════════════════════════════════
|
|
253
|
+
async executeCurrentPhase() {
|
|
254
|
+
if (!this.plan || this._state !== 'executing')
|
|
255
|
+
return;
|
|
256
|
+
const phase = this.plan.phases[this.currentPhaseIndex];
|
|
257
|
+
if (!phase) {
|
|
258
|
+
// All phases done
|
|
259
|
+
await this.handleCompletion();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// Skip already completed/skipped phases
|
|
263
|
+
if (phase.status === 'passed' || phase.status === 'skipped') {
|
|
264
|
+
await this.advanceToNextPhase();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
phase.status = 'executing';
|
|
268
|
+
phase.startedAt = Date.now();
|
|
269
|
+
phase.attempts++;
|
|
270
|
+
this.persist();
|
|
271
|
+
this.emit('phaseStarted', phase);
|
|
272
|
+
try {
|
|
273
|
+
await this.assignPhaseTasks(phase);
|
|
274
|
+
this.startPhasePoll(phase);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
this.handlePhaseError(phase, getErrorMessage(err));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async assignPhaseTasks(phase) {
|
|
281
|
+
// For team strategy, send a single comprehensive prompt to a lead session
|
|
282
|
+
if (phase.teamStrategy.type === 'team') {
|
|
283
|
+
await this.assignTeamPhase(phase);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// For single/parallel strategy, add individual tasks to TaskQueue
|
|
287
|
+
for (const task of phase.tasks) {
|
|
288
|
+
if (task.status !== 'pending')
|
|
289
|
+
continue;
|
|
290
|
+
const prompt = this.buildTaskPrompt(task, phase);
|
|
291
|
+
const taskOptions = {
|
|
292
|
+
prompt,
|
|
293
|
+
workingDir: this.workingDir,
|
|
294
|
+
priority: 100 - phase.order, // Earlier phases get higher priority
|
|
295
|
+
completionPhrase: task.completionPhrase,
|
|
296
|
+
timeoutMs: Math.min(task.timeoutMs, this.config.phaseTimeoutMs),
|
|
297
|
+
};
|
|
298
|
+
const queueTask = this.taskQueue.addTask(taskOptions);
|
|
299
|
+
task.queueTaskId = queueTask.id;
|
|
300
|
+
task.status = 'running';
|
|
301
|
+
}
|
|
302
|
+
this.persist();
|
|
303
|
+
this.setupTaskHandlers();
|
|
304
|
+
// Manually assign tasks to idle sessions
|
|
305
|
+
await this.assignQueuedTasksToSessions();
|
|
306
|
+
}
|
|
307
|
+
async assignTeamPhase(phase) {
|
|
308
|
+
const teamConfig = phase.teamStrategy.type === 'team' ? phase.teamStrategy.config : null;
|
|
309
|
+
if (!teamConfig)
|
|
310
|
+
return;
|
|
311
|
+
// Find or use an idle session
|
|
312
|
+
const sessions = this.sessionManager.getIdleSessions();
|
|
313
|
+
if (sessions.length === 0) {
|
|
314
|
+
throw new Error('No idle sessions available for team phase execution');
|
|
315
|
+
}
|
|
316
|
+
const session = sessions[0];
|
|
317
|
+
this.phaseSessionIds.add(session.id);
|
|
318
|
+
// Mark all tasks as running under this session
|
|
319
|
+
for (const task of phase.tasks) {
|
|
320
|
+
task.status = 'running';
|
|
321
|
+
task.assignedSessionId = session.id;
|
|
322
|
+
}
|
|
323
|
+
// Build and send the team lead prompt
|
|
324
|
+
const prompt = TEAM_LEAD_PROMPT.replace('{PHASE_NAME}', phase.name)
|
|
325
|
+
.replace('{TASK_LIST}', phase.tasks.map((t, i) => `${i + 1}. ${t.prompt}`).join('\n'))
|
|
326
|
+
.replace('{TEAMMATE_HINTS}', teamConfig.suggestedTeammates.map((h, i) => `${i + 1}. ${h}`).join('\n'))
|
|
327
|
+
.replace('{COMPLETION_PHRASE}', `${phase.id.toUpperCase()}_COMPLETE`);
|
|
328
|
+
// Create a TaskQueue task for the entire phase
|
|
329
|
+
const queueTask = this.taskQueue.addTask({
|
|
330
|
+
prompt,
|
|
331
|
+
workingDir: this.workingDir,
|
|
332
|
+
priority: 100 - phase.order,
|
|
333
|
+
completionPhrase: `${phase.id.toUpperCase()}_COMPLETE`,
|
|
334
|
+
timeoutMs: this.config.phaseTimeoutMs,
|
|
335
|
+
});
|
|
336
|
+
// Link all phase tasks to this single queue task
|
|
337
|
+
for (const task of phase.tasks) {
|
|
338
|
+
task.queueTaskId = queueTask.id;
|
|
339
|
+
}
|
|
340
|
+
this.persist();
|
|
341
|
+
this.setupTaskHandlers();
|
|
342
|
+
// Assign the task to the session
|
|
343
|
+
try {
|
|
344
|
+
queueTask.assign(session.id);
|
|
345
|
+
session.assignTask(queueTask.id);
|
|
346
|
+
this.taskQueue.updateTask(queueTask);
|
|
347
|
+
await session.sendInput(prompt);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
queueTask.fail(getErrorMessage(err));
|
|
351
|
+
this.taskQueue.updateTask(queueTask);
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async assignQueuedTasksToSessions() {
|
|
356
|
+
const idleSessions = this.sessionManager.getIdleSessions();
|
|
357
|
+
const maxSessions = this.getCurrentPhase()?.teamStrategy.type === 'parallel'
|
|
358
|
+
? (this.getCurrentPhase()?.teamStrategy).maxSessions
|
|
359
|
+
: 1;
|
|
360
|
+
const sessionsToUse = idleSessions.slice(0, maxSessions);
|
|
361
|
+
for (const session of sessionsToUse) {
|
|
362
|
+
const task = this.taskQueue.next();
|
|
363
|
+
if (!task)
|
|
364
|
+
break;
|
|
365
|
+
try {
|
|
366
|
+
task.assign(session.id);
|
|
367
|
+
session.assignTask(task.id);
|
|
368
|
+
this.taskQueue.updateTask(task);
|
|
369
|
+
await session.sendInput(task.prompt);
|
|
370
|
+
this.phaseSessionIds.add(session.id);
|
|
371
|
+
// Find the orchestrator task linked to this queue task
|
|
372
|
+
const orchTask = this.findOrchestratorTaskByQueueId(task.id);
|
|
373
|
+
if (orchTask) {
|
|
374
|
+
orchTask.assignedSessionId = session.id;
|
|
375
|
+
orchTask.startedAt = Date.now();
|
|
376
|
+
this.emit('taskAssigned', orchTask, session.id);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
task.fail(getErrorMessage(err));
|
|
381
|
+
session.clearTask();
|
|
382
|
+
this.taskQueue.updateTask(task);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// ═══════════════════════════════════════════════════════════════
|
|
387
|
+
// Internal — Task Completion Tracking
|
|
388
|
+
// ═══════════════════════════════════════════════════════════════
|
|
389
|
+
setupTaskHandlers() {
|
|
390
|
+
this.cleanupTaskHandlers();
|
|
391
|
+
this.sessionCompletionListener = (_sessionId, _phrase) => {
|
|
392
|
+
// Session completion — check if it's related to our phase tasks
|
|
393
|
+
this.checkPhaseCompletion();
|
|
394
|
+
};
|
|
395
|
+
this.sessionManager.on('sessionCompletion', this.sessionCompletionListener);
|
|
396
|
+
}
|
|
397
|
+
cleanupTaskHandlers() {
|
|
398
|
+
if (this.sessionCompletionListener) {
|
|
399
|
+
this.sessionManager.off('sessionCompletion', this.sessionCompletionListener);
|
|
400
|
+
this.sessionCompletionListener = null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
handleTaskCompleted(queueTaskId) {
|
|
404
|
+
const orchTask = this.findOrchestratorTaskByQueueId(queueTaskId);
|
|
405
|
+
if (!orchTask)
|
|
406
|
+
return;
|
|
407
|
+
orchTask.status = 'completed';
|
|
408
|
+
orchTask.completedAt = Date.now();
|
|
409
|
+
this.stats.totalTasksCompleted++;
|
|
410
|
+
this.persist();
|
|
411
|
+
this.emit('taskCompleted', orchTask);
|
|
412
|
+
this.checkPhaseCompletion();
|
|
413
|
+
}
|
|
414
|
+
handleTaskFailed(queueTaskId, error) {
|
|
415
|
+
const orchTask = this.findOrchestratorTaskByQueueId(queueTaskId);
|
|
416
|
+
if (!orchTask)
|
|
417
|
+
return;
|
|
418
|
+
orchTask.status = 'failed';
|
|
419
|
+
orchTask.error = error;
|
|
420
|
+
this.stats.totalTasksFailed++;
|
|
421
|
+
this.persist();
|
|
422
|
+
this.emit('taskFailed', orchTask, error);
|
|
423
|
+
// Check if we should retry the task or fail the phase
|
|
424
|
+
if (orchTask.retries < 2) {
|
|
425
|
+
orchTask.retries++;
|
|
426
|
+
orchTask.status = 'pending';
|
|
427
|
+
orchTask.error = null;
|
|
428
|
+
orchTask.queueTaskId = null;
|
|
429
|
+
// Will be re-queued on next poll
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
this.checkPhaseCompletion();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
startPhasePoll(phase) {
|
|
436
|
+
this.clearPhasePoll();
|
|
437
|
+
this.phasePollTimer = setInterval(() => {
|
|
438
|
+
if (this._state !== 'executing') {
|
|
439
|
+
this.clearPhasePoll();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
this.pollPhaseStatus(phase);
|
|
443
|
+
}, PHASE_POLL_INTERVAL_MS);
|
|
444
|
+
// Phase-level timeout — fail the phase if it exceeds the configured timeout
|
|
445
|
+
this.phaseTimeoutTimer = setTimeout(() => {
|
|
446
|
+
if (this._state === 'executing' && phase.status === 'executing') {
|
|
447
|
+
console.warn(`[Orchestrator] Phase "${phase.name}" timed out after ${this.config.phaseTimeoutMs}ms`);
|
|
448
|
+
this.handlePhaseError(phase, `Phase timed out after ${Math.round(this.config.phaseTimeoutMs / 60000)} minutes`);
|
|
449
|
+
}
|
|
450
|
+
}, this.config.phaseTimeoutMs);
|
|
451
|
+
}
|
|
452
|
+
clearPhasePoll() {
|
|
453
|
+
if (this.phasePollTimer) {
|
|
454
|
+
clearInterval(this.phasePollTimer);
|
|
455
|
+
this.phasePollTimer = null;
|
|
456
|
+
}
|
|
457
|
+
if (this.phaseTimeoutTimer) {
|
|
458
|
+
clearTimeout(this.phaseTimeoutTimer);
|
|
459
|
+
this.phaseTimeoutTimer = null;
|
|
460
|
+
}
|
|
461
|
+
if (this.postPhaseTimer) {
|
|
462
|
+
clearTimeout(this.postPhaseTimer);
|
|
463
|
+
this.postPhaseTimer = null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
pollPhaseStatus(phase) {
|
|
467
|
+
// Check for queued tasks that need assignment
|
|
468
|
+
const pendingTasks = phase.tasks.filter((t) => t.status === 'pending' && !t.queueTaskId);
|
|
469
|
+
if (pendingTasks.length > 0) {
|
|
470
|
+
// Re-queue pending tasks
|
|
471
|
+
for (const task of pendingTasks) {
|
|
472
|
+
const prompt = this.buildTaskPrompt(task, phase);
|
|
473
|
+
const queueTask = this.taskQueue.addTask({
|
|
474
|
+
prompt,
|
|
475
|
+
workingDir: this.workingDir,
|
|
476
|
+
priority: 100 - phase.order,
|
|
477
|
+
completionPhrase: task.completionPhrase,
|
|
478
|
+
timeoutMs: Math.min(task.timeoutMs, this.config.phaseTimeoutMs),
|
|
479
|
+
});
|
|
480
|
+
task.queueTaskId = queueTask.id;
|
|
481
|
+
task.status = 'running';
|
|
482
|
+
}
|
|
483
|
+
this.assignQueuedTasksToSessions().catch(() => { }); // Best effort
|
|
484
|
+
}
|
|
485
|
+
// Check completion status of queue tasks
|
|
486
|
+
for (const task of phase.tasks) {
|
|
487
|
+
if (task.status === 'running' && task.queueTaskId) {
|
|
488
|
+
const queueTask = this.taskQueue.getTask(task.queueTaskId);
|
|
489
|
+
if (queueTask) {
|
|
490
|
+
if (queueTask.isCompleted()) {
|
|
491
|
+
this.handleTaskCompleted(task.queueTaskId);
|
|
492
|
+
}
|
|
493
|
+
else if (queueTask.isFailed()) {
|
|
494
|
+
this.handleTaskFailed(task.queueTaskId, queueTask.error || 'Task failed');
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
this.checkPhaseCompletion();
|
|
500
|
+
}
|
|
501
|
+
checkPhaseCompletion() {
|
|
502
|
+
if (this._state !== 'executing')
|
|
503
|
+
return;
|
|
504
|
+
const phase = this.getCurrentPhase();
|
|
505
|
+
if (!phase)
|
|
506
|
+
return;
|
|
507
|
+
const allDone = phase.tasks.every((t) => t.status === 'completed' || t.status === 'failed');
|
|
508
|
+
if (!allDone)
|
|
509
|
+
return;
|
|
510
|
+
const anyFailed = phase.tasks.some((t) => t.status === 'failed');
|
|
511
|
+
this.clearPhasePoll();
|
|
512
|
+
if (anyFailed) {
|
|
513
|
+
// Phase has failed tasks
|
|
514
|
+
this.handlePhaseError(phase, 'One or more tasks failed');
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
// All tasks completed — run verification after brief delay
|
|
518
|
+
this.postPhaseTimer = setTimeout(() => {
|
|
519
|
+
this.postPhaseTimer = null;
|
|
520
|
+
this.verifyCurrentPhase().catch((err) => this.handleError(err));
|
|
521
|
+
}, POST_PHASE_DELAY_MS);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// ═══════════════════════════════════════════════════════════════
|
|
525
|
+
// Internal — Verification
|
|
526
|
+
// ═══════════════════════════════════════════════════════════════
|
|
527
|
+
async verifyCurrentPhase() {
|
|
528
|
+
if (!this.plan)
|
|
529
|
+
return;
|
|
530
|
+
const phase = this.plan.phases[this.currentPhaseIndex];
|
|
531
|
+
if (!phase)
|
|
532
|
+
return;
|
|
533
|
+
// Skip verification if no criteria defined
|
|
534
|
+
if (phase.verificationCriteria.length === 0 && phase.testCommands.length === 0) {
|
|
535
|
+
phase.status = 'passed';
|
|
536
|
+
phase.completedAt = Date.now();
|
|
537
|
+
phase.durationMs = phase.startedAt ? Date.now() - phase.startedAt : null;
|
|
538
|
+
this.stats.phasesCompleted++;
|
|
539
|
+
this.persist();
|
|
540
|
+
this.emit('phaseCompleted', phase);
|
|
541
|
+
await this.advanceToNextPhase();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
this.setState('verifying');
|
|
545
|
+
// Get a session for verification — wait briefly for sessions to become idle
|
|
546
|
+
let sessions = this.sessionManager.getIdleSessions();
|
|
547
|
+
if (sessions.length === 0) {
|
|
548
|
+
// Wait up to 10s for a session to become idle
|
|
549
|
+
await new Promise((resolve) => setTimeout(resolve, 10_000));
|
|
550
|
+
sessions = this.sessionManager.getIdleSessions();
|
|
551
|
+
}
|
|
552
|
+
if (sessions.length === 0) {
|
|
553
|
+
// Still no sessions — log warning and skip verification (don't silently pass)
|
|
554
|
+
console.warn('[Orchestrator] No idle sessions for verification — skipping (marking passed with warning)');
|
|
555
|
+
phase.status = 'passed';
|
|
556
|
+
phase.completedAt = Date.now();
|
|
557
|
+
phase.durationMs = phase.startedAt ? Date.now() - phase.startedAt : null;
|
|
558
|
+
this.stats.phasesCompleted++;
|
|
559
|
+
this.persist();
|
|
560
|
+
this.emit('phaseCompleted', phase);
|
|
561
|
+
this.setState('executing');
|
|
562
|
+
await this.advanceToNextPhase();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
const result = await this.verifier.verifyPhase(phase, sessions[0]);
|
|
567
|
+
this.emit('verificationResult', phase, result);
|
|
568
|
+
if (result.passed) {
|
|
569
|
+
phase.status = 'passed';
|
|
570
|
+
phase.completedAt = Date.now();
|
|
571
|
+
phase.durationMs = phase.startedAt ? Date.now() - phase.startedAt : null;
|
|
572
|
+
this.stats.phasesCompleted++;
|
|
573
|
+
this.persist();
|
|
574
|
+
this.emit('phaseCompleted', phase);
|
|
575
|
+
this.setState('executing');
|
|
576
|
+
await this.advanceToNextPhase();
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
// Verification failed — attempt replan
|
|
580
|
+
await this.handleVerificationFailure(phase, result);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
// Verification error — treat as pass (don't block on verification bugs)
|
|
585
|
+
console.warn('[Orchestrator] Verification error, treating as pass:', err);
|
|
586
|
+
phase.status = 'passed';
|
|
587
|
+
phase.completedAt = Date.now();
|
|
588
|
+
phase.durationMs = phase.startedAt ? Date.now() - phase.startedAt : null;
|
|
589
|
+
this.stats.phasesCompleted++;
|
|
590
|
+
this.persist();
|
|
591
|
+
this.emit('phaseCompleted', phase);
|
|
592
|
+
this.setState('executing');
|
|
593
|
+
await this.advanceToNextPhase();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async handleVerificationFailure(phase, result) {
|
|
597
|
+
if (phase.attempts >= phase.maxAttempts) {
|
|
598
|
+
// Max retries exceeded
|
|
599
|
+
phase.status = 'failed';
|
|
600
|
+
phase.completedAt = Date.now();
|
|
601
|
+
phase.durationMs = phase.startedAt ? Date.now() - phase.startedAt : null;
|
|
602
|
+
this.stats.phasesFailed++;
|
|
603
|
+
this.persist();
|
|
604
|
+
this.emit('phaseFailed', phase, `Verification failed after ${phase.attempts} attempts: ${result.summary}`);
|
|
605
|
+
this.setState('failed');
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
// Replan and retry
|
|
609
|
+
this.stats.replanCount++;
|
|
610
|
+
this.setState('replanning');
|
|
611
|
+
try {
|
|
612
|
+
await this.replanPhase(phase, result);
|
|
613
|
+
// Reset task states for retry
|
|
614
|
+
for (const task of phase.tasks) {
|
|
615
|
+
task.status = 'pending';
|
|
616
|
+
task.error = null;
|
|
617
|
+
task.assignedSessionId = null;
|
|
618
|
+
task.queueTaskId = null;
|
|
619
|
+
task.completedAt = null;
|
|
620
|
+
task.startedAt = null;
|
|
621
|
+
}
|
|
622
|
+
phase.status = 'pending';
|
|
623
|
+
phase.startedAt = null;
|
|
624
|
+
this.persist();
|
|
625
|
+
this.setState('executing');
|
|
626
|
+
await this.executeCurrentPhase();
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
this.handleError(err);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async replanPhase(phase, result) {
|
|
633
|
+
const completionPhrase = phase.tasks[0]?.completionPhrase || `${phase.id.toUpperCase()}_FIXED`;
|
|
634
|
+
const prompt = REPLAN_PROMPT.replace('{PHASE_NAME}', phase.name)
|
|
635
|
+
.replace('{ATTEMPT_NUMBER}', String(phase.attempts))
|
|
636
|
+
.replace('{MAX_ATTEMPTS}', String(phase.maxAttempts))
|
|
637
|
+
.replace('{FAILURE_SUMMARY}', result.summary)
|
|
638
|
+
.replace('{SUGGESTIONS}', result.suggestions.join('\n'))
|
|
639
|
+
.replace('{ORIGINAL_TASKS}', phase.tasks.map((t, i) => `${i + 1}. ${t.prompt}`).join('\n'))
|
|
640
|
+
.replace('{COMPLETION_PHRASE}', completionPhrase);
|
|
641
|
+
// Create a tracked queue task for the replan (so completion is detected)
|
|
642
|
+
const queueTask = this.taskQueue.addTask({
|
|
643
|
+
prompt,
|
|
644
|
+
workingDir: this.workingDir,
|
|
645
|
+
priority: 100,
|
|
646
|
+
completionPhrase,
|
|
647
|
+
timeoutMs: this.config.phaseTimeoutMs,
|
|
648
|
+
});
|
|
649
|
+
// Link to first phase task for tracking
|
|
650
|
+
if (phase.tasks[0]) {
|
|
651
|
+
phase.tasks[0].queueTaskId = queueTask.id;
|
|
652
|
+
phase.tasks[0].status = 'running';
|
|
653
|
+
}
|
|
654
|
+
this.persist();
|
|
655
|
+
// Set up handlers so task completion is tracked
|
|
656
|
+
this.setupTaskHandlers();
|
|
657
|
+
// Assign to a session
|
|
658
|
+
const sessions = this.sessionManager.getIdleSessions();
|
|
659
|
+
if (sessions.length === 0) {
|
|
660
|
+
console.warn('[Orchestrator] No idle sessions for replan — task queued, will pick up on next poll');
|
|
661
|
+
// Start polling so the task gets assigned when a session becomes idle
|
|
662
|
+
this.startPhasePoll(phase);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
queueTask.assign(sessions[0].id);
|
|
667
|
+
sessions[0].assignTask(queueTask.id);
|
|
668
|
+
this.taskQueue.updateTask(queueTask);
|
|
669
|
+
await sessions[0].sendInput(prompt);
|
|
670
|
+
}
|
|
671
|
+
catch (err) {
|
|
672
|
+
queueTask.fail(getErrorMessage(err));
|
|
673
|
+
this.taskQueue.updateTask(queueTask);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// ═══════════════════════════════════════════════════════════════
|
|
677
|
+
// Internal — State Machine
|
|
678
|
+
// ═══════════════════════════════════════════════════════════════
|
|
679
|
+
/** Read current state (bypasses TypeScript narrowing from guards) */
|
|
680
|
+
currentState() {
|
|
681
|
+
return this._state;
|
|
682
|
+
}
|
|
683
|
+
/** Assert state matches expected or throw */
|
|
684
|
+
requireState(...expected) {
|
|
685
|
+
if (!expected.includes(this._state)) {
|
|
686
|
+
throw new Error(`Expected state "${expected.join('|')}", got "${this._state}"`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
setState(newState) {
|
|
690
|
+
const prev = this._state;
|
|
691
|
+
if (prev === newState)
|
|
692
|
+
return;
|
|
693
|
+
this._state = newState;
|
|
694
|
+
this.persist();
|
|
695
|
+
this.emit('stateChanged', newState, prev);
|
|
696
|
+
}
|
|
697
|
+
async advanceToNextPhase() {
|
|
698
|
+
this.currentPhaseIndex++;
|
|
699
|
+
this.phaseSessionIds.clear();
|
|
700
|
+
this.persist();
|
|
701
|
+
if (!this.plan || this.currentPhaseIndex >= this.plan.phases.length) {
|
|
702
|
+
await this.handleCompletion();
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
// Compact between phases if configured
|
|
706
|
+
if (this.config.compactBetweenPhases) {
|
|
707
|
+
const sessions = this.sessionManager.getIdleSessions();
|
|
708
|
+
for (const session of sessions) {
|
|
709
|
+
try {
|
|
710
|
+
await session.writeViaMux('/compact');
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
// Best effort
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// Brief delay for compact to take effect
|
|
717
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
718
|
+
}
|
|
719
|
+
await this.executeCurrentPhase();
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async handleCompletion() {
|
|
723
|
+
this.completedAt = Date.now();
|
|
724
|
+
this.stats.totalDurationMs = this.startedAt ? this.completedAt - this.startedAt : 0;
|
|
725
|
+
this.clearPhasePoll();
|
|
726
|
+
this.cleanupTaskHandlers();
|
|
727
|
+
this.setState('completed');
|
|
728
|
+
this.emit('completed', this.stats);
|
|
729
|
+
}
|
|
730
|
+
handlePhaseError(phase, error) {
|
|
731
|
+
if (phase.attempts >= phase.maxAttempts) {
|
|
732
|
+
phase.status = 'failed';
|
|
733
|
+
phase.completedAt = Date.now();
|
|
734
|
+
phase.durationMs = phase.startedAt ? Date.now() - phase.startedAt : null;
|
|
735
|
+
this.stats.phasesFailed++;
|
|
736
|
+
this.persist();
|
|
737
|
+
this.emit('phaseFailed', phase, error);
|
|
738
|
+
this.setState('failed');
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
// Retry the phase
|
|
742
|
+
for (const task of phase.tasks) {
|
|
743
|
+
if (task.status === 'failed') {
|
|
744
|
+
task.status = 'pending';
|
|
745
|
+
task.error = null;
|
|
746
|
+
task.queueTaskId = null;
|
|
747
|
+
task.assignedSessionId = null;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
phase.status = 'pending';
|
|
751
|
+
this.persist();
|
|
752
|
+
this.executeCurrentPhase().catch((err) => this.handleError(err));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
handleError(err) {
|
|
756
|
+
const error = err instanceof Error ? err : new Error(getErrorMessage(err));
|
|
757
|
+
console.error('[Orchestrator] Error:', error.message);
|
|
758
|
+
this.setState('failed');
|
|
759
|
+
this.emit('error', error);
|
|
760
|
+
}
|
|
761
|
+
// ═══════════════════════════════════════════════════════════════
|
|
762
|
+
// Internal — Persistence
|
|
763
|
+
// ═══════════════════════════════════════════════════════════════
|
|
764
|
+
persist() {
|
|
765
|
+
this.store.setOrchestratorState(this.getStatus());
|
|
766
|
+
}
|
|
767
|
+
restore() {
|
|
768
|
+
const saved = this.store.getOrchestratorState();
|
|
769
|
+
if (!saved)
|
|
770
|
+
return;
|
|
771
|
+
// If we crashed while running, reset to failed
|
|
772
|
+
if (saved.state === 'executing' || saved.state === 'verifying' || saved.state === 'replanning') {
|
|
773
|
+
this._state = 'failed';
|
|
774
|
+
this.plan = saved.plan;
|
|
775
|
+
this.currentPhaseIndex = saved.currentPhaseIndex;
|
|
776
|
+
this.startedAt = saved.startedAt;
|
|
777
|
+
this.config = saved.config;
|
|
778
|
+
this.stats = saved.stats;
|
|
779
|
+
this.store.setOrchestratorState({ ...saved, state: 'failed' });
|
|
780
|
+
}
|
|
781
|
+
else if (saved.state === 'planning' || saved.state === 'approval') {
|
|
782
|
+
// Planning/approval — reset to idle (plan is lost)
|
|
783
|
+
this.store.clearOrchestratorState();
|
|
784
|
+
}
|
|
785
|
+
else if (saved.state === 'completed' || saved.state === 'failed') {
|
|
786
|
+
// Preserve completed/failed state for UI display
|
|
787
|
+
this._state = saved.state;
|
|
788
|
+
this.plan = saved.plan;
|
|
789
|
+
this.currentPhaseIndex = saved.currentPhaseIndex;
|
|
790
|
+
this.startedAt = saved.startedAt;
|
|
791
|
+
this.completedAt = saved.completedAt;
|
|
792
|
+
this.config = saved.config;
|
|
793
|
+
this.stats = saved.stats;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
reset() {
|
|
797
|
+
this._state = 'idle';
|
|
798
|
+
this.plan = null;
|
|
799
|
+
this.currentPhaseIndex = 0;
|
|
800
|
+
this.startedAt = null;
|
|
801
|
+
this.completedAt = null;
|
|
802
|
+
this.stats = createInitialOrchestratorStats();
|
|
803
|
+
this.pausedState = null;
|
|
804
|
+
this.phaseSessionIds.clear();
|
|
805
|
+
this.clearPhasePoll();
|
|
806
|
+
this.cleanupTaskHandlers();
|
|
807
|
+
}
|
|
808
|
+
// ═══════════════════════════════════════════════════════════════
|
|
809
|
+
// Internal — Helpers
|
|
810
|
+
// ═══════════════════════════════════════════════════════════════
|
|
811
|
+
buildTaskPrompt(task, phase) {
|
|
812
|
+
if (phase.tasks.length === 1) {
|
|
813
|
+
// Single task — use simpler prompt
|
|
814
|
+
const completedPhases = this.getCompletedPhasesSummary();
|
|
815
|
+
return SINGLE_TASK_PROMPT.replace('{TASK}', task.prompt)
|
|
816
|
+
.replace('{GOAL}', this.plan?.goal || '')
|
|
817
|
+
.replace('{CONTEXT}', completedPhases ? `Previous phases completed: ${completedPhases}` : '')
|
|
818
|
+
.replace('{COMPLETION_PHRASE}', task.completionPhrase);
|
|
819
|
+
}
|
|
820
|
+
// Multi-task phase — use full prompt
|
|
821
|
+
return PHASE_EXECUTION_PROMPT.replace('{PHASE_NAME}', phase.name)
|
|
822
|
+
.replace('{GOAL}', this.plan?.goal || '')
|
|
823
|
+
.replace('{COMPLETED_PHASES}', this.getCompletedPhasesSummary() || 'None yet')
|
|
824
|
+
.replace('{TASK_LIST}', phase.tasks.map((t, i) => `${i + 1}. ${t.prompt}`).join('\n'))
|
|
825
|
+
.replace('{VERIFICATION_CRITERIA}', phase.verificationCriteria.join('\n') || 'No specific criteria')
|
|
826
|
+
.replace('{COMPLETION_PHRASE}', task.completionPhrase);
|
|
827
|
+
}
|
|
828
|
+
getCompletedPhasesSummary() {
|
|
829
|
+
if (!this.plan)
|
|
830
|
+
return '';
|
|
831
|
+
return this.plan.phases
|
|
832
|
+
.filter((p) => p.status === 'passed' || p.status === 'skipped')
|
|
833
|
+
.map((p) => `${p.name}: ${p.status}`)
|
|
834
|
+
.join(', ');
|
|
835
|
+
}
|
|
836
|
+
findOrchestratorTaskByQueueId(queueTaskId) {
|
|
837
|
+
if (!this.plan)
|
|
838
|
+
return null;
|
|
839
|
+
for (const phase of this.plan.phases) {
|
|
840
|
+
for (const task of phase.tasks) {
|
|
841
|
+
if (task.queueTaskId === queueTaskId)
|
|
842
|
+
return task;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
/** Clean up resources when the loop is being destroyed. */
|
|
848
|
+
destroy() {
|
|
849
|
+
this.clearPhasePoll();
|
|
850
|
+
this.cleanupTaskHandlers();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
//# sourceMappingURL=orchestrator-loop.js.map
|