oxe-cc 1.2.1 → 1.3.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 (276) hide show
  1. package/.cursor/commands/oxe-ask.md +2 -2
  2. package/.cursor/commands/oxe-capabilities.md +2 -2
  3. package/.cursor/commands/oxe-checkpoint.md +2 -2
  4. package/.cursor/commands/oxe-compact.md +2 -2
  5. package/.cursor/commands/oxe-dashboard.md +2 -2
  6. package/.cursor/commands/oxe-debug.md +2 -2
  7. package/.cursor/commands/oxe-discuss.md +2 -2
  8. package/.cursor/commands/oxe-execute.md +5 -2
  9. package/.cursor/commands/oxe-forensics.md +2 -2
  10. package/.cursor/commands/oxe-help.md +2 -2
  11. package/.cursor/commands/oxe-loop.md +2 -2
  12. package/.cursor/commands/oxe-milestone.md +2 -2
  13. package/.cursor/commands/oxe-next.md +2 -2
  14. package/.cursor/commands/oxe-obs.md +2 -2
  15. package/.cursor/commands/oxe-plan-agent.md +2 -2
  16. package/.cursor/commands/oxe-plan.md +2 -2
  17. package/.cursor/commands/oxe-project.md +2 -2
  18. package/.cursor/commands/oxe-quick.md +2 -2
  19. package/.cursor/commands/oxe-research.md +2 -2
  20. package/.cursor/commands/oxe-retro.md +2 -2
  21. package/.cursor/commands/oxe-review-pr.md +2 -2
  22. package/.cursor/commands/oxe-route.md +2 -2
  23. package/.cursor/commands/oxe-scan.md +2 -2
  24. package/.cursor/commands/oxe-security.md +2 -2
  25. package/.cursor/commands/oxe-session.md +2 -2
  26. package/.cursor/commands/oxe-ship.md +2 -2
  27. package/.cursor/commands/oxe-skill.md +2 -2
  28. package/.cursor/commands/oxe-spec.md +2 -2
  29. package/.cursor/commands/oxe-ui-review.md +2 -2
  30. package/.cursor/commands/oxe-ui-spec.md +2 -2
  31. package/.cursor/commands/oxe-update.md +2 -2
  32. package/.cursor/commands/oxe-validate-gaps.md +2 -2
  33. package/.cursor/commands/oxe-verify.md +5 -2
  34. package/.cursor/commands/oxe-workstream.md +2 -2
  35. package/.cursor/commands/oxe.md +2 -2
  36. package/.github/copilot-instructions.md +13 -13
  37. package/.github/prompts/oxe-ask.prompt.md +2 -2
  38. package/.github/prompts/oxe-capabilities.prompt.md +2 -2
  39. package/.github/prompts/oxe-checkpoint.prompt.md +2 -2
  40. package/.github/prompts/oxe-compact.prompt.md +2 -2
  41. package/.github/prompts/oxe-dashboard.prompt.md +2 -2
  42. package/.github/prompts/oxe-debug.prompt.md +2 -2
  43. package/.github/prompts/oxe-discuss.prompt.md +2 -2
  44. package/.github/prompts/oxe-execute.prompt.md +5 -2
  45. package/.github/prompts/oxe-forensics.prompt.md +2 -2
  46. package/.github/prompts/oxe-help.prompt.md +2 -2
  47. package/.github/prompts/oxe-loop.prompt.md +2 -2
  48. package/.github/prompts/oxe-milestone.prompt.md +2 -2
  49. package/.github/prompts/oxe-next.prompt.md +2 -2
  50. package/.github/prompts/oxe-obs.prompt.md +2 -2
  51. package/.github/prompts/oxe-plan-agent.prompt.md +2 -2
  52. package/.github/prompts/oxe-plan.prompt.md +2 -2
  53. package/.github/prompts/oxe-project.prompt.md +2 -2
  54. package/.github/prompts/oxe-quick.prompt.md +2 -2
  55. package/.github/prompts/oxe-research.prompt.md +2 -2
  56. package/.github/prompts/oxe-retro.prompt.md +2 -2
  57. package/.github/prompts/oxe-review-pr.prompt.md +2 -2
  58. package/.github/prompts/oxe-route.prompt.md +2 -2
  59. package/.github/prompts/oxe-scan.prompt.md +2 -2
  60. package/.github/prompts/oxe-security.prompt.md +2 -2
  61. package/.github/prompts/oxe-session.prompt.md +2 -2
  62. package/.github/prompts/oxe-ship.prompt.md +2 -2
  63. package/.github/prompts/oxe-skill.prompt.md +2 -2
  64. package/.github/prompts/oxe-spec.prompt.md +2 -2
  65. package/.github/prompts/oxe-ui-review.prompt.md +2 -2
  66. package/.github/prompts/oxe-ui-spec.prompt.md +2 -2
  67. package/.github/prompts/oxe-update.prompt.md +2 -2
  68. package/.github/prompts/oxe-validate-gaps.prompt.md +2 -2
  69. package/.github/prompts/oxe-verify.prompt.md +5 -2
  70. package/.github/prompts/oxe-workstream.prompt.md +2 -2
  71. package/.github/prompts/oxe.prompt.md +2 -2
  72. package/CHANGELOG.md +52 -17
  73. package/README.md +610 -551
  74. package/bin/banner.txt +1 -1
  75. package/bin/lib/oxe-agent-install.cjs +69 -69
  76. package/bin/lib/oxe-azure.cjs +1445 -1445
  77. package/bin/lib/oxe-context-engine.cjs +867 -867
  78. package/bin/lib/oxe-dashboard.cjs +76 -28
  79. package/bin/lib/oxe-operational.cjs +2144 -1340
  80. package/bin/lib/oxe-project-health.cjs +483 -1
  81. package/bin/lib/oxe-runtime-semantics.cjs +12 -0
  82. package/bin/oxe-cc.js +554 -152
  83. package/commands/oxe/ask.md +2 -2
  84. package/commands/oxe/capabilities.md +2 -2
  85. package/commands/oxe/checkpoint.md +2 -2
  86. package/commands/oxe/compact.md +2 -2
  87. package/commands/oxe/dashboard.md +2 -2
  88. package/commands/oxe/debug.md +2 -2
  89. package/commands/oxe/discuss.md +2 -2
  90. package/commands/oxe/execute.md +5 -2
  91. package/commands/oxe/forensics.md +2 -2
  92. package/commands/oxe/help.md +2 -2
  93. package/commands/oxe/loop.md +2 -2
  94. package/commands/oxe/milestone.md +2 -2
  95. package/commands/oxe/next.md +2 -2
  96. package/commands/oxe/obs.md +2 -2
  97. package/commands/oxe/oxe.md +2 -2
  98. package/commands/oxe/plan-agent.md +2 -2
  99. package/commands/oxe/plan.md +2 -2
  100. package/commands/oxe/project.md +2 -2
  101. package/commands/oxe/quick.md +2 -2
  102. package/commands/oxe/research.md +2 -2
  103. package/commands/oxe/retro.md +2 -2
  104. package/commands/oxe/review-pr.md +2 -2
  105. package/commands/oxe/route.md +2 -2
  106. package/commands/oxe/scan.md +2 -2
  107. package/commands/oxe/security.md +2 -2
  108. package/commands/oxe/session.md +2 -2
  109. package/commands/oxe/ship.md +2 -2
  110. package/commands/oxe/skill.md +2 -2
  111. package/commands/oxe/spec.md +2 -2
  112. package/commands/oxe/ui-review.md +2 -2
  113. package/commands/oxe/ui-spec.md +2 -2
  114. package/commands/oxe/update.md +2 -2
  115. package/commands/oxe/validate-gaps.md +2 -2
  116. package/commands/oxe/verify.md +5 -2
  117. package/commands/oxe/workstream.md +2 -2
  118. package/lib/runtime/delivery/branch-manager.d.ts +1 -0
  119. package/lib/runtime/delivery/branch-manager.js +7 -0
  120. package/lib/runtime/delivery/ci-checks.js +34 -1
  121. package/lib/runtime/delivery/delivery-records.d.ts +34 -0
  122. package/lib/runtime/delivery/delivery-records.js +48 -0
  123. package/lib/runtime/delivery/index.d.ts +1 -0
  124. package/lib/runtime/delivery/index.js +1 -0
  125. package/lib/runtime/delivery/promotion-pipeline.d.ts +26 -2
  126. package/lib/runtime/delivery/promotion-pipeline.js +111 -14
  127. package/lib/runtime/gate/gate-manager.d.ts +41 -0
  128. package/lib/runtime/gate/gate-manager.js +108 -1
  129. package/lib/runtime/index.d.ts +2 -2
  130. package/lib/runtime/index.js +3 -1
  131. package/lib/runtime/models/gate-decision.d.ts +4 -1
  132. package/lib/runtime/models/workspace.d.ts +3 -0
  133. package/lib/runtime/plugins/capability-adapter.d.ts +12 -0
  134. package/lib/runtime/plugins/capability-adapter.js +204 -0
  135. package/lib/runtime/plugins/capability-matrix.d.ts +5 -0
  136. package/lib/runtime/plugins/capability-matrix.js +48 -17
  137. package/lib/runtime/plugins/index.d.ts +1 -0
  138. package/lib/runtime/plugins/index.js +1 -0
  139. package/lib/runtime/plugins/plugin-abi.d.ts +2 -0
  140. package/lib/runtime/plugins/plugin-manifest.d.ts +1 -1
  141. package/lib/runtime/plugins/plugin-manifest.js +6 -2
  142. package/lib/runtime/plugins/plugin-registry.d.ts +46 -0
  143. package/lib/runtime/plugins/plugin-registry.js +79 -2
  144. package/lib/runtime/policy/policy-engine.d.ts +19 -0
  145. package/lib/runtime/policy/policy-engine.js +76 -4
  146. package/lib/runtime/projection/projection-engine.d.ts +9 -1
  147. package/lib/runtime/projection/projection-engine.js +73 -3
  148. package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +43 -1
  149. package/lib/runtime/scheduler/multi-agent-coordinator.js +151 -39
  150. package/lib/runtime/scheduler/run-journal.d.ts +1 -1
  151. package/lib/runtime/scheduler/scheduler.d.ts +19 -1
  152. package/lib/runtime/scheduler/scheduler.js +258 -13
  153. package/lib/runtime/verification/verification-compiler.d.ts +43 -0
  154. package/lib/runtime/verification/verification-compiler.js +137 -0
  155. package/lib/runtime/verification/verification-manifest.d.ts +9 -0
  156. package/lib/runtime/verification/verification-manifest.js +56 -6
  157. package/lib/runtime/workspace/strategies/ephemeral-container.d.ts +1 -0
  158. package/lib/runtime/workspace/strategies/ephemeral-container.js +4 -0
  159. package/lib/runtime/workspace/strategies/git-worktree.d.ts +1 -0
  160. package/lib/runtime/workspace/strategies/git-worktree.js +2 -0
  161. package/lib/runtime/workspace/strategies/inplace.d.ts +1 -0
  162. package/lib/runtime/workspace/strategies/inplace.js +2 -0
  163. package/lib/runtime/workspace/workspace-manager.d.ts +2 -1
  164. package/lib/sdk/README.md +9 -9
  165. package/lib/sdk/index.cjs +33 -24
  166. package/lib/sdk/index.d.ts +149 -14
  167. package/oxe/templates/ACTIVE-RUN.template.json +32 -32
  168. package/oxe/templates/CAPABILITIES.template.md +7 -7
  169. package/oxe/templates/CAPABILITY.template.md +45 -45
  170. package/oxe/templates/CHECKPOINTS.template.md +7 -7
  171. package/oxe/templates/EXECUTION-RUNTIME.template.md +68 -68
  172. package/oxe/templates/HYPOTHESES.template.md +33 -33
  173. package/oxe/templates/LESSONS-METRICS.template.json +13 -13
  174. package/oxe/templates/NOTES.template.md +16 -16
  175. package/oxe/templates/PLAN-REVIEW.template.md +31 -31
  176. package/oxe/templates/SESSION.template.md +34 -34
  177. package/oxe/templates/SKILL.template.md +26 -26
  178. package/oxe/templates/STATE.md +55 -55
  179. package/oxe/templates/WORKFLOW_AUTHORING.md +18 -18
  180. package/oxe/workflows/ask.md +96 -96
  181. package/oxe/workflows/capabilities.md +25 -25
  182. package/oxe/workflows/dashboard.md +33 -33
  183. package/oxe/workflows/discuss.md +12 -12
  184. package/oxe/workflows/execute.md +14 -0
  185. package/oxe/workflows/help.md +352 -352
  186. package/oxe/workflows/next.md +22 -22
  187. package/oxe/workflows/oxe.md +6 -6
  188. package/oxe/workflows/plan-agent.md +9 -9
  189. package/oxe/workflows/quick.md +10 -10
  190. package/oxe/workflows/references/reasoning-discovery.md +28 -28
  191. package/oxe/workflows/references/reasoning-execution.md +29 -29
  192. package/oxe/workflows/references/reasoning-planning.md +32 -32
  193. package/oxe/workflows/references/reasoning-review.md +29 -29
  194. package/oxe/workflows/references/reasoning-status.md +24 -24
  195. package/oxe/workflows/references/robustness-elevation.md +295 -295
  196. package/oxe/workflows/references/workflow-runtime-contracts.json +952 -930
  197. package/oxe/workflows/route.md +16 -16
  198. package/oxe/workflows/session.md +213 -213
  199. package/oxe/workflows/ship.md +142 -142
  200. package/oxe/workflows/skill.md +44 -44
  201. package/oxe/workflows/ui-review.md +36 -36
  202. package/oxe/workflows/verify-audit.md +73 -73
  203. package/oxe/workflows/verify.md +10 -0
  204. package/package.json +92 -92
  205. package/packages/runtime/package.json +17 -17
  206. package/packages/runtime/src/audit/audit-trail.ts +243 -243
  207. package/packages/runtime/src/audit/index.ts +2 -2
  208. package/packages/runtime/src/audit/policy-pack.ts +62 -62
  209. package/packages/runtime/src/compiler/graph-compiler.ts +245 -245
  210. package/packages/runtime/src/compiler/index.ts +1 -1
  211. package/packages/runtime/src/context/context-pack-builder.ts +259 -259
  212. package/packages/runtime/src/context/context-pack-store.ts +197 -197
  213. package/packages/runtime/src/context/context-profiles.ts +60 -60
  214. package/packages/runtime/src/context/index.ts +3 -3
  215. package/packages/runtime/src/decision/decision-engine.ts +174 -174
  216. package/packages/runtime/src/decision/decision-memo.ts +211 -211
  217. package/packages/runtime/src/decision/index.ts +2 -2
  218. package/packages/runtime/src/delivery/branch-manager.ts +91 -84
  219. package/packages/runtime/src/delivery/ci-checks.ts +285 -252
  220. package/packages/runtime/src/delivery/delivery-records.ts +75 -0
  221. package/packages/runtime/src/delivery/index.ts +5 -4
  222. package/packages/runtime/src/delivery/pr-manager.ts +112 -112
  223. package/packages/runtime/src/delivery/promotion-pipeline.ts +334 -180
  224. package/packages/runtime/src/events/bus.ts +92 -92
  225. package/packages/runtime/src/events/catalog.ts +29 -29
  226. package/packages/runtime/src/events/envelope.ts +14 -14
  227. package/packages/runtime/src/events/index.ts +3 -3
  228. package/packages/runtime/src/evidence/evidence-store.ts +130 -130
  229. package/packages/runtime/src/evidence/index.ts +1 -1
  230. package/packages/runtime/src/gate/gate-manager.ts +289 -137
  231. package/packages/runtime/src/gate/index.ts +1 -1
  232. package/packages/runtime/src/index.ts +41 -37
  233. package/packages/runtime/src/models/attempt.ts +19 -19
  234. package/packages/runtime/src/models/evidence.ts +21 -21
  235. package/packages/runtime/src/models/gate-decision.ts +25 -21
  236. package/packages/runtime/src/models/index.ts +8 -8
  237. package/packages/runtime/src/models/run.ts +24 -24
  238. package/packages/runtime/src/models/session.ts +11 -11
  239. package/packages/runtime/src/models/verification-result.ts +10 -10
  240. package/packages/runtime/src/models/work-item.ts +25 -25
  241. package/packages/runtime/src/models/workspace.ts +31 -28
  242. package/packages/runtime/src/plugins/capability-adapter.ts +206 -0
  243. package/packages/runtime/src/plugins/capability-matrix.ts +126 -83
  244. package/packages/runtime/src/plugins/index.ts +5 -4
  245. package/packages/runtime/src/plugins/plugin-abi.ts +97 -95
  246. package/packages/runtime/src/plugins/plugin-manifest.ts +118 -113
  247. package/packages/runtime/src/plugins/plugin-registry.ts +232 -124
  248. package/packages/runtime/src/policy/index.ts +1 -1
  249. package/packages/runtime/src/policy/policy-engine.ts +330 -244
  250. package/packages/runtime/src/projection/index.ts +1 -1
  251. package/packages/runtime/src/projection/projection-engine.ts +328 -249
  252. package/packages/runtime/src/reducers/debug-reducer.ts +36 -36
  253. package/packages/runtime/src/reducers/index.ts +2 -2
  254. package/packages/runtime/src/reducers/run-state-reducer.ts +269 -269
  255. package/packages/runtime/src/scheduler/agent-registry.ts +132 -132
  256. package/packages/runtime/src/scheduler/agent-roles.ts +109 -109
  257. package/packages/runtime/src/scheduler/index.ts +4 -4
  258. package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +521 -333
  259. package/packages/runtime/src/scheduler/run-journal.ts +62 -62
  260. package/packages/runtime/src/scheduler/scheduler.ts +722 -441
  261. package/packages/runtime/src/verification/index.ts +2 -2
  262. package/packages/runtime/src/verification/verification-compiler.ts +436 -225
  263. package/packages/runtime/src/verification/verification-manifest.ts +252 -192
  264. package/packages/runtime/src/workspace/index.ts +5 -5
  265. package/packages/runtime/src/workspace/strategies/ephemeral-container.ts +126 -121
  266. package/packages/runtime/src/workspace/strategies/git-worktree.ts +79 -77
  267. package/packages/runtime/src/workspace/strategies/inplace.ts +38 -35
  268. package/packages/runtime/src/workspace/workspace-manager.ts +16 -15
  269. package/packages/runtime/tsconfig.json +17 -17
  270. package/vscode-extension/.vscodeignore +7 -7
  271. package/vscode-extension/oxe-agents-1.0.0.vsix +0 -0
  272. package/vscode-extension/package.json +185 -185
  273. package/vscode-extension/src/extension.js +310 -310
  274. package/vscode-extension/src/shared/contextLoader.js +137 -137
  275. package/vscode-extension/src/shared/contractBuilder.js +159 -159
  276. package/vscode-extension/src/shared/stateReader.js +101 -101
@@ -1,333 +1,521 @@
1
- import { appendEvent } from '../events/bus';
2
- import type { ExecutionGraph, GraphNode } from '../compiler/graph-compiler';
3
- import type { WorkspaceManager } from '../workspace/workspace-manager';
4
- import type { TaskExecutor, TaskResult, SchedulerContext } from './scheduler';
5
- import { Scheduler } from './scheduler';
6
- import type { WorkspaceLease } from '../models/workspace';
7
- import { buildHandoff } from './agent-roles';
8
- import type { CooperativeHandoff } from './agent-roles';
9
-
10
- export type CoordinationMode = 'parallel' | 'competitive' | 'cooperative';
11
-
12
- export interface AgentSpec {
13
- id: string;
14
- executor: TaskExecutor;
15
- workspaceManager: WorkspaceManager;
16
- /** Task IDs this agent is responsible for (used in parallel mode) */
17
- assignedTaskIds?: string[];
18
- }
19
-
20
- export interface CoordinationOptions {
21
- mode: CoordinationMode;
22
- agents: AgentSpec[];
23
- projectRoot: string;
24
- sessionId: string | null;
25
- runId: string;
26
- onEvent?: SchedulerContext['onEvent'];
27
- }
28
-
29
- export interface CoordinationResult {
30
- mode: CoordinationMode;
31
- run_id: string;
32
- completed: string[];
33
- failed: string[];
34
- blocked: string[];
35
- agent_results: Array<{ agent_id: string; completed: string[]; failed: string[] }>;
36
- handoffs?: CooperativeHandoff[];
37
- }
38
-
39
- // ─── Parallel mode ───────────────────────────────────────────────────────────
40
- // Tasks are partitioned across agents. Each agent runs its own Scheduler
41
- // on a sub-graph. Results are merged.
42
-
43
- async function runParallel(
44
- graph: ExecutionGraph,
45
- opts: CoordinationOptions
46
- ): Promise<CoordinationResult> {
47
- const { agents, projectRoot, sessionId, runId } = opts;
48
-
49
- // Partition tasks across agents (round-robin if assignedTaskIds not set)
50
- const partitions = agents.map((a) => a.assignedTaskIds ?? []);
51
- if (partitions.every((p) => p.length === 0)) {
52
- const allIds = [...graph.nodes.keys()];
53
- allIds.forEach((id, i) => {
54
- partitions[i % agents.length].push(id);
55
- });
56
- }
57
-
58
- appendEvent(projectRoot, sessionId, {
59
- type: 'RunStarted',
60
- run_id: runId,
61
- payload: { mode: 'parallel', agent_count: agents.length },
62
- });
63
-
64
- const agentResults = await Promise.all(
65
- agents.map(async (agent, idx) => {
66
- const subGraph = subGraphFor(graph, partitions[idx]);
67
- if (subGraph.nodes.size === 0) {
68
- return { agent_id: agent.id, completed: [], failed: [] };
69
- }
70
- const ctx: SchedulerContext = {
71
- projectRoot,
72
- sessionId,
73
- runId: `${runId}-agent${idx}`,
74
- executor: agent.executor,
75
- workspaceManager: agent.workspaceManager,
76
- onEvent: opts.onEvent,
77
- };
78
- const scheduler = new Scheduler();
79
- const result = await scheduler.run(subGraph, ctx);
80
- return { agent_id: agent.id, completed: result.completed, failed: result.failed };
81
- })
82
- );
83
-
84
- const completed = agentResults.flatMap((r) => r.completed);
85
- const failed = agentResults.flatMap((r) => r.failed);
86
-
87
- appendEvent(projectRoot, sessionId, {
88
- type: 'RunCompleted',
89
- run_id: runId,
90
- payload: { mode: 'parallel', completed: completed.length, failed: failed.length },
91
- });
92
-
93
- return { mode: 'parallel', run_id: runId, completed, failed, blocked: [], agent_results: agentResults };
94
- }
95
-
96
- // ─── Competitive mode ────────────────────────────────────────────────────────
97
- // Two agents attempt the same task. First success wins; the loser's workspace
98
- // is disposed. Requires exactly 2 agents.
99
-
100
- async function runCompetitive(
101
- graph: ExecutionGraph,
102
- opts: CoordinationOptions
103
- ): Promise<CoordinationResult> {
104
- if (opts.agents.length < 2) {
105
- throw new Error('Competitive mode requires at least 2 agents');
106
- }
107
- const [agentA, agentB] = opts.agents;
108
- const { projectRoot, sessionId, runId } = opts;
109
-
110
- appendEvent(projectRoot, sessionId, {
111
- type: 'RunStarted',
112
- run_id: runId,
113
- payload: { mode: 'competitive' },
114
- });
115
-
116
- const completed: string[] = [];
117
- const failed: string[] = [];
118
-
119
- for (const wave of graph.waves) {
120
- for (const nodeId of wave.node_ids) {
121
- const node = graph.nodes.get(nodeId)!;
122
- const result = await competeTwoAgents(nodeId, node, agentA, agentB, opts);
123
- if (result.success) completed.push(nodeId);
124
- else failed.push(nodeId);
125
- if (failed.length > 0) break;
126
- }
127
- if (failed.length > 0) break;
128
- }
129
-
130
- appendEvent(projectRoot, sessionId, {
131
- type: 'RunCompleted',
132
- run_id: runId,
133
- payload: { mode: 'competitive', completed: completed.length },
134
- });
135
-
136
- return {
137
- mode: 'competitive',
138
- run_id: runId,
139
- completed,
140
- failed,
141
- blocked: [],
142
- agent_results: [
143
- { agent_id: agentA.id, completed, failed },
144
- { agent_id: agentB.id, completed: [], failed: [] },
145
- ],
146
- };
147
- }
148
-
149
- async function competeTwoAgents(
150
- nodeId: string,
151
- node: GraphNode,
152
- agentA: AgentSpec,
153
- agentB: AgentSpec,
154
- opts: CoordinationOptions
155
- ): Promise<TaskResult> {
156
- const { projectRoot, sessionId, runId } = opts;
157
-
158
- const allocA = await agentA.workspaceManager.allocate({
159
- work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
160
- });
161
- const allocB = await agentB.workspaceManager.allocate({
162
- work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
163
- });
164
-
165
- appendEvent(projectRoot, sessionId, {
166
- type: 'AttemptStarted',
167
- run_id: runId,
168
- work_item_id: nodeId,
169
- payload: { mode: 'competitive', agents: [agentA.id, agentB.id] },
170
- });
171
-
172
- // Race both agents — first success wins
173
- const [resultA, resultB] = await Promise.all([
174
- agentA.executor.execute(node, allocA, runId, 1).catch((e) => ({
175
- success: false, failure_class: 'env' as const, evidence: [], output: String(e),
176
- })),
177
- agentB.executor.execute(node, allocB, runId, 1).catch((e) => ({
178
- success: false, failure_class: 'env' as const, evidence: [], output: String(e),
179
- })),
180
- ]);
181
-
182
- // Clean up both workspaces
183
- await Promise.all([
184
- agentA.workspaceManager.dispose(allocA.workspace_id).catch(() => {}),
185
- agentB.workspaceManager.dispose(allocB.workspace_id).catch(() => {}),
186
- ]);
187
-
188
- // Pick winner: prefer success; if both succeed, prefer A (primary agent)
189
- const winner = resultA.success ? resultA : resultB.success ? resultB : resultA;
190
-
191
- if (winner.success) {
192
- appendEvent(projectRoot, sessionId, { type: 'WorkItemCompleted', run_id: runId, work_item_id: nodeId, payload: { mode: 'competitive' } });
193
- } else {
194
- appendEvent(projectRoot, sessionId, { type: 'WorkItemBlocked', run_id: runId, work_item_id: nodeId, payload: { mode: 'competitive', failure_class: winner.failure_class } });
195
- }
196
-
197
- return winner;
198
- }
199
-
200
- // ─── Cooperative mode ────────────────────────────────────────────────────────
201
- // planner (agent[0]) does a dry-run to collect context, then hands off to
202
- // executor (agent[1]) which performs the real run. Handoffs are recorded.
203
-
204
- async function runCooperative(
205
- graph: ExecutionGraph,
206
- opts: CoordinationOptions
207
- ): Promise<CoordinationResult> {
208
- if (opts.agents.length < 2) {
209
- throw new Error('Cooperative mode requires at least 2 agents');
210
- }
211
- const [planner, executor] = opts.agents;
212
- const { projectRoot, sessionId, runId } = opts;
213
- const handoffs: CooperativeHandoff[] = [];
214
-
215
- appendEvent(projectRoot, sessionId, {
216
- type: 'RunStarted',
217
- run_id: runId,
218
- payload: { mode: 'cooperative', planner: planner.id, executor: executor.id },
219
- });
220
-
221
- const completed: string[] = [];
222
- const failed: string[] = [];
223
-
224
- for (const wave of graph.waves) {
225
- for (const nodeId of wave.node_ids) {
226
- const node = graph.nodes.get(nodeId)!;
227
-
228
- // Phase 1: planner allocates workspace + signals readiness (no output used)
229
- const planAlloc = await planner.workspaceManager.allocate({
230
- work_item_id: nodeId,
231
- attempt_number: 1,
232
- strategy: node.workspace_strategy,
233
- mutation_scope: node.mutation_scope,
234
- });
235
- await planner.workspaceManager.dispose(planAlloc.workspace_id).catch(() => {});
236
-
237
- const handoff = buildHandoff({
238
- from_agent_id: planner.id,
239
- to_agent_id: executor.id,
240
- from_role: 'planner',
241
- to_role: 'executor',
242
- work_item_id: nodeId,
243
- context_pack_ref: null,
244
- });
245
- handoffs.push(handoff);
246
-
247
- appendEvent(projectRoot, sessionId, {
248
- type: 'AttemptStarted',
249
- run_id: runId,
250
- work_item_id: nodeId,
251
- payload: { mode: 'cooperative', handoff_id: handoff.handoff_id },
252
- });
253
-
254
- // Phase 2: executor performs the real task
255
- const execAlloc = await executor.workspaceManager.allocate({
256
- work_item_id: nodeId,
257
- attempt_number: 1,
258
- strategy: node.workspace_strategy,
259
- mutation_scope: node.mutation_scope,
260
- });
261
-
262
- let result: TaskResult;
263
- try {
264
- result = await executor.executor.execute(node, execAlloc, runId, 1);
265
- } catch (e) {
266
- result = { success: false, failure_class: 'env', evidence: [], output: String(e) };
267
- }
268
- await executor.workspaceManager.dispose(execAlloc.workspace_id).catch(() => {});
269
-
270
- if (result.success) {
271
- completed.push(nodeId);
272
- appendEvent(projectRoot, sessionId, { type: 'WorkItemCompleted', run_id: runId, work_item_id: nodeId, payload: { mode: 'cooperative' } });
273
- } else {
274
- failed.push(nodeId);
275
- appendEvent(projectRoot, sessionId, { type: 'WorkItemBlocked', run_id: runId, work_item_id: nodeId, payload: { mode: 'cooperative', failure_class: result.failure_class } });
276
- break;
277
- }
278
- }
279
- if (failed.length > 0) break;
280
- }
281
-
282
- appendEvent(projectRoot, sessionId, {
283
- type: 'RunCompleted',
284
- run_id: runId,
285
- payload: { mode: 'cooperative', completed: completed.length, failed: failed.length },
286
- });
287
-
288
- return {
289
- mode: 'cooperative',
290
- run_id: runId,
291
- completed,
292
- failed,
293
- blocked: [],
294
- agent_results: [
295
- { agent_id: planner.id, completed: [], failed: [] },
296
- { agent_id: executor.id, completed, failed },
297
- ],
298
- handoffs,
299
- };
300
- }
301
-
302
- // ─── Public API ──────────────────────────────────────────────────────────────
303
-
304
- export class MultiAgentCoordinator {
305
- async run(graph: ExecutionGraph, opts: CoordinationOptions): Promise<CoordinationResult> {
306
- switch (opts.mode) {
307
- case 'parallel': return runParallel(graph, opts);
308
- case 'competitive': return runCompetitive(graph, opts);
309
- case 'cooperative': return runCooperative(graph, opts);
310
- default:
311
- throw new Error(`Unknown coordination mode: ${opts.mode}`);
312
- }
313
- }
314
- }
315
-
316
- // ─── Helpers ─────────────────────────────────────────────────────────────────
317
-
318
- function subGraphFor(graph: ExecutionGraph, nodeIds: string[]): ExecutionGraph {
319
- const ids = new Set(nodeIds);
320
- const nodes = new Map([...graph.nodes].filter(([id]) => ids.has(id)));
321
- const edges = graph.edges.filter((e) => ids.has(e.from) && ids.has(e.to));
322
- const waves = graph.waves.map((w) => ({
323
- wave_number: w.wave_number,
324
- node_ids: w.node_ids.filter((id) => ids.has(id)),
325
- })).filter((w) => w.node_ids.length > 0);
326
-
327
- return {
328
- nodes,
329
- edges,
330
- waves,
331
- metadata: { ...graph.metadata, node_count: nodes.size, wave_count: waves.length },
332
- };
333
- }
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { appendEvent } from '../events/bus';
4
+ import type { ExecutionGraph, GraphNode } from '../compiler/graph-compiler';
5
+ import type { WorkspaceManager } from '../workspace/workspace-manager';
6
+ import type { TaskExecutor, TaskResult, SchedulerContext } from './scheduler';
7
+ import { Scheduler } from './scheduler';
8
+ import { buildHandoff } from './agent-roles';
9
+ import type { CooperativeHandoff } from './agent-roles';
10
+
11
+ export type CoordinationMode = 'parallel' | 'competitive' | 'cooperative';
12
+
13
+ export interface AgentSpec {
14
+ id: string;
15
+ executor: TaskExecutor;
16
+ workspaceManager: WorkspaceManager;
17
+ /** Task IDs this agent is responsible for (used in parallel mode) */
18
+ assignedTaskIds?: string[];
19
+ }
20
+
21
+ export interface CoordinationOptions {
22
+ mode: CoordinationMode;
23
+ agents: AgentSpec[];
24
+ projectRoot: string;
25
+ sessionId: string | null;
26
+ runId: string;
27
+ onEvent?: SchedulerContext['onEvent'];
28
+ }
29
+
30
+ export interface ArbitrationRecord {
31
+ work_item_id: string;
32
+ mode: CoordinationMode;
33
+ winner_agent_id: string | null;
34
+ participant_agent_ids: string[];
35
+ success: boolean;
36
+ failure_class: TaskResult['failure_class'];
37
+ evidence_count: number;
38
+ recorded_at: string;
39
+ }
40
+
41
+ export interface MultiAgentOwnership {
42
+ work_item_id: string;
43
+ owner_agent_id: string;
44
+ }
45
+
46
+ export interface MultiAgentStatusSnapshot {
47
+ run_id: string;
48
+ mode: CoordinationMode;
49
+ workspace_isolation_required: 'isolated';
50
+ workspace_isolation_enforced: boolean;
51
+ agent_count: number;
52
+ ownership: MultiAgentOwnership[];
53
+ completed: string[];
54
+ failed: string[];
55
+ blocked: string[];
56
+ agent_results: Array<{
57
+ agent_id: string;
58
+ isolation_level: 'shared' | 'isolated';
59
+ assigned_task_ids: string[];
60
+ completed: string[];
61
+ failed: string[];
62
+ }>;
63
+ orphan_reassignments: Array<{ from_agent_id: string; to_agent_id: string; work_item_ids: string[] }>;
64
+ updated_at: string;
65
+ }
66
+
67
+ export interface CoordinationResult {
68
+ mode: CoordinationMode;
69
+ run_id: string;
70
+ completed: string[];
71
+ failed: string[];
72
+ blocked: string[];
73
+ agent_results: Array<{ agent_id: string; completed: string[]; failed: string[] }>;
74
+ handoffs?: CooperativeHandoff[];
75
+ arbitration_results?: ArbitrationRecord[];
76
+ state?: MultiAgentStatusSnapshot;
77
+ }
78
+
79
+ function ensureRunDir(projectRoot: string, runId: string): string {
80
+ const dir = path.join(projectRoot, '.oxe', 'runs', runId);
81
+ fs.mkdirSync(dir, { recursive: true });
82
+ return dir;
83
+ }
84
+
85
+ function persistMultiAgentArtifacts(
86
+ projectRoot: string,
87
+ runId: string,
88
+ state: MultiAgentStatusSnapshot,
89
+ handoffs: CooperativeHandoff[] = [],
90
+ arbitrationResults: ArbitrationRecord[] = []
91
+ ): void {
92
+ const runDir = ensureRunDir(projectRoot, runId);
93
+ fs.writeFileSync(path.join(runDir, 'multi-agent-state.json'), JSON.stringify(state, null, 2), 'utf8');
94
+ fs.writeFileSync(path.join(runDir, 'handoffs.json'), JSON.stringify(handoffs, null, 2), 'utf8');
95
+ fs.writeFileSync(path.join(runDir, 'arbitration-results.json'), JSON.stringify(arbitrationResults, null, 2), 'utf8');
96
+ }
97
+
98
+ function ensureIsolatedAgents(agents: AgentSpec[]): void {
99
+ const shared = agents.filter((agent) => agent.workspaceManager.isolation_level !== 'isolated');
100
+ if (shared.length === 0) return;
101
+ const ids = shared.map((agent) => `${agent.id}:${agent.workspaceManager.isolation_level}`).join(', ');
102
+ throw new Error(`Multi-agent requires isolated workspaces. Invalid agents: ${ids}`);
103
+ }
104
+
105
+ function buildOwnership(agents: AgentSpec[], partitions: string[][]): MultiAgentOwnership[] {
106
+ const ownership: MultiAgentOwnership[] = [];
107
+ for (let idx = 0; idx < agents.length; idx += 1) {
108
+ for (const workItemId of partitions[idx] ?? []) {
109
+ ownership.push({
110
+ work_item_id: workItemId,
111
+ owner_agent_id: agents[idx].id,
112
+ });
113
+ }
114
+ }
115
+ return ownership;
116
+ }
117
+
118
+ function makeState(
119
+ mode: CoordinationMode,
120
+ runId: string,
121
+ agents: AgentSpec[],
122
+ partitions: string[][],
123
+ agentResults: Array<{ agent_id: string; completed: string[]; failed: string[] }>,
124
+ completed: string[],
125
+ failed: string[],
126
+ blocked: string[],
127
+ orphanReassignments: Array<{ from_agent_id: string; to_agent_id: string; work_item_ids: string[] }>
128
+ ): MultiAgentStatusSnapshot {
129
+ return {
130
+ run_id: runId,
131
+ mode,
132
+ workspace_isolation_required: 'isolated',
133
+ workspace_isolation_enforced: true,
134
+ agent_count: agents.length,
135
+ ownership: buildOwnership(agents, partitions),
136
+ completed,
137
+ failed,
138
+ blocked,
139
+ agent_results: agents.map((agent, idx) => {
140
+ const result = agentResults.find((entry) => entry.agent_id === agent.id);
141
+ return {
142
+ agent_id: agent.id,
143
+ isolation_level: agent.workspaceManager.isolation_level,
144
+ assigned_task_ids: partitions[idx] ?? agent.assignedTaskIds ?? [],
145
+ completed: result?.completed ?? [],
146
+ failed: result?.failed ?? [],
147
+ };
148
+ }),
149
+ orphan_reassignments: orphanReassignments,
150
+ updated_at: new Date().toISOString(),
151
+ };
152
+ }
153
+
154
+ // ─── Parallel mode ───────────────────────────────────────────────────────────
155
+
156
+ async function runParallel(
157
+ graph: ExecutionGraph,
158
+ opts: CoordinationOptions
159
+ ): Promise<CoordinationResult> {
160
+ const { agents, projectRoot, sessionId, runId } = opts;
161
+ ensureIsolatedAgents(agents);
162
+
163
+ const partitions = agents.map((agent) => [...(agent.assignedTaskIds ?? [])]);
164
+ if (partitions.every((partition) => partition.length === 0)) {
165
+ const allIds = [...graph.nodes.keys()];
166
+ allIds.forEach((id, index) => {
167
+ partitions[index % agents.length].push(id);
168
+ });
169
+ }
170
+
171
+ appendEvent(projectRoot, sessionId, {
172
+ type: 'RunStarted',
173
+ run_id: runId,
174
+ payload: { mode: 'parallel', agent_count: agents.length, isolation_level: 'isolated' },
175
+ });
176
+
177
+ const agentResults = await Promise.all(
178
+ agents.map(async (agent, idx) => {
179
+ const subGraph = subGraphFor(graph, partitions[idx]);
180
+ if (subGraph.nodes.size === 0) {
181
+ return { agent_id: agent.id, completed: [], failed: [] };
182
+ }
183
+ const ctx: SchedulerContext = {
184
+ projectRoot,
185
+ sessionId,
186
+ runId: `${runId}-agent${idx}`,
187
+ executor: agent.executor,
188
+ workspaceManager: agent.workspaceManager,
189
+ onEvent: opts.onEvent,
190
+ };
191
+ const scheduler = new Scheduler();
192
+ const result = await scheduler.run(subGraph, ctx);
193
+ return { agent_id: agent.id, completed: result.completed, failed: result.failed };
194
+ })
195
+ );
196
+
197
+ const completed = agentResults.flatMap((result) => result.completed);
198
+ const failed = agentResults.flatMap((result) => result.failed);
199
+ const blocked: string[] = [];
200
+ const orphanReassignments: Array<{ from_agent_id: string; to_agent_id: string; work_item_ids: string[] }> = [];
201
+ const state = makeState('parallel', runId, agents, partitions, agentResults, completed, failed, blocked, orphanReassignments);
202
+ persistMultiAgentArtifacts(projectRoot, runId, state);
203
+
204
+ appendEvent(projectRoot, sessionId, {
205
+ type: 'RunCompleted',
206
+ run_id: runId,
207
+ payload: { mode: 'parallel', completed: completed.length, failed: failed.length },
208
+ });
209
+
210
+ return {
211
+ mode: 'parallel',
212
+ run_id: runId,
213
+ completed,
214
+ failed,
215
+ blocked,
216
+ agent_results: agentResults,
217
+ arbitration_results: [],
218
+ state,
219
+ };
220
+ }
221
+
222
+ // ─── Competitive mode ────────────────────────────────────────────────────────
223
+
224
+ async function runCompetitive(
225
+ graph: ExecutionGraph,
226
+ opts: CoordinationOptions
227
+ ): Promise<CoordinationResult> {
228
+ if (opts.agents.length < 2) {
229
+ throw new Error('Competitive mode requires at least 2 agents');
230
+ }
231
+ ensureIsolatedAgents(opts.agents);
232
+ const [agentA, agentB] = opts.agents;
233
+ const { projectRoot, sessionId, runId } = opts;
234
+
235
+ appendEvent(projectRoot, sessionId, {
236
+ type: 'RunStarted',
237
+ run_id: runId,
238
+ payload: { mode: 'competitive', isolation_level: 'isolated' },
239
+ });
240
+
241
+ const completed: string[] = [];
242
+ const failed: string[] = [];
243
+ const blocked: string[] = [];
244
+ const arbitrationResults: ArbitrationRecord[] = [];
245
+
246
+ for (const wave of graph.waves) {
247
+ for (const nodeId of wave.node_ids) {
248
+ const node = graph.nodes.get(nodeId)!;
249
+ const result = await competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults);
250
+ if (result.success) completed.push(nodeId);
251
+ else failed.push(nodeId);
252
+ if (failed.length > 0) break;
253
+ }
254
+ if (failed.length > 0) break;
255
+ }
256
+
257
+ const partitions = [Array.from(graph.nodes.keys()), Array.from(graph.nodes.keys())];
258
+ const state = makeState(
259
+ 'competitive',
260
+ runId,
261
+ opts.agents,
262
+ partitions,
263
+ [
264
+ { agent_id: agentA.id, completed, failed },
265
+ { agent_id: agentB.id, completed: [], failed: [] },
266
+ ],
267
+ completed,
268
+ failed,
269
+ blocked,
270
+ []
271
+ );
272
+ persistMultiAgentArtifacts(projectRoot, runId, state, [], arbitrationResults);
273
+
274
+ appendEvent(projectRoot, sessionId, {
275
+ type: 'RunCompleted',
276
+ run_id: runId,
277
+ payload: { mode: 'competitive', completed: completed.length },
278
+ });
279
+
280
+ return {
281
+ mode: 'competitive',
282
+ run_id: runId,
283
+ completed,
284
+ failed,
285
+ blocked,
286
+ agent_results: [
287
+ { agent_id: agentA.id, completed, failed },
288
+ { agent_id: agentB.id, completed: [], failed: [] },
289
+ ],
290
+ arbitration_results: arbitrationResults,
291
+ state,
292
+ };
293
+ }
294
+
295
+ async function competeTwoAgents(
296
+ nodeId: string,
297
+ node: GraphNode,
298
+ agentA: AgentSpec,
299
+ agentB: AgentSpec,
300
+ opts: CoordinationOptions,
301
+ arbitrationResults: ArbitrationRecord[]
302
+ ): Promise<TaskResult> {
303
+ const { projectRoot, sessionId, runId } = opts;
304
+
305
+ const allocA = await agentA.workspaceManager.allocate({
306
+ work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
307
+ });
308
+ const allocB = await agentB.workspaceManager.allocate({
309
+ work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
310
+ });
311
+
312
+ appendEvent(projectRoot, sessionId, {
313
+ type: 'AttemptStarted',
314
+ run_id: runId,
315
+ work_item_id: nodeId,
316
+ payload: { mode: 'competitive', agents: [agentA.id, agentB.id] },
317
+ });
318
+
319
+ const [resultA, resultB] = await Promise.all([
320
+ agentA.executor.execute(node, allocA, runId, 1).catch((error) => ({
321
+ success: false, failure_class: 'env' as const, evidence: [], output: String(error),
322
+ })),
323
+ agentB.executor.execute(node, allocB, runId, 1).catch((error) => ({
324
+ success: false, failure_class: 'env' as const, evidence: [], output: String(error),
325
+ })),
326
+ ]);
327
+
328
+ await Promise.all([
329
+ agentA.workspaceManager.dispose(allocA.workspace_id).catch(() => {}),
330
+ agentB.workspaceManager.dispose(allocB.workspace_id).catch(() => {}),
331
+ ]);
332
+
333
+ const winner = resultA.success ? resultA : resultB.success ? resultB : resultA;
334
+ const winnerAgentId = resultA.success ? agentA.id : resultB.success ? agentB.id : agentA.id;
335
+ arbitrationResults.push({
336
+ work_item_id: nodeId,
337
+ mode: 'competitive',
338
+ winner_agent_id: winnerAgentId,
339
+ participant_agent_ids: [agentA.id, agentB.id],
340
+ success: winner.success,
341
+ failure_class: winner.failure_class,
342
+ evidence_count: winner.evidence.length,
343
+ recorded_at: new Date().toISOString(),
344
+ });
345
+
346
+ if (winner.success) {
347
+ appendEvent(projectRoot, sessionId, { type: 'WorkItemCompleted', run_id: runId, work_item_id: nodeId, payload: { mode: 'competitive', winner_agent_id: winnerAgentId } });
348
+ } else {
349
+ appendEvent(projectRoot, sessionId, { type: 'WorkItemBlocked', run_id: runId, work_item_id: nodeId, payload: { mode: 'competitive', failure_class: winner.failure_class, winner_agent_id: winnerAgentId } });
350
+ }
351
+
352
+ return winner;
353
+ }
354
+
355
+ // ─── Cooperative mode ────────────────────────────────────────────────────────
356
+
357
+ async function runCooperative(
358
+ graph: ExecutionGraph,
359
+ opts: CoordinationOptions
360
+ ): Promise<CoordinationResult> {
361
+ if (opts.agents.length < 2) {
362
+ throw new Error('Cooperative mode requires at least 2 agents');
363
+ }
364
+ ensureIsolatedAgents(opts.agents);
365
+ const [planner, executor] = opts.agents;
366
+ const { projectRoot, sessionId, runId } = opts;
367
+ const handoffs: CooperativeHandoff[] = [];
368
+
369
+ appendEvent(projectRoot, sessionId, {
370
+ type: 'RunStarted',
371
+ run_id: runId,
372
+ payload: { mode: 'cooperative', planner: planner.id, executor: executor.id, isolation_level: 'isolated' },
373
+ });
374
+
375
+ const completed: string[] = [];
376
+ const failed: string[] = [];
377
+ const blocked: string[] = [];
378
+
379
+ for (const wave of graph.waves) {
380
+ for (const nodeId of wave.node_ids) {
381
+ const node = graph.nodes.get(nodeId)!;
382
+
383
+ const planAlloc = await planner.workspaceManager.allocate({
384
+ work_item_id: nodeId,
385
+ attempt_number: 1,
386
+ strategy: node.workspace_strategy,
387
+ mutation_scope: node.mutation_scope,
388
+ });
389
+ await planner.workspaceManager.dispose(planAlloc.workspace_id).catch(() => {});
390
+
391
+ const handoff = buildHandoff({
392
+ from_agent_id: planner.id,
393
+ to_agent_id: executor.id,
394
+ from_role: 'planner',
395
+ to_role: 'executor',
396
+ work_item_id: nodeId,
397
+ context_pack_ref: null,
398
+ });
399
+ handoffs.push(handoff);
400
+
401
+ appendEvent(projectRoot, sessionId, {
402
+ type: 'AttemptStarted',
403
+ run_id: runId,
404
+ work_item_id: nodeId,
405
+ payload: { mode: 'cooperative', handoff_id: handoff.handoff_id },
406
+ });
407
+
408
+ const execAlloc = await executor.workspaceManager.allocate({
409
+ work_item_id: nodeId,
410
+ attempt_number: 1,
411
+ strategy: node.workspace_strategy,
412
+ mutation_scope: node.mutation_scope,
413
+ });
414
+
415
+ let result: TaskResult;
416
+ try {
417
+ result = await executor.executor.execute(node, execAlloc, runId, 1);
418
+ } catch (error) {
419
+ result = { success: false, failure_class: 'env', evidence: [], output: String(error) };
420
+ }
421
+ await executor.workspaceManager.dispose(execAlloc.workspace_id).catch(() => {});
422
+
423
+ if (result.success) {
424
+ completed.push(nodeId);
425
+ appendEvent(projectRoot, sessionId, { type: 'WorkItemCompleted', run_id: runId, work_item_id: nodeId, payload: { mode: 'cooperative' } });
426
+ } else {
427
+ failed.push(nodeId);
428
+ appendEvent(projectRoot, sessionId, { type: 'WorkItemBlocked', run_id: runId, work_item_id: nodeId, payload: { mode: 'cooperative', failure_class: result.failure_class } });
429
+ break;
430
+ }
431
+ }
432
+ if (failed.length > 0) break;
433
+ }
434
+
435
+ const partitions = [Array.from(graph.nodes.keys()), Array.from(graph.nodes.keys())];
436
+ const state = makeState(
437
+ 'cooperative',
438
+ runId,
439
+ opts.agents,
440
+ partitions,
441
+ [
442
+ { agent_id: planner.id, completed: [], failed: [] },
443
+ { agent_id: executor.id, completed, failed },
444
+ ],
445
+ completed,
446
+ failed,
447
+ blocked,
448
+ []
449
+ );
450
+ persistMultiAgentArtifacts(projectRoot, runId, state, handoffs, []);
451
+
452
+ appendEvent(projectRoot, sessionId, {
453
+ type: 'RunCompleted',
454
+ run_id: runId,
455
+ payload: { mode: 'cooperative', completed: completed.length, failed: failed.length },
456
+ });
457
+
458
+ return {
459
+ mode: 'cooperative',
460
+ run_id: runId,
461
+ completed,
462
+ failed,
463
+ blocked,
464
+ agent_results: [
465
+ { agent_id: planner.id, completed: [], failed: [] },
466
+ { agent_id: executor.id, completed, failed },
467
+ ],
468
+ handoffs,
469
+ arbitration_results: [],
470
+ state,
471
+ };
472
+ }
473
+
474
+ // ─── Public API ──────────────────────────────────────────────────────────────
475
+
476
+ export class MultiAgentCoordinator {
477
+ async run(graph: ExecutionGraph, opts: CoordinationOptions): Promise<CoordinationResult> {
478
+ switch (opts.mode) {
479
+ case 'parallel': return runParallel(graph, opts);
480
+ case 'competitive': return runCompetitive(graph, opts);
481
+ case 'cooperative': return runCooperative(graph, opts);
482
+ default:
483
+ throw new Error(`Unknown coordination mode: ${opts.mode}`);
484
+ }
485
+ }
486
+ }
487
+
488
+ export function multiAgentStatePath(projectRoot: string, runId: string): string {
489
+ return path.join(projectRoot, '.oxe', 'runs', runId, 'multi-agent-state.json');
490
+ }
491
+
492
+ export function loadMultiAgentState(projectRoot: string, runId: string): MultiAgentStatusSnapshot | null {
493
+ const statePath = multiAgentStatePath(projectRoot, runId);
494
+ if (!fs.existsSync(statePath)) return null;
495
+ try {
496
+ return JSON.parse(fs.readFileSync(statePath, 'utf8')) as MultiAgentStatusSnapshot;
497
+ } catch {
498
+ return null;
499
+ }
500
+ }
501
+
502
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
503
+
504
+ function subGraphFor(graph: ExecutionGraph, nodeIds: string[]): ExecutionGraph {
505
+ const ids = new Set(nodeIds);
506
+ const nodes = new Map([...graph.nodes].filter(([id]) => ids.has(id)));
507
+ const edges = graph.edges.filter((edge) => ids.has(edge.from) && ids.has(edge.to));
508
+ const waves = graph.waves
509
+ .map((wave) => ({
510
+ wave_number: wave.wave_number,
511
+ node_ids: wave.node_ids.filter((id) => ids.has(id)),
512
+ }))
513
+ .filter((wave) => wave.node_ids.length > 0);
514
+
515
+ return {
516
+ nodes,
517
+ edges,
518
+ waves,
519
+ metadata: { ...graph.metadata, node_count: nodes.size, wave_count: waves.length },
520
+ };
521
+ }