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.
Files changed (129) hide show
  1. package/dist/orchestrator-loop.d.ts +124 -0
  2. package/dist/orchestrator-loop.d.ts.map +1 -0
  3. package/dist/orchestrator-loop.js +853 -0
  4. package/dist/orchestrator-loop.js.map +1 -0
  5. package/dist/orchestrator-planner.d.ts +58 -0
  6. package/dist/orchestrator-planner.d.ts.map +1 -0
  7. package/dist/orchestrator-planner.js +342 -0
  8. package/dist/orchestrator-planner.js.map +1 -0
  9. package/dist/orchestrator-verifier.d.ts +52 -0
  10. package/dist/orchestrator-verifier.d.ts.map +1 -0
  11. package/dist/orchestrator-verifier.js +234 -0
  12. package/dist/orchestrator-verifier.js.map +1 -0
  13. package/dist/prompts/index.d.ts +1 -0
  14. package/dist/prompts/index.d.ts.map +1 -1
  15. package/dist/prompts/index.js +1 -0
  16. package/dist/prompts/index.js.map +1 -1
  17. package/dist/prompts/orchestrator.d.ts +65 -0
  18. package/dist/prompts/orchestrator.d.ts.map +1 -0
  19. package/dist/prompts/orchestrator.js +112 -0
  20. package/dist/prompts/orchestrator.js.map +1 -0
  21. package/dist/state-store.d.ts +6 -0
  22. package/dist/state-store.d.ts.map +1 -1
  23. package/dist/state-store.js +21 -0
  24. package/dist/state-store.js.map +1 -1
  25. package/dist/types/app-state.d.ts +2 -0
  26. package/dist/types/app-state.d.ts.map +1 -1
  27. package/dist/types/app-state.js.map +1 -1
  28. package/dist/types/index.d.ts +2 -0
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/dist/types/index.js +2 -0
  31. package/dist/types/index.js.map +1 -1
  32. package/dist/types/orchestrator.d.ts +211 -0
  33. package/dist/types/orchestrator.d.ts.map +1 -0
  34. package/dist/types/orchestrator.js +58 -0
  35. package/dist/types/orchestrator.js.map +1 -0
  36. package/dist/web/ports/index.d.ts +1 -0
  37. package/dist/web/ports/index.d.ts.map +1 -1
  38. package/dist/web/ports/orchestrator-port.d.ts +10 -0
  39. package/dist/web/ports/orchestrator-port.d.ts.map +1 -0
  40. package/dist/web/ports/orchestrator-port.js +6 -0
  41. package/dist/web/ports/orchestrator-port.js.map +1 -0
  42. package/dist/web/public/api-client.3adebdc2.js.gz +0 -0
  43. package/dist/web/public/app.cbf6e9e8.js +26 -0
  44. package/dist/web/public/app.cbf6e9e8.js.br +0 -0
  45. package/dist/web/public/app.cbf6e9e8.js.gz +0 -0
  46. package/dist/web/public/{constants.cd61abbc.js → constants.64161167.js} +14 -0
  47. package/dist/web/public/constants.64161167.js.br +0 -0
  48. package/dist/web/public/constants.64161167.js.gz +0 -0
  49. package/dist/web/public/index.html +23 -9
  50. package/dist/web/public/index.html.br +0 -0
  51. package/dist/web/public/index.html.gz +0 -0
  52. package/dist/web/public/input-cjk.92544c51.js.gz +0 -0
  53. package/dist/web/public/keyboard-accessory.9fb81db6.js.gz +0 -0
  54. package/dist/web/public/{mobile-handlers.65e5638d.js → mobile-handlers.1e2a8ef8.js} +86 -22
  55. package/dist/web/public/mobile-handlers.1e2a8ef8.js.br +0 -0
  56. package/dist/web/public/mobile-handlers.1e2a8ef8.js.gz +0 -0
  57. package/dist/web/public/mobile.fdd28a54.css +1 -0
  58. package/dist/web/public/mobile.fdd28a54.css.br +0 -0
  59. package/dist/web/public/mobile.fdd28a54.css.gz +0 -0
  60. package/dist/web/public/notification-manager.2d5ea8ec.js.gz +0 -0
  61. package/dist/web/public/orchestrator-panel.js +475 -0
  62. package/dist/web/public/orchestrator-panel.js.br +0 -0
  63. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  64. package/dist/web/public/{panels-ui.d7f6be08.js → panels-ui.3dd2e29b.js} +25 -25
  65. package/dist/web/public/panels-ui.3dd2e29b.js.br +0 -0
  66. package/dist/web/public/panels-ui.3dd2e29b.js.gz +0 -0
  67. package/dist/web/public/ralph-panel.7b014f16.js.gz +0 -0
  68. package/dist/web/public/ralph-wizard.f31ab90e.js.gz +0 -0
  69. package/dist/web/public/respawn-ui.372c6ea7.js.gz +0 -0
  70. package/dist/web/public/session-ui.0a07c3b7.js.gz +0 -0
  71. package/dist/web/public/settings-ui.94c57184.js.gz +0 -0
  72. package/dist/web/public/styles.8e110d27.css +1 -0
  73. package/dist/web/public/styles.8e110d27.css.br +0 -0
  74. package/dist/web/public/styles.8e110d27.css.gz +0 -0
  75. package/dist/web/public/subagent-windows.a366a4ad.js.gz +0 -0
  76. package/dist/web/public/sw.js.gz +0 -0
  77. package/dist/web/public/terminal-ui.9b40798a.js +3 -0
  78. package/dist/web/public/terminal-ui.9b40798a.js.br +0 -0
  79. package/dist/web/public/terminal-ui.9b40798a.js.gz +0 -0
  80. package/dist/web/public/upload.html.gz +0 -0
  81. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  82. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  83. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  84. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  85. package/dist/web/public/vendor/xterm.css.gz +0 -0
  86. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  87. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  88. package/dist/web/routes/index.d.ts +1 -0
  89. package/dist/web/routes/index.d.ts.map +1 -1
  90. package/dist/web/routes/index.js +1 -0
  91. package/dist/web/routes/index.js.map +1 -1
  92. package/dist/web/routes/orchestrator-routes.d.ts +21 -0
  93. package/dist/web/routes/orchestrator-routes.d.ts.map +1 -0
  94. package/dist/web/routes/orchestrator-routes.js +230 -0
  95. package/dist/web/routes/orchestrator-routes.js.map +1 -0
  96. package/dist/web/routes/session-routes.d.ts.map +1 -1
  97. package/dist/web/routes/session-routes.js +62 -9
  98. package/dist/web/routes/session-routes.js.map +1 -1
  99. package/dist/web/schemas.d.ts +25 -0
  100. package/dist/web/schemas.d.ts.map +1 -1
  101. package/dist/web/schemas.js +22 -0
  102. package/dist/web/schemas.js.map +1 -1
  103. package/dist/web/server.d.ts +2 -0
  104. package/dist/web/server.d.ts.map +1 -1
  105. package/dist/web/server.js +23 -1
  106. package/dist/web/server.js.map +1 -1
  107. package/dist/web/sse-events.d.ts +37 -1
  108. package/dist/web/sse-events.d.ts.map +1 -1
  109. package/dist/web/sse-events.js +39 -1
  110. package/dist/web/sse-events.js.map +1 -1
  111. package/package.json +1 -1
  112. package/dist/web/public/app.9dab4ee2.js +0 -26
  113. package/dist/web/public/app.9dab4ee2.js.br +0 -0
  114. package/dist/web/public/app.9dab4ee2.js.gz +0 -0
  115. package/dist/web/public/constants.cd61abbc.js.br +0 -0
  116. package/dist/web/public/constants.cd61abbc.js.gz +0 -0
  117. package/dist/web/public/mobile-handlers.65e5638d.js.br +0 -0
  118. package/dist/web/public/mobile-handlers.65e5638d.js.gz +0 -0
  119. package/dist/web/public/mobile.b09497a4.css +0 -1
  120. package/dist/web/public/mobile.b09497a4.css.br +0 -0
  121. package/dist/web/public/mobile.b09497a4.css.gz +0 -0
  122. package/dist/web/public/panels-ui.d7f6be08.js.br +0 -0
  123. package/dist/web/public/panels-ui.d7f6be08.js.gz +0 -0
  124. package/dist/web/public/styles.b8ec2f5a.css +0 -1
  125. package/dist/web/public/styles.b8ec2f5a.css.br +0 -0
  126. package/dist/web/public/styles.b8ec2f5a.css.gz +0 -0
  127. package/dist/web/public/terminal-ui.e4565c7b.js +0 -3
  128. package/dist/web/public/terminal-ui.e4565c7b.js.br +0 -0
  129. 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