@oxgeneral/orch 0.2.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/dist/App-CPQPQTZU.js +4751 -0
  3. package/dist/agent-J62U7ABO.js +157 -0
  4. package/dist/chunk-2B32FPEB.js +11 -0
  5. package/dist/chunk-2B32FPEB.js.map +1 -0
  6. package/dist/chunk-2VSAM7RH.js +166 -0
  7. package/dist/chunk-33QNTNR6.js +46 -0
  8. package/dist/chunk-45K2XID7.js +29 -0
  9. package/dist/chunk-6GFVB6EK.js +101 -0
  10. package/dist/chunk-6HENRUYZ.js +2 -0
  11. package/dist/chunk-6HENRUYZ.js.map +1 -0
  12. package/dist/chunk-AELEEEV3.js +92 -0
  13. package/dist/chunk-AELEEEV3.js.map +1 -0
  14. package/dist/chunk-CHIP7O6V.js +83 -0
  15. package/dist/chunk-CIIE6LNG.js +217 -0
  16. package/dist/chunk-E3TCKHU6.js +13 -0
  17. package/dist/chunk-E3TCKHU6.js.map +1 -0
  18. package/dist/chunk-ED47GL3F.js +29 -0
  19. package/dist/chunk-HNKJ4IF7.js +177 -0
  20. package/dist/chunk-HXYAZGLP.js +15 -0
  21. package/dist/chunk-IRN2U2NE.js +79 -0
  22. package/dist/chunk-IZYSGYXG.js +2 -0
  23. package/dist/chunk-IZYSGYXG.js.map +1 -0
  24. package/dist/chunk-O5AO5QIR.js +76 -0
  25. package/dist/chunk-P6ATSXGL.js +107 -0
  26. package/dist/chunk-PBFE5V3G.js +2 -0
  27. package/dist/chunk-PBFE5V3G.js.map +1 -0
  28. package/dist/chunk-PNE6LQRF.js +5 -0
  29. package/dist/chunk-POUC4CPC.js +2 -0
  30. package/dist/chunk-POUC4CPC.js.map +1 -0
  31. package/dist/chunk-TX7WOFCW.js +59 -0
  32. package/dist/chunk-VTA74YWX.js +291 -0
  33. package/dist/chunk-XI4TU6VU.js +50 -0
  34. package/dist/chunk-ZU6AY2VU.js +2 -0
  35. package/dist/chunk-ZU6AY2VU.js.map +1 -0
  36. package/dist/claude-GH6P2DC5.js +4 -0
  37. package/dist/claude-S47YTIHU.js +2 -0
  38. package/dist/claude-S47YTIHU.js.map +1 -0
  39. package/dist/cli.js +205 -0
  40. package/dist/codex-2CH57B7G.js +2 -0
  41. package/dist/codex-2CH57B7G.js.map +1 -0
  42. package/dist/codex-U7LTJTX6.js +115 -0
  43. package/dist/config-VN4MYHSY.js +75 -0
  44. package/dist/container-74P43KDY.js +1532 -0
  45. package/dist/context-EPHCF34F.js +83 -0
  46. package/dist/cursor-3DI5GKRF.js +92 -0
  47. package/dist/cursor-QFUNKPCQ.js +2 -0
  48. package/dist/cursor-QFUNKPCQ.js.map +1 -0
  49. package/dist/doctor-BK46WCQ5.js +67 -0
  50. package/dist/doctor-service-A34DHPKI.js +2 -0
  51. package/dist/doctor-service-NTWBWOM2.js +2 -0
  52. package/dist/doctor-service-NTWBWOM2.js.map +1 -0
  53. package/dist/goal-KGAIM3ZK.js +110 -0
  54. package/dist/index.d.ts +1356 -0
  55. package/dist/index.js +6 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/init-QBWCEDCI.js +152 -0
  58. package/dist/logs-PYEKMQE2.js +207 -0
  59. package/dist/msg-BBIPCGDO.js +95 -0
  60. package/dist/orchestrator-TAFBYQQ5.js +2 -0
  61. package/dist/orchestrator-TAFBYQQ5.js.map +1 -0
  62. package/dist/orchestrator-VGYKSOZJ.js +1292 -0
  63. package/dist/output-5VQVCJ2K.js +2 -0
  64. package/dist/process-manager-HUVNAPQV.js +2 -0
  65. package/dist/process-manager-TLZOTO4Y.js +2 -0
  66. package/dist/process-manager-TLZOTO4Y.js.map +1 -0
  67. package/dist/registry-PQWRVNF2.js +2 -0
  68. package/dist/registry-UQAHK77P.js +2 -0
  69. package/dist/registry-UQAHK77P.js.map +1 -0
  70. package/dist/run-4GSZFGQZ.js +95 -0
  71. package/dist/shell-5ZNXFGXV.js +3 -0
  72. package/dist/shell-OGTSH4RJ.js +3 -0
  73. package/dist/shell-OGTSH4RJ.js.map +1 -0
  74. package/dist/status-KIISF542.js +56 -0
  75. package/dist/task-NUCRHYW7.js +209 -0
  76. package/dist/team-IBUP5XV4.js +97 -0
  77. package/dist/template-engine-322SCRR6.js +2 -0
  78. package/dist/template-engine-322SCRR6.js.map +1 -0
  79. package/dist/template-engine-3CDRZNMJ.js +3 -0
  80. package/dist/tui-WWZA73IO.js +225 -0
  81. package/dist/update-RJ4IYACQ.js +64 -0
  82. package/dist/update-check-4RV7Z6WT.js +2 -0
  83. package/dist/workspace-manager-47KI7B27.js +179 -0
  84. package/dist/workspace-manager-7M46ESUL.js +2 -0
  85. package/dist/workspace-manager-7M46ESUL.js.map +1 -0
  86. package/package.json +79 -0
  87. package/readme.md +270 -0
@@ -0,0 +1,1292 @@
1
+ #!/usr/bin/env node
2
+ import { DEFAULT_PROMPT_TEMPLATE, buildPromptContext } from './chunk-HNKJ4IF7.js';
3
+ import { resolveFailureStatus, isDispatchable, isTerminal, isBlocked, resolveCompletionStatus, calculateRetryDelay } from './chunk-33QNTNR6.js';
4
+ import { AUTONOMOUS_LABEL } from './chunk-PNE6LQRF.js';
5
+ import { LockConflictError, TaskAlreadyRunningError, NoAgentsError } from './chunk-O5AO5QIR.js';
6
+ import { dirname } from 'path';
7
+ import fs from 'fs/promises';
8
+ import { execFile } from 'child_process';
9
+
10
+ function scopesOverlap(a, b) {
11
+ if (!a?.length || !b?.length) return false;
12
+ for (const pa of a) {
13
+ for (const pb of b) {
14
+ if (patternsOverlap(pa, pb)) return true;
15
+ }
16
+ }
17
+ return false;
18
+ }
19
+ function patternsOverlap(a, b) {
20
+ if (a === b) return true;
21
+ const aBase = a.split("*")[0];
22
+ const bBase = b.split("*")[0];
23
+ if (aBase.startsWith(bBase) || bBase.startsWith(aBase)) return true;
24
+ if (!aBase.endsWith("/") && !bBase.endsWith("/")) {
25
+ const aDir = dirname(aBase);
26
+ const bDir = dirname(bBase);
27
+ return aDir === bDir && aDir !== ".";
28
+ }
29
+ return false;
30
+ }
31
+ async function acquireLock(lockPath) {
32
+ const bakPath = lockPath + ".bak";
33
+ const existing = await readLockPid(lockPath);
34
+ if (existing !== null) {
35
+ if (isProcessAlive(existing)) {
36
+ return { acquired: false, pid: existing };
37
+ }
38
+ try {
39
+ await fs.rename(lockPath, bakPath);
40
+ } catch {
41
+ }
42
+ }
43
+ try {
44
+ const fd = await fs.open(lockPath, "wx");
45
+ await fd.writeFile(String(process.pid), "utf-8");
46
+ await fd.close();
47
+ await fs.unlink(bakPath).catch(() => {
48
+ });
49
+ return { acquired: true, pid: process.pid };
50
+ } catch (err) {
51
+ if (err.code === "EEXIST") {
52
+ await fs.rename(bakPath, lockPath).catch(() => {
53
+ });
54
+ const pid = await readLockPid(lockPath);
55
+ return { acquired: false, pid: pid ?? void 0 };
56
+ }
57
+ throw err;
58
+ }
59
+ }
60
+ async function releaseLock(lockPath) {
61
+ await fs.unlink(lockPath).catch(() => {
62
+ });
63
+ }
64
+ async function readLockPid(lockPath) {
65
+ try {
66
+ const content = await fs.readFile(lockPath, "utf-8");
67
+ const pid = parseInt(content.trim(), 10);
68
+ return isNaN(pid) ? null : pid;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ function isProcessAlive(pid) {
74
+ try {
75
+ process.kill(pid, 0);
76
+ return true;
77
+ } catch (err) {
78
+ if (err.code === "EPERM") return true;
79
+ return false;
80
+ }
81
+ }
82
+
83
+ // src/infrastructure/storage/cached-stores.ts
84
+ var CachedTaskStore = class {
85
+ constructor(inner) {
86
+ this.inner = inner;
87
+ }
88
+ cache = /* @__PURE__ */ new Map();
89
+ async list(filter) {
90
+ const key = filter?.status ?? "__all__";
91
+ if (this.cache.has(key)) {
92
+ return this.cache.get(key);
93
+ }
94
+ const result = await this.inner.list(filter);
95
+ this.cache.set(key, result);
96
+ return result;
97
+ }
98
+ async get(id) {
99
+ return this.inner.get(id);
100
+ }
101
+ async save(task) {
102
+ await this.inner.save(task);
103
+ this.cache.clear();
104
+ }
105
+ async delete(id) {
106
+ await this.inner.delete(id);
107
+ this.cache.clear();
108
+ }
109
+ invalidate() {
110
+ this.cache.clear();
111
+ }
112
+ };
113
+ var CachedAgentStore = class {
114
+ constructor(inner) {
115
+ this.inner = inner;
116
+ }
117
+ listCache = null;
118
+ async list() {
119
+ if (this.listCache) {
120
+ return this.listCache;
121
+ }
122
+ const result = await this.inner.list();
123
+ this.listCache = result;
124
+ return result;
125
+ }
126
+ async get(id) {
127
+ return this.inner.get(id);
128
+ }
129
+ async getByName(name) {
130
+ return this.inner.getByName(name);
131
+ }
132
+ async save(agent) {
133
+ await this.inner.save(agent);
134
+ this.listCache = null;
135
+ }
136
+ async delete(id) {
137
+ await this.inner.delete(id);
138
+ this.listCache = null;
139
+ }
140
+ invalidate() {
141
+ this.listCache = null;
142
+ }
143
+ };
144
+ var CachedGoalStore = class {
145
+ constructor(inner) {
146
+ this.inner = inner;
147
+ }
148
+ cache = /* @__PURE__ */ new Map();
149
+ async list(filter) {
150
+ const key = filter?.status ?? "__all__";
151
+ if (this.cache.has(key)) return this.cache.get(key);
152
+ const result = await this.inner.list(filter);
153
+ this.cache.set(key, result);
154
+ return result;
155
+ }
156
+ async get(id) {
157
+ return this.inner.get(id);
158
+ }
159
+ async save(goal) {
160
+ await this.inner.save(goal);
161
+ this.cache.clear();
162
+ }
163
+ async delete(id) {
164
+ await this.inner.delete(id);
165
+ this.cache.clear();
166
+ }
167
+ invalidate() {
168
+ this.cache.clear();
169
+ }
170
+ };
171
+ var CRITERION_COMMANDS = {
172
+ test_pass: { cmd: "npm", args: ["test"] },
173
+ typecheck: { cmd: "npx", args: ["tsc", "--noEmit"] },
174
+ lint: { cmd: "npm", args: ["run", "lint"] }
175
+ };
176
+ var ReviewRunner = class {
177
+ cwd;
178
+ timeoutMs;
179
+ constructor(options) {
180
+ this.cwd = options.cwd;
181
+ this.timeoutMs = options.timeout_ms ?? 12e4;
182
+ }
183
+ /**
184
+ * Run all criteria and return results.
185
+ * Continues running even if one criterion fails.
186
+ */
187
+ async runAll(criteria) {
188
+ const results = [];
189
+ for (const criterion of criteria) {
190
+ const result = await this.runCriterion(criterion);
191
+ results.push(result);
192
+ }
193
+ return results;
194
+ }
195
+ /**
196
+ * Check if all results passed.
197
+ */
198
+ static allPassed(results) {
199
+ return results.length > 0 && results.every((r) => r.passed);
200
+ }
201
+ /**
202
+ * Format results into a human-readable report.
203
+ */
204
+ static formatReport(results) {
205
+ const lines = results.map((r) => {
206
+ const icon = r.passed ? "\u2713" : "\u2717";
207
+ const truncated = r.output.slice(0, 500);
208
+ return `${icon} ${r.criterion}: ${r.passed ? "PASSED" : "FAILED"}
209
+ ${truncated}`;
210
+ });
211
+ return lines.join("\n\n");
212
+ }
213
+ runCriterion(criterion) {
214
+ const { cmd, args } = CRITERION_COMMANDS[criterion];
215
+ return new Promise((resolve) => {
216
+ execFile(
217
+ cmd,
218
+ args,
219
+ { cwd: this.cwd, timeout: this.timeoutMs, maxBuffer: 1024 * 1024 },
220
+ (error, stdout, stderr) => {
221
+ const output = (stdout + "\n" + stderr).trim();
222
+ resolve({
223
+ criterion,
224
+ passed: !error,
225
+ output: output.slice(0, 2e3)
226
+ });
227
+ }
228
+ );
229
+ });
230
+ }
231
+ };
232
+
233
+ // src/application/orchestrator.ts
234
+ var MAX_EVENT_DATA_LEN = 8192;
235
+ var MAX_BUS_DATA_LEN = 4096;
236
+ var Orchestrator = class {
237
+ constructor(deps) {
238
+ this.deps = deps;
239
+ this.cachedTaskStore = new CachedTaskStore(deps.taskStore);
240
+ this.cachedAgentStore = new CachedAgentStore(deps.agentStore);
241
+ this.cachedGoalStore = deps.goalStore ? new CachedGoalStore(deps.goalStore) : null;
242
+ }
243
+ intervalId = null;
244
+ shuttingDown = false;
245
+ state = null;
246
+ abortControllers = /* @__PURE__ */ new Map();
247
+ cachedTaskStore;
248
+ cachedAgentStore;
249
+ cachedGoalStore;
250
+ saveStateTimer = null;
251
+ saveStateDirty = false;
252
+ lockAcquired = false;
253
+ consecutiveTickFailures = 0;
254
+ maxConsecutiveTickFailures = 5;
255
+ maxRetryQueueSize = 100;
256
+ signalHandlers = [];
257
+ immediateDispatchTimer = null;
258
+ taskCreatedUnsub = null;
259
+ tickInProgress = false;
260
+ /** Promise-chain mutex to serialize critical state mutations. */
261
+ stateMutex = Promise.resolve();
262
+ /**
263
+ * Check if this instance owns the lock (can mutate state).
264
+ */
265
+ get isOwner() {
266
+ return this.lockAcquired;
267
+ }
268
+ /**
269
+ * Serialize access to state mutations via a Promise-chain mutex.
270
+ * Prevents concurrent tick/stop/reconcile from reading stale state.
271
+ */
272
+ withStateLock(fn) {
273
+ let release;
274
+ const next = new Promise((resolve) => {
275
+ release = resolve;
276
+ });
277
+ const prev = this.stateMutex;
278
+ this.stateMutex = next;
279
+ return prev.then(async () => {
280
+ try {
281
+ return await fn();
282
+ } finally {
283
+ release();
284
+ }
285
+ });
286
+ }
287
+ /**
288
+ * Run a single task by ID. Acquires lock for the duration of the run.
289
+ */
290
+ async runTask(taskId) {
291
+ await this.withTemporaryLock(async () => {
292
+ await this.loadState();
293
+ await this.dispatchTask(taskId);
294
+ });
295
+ }
296
+ /**
297
+ * Run all dispatchable tasks. Acquires lock for the duration of the run.
298
+ */
299
+ async runAll() {
300
+ await this.withTemporaryLock(async () => {
301
+ await this.loadState();
302
+ await this.dispatchAll();
303
+ });
304
+ }
305
+ /**
306
+ * Acquire lock, run fn, then release lock.
307
+ * Used by single-shot commands (runTask, runAll) that don't go through startWatch.
308
+ */
309
+ async withTemporaryLock(fn) {
310
+ const lockResult = await acquireLock(this.deps.lockPath);
311
+ if (!lockResult.acquired) {
312
+ throw new LockConflictError(lockResult.pid);
313
+ }
314
+ this.lockAcquired = true;
315
+ try {
316
+ await fn();
317
+ } finally {
318
+ this.lockAcquired = false;
319
+ await releaseLock(this.deps.lockPath);
320
+ }
321
+ }
322
+ /**
323
+ * Start watch mode — continuous tick loop.
324
+ * Acquires a PID lock to prevent multiple orchestrators.
325
+ */
326
+ async startWatch() {
327
+ const lockResult = await acquireLock(this.deps.lockPath);
328
+ if (!lockResult.acquired) {
329
+ throw new LockConflictError(lockResult.pid);
330
+ }
331
+ this.lockAcquired = true;
332
+ await this.loadState();
333
+ this.state.pid = process.pid;
334
+ this.state.started_at = (/* @__PURE__ */ new Date()).toISOString();
335
+ await this.saveState();
336
+ this.registerSignalHandlers();
337
+ this.taskCreatedUnsub = this.deps.eventBus.on("task:created", () => {
338
+ this.scheduleImmediateDispatch();
339
+ });
340
+ await this.tick();
341
+ this.intervalId = setInterval(
342
+ () => this.tick().then(
343
+ () => {
344
+ this.consecutiveTickFailures = 0;
345
+ },
346
+ (err) => {
347
+ this.consecutiveTickFailures++;
348
+ const error = err instanceof Error ? err.message : String(err);
349
+ this.deps.eventBus.emit({
350
+ type: "orchestrator:error",
351
+ error,
352
+ context: "tick",
353
+ fatal: this.consecutiveTickFailures >= this.maxConsecutiveTickFailures
354
+ });
355
+ if (this.consecutiveTickFailures >= this.maxConsecutiveTickFailures) {
356
+ this.deps.eventBus.emit({
357
+ type: "orchestrator:shutdown",
358
+ reason: `${this.consecutiveTickFailures} consecutive tick failures`
359
+ });
360
+ this.stop().catch((err2) => {
361
+ this.deps.eventBus.emit({ type: "orchestrator:error", error: err2 instanceof Error ? err2.message : String(err2), context: "stop after consecutive tick failures", fatal: false });
362
+ });
363
+ }
364
+ }
365
+ ),
366
+ this.deps.config.scheduling.poll_interval_ms
367
+ );
368
+ }
369
+ /**
370
+ * Register SIGINT/SIGTERM handlers for graceful shutdown.
371
+ */
372
+ registerSignalHandlers() {
373
+ const handler = (signal) => {
374
+ this.deps.eventBus.emit({
375
+ type: "orchestrator:shutdown",
376
+ reason: `Received ${signal}`
377
+ });
378
+ this.stop().catch((err) => {
379
+ this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `stop after ${signal} signal`, fatal: false });
380
+ });
381
+ };
382
+ for (const sig of ["SIGINT", "SIGTERM"]) {
383
+ const bound = () => handler(sig);
384
+ this.signalHandlers.push([sig, bound]);
385
+ process.on(sig, bound);
386
+ }
387
+ }
388
+ /**
389
+ * Remove signal handlers to avoid listener leaks.
390
+ */
391
+ removeSignalHandlers() {
392
+ for (const [sig, handler] of this.signalHandlers) {
393
+ process.removeListener(sig, handler);
394
+ }
395
+ this.signalHandlers = [];
396
+ }
397
+ /**
398
+ * Stop the watch loop and clean up.
399
+ */
400
+ async stop() {
401
+ if (this.shuttingDown) return;
402
+ this.shuttingDown = true;
403
+ if (this.intervalId) {
404
+ clearInterval(this.intervalId);
405
+ this.intervalId = null;
406
+ }
407
+ if (this.taskCreatedUnsub) {
408
+ this.taskCreatedUnsub();
409
+ this.taskCreatedUnsub = null;
410
+ }
411
+ if (this.immediateDispatchTimer) {
412
+ clearTimeout(this.immediateDispatchTimer);
413
+ this.immediateDispatchTimer = null;
414
+ }
415
+ await this.flushStateLazy();
416
+ await this.withStateLock(async () => {
417
+ if (this.state) {
418
+ for (const [taskId, entry] of Object.entries(this.state.running)) {
419
+ this.abortControllers.get(taskId)?.abort();
420
+ this.abortControllers.delete(taskId);
421
+ await this.deps.processManager.killWithGrace(entry.pid);
422
+ await this.deps.runService.finish(entry.run_id, "cancelled");
423
+ const task = await this.deps.taskStore.get(taskId);
424
+ if (task) {
425
+ await this.deps.taskService.updateStatus(taskId, resolveFailureStatus(task));
426
+ }
427
+ await this.deps.agentService.setStatus(entry.agent_id, "idle");
428
+ }
429
+ this.state.running = {};
430
+ this.state.claimed = [];
431
+ this.state.pid = void 0;
432
+ this.state.started_at = void 0;
433
+ await this.saveState();
434
+ }
435
+ });
436
+ if (this.lockAcquired) {
437
+ await releaseLock(this.deps.lockPath);
438
+ this.lockAcquired = false;
439
+ }
440
+ this.removeSignalHandlers();
441
+ }
442
+ /**
443
+ * Cancel a running task: kill agent process, clean state, mark cancelled.
444
+ * Acquires lock if not already owned (standalone CLI invocation).
445
+ */
446
+ async cancelTask(taskId) {
447
+ if (!this.lockAcquired) {
448
+ return this.withTemporaryLock(() => this.cancelTask(taskId));
449
+ }
450
+ await this.withStateLock(async () => {
451
+ await this.loadState();
452
+ const state = this.state;
453
+ const entry = state.running[taskId];
454
+ if (entry) {
455
+ this.abortControllers.get(taskId)?.abort();
456
+ this.abortControllers.delete(taskId);
457
+ await this.deps.processManager.killWithGrace(entry.pid, 3e3).catch((err) => {
458
+ this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `cancelTask kill process ${entry.pid} for task ${taskId}`, fatal: false });
459
+ });
460
+ await this.deps.runService.finish(entry.run_id, "cancelled").catch((err) => {
461
+ this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `cancelTask finish run ${entry.run_id}`, fatal: false });
462
+ });
463
+ await this.deps.agentService.setStatus(entry.agent_id, "idle").catch((err) => {
464
+ this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `cancelTask setStatus idle for agent ${entry.agent_id}`, fatal: false });
465
+ });
466
+ delete state.running[taskId];
467
+ await this.saveState();
468
+ }
469
+ state.retry_queue = state.retry_queue.filter((r) => r.task_id !== taskId);
470
+ try {
471
+ await this.deps.taskService.cancel(taskId);
472
+ } catch {
473
+ try {
474
+ await this.deps.taskService.updateStatus(taskId, "cancelled");
475
+ } catch {
476
+ }
477
+ }
478
+ await this.saveState();
479
+ });
480
+ }
481
+ /**
482
+ * Force-stop a specific agent: kill process, clean state, release agent.
483
+ * Acquires lock if not already owned (standalone CLI invocation).
484
+ */
485
+ async forceStopAgent(agentId) {
486
+ if (!this.lockAcquired) {
487
+ return this.withTemporaryLock(() => this.forceStopAgent(agentId));
488
+ }
489
+ await this.withStateLock(async () => {
490
+ await this.loadState();
491
+ const state = this.state;
492
+ for (const [taskId, entry] of Object.entries(state.running)) {
493
+ if (entry.agent_id === agentId) {
494
+ this.abortControllers.get(taskId)?.abort();
495
+ this.abortControllers.delete(taskId);
496
+ await this.deps.processManager.killWithGrace(entry.pid, 3e3);
497
+ await this.deps.runService.finish(entry.run_id, "cancelled");
498
+ try {
499
+ await this.deps.taskService.updateStatus(taskId, "failed");
500
+ } catch {
501
+ }
502
+ delete state.running[taskId];
503
+ }
504
+ }
505
+ await this.deps.agentService.setStatus(agentId, "idle");
506
+ await this.saveState();
507
+ });
508
+ }
509
+ /**
510
+ * Single tick: Reconcile → Dispatch → Collect
511
+ * Serialized via mutex to prevent concurrent ticks from racing on state.
512
+ */
513
+ async tick() {
514
+ if (this.shuttingDown) return;
515
+ this.tickInProgress = true;
516
+ try {
517
+ await this.withStateLock(async () => {
518
+ if (this.shuttingDown) return;
519
+ this.cachedTaskStore.invalidate();
520
+ this.cachedAgentStore.invalidate();
521
+ this.cachedGoalStore?.invalidate();
522
+ await this.loadState();
523
+ await this.reconcile();
524
+ await this.seedAutonomousTasks();
525
+ await this.dispatchAll();
526
+ const tasks = await this.cachedTaskStore.list();
527
+ const running = Object.keys(this.state.running).length;
528
+ const queued = tasks.filter((t) => isDispatchable(t.status)).length;
529
+ this.deps.eventBus.emit({
530
+ type: "orchestrator:tick",
531
+ running,
532
+ queued
533
+ });
534
+ });
535
+ } finally {
536
+ this.tickInProgress = false;
537
+ }
538
+ }
539
+ /**
540
+ * Schedule an immediate dispatch with 500ms debounce.
541
+ * Called on task:created to avoid waiting for the next 30s tick.
542
+ */
543
+ scheduleImmediateDispatch() {
544
+ if (this.shuttingDown) return;
545
+ if (this.immediateDispatchTimer) return;
546
+ this.immediateDispatchTimer = setTimeout(() => {
547
+ this.immediateDispatchTimer = null;
548
+ if (this.shuttingDown || this.tickInProgress) return;
549
+ this.immediateDispatch().catch((err) => {
550
+ this.deps.eventBus.emit({
551
+ type: "orchestrator:error",
552
+ error: err instanceof Error ? err.message : String(err),
553
+ context: "immediate dispatch on task:created",
554
+ fatal: false
555
+ });
556
+ });
557
+ }, 500);
558
+ }
559
+ /**
560
+ * Mini-tick: invalidate caches → loadState → dispatchAll → saveState.
561
+ * Skips reconcile/collect — only dispatches new tasks immediately.
562
+ */
563
+ async immediateDispatch() {
564
+ if (this.shuttingDown) return;
565
+ await this.withStateLock(async () => {
566
+ if (this.shuttingDown) return;
567
+ this.cachedTaskStore.invalidate();
568
+ this.cachedAgentStore.invalidate();
569
+ await this.loadState();
570
+ await this.dispatchAll();
571
+ await this.saveState();
572
+ });
573
+ }
574
+ /**
575
+ * Reconcile: check PID liveness, detect stalls, process retry queue.
576
+ */
577
+ async reconcile() {
578
+ const state = this.state;
579
+ const now = Date.now();
580
+ for (const [taskId, entry] of Object.entries(state.running)) {
581
+ const taskData = await this.deps.taskStore.get(taskId);
582
+ if (!taskData || isTerminal(taskData.status)) {
583
+ this.abortControllers.delete(taskId);
584
+ delete state.running[taskId];
585
+ await this.deps.agentService.setStatus(entry.agent_id, "idle").catch((err) => {
586
+ this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `reconcile setStatus idle for stale agent ${entry.agent_id} (task ${taskId})`, fatal: false });
587
+ });
588
+ continue;
589
+ }
590
+ if (!this.deps.processManager.isAlive(entry.pid)) {
591
+ try {
592
+ await this._handleRunFailure(taskId, entry, "Process crashed unexpectedly");
593
+ } catch {
594
+ delete state.running[taskId];
595
+ await this.deps.agentService.setStatus(entry.agent_id, "idle").catch((err) => {
596
+ this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `reconcile crash fallback setStatus idle for agent ${entry.agent_id} (task ${taskId})`, fatal: false });
597
+ });
598
+ }
599
+ continue;
600
+ }
601
+ const lastEventAt = new Date(entry.last_event_at).getTime();
602
+ const stallTimeout = this.deps.config.defaults.agent.stall_timeout_ms;
603
+ if (now - lastEventAt > stallTimeout) {
604
+ this.deps.eventBus.emit({
605
+ type: "orchestrator:stall_detected",
606
+ runId: entry.run_id
607
+ });
608
+ this.abortControllers.get(taskId)?.abort();
609
+ await this.deps.processManager.killWithGrace(entry.pid, 5e3);
610
+ try {
611
+ await this._handleRunFailure(taskId, entry, "Agent stalled (no events)");
612
+ } catch {
613
+ delete state.running[taskId];
614
+ await this.deps.agentService.setStatus(entry.agent_id, "idle").catch((err) => {
615
+ this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `reconcile stall fallback setStatus idle for agent ${entry.agent_id} (task ${taskId})`, fatal: false });
616
+ });
617
+ }
618
+ }
619
+ }
620
+ const runningAgentIds = new Set(Object.values(state.running).map((e) => e.agent_id));
621
+ const allAgents = await this.cachedAgentStore.list();
622
+ for (const agent of allAgents) {
623
+ if (agent.status === "running" && !runningAgentIds.has(agent.id)) {
624
+ await this.deps.agentService.setStatus(agent.id, "idle");
625
+ }
626
+ }
627
+ const allTasks = await this.cachedTaskStore.list();
628
+ for (const task of allTasks) {
629
+ if (task.status === "in_progress" && !state.running[task.id]) {
630
+ try {
631
+ await this.deps.taskService.updateStatus(task.id, "failed");
632
+ } catch {
633
+ task.status = "failed";
634
+ task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
635
+ await this.deps.taskStore.save(task).catch((err) => {
636
+ this.deps.eventBus.emit({
637
+ type: "orchestrator:error",
638
+ error: err instanceof Error ? err.message : String(err),
639
+ context: `force-write orphaned task ${task.id}`,
640
+ fatal: false
641
+ });
642
+ });
643
+ }
644
+ this.deps.eventBus.emit({
645
+ type: "task:orphaned",
646
+ taskId: task.id
647
+ });
648
+ }
649
+ }
650
+ for (let i = state.retry_queue.length - 1; i >= 0; i--) {
651
+ const retry = state.retry_queue[i];
652
+ if (now >= new Date(retry.due_at).getTime()) {
653
+ state.retry_queue.splice(i, 1);
654
+ await this.dispatchTask(retry.task_id);
655
+ }
656
+ }
657
+ await this.saveState();
658
+ }
659
+ /**
660
+ * Create tasks for autonomous agents that have no active work.
661
+ *
662
+ * Priority: active Goals assigned to the agent come first.
663
+ * If no goals, falls back to role-based autonomous work.
664
+ */
665
+ async seedAutonomousTasks() {
666
+ const agents = await this.cachedAgentStore.list();
667
+ const autonomousAgents = agents.filter(
668
+ (a) => a.autonomous && a.status === "idle"
669
+ );
670
+ if (autonomousAgents.length === 0) return;
671
+ const allTasks = await this.cachedTaskStore.list();
672
+ const activeGoals = this.cachedGoalStore ? await this.cachedGoalStore.list({ status: "active" }) : [];
673
+ let anyCreated = false;
674
+ const claimedGoalIds = /* @__PURE__ */ new Set();
675
+ for (const agent of autonomousAgents) {
676
+ const hasActiveTask = allTasks.some(
677
+ (t) => t.assignee === agent.id && !isTerminal(t.status)
678
+ );
679
+ if (hasActiveTask) continue;
680
+ const goal = activeGoals.find(
681
+ (g) => g.assignee === agent.id && !claimedGoalIds.has(g.id)
682
+ ) ?? activeGoals.find(
683
+ (g) => !g.assignee && !claimedGoalIds.has(g.id)
684
+ );
685
+ if (goal) claimedGoalIds.add(goal.id);
686
+ const role = agent.role ?? "general assistant";
687
+ const title = goal ? `[auto] ${agent.name}: ${goal.title.slice(0, 60)}` : `[auto] ${agent.name}: ${role.slice(0, 60)}`;
688
+ const description = goal ? `## GOAL (highest priority)
689
+
690
+ ${goal.description || goal.title}
691
+
692
+ ---
693
+ Agent role: ${role}` : `Autonomous work cycle. Agent role: ${role}`;
694
+ try {
695
+ await this.deps.taskService.create({
696
+ title,
697
+ description,
698
+ assignee: agent.id,
699
+ labels: [AUTONOMOUS_LABEL],
700
+ priority: 3
701
+ });
702
+ anyCreated = true;
703
+ } catch (err) {
704
+ this.deps.eventBus.emit({
705
+ type: "orchestrator:error",
706
+ error: err instanceof Error ? err.message : String(err),
707
+ context: `autonomous task for agent ${agent.id}`,
708
+ fatal: false
709
+ });
710
+ }
711
+ }
712
+ if (anyCreated) this.cachedTaskStore.invalidate();
713
+ }
714
+ /**
715
+ * Dispatch all dispatchable tasks up to max_concurrent_agents.
716
+ */
717
+ async dispatchAll() {
718
+ const state = this.state;
719
+ const maxConcurrent = this.deps.config.scheduling.max_concurrent_agents;
720
+ const currentRunning = Object.keys(state.running).length;
721
+ const availableSlots = maxConcurrent - currentRunning;
722
+ if (availableSlots <= 0) return;
723
+ const allTasks = await this.cachedTaskStore.list();
724
+ const candidates = allTasks.filter(
725
+ (t) => isDispatchable(t.status) && !isBlocked(t, allTasks) && !state.running[t.id] && !state.claimed.includes(t.id)
726
+ ).sort((a, b) => {
727
+ const bTime = b.updated_at ?? "";
728
+ const aTime = a.updated_at ?? "";
729
+ return bTime < aTime ? -1 : bTime > aTime ? 1 : 0;
730
+ }).slice(0, availableSlots);
731
+ const blockedIds = /* @__PURE__ */ new Set();
732
+ const inProgressScoped = allTasks.filter((t) => t.status === "in_progress" && t.scope?.length);
733
+ for (let i = 0; i < candidates.length; i++) {
734
+ const candidate = candidates[i];
735
+ if (!candidate.scope?.length) continue;
736
+ const approvedPeers = candidates.slice(0, i).filter((c) => !blockedIds.has(c.id));
737
+ const compareTo = [...inProgressScoped, ...approvedPeers];
738
+ let overlapping = false;
739
+ for (const other of compareTo) {
740
+ if (scopesOverlap(candidate.scope, other.scope)) {
741
+ this.deps.eventBus.emit({
742
+ type: "task:scope_overlap",
743
+ taskId: candidate.id,
744
+ overlappingTaskId: other.id,
745
+ patterns: candidate.scope
746
+ });
747
+ overlapping = true;
748
+ break;
749
+ }
750
+ }
751
+ if (overlapping) blockedIds.add(candidate.id);
752
+ }
753
+ for (const task of candidates) {
754
+ if (blockedIds.has(task.id)) continue;
755
+ try {
756
+ await this.dispatchTask(task.id);
757
+ } catch (err) {
758
+ this.deps.eventBus.emit({
759
+ type: "orchestrator:error",
760
+ error: err instanceof Error ? err.message : String(err),
761
+ context: `dispatch task ${task.id}`,
762
+ fatal: false
763
+ });
764
+ }
765
+ }
766
+ }
767
+ /**
768
+ * Dispatch a single task: claim → assign → execute.
769
+ */
770
+ async dispatchTask(taskId) {
771
+ const state = this.state;
772
+ if (state.running[taskId]) {
773
+ const entry = state.running[taskId];
774
+ throw new TaskAlreadyRunningError(taskId, entry.run_id, entry.agent_id);
775
+ }
776
+ const task = await this.deps.taskService.get(taskId);
777
+ state.claimed.push(taskId);
778
+ await this.saveState();
779
+ try {
780
+ const agent = await this.deps.agentService.findBestAgent(task);
781
+ if (!agent) {
782
+ const allAgents2 = await this.cachedAgentStore.list();
783
+ if (allAgents2.length === 0) {
784
+ throw new NoAgentsError();
785
+ }
786
+ this.unclaim(taskId);
787
+ await this.saveState();
788
+ return;
789
+ }
790
+ const { path: workspacePath, branch: worktreeBranch } = await this.deps.workspaceManager.prepare(
791
+ task,
792
+ agent,
793
+ this.deps.config
794
+ );
795
+ const template = this.deps.config.prompt?.template ?? DEFAULT_PROMPT_TEMPLATE;
796
+ const allAgents = await this.cachedAgentStore.list();
797
+ const attempt = task.attempts + 1;
798
+ let retryContext;
799
+ if (attempt > 1) {
800
+ const failedData = await this.deps.runService.getLastFailedRunContext(task.id);
801
+ if (failedData) {
802
+ retryContext = {
803
+ previous_error: failedData.error,
804
+ previous_output: failedData.output
805
+ };
806
+ }
807
+ }
808
+ const sharedContext = this.deps.contextStore ? await this.deps.contextStore.getAll() : void 0;
809
+ const pendingMessages = this.deps.messageService ? await this.deps.messageService.drainMailbox(agent.id, task.id) : [];
810
+ const context = buildPromptContext(
811
+ task,
812
+ agent,
813
+ attempt,
814
+ workspacePath,
815
+ this.deps.config,
816
+ { allAgents, retryContext, sharedContext, feedback: task.feedback, messages: pendingMessages.length ? pendingMessages : void 0 }
817
+ );
818
+ const prompt = await this.deps.templateEngine.render(template, context);
819
+ const run = await this.deps.runService.create({
820
+ taskId: task.id,
821
+ agentId: agent.id,
822
+ attempt,
823
+ prompt,
824
+ workspacePath
825
+ });
826
+ if (task.status === "failed" || task.status === "cancelled") {
827
+ await this.deps.taskService.retry(taskId);
828
+ task.status = "todo";
829
+ task.attempts = 0;
830
+ }
831
+ await this.deps.taskService.updateStatus(taskId, "in_progress");
832
+ await this.deps.taskService.assign(taskId, agent.id);
833
+ await this.deps.taskService.incrementAttempts(taskId);
834
+ if (worktreeBranch) {
835
+ task.proof = { ...task.proof ?? { files_changed: [] }, branch: worktreeBranch };
836
+ task.workspace = workspacePath;
837
+ await this.deps.taskStore.save(task);
838
+ }
839
+ await this.deps.agentService.setStatus(agent.id, "running");
840
+ const agentData = await this.deps.agentService.get(agent.id);
841
+ agentData.current_task = taskId;
842
+ await this.deps.agentStore.save(agentData);
843
+ const adapter = this.deps.adapterRegistry.require(agent.adapter);
844
+ const abortController = new AbortController();
845
+ this.abortControllers.set(taskId, abortController);
846
+ const handle = adapter.execute({
847
+ prompt,
848
+ workspace: workspacePath,
849
+ env: {
850
+ ...agent.config.env,
851
+ ORCH_AGENT_ID: agent.id,
852
+ ORCH_AGENT_NAME: agent.name,
853
+ ORCH_TASK_ID: task.id
854
+ },
855
+ config: agentData.config,
856
+ signal: abortController.signal
857
+ });
858
+ const agentPid = handle.pid;
859
+ const now = (/* @__PURE__ */ new Date()).toISOString();
860
+ await this.deps.runService.start(run.id, agentPid);
861
+ this.unclaim(taskId);
862
+ state.running[taskId] = {
863
+ run_id: run.id,
864
+ agent_id: agent.id,
865
+ task_id: taskId,
866
+ pid: agentPid,
867
+ started_at: now,
868
+ last_event_at: now
869
+ };
870
+ await this.saveState();
871
+ this.collectEvents(
872
+ handle.events,
873
+ run.id,
874
+ taskId,
875
+ agent.id
876
+ ).catch((err) => {
877
+ this.deps.eventBus.emit({
878
+ type: "orchestrator:error",
879
+ error: err instanceof Error ? err.message : String(err),
880
+ context: `adapter execution for ${taskId}`,
881
+ fatal: false
882
+ });
883
+ });
884
+ } catch (err) {
885
+ this.unclaim(taskId);
886
+ await this.saveState();
887
+ throw err;
888
+ }
889
+ }
890
+ /**
891
+ * Collect events from an adapter's async generator.
892
+ */
893
+ async collectEvents(generator, runId, taskId, agentId) {
894
+ let collectedTokens;
895
+ let resultText;
896
+ let lastAgentMessage;
897
+ const filesChangedSet = /* @__PURE__ */ new Set();
898
+ try {
899
+ for await (const event of generator) {
900
+ if (this.shuttingDown) break;
901
+ if (event.type === "done") {
902
+ if (event.tokens) collectedTokens = event.tokens;
903
+ const data = event.data;
904
+ if (data && typeof data.result === "string") {
905
+ resultText = data.result;
906
+ }
907
+ }
908
+ if (event.type === "output") {
909
+ const data = event.data;
910
+ if (data) {
911
+ const text = typeof data.text === "string" ? data.text : typeof data.message === "string" ? data.message : void 0;
912
+ if (text?.trim()) lastAgentMessage = text;
913
+ }
914
+ }
915
+ if (event.type === "file_change") {
916
+ const data = event.data;
917
+ if (data && Array.isArray(data.paths)) {
918
+ for (const p of data.paths) {
919
+ if (typeof p === "string") filesChangedSet.add(p);
920
+ }
921
+ } else {
922
+ const filePath = data && typeof data.path === "string" ? data.path : typeof event.data === "string" ? event.data : String(event.data);
923
+ filesChangedSet.add(filePath);
924
+ }
925
+ }
926
+ const eventTimestamp = isValidISOTimestamp(event.timestamp) ? event.timestamp : (/* @__PURE__ */ new Date()).toISOString();
927
+ const serialized = serializeEventData(event.data, MAX_EVENT_DATA_LEN);
928
+ event.data = void 0;
929
+ const runEvent = {
930
+ timestamp: eventTimestamp,
931
+ type: event.type === "output" ? "agent_output" : event.type === "file_change" ? "file_changed" : event.type === "command" ? "command_run" : event.type === "tool_call" ? "tool_call" : event.type === "error" ? "error" : "done",
932
+ data: serialized
933
+ };
934
+ await this.deps.runService.appendEvent(runId, runEvent);
935
+ if (this.state?.running[taskId]) {
936
+ this.state.running[taskId].last_event_at = eventTimestamp;
937
+ this.saveStateLazy();
938
+ }
939
+ const busData = serializeEventData(serialized, MAX_BUS_DATA_LEN);
940
+ if (event.type === "output" || event.type === "tool_call") {
941
+ this.deps.eventBus.emit({
942
+ type: "agent:output",
943
+ runId,
944
+ agentId,
945
+ data: busData
946
+ });
947
+ } else if (event.type === "file_change") {
948
+ this.deps.eventBus.emit({
949
+ type: "agent:file_changed",
950
+ runId,
951
+ agentId,
952
+ path: typeof event.data === "string" ? event.data : String(event.data)
953
+ });
954
+ } else if (event.type === "error") {
955
+ this.deps.eventBus.emit({
956
+ type: "agent:error",
957
+ runId,
958
+ agentId,
959
+ error: busData
960
+ });
961
+ }
962
+ }
963
+ const finalResult = resultText ?? lastAgentMessage;
964
+ await this.handleRunSuccess(taskId, runId, agentId, collectedTokens, finalResult, [...filesChangedSet]);
965
+ } catch (err) {
966
+ const error = err instanceof Error ? err.message : String(err);
967
+ const entry = this.state?.running[taskId];
968
+ if (entry) {
969
+ await this.handleRunFailure(taskId, entry, error);
970
+ }
971
+ }
972
+ }
973
+ async handleRunSuccess(taskId, runId, agentId, tokens, resultText, filesChanged) {
974
+ return this.withStateLock(() => this._handleRunSuccess(taskId, runId, agentId, tokens, resultText, filesChanged));
975
+ }
976
+ async _handleRunSuccess(taskId, runId, agentId, tokens, resultText, filesChanged) {
977
+ await this.flushStateLazy();
978
+ this.abortControllers.delete(taskId);
979
+ const state = this.state;
980
+ if (!state.running[taskId]) return;
981
+ const task = await this.deps.taskStore.get(taskId);
982
+ if (!task) return;
983
+ task.proof = {
984
+ ...task.proof,
985
+ agent_summary: resultText?.slice(0, 2e3) ?? task.proof?.agent_summary,
986
+ files_changed: filesChanged?.length ? filesChanged : task.proof?.files_changed ?? []
987
+ };
988
+ delete task.feedback;
989
+ await this.deps.taskStore.save(task);
990
+ const agent = await this.deps.agentStore.get(agentId);
991
+ const isAutonomousTask = task.labels?.includes(AUTONOMOUS_LABEL);
992
+ const autoApprove = isAutonomousTask || agent?.config.approval_policy === "auto";
993
+ const newStatus = resolveCompletionStatus(task, true, autoApprove);
994
+ await this.deps.runService.finish(runId, "succeeded", tokens);
995
+ const runningEntry = state.running[taskId];
996
+ const successRuntimeMs = runningEntry ? Date.now() - new Date(runningEntry.started_at).getTime() : 0;
997
+ if (runningEntry) {
998
+ state.stats.total_runtime_ms += successRuntimeMs;
999
+ }
1000
+ delete state.running[taskId];
1001
+ const statsUpdate = {
1002
+ tasks_completed: (agent?.stats.tasks_completed ?? 0) + 1,
1003
+ total_runs: (agent?.stats.total_runs ?? 0) + 1,
1004
+ total_runtime_ms: (agent?.stats.total_runtime_ms ?? 0) + successRuntimeMs
1005
+ };
1006
+ if (tokens) {
1007
+ statsUpdate.tokens_used = (agent?.stats.tokens_used ?? 0) + tokens.total;
1008
+ }
1009
+ await this.deps.agentService.updateStats(agentId, statsUpdate).catch((err) => {
1010
+ this.deps.eventBus.emit({
1011
+ type: "orchestrator:error",
1012
+ error: err instanceof Error ? err.message : String(err),
1013
+ context: `agent stats update for ${agentId}`,
1014
+ fatal: false
1015
+ });
1016
+ });
1017
+ state.stats.total_tasks_completed++;
1018
+ state.stats.total_runs++;
1019
+ if (tokens) {
1020
+ state.stats.total_tokens.input += tokens.input;
1021
+ state.stats.total_tokens.output += tokens.output;
1022
+ state.stats.total_tokens.total = state.stats.total_tokens.input + state.stats.total_tokens.output;
1023
+ }
1024
+ if (task.proof?.branch) {
1025
+ try {
1026
+ const mergeResult = await this.deps.workspaceManager.mergeBack(task.proof.branch);
1027
+ if (mergeResult.success) {
1028
+ this.deps.eventBus.emit({
1029
+ type: "workspace:merge_succeeded",
1030
+ taskId,
1031
+ branch: task.proof.branch
1032
+ });
1033
+ await this.deps.workspaceManager.cleanup(taskId).catch((err) => {
1034
+ this.deps.eventBus.emit({
1035
+ type: "orchestrator:error",
1036
+ error: err instanceof Error ? err.message : String(err),
1037
+ context: `workspace cleanup for ${taskId}`,
1038
+ fatal: false
1039
+ });
1040
+ });
1041
+ } else {
1042
+ this.deps.eventBus.emit({
1043
+ type: "workspace:merge_conflict",
1044
+ taskId,
1045
+ branch: task.proof.branch,
1046
+ conflictInfo: mergeResult.conflictInfo
1047
+ });
1048
+ await this.forceTaskToReview(task, agentId, `MERGE CONFLICT: ${mergeResult.conflictInfo}`);
1049
+ return;
1050
+ }
1051
+ } catch (err) {
1052
+ const error = err instanceof Error ? err.message : String(err);
1053
+ await this.forceTaskToReview(task, agentId, `MERGE ERROR: ${error}`);
1054
+ return;
1055
+ }
1056
+ }
1057
+ try {
1058
+ await this.deps.taskService.updateStatus(taskId, newStatus);
1059
+ } catch (validationErr) {
1060
+ const error = validationErr instanceof Error ? validationErr.message : String(validationErr);
1061
+ this.deps.eventBus.emit({
1062
+ type: "orchestrator:error",
1063
+ error,
1064
+ context: `state machine validation failed for task ${taskId} -> ${newStatus}, force-writing`,
1065
+ fatal: false
1066
+ });
1067
+ task.status = newStatus;
1068
+ task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1069
+ await this.deps.taskStore.save(task).catch((saveErr) => {
1070
+ this.deps.eventBus.emit({
1071
+ type: "orchestrator:error",
1072
+ error: saveErr instanceof Error ? saveErr.message : String(saveErr),
1073
+ context: `force-write task ${taskId} to store failed`,
1074
+ fatal: false
1075
+ });
1076
+ });
1077
+ }
1078
+ await this.deps.agentService.setStatus(agentId, "idle").catch((err) => {
1079
+ this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `_handleRunSuccess setStatus idle for agent ${agentId}`, fatal: false });
1080
+ });
1081
+ const agentAfter = await this.deps.agentStore.get(agentId);
1082
+ if (agentAfter) {
1083
+ agentAfter.current_task = void 0;
1084
+ await this.deps.agentStore.save(agentAfter);
1085
+ }
1086
+ if (newStatus === "review" && task.review_criteria?.length) {
1087
+ await this.runAutoReview(taskId, task.review_criteria, task.workspace ?? this.deps.projectRoot);
1088
+ } else if (newStatus === "review" && autoApprove) {
1089
+ await this.deps.taskService.updateStatus(taskId, "done");
1090
+ }
1091
+ await this.saveState();
1092
+ this.scheduleImmediateDispatch();
1093
+ }
1094
+ async handleRunFailure(taskId, entry, error) {
1095
+ return this.withStateLock(() => this._handleRunFailure(taskId, entry, error));
1096
+ }
1097
+ async _handleRunFailure(taskId, entry, error) {
1098
+ await this.flushStateLazy();
1099
+ this.abortControllers.delete(taskId);
1100
+ const state = this.state;
1101
+ const task = await this.deps.taskStore.get(taskId);
1102
+ if (!task) return;
1103
+ await this.deps.runService.finish(entry.run_id, "failed", void 0, error);
1104
+ await this.deps.agentService.setStatus(entry.agent_id, "idle");
1105
+ const agentAfterIdle = await this.deps.agentStore.get(entry.agent_id);
1106
+ if (agentAfterIdle) {
1107
+ agentAfterIdle.current_task = void 0;
1108
+ await this.deps.agentStore.save(agentAfterIdle);
1109
+ }
1110
+ const agent = await this.deps.agentStore.get(entry.agent_id);
1111
+ const runtimeMs = Date.now() - new Date(entry.started_at).getTime();
1112
+ await this.deps.agentService.updateStats(entry.agent_id, {
1113
+ tasks_failed: (agent?.stats.tasks_failed ?? 0) + 1,
1114
+ total_runs: (agent?.stats.total_runs ?? 0) + 1,
1115
+ total_runtime_ms: (agent?.stats.total_runtime_ms ?? 0) + runtimeMs
1116
+ });
1117
+ const failureStatus = resolveFailureStatus(task);
1118
+ await this.deps.taskService.updateStatus(taskId, failureStatus);
1119
+ if (failureStatus === "retrying") {
1120
+ const delay = calculateRetryDelay(
1121
+ task.attempts - 1,
1122
+ this.deps.config.scheduling.retry_base_delay_ms,
1123
+ this.deps.config.scheduling.retry_max_delay_ms
1124
+ );
1125
+ const alreadyQueued = state.retry_queue.some((r) => r.task_id === taskId);
1126
+ if (!alreadyQueued) {
1127
+ if (state.retry_queue.length >= this.maxRetryQueueSize) {
1128
+ state.retry_queue.shift();
1129
+ }
1130
+ state.retry_queue.push({
1131
+ task_id: taskId,
1132
+ attempt: task.attempts + 1,
1133
+ due_at: new Date(Date.now() + delay).toISOString(),
1134
+ error
1135
+ });
1136
+ }
1137
+ this.deps.eventBus.emit({
1138
+ type: "run:retry",
1139
+ runId: entry.run_id,
1140
+ attempt: task.attempts + 1,
1141
+ delay_ms: delay
1142
+ });
1143
+ } else {
1144
+ state.stats.total_tasks_failed++;
1145
+ }
1146
+ state.stats.total_runtime_ms += runtimeMs;
1147
+ delete state.running[taskId];
1148
+ state.stats.total_runs++;
1149
+ await this.saveState();
1150
+ this.scheduleImmediateDispatch();
1151
+ }
1152
+ /**
1153
+ * Run automatic review criteria on a task in 'review' status.
1154
+ * If all criteria pass, transition review → done.
1155
+ * If any fail, stay in review with results attached.
1156
+ */
1157
+ async runAutoReview(taskId, criteria, cwd) {
1158
+ const runner = new ReviewRunner({ cwd });
1159
+ const results = await runner.runAll(criteria);
1160
+ const allPassed = ReviewRunner.allPassed(results);
1161
+ const task = await this.deps.taskStore.get(taskId);
1162
+ if (!task) return;
1163
+ task.review_results = results;
1164
+ task.proof = {
1165
+ ...task.proof,
1166
+ test_results: ReviewRunner.formatReport(results),
1167
+ files_changed: task.proof?.files_changed ?? []
1168
+ };
1169
+ await this.deps.taskStore.save(task);
1170
+ this.deps.eventBus.emit({
1171
+ type: "task:auto_reviewed",
1172
+ taskId,
1173
+ passed: allPassed,
1174
+ results
1175
+ });
1176
+ if (allPassed) {
1177
+ try {
1178
+ await this.deps.taskService.updateStatus(taskId, "done");
1179
+ } catch (validationErr) {
1180
+ const error = validationErr instanceof Error ? validationErr.message : String(validationErr);
1181
+ this.deps.eventBus.emit({
1182
+ type: "orchestrator:error",
1183
+ error,
1184
+ context: `auto-review transition failed for task ${taskId} -> done, force-writing`,
1185
+ fatal: false
1186
+ });
1187
+ task.status = "done";
1188
+ task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1189
+ await this.deps.taskStore.save(task).catch((saveErr) => {
1190
+ this.deps.eventBus.emit({
1191
+ type: "orchestrator:error",
1192
+ error: saveErr instanceof Error ? saveErr.message : String(saveErr),
1193
+ context: `force-write task ${taskId} to store failed (auto-review)`,
1194
+ fatal: false
1195
+ });
1196
+ });
1197
+ }
1198
+ }
1199
+ }
1200
+ /**
1201
+ * Force a task to 'review' status with a summary prefix.
1202
+ * Used when merge-back fails (conflict or infrastructure error).
1203
+ */
1204
+ async forceTaskToReview(task, agentId, summaryPrefix) {
1205
+ task.proof = {
1206
+ ...task.proof,
1207
+ agent_summary: `${summaryPrefix}
1208
+
1209
+ ${task.proof?.agent_summary ?? ""}`.slice(0, 2e3),
1210
+ files_changed: task.proof?.files_changed ?? []
1211
+ };
1212
+ task.status = "review";
1213
+ task.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1214
+ await this.deps.taskStore.save(task);
1215
+ await this.deps.agentService.setStatus(agentId, "idle").catch((err) => {
1216
+ this.deps.eventBus.emit({ type: "orchestrator:error", error: err instanceof Error ? err.message : String(err), context: `forceTaskToReview setStatus idle for agent ${agentId}`, fatal: false });
1217
+ });
1218
+ const agentAfter = await this.deps.agentStore.get(agentId);
1219
+ if (agentAfter) {
1220
+ agentAfter.current_task = void 0;
1221
+ await this.deps.agentStore.save(agentAfter);
1222
+ }
1223
+ await this.saveState();
1224
+ }
1225
+ unclaim(taskId) {
1226
+ const idx = this.state.claimed.indexOf(taskId);
1227
+ if (idx !== -1) this.state.claimed.splice(idx, 1);
1228
+ }
1229
+ /**
1230
+ * Throw if this instance doesn't own the lock (read-only session).
1231
+ */
1232
+ requireOwnership() {
1233
+ if (!this.lockAcquired) {
1234
+ throw new LockConflictError(0);
1235
+ }
1236
+ }
1237
+ async loadState() {
1238
+ this.state = await this.deps.stateStore.read();
1239
+ }
1240
+ async saveState() {
1241
+ if (this.state) {
1242
+ await this.deps.stateStore.write(this.state);
1243
+ }
1244
+ }
1245
+ /**
1246
+ * Debounced saveState — batches rapid writes within 500ms window.
1247
+ * Used for non-critical updates like last_event_at in collectEvents.
1248
+ */
1249
+ saveStateLazy() {
1250
+ this.saveStateDirty = true;
1251
+ if (this.saveStateTimer) return;
1252
+ this.saveStateTimer = setTimeout(() => {
1253
+ this.saveStateTimer = null;
1254
+ if (this.saveStateDirty) {
1255
+ this.saveStateDirty = false;
1256
+ this.saveState().catch((err) => {
1257
+ this.deps.eventBus.emit({
1258
+ type: "orchestrator:error",
1259
+ error: err instanceof Error ? err.message : String(err),
1260
+ context: "debounced state save",
1261
+ fatal: false
1262
+ });
1263
+ });
1264
+ }
1265
+ }, 500);
1266
+ }
1267
+ /**
1268
+ * Flush any pending debounced saveState immediately.
1269
+ * Call before critical transitions to ensure state is persisted.
1270
+ */
1271
+ async flushStateLazy() {
1272
+ if (this.saveStateTimer) {
1273
+ clearTimeout(this.saveStateTimer);
1274
+ this.saveStateTimer = null;
1275
+ }
1276
+ if (this.saveStateDirty) {
1277
+ this.saveStateDirty = false;
1278
+ await this.saveState();
1279
+ }
1280
+ }
1281
+ };
1282
+ function isValidISOTimestamp(value) {
1283
+ if (typeof value !== "string") return false;
1284
+ const d = new Date(value);
1285
+ return !isNaN(d.getTime()) && d.toISOString() === value;
1286
+ }
1287
+ function serializeEventData(data, maxLen) {
1288
+ const str = typeof data === "string" ? data : JSON.stringify(data);
1289
+ return str.length > maxLen ? str.slice(0, maxLen) + "\u2026" : str;
1290
+ }
1291
+
1292
+ export { Orchestrator };