oxe-cc 1.12.0 → 1.16.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 (40) hide show
  1. package/.github/dependabot.yml +31 -0
  2. package/.github/workflows/ci.yml +141 -56
  3. package/.github/workflows/release.yml +114 -89
  4. package/CHANGELOG.md +866 -754
  5. package/README.md +600 -736
  6. package/bin/lib/oxe-agent-install.cjs +299 -284
  7. package/bin/lib/oxe-artifact-catalog.cjs +376 -0
  8. package/bin/lib/oxe-command-registry.cjs +31 -0
  9. package/bin/lib/oxe-context-engine.cjs +11 -11
  10. package/bin/lib/oxe-core-command-handlers.cjs +82 -0
  11. package/bin/lib/oxe-dashboard.cjs +140 -140
  12. package/bin/lib/oxe-manifest.cjs +20 -20
  13. package/bin/lib/oxe-npm-version.cjs +6 -4
  14. package/bin/lib/oxe-plugin-cli.cjs +95 -0
  15. package/bin/lib/oxe-plugins.cjs +94 -3
  16. package/bin/lib/oxe-process.cjs +67 -0
  17. package/bin/lib/oxe-project-health.cjs +2846 -2781
  18. package/bin/lib/oxe-runtime-semantics.cjs +68 -69
  19. package/bin/oxe-cc.js +369 -353
  20. package/docs/INTEGRATION.md +182 -0
  21. package/docs/QUALITY-GATES.md +46 -0
  22. package/docs/RELEASE-READINESS.md +86 -61
  23. package/docs/RUNTIME-SMOKE-MATRIX.md +137 -135
  24. package/docs/oxe-artifact-map.html +1172 -0
  25. package/lib/sdk/index.cjs +20 -0
  26. package/lib/sdk/index.d.ts +971 -876
  27. package/lib/sdk/index.types.ts +933 -0
  28. package/oxe/templates/PLUGINS.md +8 -1
  29. package/oxe/templates/STATE-REFERENCE.md +125 -0
  30. package/oxe/templates/STATE.md +11 -121
  31. package/oxe/workflows/help.md +2 -0
  32. package/package.json +129 -108
  33. package/packages/runtime/package.json +18 -18
  34. package/packages/runtime/src/evidence/evidence-store.ts +2 -2
  35. package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +728 -728
  36. package/packages/runtime/src/workspace/strategies/git-worktree.ts +24 -24
  37. package/packages/runtime/tsconfig.json +8 -2
  38. package/vscode-extension/.vscodeignore +2 -0
  39. package/vscode-extension/package.json +193 -185
  40. package/vscode-extension/src/extension.js +11 -1
@@ -2,13 +2,13 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { appendEvent } from '../events/bus';
4
4
  import type { ExecutionGraph, GraphNode } from '../compiler/graph-compiler';
5
- import type { WorkspaceManager, WorkspaceRequest } from '../workspace/workspace-manager';
6
- import type { WorkspaceLease, SnapshotRef } from '../models/workspace';
7
- import type { TaskExecutor, TaskResult, SchedulerContext, RunResult } from './scheduler';
8
- import { Scheduler } from './scheduler';
9
- import { buildHandoff } from './agent-roles';
10
- import type { CooperativeHandoff } from './agent-roles';
11
- import { AgentRegistry } from './agent-registry';
5
+ import type { WorkspaceManager, WorkspaceRequest } from '../workspace/workspace-manager';
6
+ import type { WorkspaceLease, SnapshotRef } from '../models/workspace';
7
+ import type { TaskExecutor, TaskResult, SchedulerContext, RunResult } from './scheduler';
8
+ import { Scheduler } from './scheduler';
9
+ import { buildHandoff } from './agent-roles';
10
+ import type { CooperativeHandoff } from './agent-roles';
11
+ import { AgentRegistry } from './agent-registry';
12
12
 
13
13
  export type CoordinationMode = 'parallel' | 'competitive' | 'cooperative';
14
14
 
@@ -20,16 +20,16 @@ export interface AgentSpec {
20
20
  assignedTaskIds?: string[];
21
21
  }
22
22
 
23
- export interface CoordinationOptions {
24
- mode: CoordinationMode;
25
- agents: AgentSpec[];
26
- projectRoot: string;
27
- sessionId: string | null;
28
- runId: string;
29
- onEvent?: SchedulerContext['onEvent'];
30
- heartbeatTimeoutMs?: number;
31
- applyWorkspaceMerges?: boolean;
32
- }
23
+ export interface CoordinationOptions {
24
+ mode: CoordinationMode;
25
+ agents: AgentSpec[];
26
+ projectRoot: string;
27
+ sessionId: string | null;
28
+ runId: string;
29
+ onEvent?: SchedulerContext['onEvent'];
30
+ heartbeatTimeoutMs?: number;
31
+ applyWorkspaceMerges?: boolean;
32
+ }
33
33
 
34
34
  export interface ArbitrationRecord {
35
35
  work_item_id: string;
@@ -42,112 +42,112 @@ export interface ArbitrationRecord {
42
42
  recorded_at: string;
43
43
  }
44
44
 
45
- export interface MultiAgentOwnership {
46
- work_item_id: string;
47
- owner_agent_id: string;
48
- }
49
-
50
- export interface WorkspaceMergeRecord {
51
- work_item_id: string;
52
- agent_id: string;
53
- workspace_id: string;
54
- strategy: string;
55
- isolation_level: 'shared' | 'isolated';
56
- branch: string | null;
57
- base_commit: string | null;
58
- root_path: string | null;
59
- mutation_scope: string[];
60
- diff_paths: string[];
61
- evidence_refs: string[];
62
- evidence_count: number;
63
- verify_status: 'pass' | 'fail' | 'partial';
64
- status: 'ready' | 'merged' | 'blocked' | 'skipped';
65
- blocker: string | null;
66
- applied_paths: string[];
67
- diff_summary: {
68
- added: number;
69
- modified: number;
70
- missing: number;
71
- paths: string[];
72
- };
73
- next_action: string | null;
74
- recorded_at: string;
75
- }
76
-
77
- export interface WorkspaceMergeReport {
78
- schema_version: 1;
79
- run_id: string;
80
- generated_at: string;
81
- workspace_isolation: 'git_worktree';
82
- merge_readiness: 'ready' | 'blocked' | 'partial';
83
- arbitration_required: boolean;
84
- blockers: string[];
85
- records: WorkspaceMergeRecord[];
86
- }
87
-
88
- export interface MultiAgentStatusSnapshot {
89
- run_id: string;
90
- mode: CoordinationMode;
91
- workspace_isolation_required: 'isolated';
92
- workspace_isolation_enforced: boolean;
45
+ export interface MultiAgentOwnership {
46
+ work_item_id: string;
47
+ owner_agent_id: string;
48
+ }
49
+
50
+ export interface WorkspaceMergeRecord {
51
+ work_item_id: string;
52
+ agent_id: string;
53
+ workspace_id: string;
54
+ strategy: string;
55
+ isolation_level: 'shared' | 'isolated';
56
+ branch: string | null;
57
+ base_commit: string | null;
58
+ root_path: string | null;
59
+ mutation_scope: string[];
60
+ diff_paths: string[];
61
+ evidence_refs: string[];
62
+ evidence_count: number;
63
+ verify_status: 'pass' | 'fail' | 'partial';
64
+ status: 'ready' | 'merged' | 'blocked' | 'skipped';
65
+ blocker: string | null;
66
+ applied_paths: string[];
67
+ diff_summary: {
68
+ added: number;
69
+ modified: number;
70
+ missing: number;
71
+ paths: string[];
72
+ };
73
+ next_action: string | null;
74
+ recorded_at: string;
75
+ }
76
+
77
+ export interface WorkspaceMergeReport {
78
+ schema_version: 1;
79
+ run_id: string;
80
+ generated_at: string;
81
+ workspace_isolation: 'git_worktree';
82
+ merge_readiness: 'ready' | 'blocked' | 'partial';
83
+ arbitration_required: boolean;
84
+ blockers: string[];
85
+ records: WorkspaceMergeRecord[];
86
+ }
87
+
88
+ export interface MultiAgentStatusSnapshot {
89
+ run_id: string;
90
+ mode: CoordinationMode;
91
+ workspace_isolation_required: 'isolated';
92
+ workspace_isolation_enforced: boolean;
93
93
  agent_count: number;
94
94
  ownership: MultiAgentOwnership[];
95
95
  completed: string[];
96
96
  failed: string[];
97
97
  blocked: string[];
98
- agent_results: Array<{
99
- agent_id: string;
100
- isolation_level: 'shared' | 'isolated';
101
- assigned_task_ids: string[];
102
- completed: string[];
103
- failed: string[];
104
- timed_out: boolean;
105
- reassigned_task_ids: string[];
106
- }>;
107
- worktrees: WorkspaceMergeRecord[];
108
- workspace_merge: WorkspaceMergeReport;
109
- merge_blockers: string[];
110
- arbitration_required: boolean;
111
- orphan_reassignments: Array<{ from_agent_id: string; to_agent_id: string; work_item_ids: string[] }>;
112
- timed_out_agents: Array<{ agent_id: string; work_item_ids: string[]; detected_at: string }>;
113
- updated_at: string;
114
- }
115
-
116
- export interface MultiAgentOperationalSummary {
117
- run_id: string;
118
- mode: CoordinationMode;
119
- workspace_isolation_enforced: boolean;
120
- agent_count: number;
121
- completed_count: number;
122
- failed_count: number;
123
- blocked_count: number;
124
- ownership_count: number;
125
- handoff_count: number;
126
- arbitration_count: number;
127
- orphan_reassignment_count: number;
128
- timeout_count: number;
129
- participating_agents: string[];
130
- workspace_isolation: 'git_worktree';
131
- merge_readiness: WorkspaceMergeReport['merge_readiness'];
132
- arbitration_required: boolean;
133
- merge_blocker_count: number;
134
- health: 'healthy' | 'degraded';
135
- updated_at: string;
136
- }
137
-
138
- export interface CoordinationResult {
139
- mode: CoordinationMode;
140
- run_id: string;
141
- completed: string[];
142
- failed: string[];
143
- blocked: string[];
144
- agent_results: Array<{ agent_id: string; completed: string[]; failed: string[] }>;
145
- handoffs?: CooperativeHandoff[];
146
- arbitration_results?: ArbitrationRecord[];
147
- workspace_merge_report?: WorkspaceMergeReport;
148
- state?: MultiAgentStatusSnapshot;
149
- summary?: MultiAgentOperationalSummary;
150
- }
98
+ agent_results: Array<{
99
+ agent_id: string;
100
+ isolation_level: 'shared' | 'isolated';
101
+ assigned_task_ids: string[];
102
+ completed: string[];
103
+ failed: string[];
104
+ timed_out: boolean;
105
+ reassigned_task_ids: string[];
106
+ }>;
107
+ worktrees: WorkspaceMergeRecord[];
108
+ workspace_merge: WorkspaceMergeReport;
109
+ merge_blockers: string[];
110
+ arbitration_required: boolean;
111
+ orphan_reassignments: Array<{ from_agent_id: string; to_agent_id: string; work_item_ids: string[] }>;
112
+ timed_out_agents: Array<{ agent_id: string; work_item_ids: string[]; detected_at: string }>;
113
+ updated_at: string;
114
+ }
115
+
116
+ export interface MultiAgentOperationalSummary {
117
+ run_id: string;
118
+ mode: CoordinationMode;
119
+ workspace_isolation_enforced: boolean;
120
+ agent_count: number;
121
+ completed_count: number;
122
+ failed_count: number;
123
+ blocked_count: number;
124
+ ownership_count: number;
125
+ handoff_count: number;
126
+ arbitration_count: number;
127
+ orphan_reassignment_count: number;
128
+ timeout_count: number;
129
+ participating_agents: string[];
130
+ workspace_isolation: 'git_worktree';
131
+ merge_readiness: WorkspaceMergeReport['merge_readiness'];
132
+ arbitration_required: boolean;
133
+ merge_blocker_count: number;
134
+ health: 'healthy' | 'degraded';
135
+ updated_at: string;
136
+ }
137
+
138
+ export interface CoordinationResult {
139
+ mode: CoordinationMode;
140
+ run_id: string;
141
+ completed: string[];
142
+ failed: string[];
143
+ blocked: string[];
144
+ agent_results: Array<{ agent_id: string; completed: string[]; failed: string[] }>;
145
+ handoffs?: CooperativeHandoff[];
146
+ arbitration_results?: ArbitrationRecord[];
147
+ workspace_merge_report?: WorkspaceMergeReport;
148
+ state?: MultiAgentStatusSnapshot;
149
+ summary?: MultiAgentOperationalSummary;
150
+ }
151
151
 
152
152
  function ensureRunDir(projectRoot: string, runId: string): string {
153
153
  const dir = path.join(projectRoot, '.oxe', 'runs', runId);
@@ -155,44 +155,44 @@ function ensureRunDir(projectRoot: string, runId: string): string {
155
155
  return dir;
156
156
  }
157
157
 
158
- function persistMultiAgentArtifacts(
159
- projectRoot: string,
160
- runId: string,
161
- state: MultiAgentStatusSnapshot,
162
- handoffs: CooperativeHandoff[] = [],
163
- arbitrationResults: ArbitrationRecord[] = [],
164
- workspaceMergeReport: WorkspaceMergeReport = emptyWorkspaceMergeReport(runId)
165
- ): void {
166
- const runDir = ensureRunDir(projectRoot, runId);
167
- fs.writeFileSync(path.join(runDir, 'multi-agent-state.json'), JSON.stringify(state, null, 2), 'utf8');
168
- fs.writeFileSync(path.join(runDir, 'handoffs.json'), JSON.stringify(handoffs, null, 2), 'utf8');
169
- fs.writeFileSync(path.join(runDir, 'arbitration-results.json'), JSON.stringify(arbitrationResults, null, 2), 'utf8');
170
- fs.writeFileSync(path.join(runDir, 'workspace-merge-report.json'), JSON.stringify(workspaceMergeReport, null, 2), 'utf8');
171
- const summary: MultiAgentOperationalSummary = {
172
- run_id: state.run_id,
173
- mode: state.mode,
174
- workspace_isolation_enforced: state.workspace_isolation_enforced,
175
- agent_count: state.agent_count,
176
- completed_count: state.completed.length,
177
- failed_count: state.failed.length,
178
- blocked_count: state.blocked.length,
179
- ownership_count: state.ownership.length,
180
- handoff_count: handoffs.length,
181
- arbitration_count: arbitrationResults.length,
182
- orphan_reassignment_count: state.orphan_reassignments.length,
183
- timeout_count: state.timed_out_agents.length,
184
- participating_agents: state.agent_results.map((entry) => entry.agent_id),
185
- workspace_isolation: 'git_worktree',
186
- merge_readiness: workspaceMergeReport.merge_readiness,
187
- arbitration_required: workspaceMergeReport.arbitration_required,
188
- merge_blocker_count: workspaceMergeReport.blockers.length,
189
- health: state.timed_out_agents.length > 0 || state.failed.length > 0 ? 'degraded' : 'healthy',
190
- updated_at: state.updated_at,
191
- };
192
- fs.writeFileSync(path.join(runDir, 'multi-agent-summary.json'), JSON.stringify(summary, null, 2), 'utf8');
193
- }
194
-
195
- function ensureIsolatedAgents(agents: AgentSpec[]): void {
158
+ function persistMultiAgentArtifacts(
159
+ projectRoot: string,
160
+ runId: string,
161
+ state: MultiAgentStatusSnapshot,
162
+ handoffs: CooperativeHandoff[] = [],
163
+ arbitrationResults: ArbitrationRecord[] = [],
164
+ workspaceMergeReport: WorkspaceMergeReport = emptyWorkspaceMergeReport(runId)
165
+ ): void {
166
+ const runDir = ensureRunDir(projectRoot, runId);
167
+ fs.writeFileSync(path.join(runDir, 'multi-agent-state.json'), JSON.stringify(state, null, 2), 'utf8');
168
+ fs.writeFileSync(path.join(runDir, 'handoffs.json'), JSON.stringify(handoffs, null, 2), 'utf8');
169
+ fs.writeFileSync(path.join(runDir, 'arbitration-results.json'), JSON.stringify(arbitrationResults, null, 2), 'utf8');
170
+ fs.writeFileSync(path.join(runDir, 'workspace-merge-report.json'), JSON.stringify(workspaceMergeReport, null, 2), 'utf8');
171
+ const summary: MultiAgentOperationalSummary = {
172
+ run_id: state.run_id,
173
+ mode: state.mode,
174
+ workspace_isolation_enforced: state.workspace_isolation_enforced,
175
+ agent_count: state.agent_count,
176
+ completed_count: state.completed.length,
177
+ failed_count: state.failed.length,
178
+ blocked_count: state.blocked.length,
179
+ ownership_count: state.ownership.length,
180
+ handoff_count: handoffs.length,
181
+ arbitration_count: arbitrationResults.length,
182
+ orphan_reassignment_count: state.orphan_reassignments.length,
183
+ timeout_count: state.timed_out_agents.length,
184
+ participating_agents: state.agent_results.map((entry) => entry.agent_id),
185
+ workspace_isolation: 'git_worktree',
186
+ merge_readiness: workspaceMergeReport.merge_readiness,
187
+ arbitration_required: workspaceMergeReport.arbitration_required,
188
+ merge_blocker_count: workspaceMergeReport.blockers.length,
189
+ health: state.timed_out_agents.length > 0 || state.failed.length > 0 ? 'degraded' : 'healthy',
190
+ updated_at: state.updated_at,
191
+ };
192
+ fs.writeFileSync(path.join(runDir, 'multi-agent-summary.json'), JSON.stringify(summary, null, 2), 'utf8');
193
+ }
194
+
195
+ function ensureIsolatedAgents(agents: AgentSpec[]): void {
196
196
  const shared = agents.filter((agent) => agent.workspaceManager.isolation_level !== 'isolated');
197
197
  if (shared.length === 0) return;
198
198
  const ids = shared.map((agent) => `${agent.id}:${agent.workspaceManager.isolation_level}`).join(', ');
@@ -212,22 +212,22 @@ function buildOwnership(agents: AgentSpec[], partitions: string[][]): MultiAgent
212
212
  return ownership;
213
213
  }
214
214
 
215
- function makeState(
216
- mode: CoordinationMode,
217
- runId: string,
218
- agents: AgentSpec[],
219
- partitions: string[][],
220
- agentResults: Array<{ agent_id: string; completed: string[]; failed: string[]; timed_out: boolean; reassigned_task_ids: string[] }>,
221
- completed: string[],
222
- failed: string[],
223
- blocked: string[],
224
- orphanReassignments: Array<{ from_agent_id: string; to_agent_id: string; work_item_ids: string[] }>,
225
- timedOutAgents: Array<{ agent_id: string; work_item_ids: string[]; detected_at: string }>,
226
- workspaceMergeReport: WorkspaceMergeReport = emptyWorkspaceMergeReport(runId)
227
- ): MultiAgentStatusSnapshot {
228
- return {
229
- run_id: runId,
230
- mode,
215
+ function makeState(
216
+ mode: CoordinationMode,
217
+ runId: string,
218
+ agents: AgentSpec[],
219
+ partitions: string[][],
220
+ agentResults: Array<{ agent_id: string; completed: string[]; failed: string[]; timed_out: boolean; reassigned_task_ids: string[] }>,
221
+ completed: string[],
222
+ failed: string[],
223
+ blocked: string[],
224
+ orphanReassignments: Array<{ from_agent_id: string; to_agent_id: string; work_item_ids: string[] }>,
225
+ timedOutAgents: Array<{ agent_id: string; work_item_ids: string[]; detected_at: string }>,
226
+ workspaceMergeReport: WorkspaceMergeReport = emptyWorkspaceMergeReport(runId)
227
+ ): MultiAgentStatusSnapshot {
228
+ return {
229
+ run_id: runId,
230
+ mode,
231
231
  workspace_isolation_required: 'isolated',
232
232
  workspace_isolation_enforced: true,
233
233
  agent_count: agents.length,
@@ -235,427 +235,427 @@ function makeState(
235
235
  completed,
236
236
  failed,
237
237
  blocked,
238
- agent_results: agents.map((agent, idx) => {
238
+ agent_results: agents.map((agent, idx) => {
239
239
  const result = agentResults.find((entry) => entry.agent_id === agent.id);
240
240
  return {
241
241
  agent_id: agent.id,
242
- isolation_level: agent.workspaceManager.isolation_level,
243
- assigned_task_ids: partitions[idx] ?? agent.assignedTaskIds ?? [],
244
- completed: result?.completed ?? [],
245
- failed: result?.failed ?? [],
246
- timed_out: Boolean(result?.timed_out),
247
- reassigned_task_ids: result?.reassigned_task_ids ?? [],
248
- };
249
- }),
250
- worktrees: workspaceMergeReport.records,
251
- workspace_merge: workspaceMergeReport,
252
- merge_blockers: workspaceMergeReport.blockers,
253
- arbitration_required: workspaceMergeReport.arbitration_required,
254
- orphan_reassignments: orphanReassignments,
255
- timed_out_agents: timedOutAgents,
256
- updated_at: new Date().toISOString(),
257
- };
258
- }
259
-
260
- function emptyWorkspaceMergeReport(runId: string): WorkspaceMergeReport {
261
- return {
262
- schema_version: 1,
263
- run_id: runId,
264
- generated_at: new Date().toISOString(),
265
- workspace_isolation: 'git_worktree',
266
- merge_readiness: 'ready',
267
- arbitration_required: false,
268
- blockers: [],
269
- records: [],
270
- };
271
- }
272
-
273
- function ensureGitWorktreeLease(agent: AgentSpec, lease: WorkspaceLease): void {
274
- if (lease.isolation_level !== 'isolated' || lease.strategy !== 'git_worktree') {
275
- throw new Error(`Multi-agent real requires git_worktree isolated workspace. Agent ${agent.id} received ${lease.strategy}/${lease.isolation_level}.`);
276
- }
277
- }
278
-
279
- function mutationScopeOf(node?: GraphNode): string[] {
280
- return Array.isArray(node?.mutation_scope) ? node!.mutation_scope.map(String).filter(Boolean) : [];
281
- }
282
-
283
- function createMergeRecord(
284
- agent: AgentSpec,
285
- node: GraphNode,
286
- lease: WorkspaceLease,
287
- result?: TaskResult
288
- ): WorkspaceMergeRecord {
289
- const evidenceRefs = Array.isArray(result?.evidence) ? result!.evidence.map(String).filter(Boolean) : [];
290
- const scope = mutationScopeOf(node);
291
- return {
292
- work_item_id: node.id,
293
- agent_id: agent.id,
294
- workspace_id: lease.workspace_id,
295
- strategy: lease.strategy,
296
- isolation_level: lease.isolation_level,
297
- branch: lease.branch ?? null,
298
- base_commit: lease.base_commit ?? null,
299
- root_path: lease.root_path ?? null,
300
- mutation_scope: scope,
301
- diff_paths: scope,
302
- evidence_refs: evidenceRefs,
303
- evidence_count: evidenceRefs.length,
304
- verify_status: result ? (result.success ? 'pass' : 'fail') : 'partial',
305
- status: result ? (result.success ? 'ready' : 'blocked') : 'ready',
306
- blocker: result && !result.success ? `verify_failed:${result.failure_class}` : null,
307
- applied_paths: [],
308
- diff_summary: { added: 0, modified: 0, missing: 0, paths: scope },
309
- next_action: result && !result.success ? 'Corrija a falha de verify/evidence antes de aplicar merge do workspace.' : null,
310
- recorded_at: new Date().toISOString(),
311
- };
312
- }
313
-
314
- function enrichMergeRecordWithResult(record: WorkspaceMergeRecord, result?: TaskResult): WorkspaceMergeRecord {
315
- if (!result) return record;
316
- const evidenceRefs = Array.isArray(result.evidence) ? result.evidence.map(String).filter(Boolean) : [];
317
- return {
318
- ...record,
319
- evidence_refs: evidenceRefs,
320
- evidence_count: evidenceRefs.length,
321
- verify_status: result.success ? 'pass' : 'fail',
322
- status: result.success ? 'ready' : 'blocked',
323
- blocker: result.success ? null : `verify_failed:${result.failure_class}`,
324
- next_action: result.success ? null : 'Corrija a falha de verify/evidence antes de aplicar merge do workspace.',
325
- recorded_at: new Date().toISOString(),
326
- };
327
- }
328
-
329
- function buildWorkspaceMergeReport(
330
- runId: string,
331
- records: WorkspaceMergeRecord[],
332
- extraBlockers: string[] = [],
333
- arbitrationRequired = false
334
- ): WorkspaceMergeReport {
335
- const blockers = [
336
- ...extraBlockers,
337
- ...records.filter((record) => record.status === 'blocked' && record.blocker).map((record) => `${record.work_item_id}:${record.blocker}`),
338
- ];
339
- return {
340
- schema_version: 1,
341
- run_id: runId,
342
- generated_at: new Date().toISOString(),
343
- workspace_isolation: 'git_worktree',
344
- merge_readiness: blockers.length > 0 ? 'blocked' : records.some((record) => record.status === 'ready') ? 'partial' : 'ready',
345
- arbitration_required: arbitrationRequired,
346
- blockers,
347
- records,
348
- };
349
- }
350
-
351
- function detectMutationConflicts(graph: ExecutionGraph, agents: AgentSpec[], partitions: string[][]): string[] {
352
- const owners = new Map<string, string>();
353
- const conflicts: string[] = [];
354
- for (let idx = 0; idx < agents.length; idx += 1) {
355
- for (const nodeId of partitions[idx] ?? []) {
356
- const node = graph.nodes.get(nodeId);
357
- for (const scope of mutationScopeOf(node)) {
358
- const previous = owners.get(scope);
359
- if (previous && previous !== agents[idx].id) {
360
- conflicts.push(`mutation_scope_overlap:${scope}:${previous}:${agents[idx].id}`);
361
- } else {
362
- owners.set(scope, agents[idx].id);
363
- }
364
- }
365
- }
366
- }
367
- return conflicts;
368
- }
369
-
370
- function applyWorkspaceRecord(projectRoot: string, record: WorkspaceMergeRecord): WorkspaceMergeRecord {
371
- if (!record.root_path || record.status !== 'ready') return record;
372
- const appliedPaths: string[] = [];
373
- const summary = { added: 0, modified: 0, missing: 0, paths: [...record.mutation_scope] };
374
- for (const relativePath of record.mutation_scope) {
375
- const source = path.join(record.root_path, relativePath);
376
- const target = path.join(projectRoot, relativePath);
377
- if (!fs.existsSync(source) || fs.statSync(source).isDirectory()) {
378
- summary.missing += 1;
379
- return {
380
- ...record,
381
- status: 'blocked',
382
- blocker: `missing_output:${relativePath}`,
383
- diff_summary: summary,
384
- next_action: `Materialize o arquivo esperado ${relativePath} no worktree do agente antes do merge.`,
385
- };
386
- }
387
- if (fs.existsSync(target)) summary.modified += 1;
388
- else summary.added += 1;
389
- fs.mkdirSync(path.dirname(target), { recursive: true });
390
- fs.copyFileSync(source, target);
391
- appliedPaths.push(relativePath);
392
- }
393
- return {
394
- ...record,
395
- status: 'merged',
396
- blocker: null,
397
- applied_paths: appliedPaths,
398
- diff_summary: summary,
399
- next_action: null,
400
- };
401
- }
402
-
403
- function createTrackingWorkspaceManager(
404
- base: WorkspaceManager,
405
- agent: AgentSpec,
406
- graph: ExecutionGraph,
407
- records: WorkspaceMergeRecord[]
408
- ): WorkspaceManager & { disposeDeferred(): Promise<void> } {
409
- const leases = new Map<string, WorkspaceLease>();
410
- return {
411
- isolation_level: base.isolation_level,
412
- allocate: async (req: WorkspaceRequest) => {
413
- const lease = await base.allocate(req);
414
- ensureGitWorktreeLease(agent, lease);
415
- leases.set(lease.workspace_id, lease);
416
- const node = graph.nodes.get(req.work_item_id);
417
- if (node) records.push(createMergeRecord(agent, node, lease));
418
- return lease;
419
- },
420
- snapshot: (id: string): Promise<SnapshotRef> => base.snapshot(id),
421
- reset: (id: string, snapRef: SnapshotRef): Promise<void> => base.reset(id, snapRef),
422
- dispose: async () => {
423
- // Scheduler calls dispose before the coordinator can reconcile diffs.
424
- // Defer real cleanup until the merge report has been produced.
425
- },
426
- disposeDeferred: async () => {
427
- await Promise.all([...leases.keys()].map((id) => base.dispose(id).catch(() => {})));
428
- leases.clear();
429
- },
430
- };
431
- }
432
-
433
- async function runGraphForAgent(
434
- graph: ExecutionGraph,
435
- nodeIds: string[],
436
- agent: AgentSpec,
437
- idx: number,
438
- opts: CoordinationOptions,
439
- heartbeatTimeoutMs: number | null
440
- ): Promise<{
441
- agent_id: string;
442
- completed: string[];
443
- failed: string[];
444
- timed_out: boolean;
445
- assigned_task_ids: string[];
446
- reassigned_task_ids: string[];
447
- workspace_records: WorkspaceMergeRecord[];
448
- cleanup: () => Promise<void>;
449
- }> {
450
- const subGraph = subGraphFor(graph, nodeIds);
451
- const workspaceRecords: WorkspaceMergeRecord[] = [];
452
- if (subGraph.nodes.size === 0) {
453
- return {
454
- agent_id: agent.id,
455
- completed: [],
456
- failed: [],
457
- timed_out: false,
458
- assigned_task_ids: nodeIds,
459
- reassigned_task_ids: [],
460
- workspace_records: workspaceRecords,
461
- cleanup: async () => {},
462
- };
463
- }
464
- const trackingWorkspaceManager = createTrackingWorkspaceManager(agent.workspaceManager, agent, graph, workspaceRecords);
465
- const taskResults = new Map<string, TaskResult>();
466
- const trackingExecutor: TaskExecutor = {
467
- execute: async (node, lease, runId, attemptNumber) => {
468
- const result = await agent.executor.execute(node, lease, runId, attemptNumber);
469
- taskResults.set(node.id, result);
470
- return result;
471
- },
472
- };
473
- const ctx: SchedulerContext = {
474
- projectRoot: opts.projectRoot,
475
- sessionId: opts.sessionId,
476
- runId: `${opts.runId}-agent${idx}`,
477
- executor: trackingExecutor,
478
- workspaceManager: trackingWorkspaceManager,
479
- onEvent: opts.onEvent,
480
- };
481
- const scheduler = new Scheduler();
482
- const work = scheduler.run(subGraph, ctx);
483
- if (!heartbeatTimeoutMs || heartbeatTimeoutMs <= 0) {
484
- const result = await work;
485
- const reconciledRecords = workspaceRecords.map((record) => enrichMergeRecordWithResult(record, taskResults.get(record.work_item_id)));
486
- return {
487
- agent_id: agent.id,
488
- completed: result.completed,
489
- failed: result.failed,
490
- timed_out: false,
491
- assigned_task_ids: nodeIds,
492
- reassigned_task_ids: [],
493
- workspace_records: reconciledRecords,
494
- cleanup: () => trackingWorkspaceManager.disposeDeferred(),
495
- };
496
- }
497
- let timer: NodeJS.Timeout | null = null;
498
- const raced: { type: 'result'; result: RunResult } | { type: 'timeout' } = await Promise.race([
499
- work.then((result) => ({ type: 'result' as const, result })),
500
- new Promise<{ type: 'timeout' }>((resolve) => {
501
- timer = setTimeout(() => resolve({ type: 'timeout' }), heartbeatTimeoutMs);
502
- }),
503
- ]);
504
- if (timer) clearTimeout(timer);
505
- if (raced && raced.type === 'timeout') {
506
- return {
507
- agent_id: agent.id,
508
- completed: [],
509
- failed: [],
510
- timed_out: true,
511
- assigned_task_ids: nodeIds,
512
- reassigned_task_ids: [],
513
- workspace_records: workspaceRecords,
514
- cleanup: () => trackingWorkspaceManager.disposeDeferred(),
515
- };
516
- }
517
- const result = raced.result;
518
- const reconciledRecords = workspaceRecords.map((record) => enrichMergeRecordWithResult(record, taskResults.get(record.work_item_id)));
519
- return {
520
- agent_id: agent.id,
521
- completed: result.completed,
522
- failed: result.failed,
523
- timed_out: false,
524
- assigned_task_ids: nodeIds,
525
- reassigned_task_ids: [],
526
- workspace_records: reconciledRecords,
527
- cleanup: () => trackingWorkspaceManager.disposeDeferred(),
528
- };
529
- }
242
+ isolation_level: agent.workspaceManager.isolation_level,
243
+ assigned_task_ids: partitions[idx] ?? agent.assignedTaskIds ?? [],
244
+ completed: result?.completed ?? [],
245
+ failed: result?.failed ?? [],
246
+ timed_out: Boolean(result?.timed_out),
247
+ reassigned_task_ids: result?.reassigned_task_ids ?? [],
248
+ };
249
+ }),
250
+ worktrees: workspaceMergeReport.records,
251
+ workspace_merge: workspaceMergeReport,
252
+ merge_blockers: workspaceMergeReport.blockers,
253
+ arbitration_required: workspaceMergeReport.arbitration_required,
254
+ orphan_reassignments: orphanReassignments,
255
+ timed_out_agents: timedOutAgents,
256
+ updated_at: new Date().toISOString(),
257
+ };
258
+ }
259
+
260
+ function emptyWorkspaceMergeReport(runId: string): WorkspaceMergeReport {
261
+ return {
262
+ schema_version: 1,
263
+ run_id: runId,
264
+ generated_at: new Date().toISOString(),
265
+ workspace_isolation: 'git_worktree',
266
+ merge_readiness: 'ready',
267
+ arbitration_required: false,
268
+ blockers: [],
269
+ records: [],
270
+ };
271
+ }
272
+
273
+ function ensureGitWorktreeLease(agent: AgentSpec, lease: WorkspaceLease): void {
274
+ if (lease.isolation_level !== 'isolated' || lease.strategy !== 'git_worktree') {
275
+ throw new Error(`Multi-agent real requires git_worktree isolated workspace. Agent ${agent.id} received ${lease.strategy}/${lease.isolation_level}.`);
276
+ }
277
+ }
278
+
279
+ function mutationScopeOf(node?: GraphNode): string[] {
280
+ return Array.isArray(node?.mutation_scope) ? node!.mutation_scope.map(String).filter(Boolean) : [];
281
+ }
282
+
283
+ function createMergeRecord(
284
+ agent: AgentSpec,
285
+ node: GraphNode,
286
+ lease: WorkspaceLease,
287
+ result?: TaskResult
288
+ ): WorkspaceMergeRecord {
289
+ const evidenceRefs = Array.isArray(result?.evidence) ? result!.evidence.map(String).filter(Boolean) : [];
290
+ const scope = mutationScopeOf(node);
291
+ return {
292
+ work_item_id: node.id,
293
+ agent_id: agent.id,
294
+ workspace_id: lease.workspace_id,
295
+ strategy: lease.strategy,
296
+ isolation_level: lease.isolation_level,
297
+ branch: lease.branch ?? null,
298
+ base_commit: lease.base_commit ?? null,
299
+ root_path: lease.root_path ?? null,
300
+ mutation_scope: scope,
301
+ diff_paths: scope,
302
+ evidence_refs: evidenceRefs,
303
+ evidence_count: evidenceRefs.length,
304
+ verify_status: result ? (result.success ? 'pass' : 'fail') : 'partial',
305
+ status: result ? (result.success ? 'ready' : 'blocked') : 'ready',
306
+ blocker: result && !result.success ? `verify_failed:${result.failure_class}` : null,
307
+ applied_paths: [],
308
+ diff_summary: { added: 0, modified: 0, missing: 0, paths: scope },
309
+ next_action: result && !result.success ? 'Corrija a falha de verify/evidence antes de aplicar merge do workspace.' : null,
310
+ recorded_at: new Date().toISOString(),
311
+ };
312
+ }
313
+
314
+ function enrichMergeRecordWithResult(record: WorkspaceMergeRecord, result?: TaskResult): WorkspaceMergeRecord {
315
+ if (!result) return record;
316
+ const evidenceRefs = Array.isArray(result.evidence) ? result.evidence.map(String).filter(Boolean) : [];
317
+ return {
318
+ ...record,
319
+ evidence_refs: evidenceRefs,
320
+ evidence_count: evidenceRefs.length,
321
+ verify_status: result.success ? 'pass' : 'fail',
322
+ status: result.success ? 'ready' : 'blocked',
323
+ blocker: result.success ? null : `verify_failed:${result.failure_class}`,
324
+ next_action: result.success ? null : 'Corrija a falha de verify/evidence antes de aplicar merge do workspace.',
325
+ recorded_at: new Date().toISOString(),
326
+ };
327
+ }
328
+
329
+ function buildWorkspaceMergeReport(
330
+ runId: string,
331
+ records: WorkspaceMergeRecord[],
332
+ extraBlockers: string[] = [],
333
+ arbitrationRequired = false
334
+ ): WorkspaceMergeReport {
335
+ const blockers = [
336
+ ...extraBlockers,
337
+ ...records.filter((record) => record.status === 'blocked' && record.blocker).map((record) => `${record.work_item_id}:${record.blocker}`),
338
+ ];
339
+ return {
340
+ schema_version: 1,
341
+ run_id: runId,
342
+ generated_at: new Date().toISOString(),
343
+ workspace_isolation: 'git_worktree',
344
+ merge_readiness: blockers.length > 0 ? 'blocked' : records.some((record) => record.status === 'ready') ? 'partial' : 'ready',
345
+ arbitration_required: arbitrationRequired,
346
+ blockers,
347
+ records,
348
+ };
349
+ }
350
+
351
+ function detectMutationConflicts(graph: ExecutionGraph, agents: AgentSpec[], partitions: string[][]): string[] {
352
+ const owners = new Map<string, string>();
353
+ const conflicts: string[] = [];
354
+ for (let idx = 0; idx < agents.length; idx += 1) {
355
+ for (const nodeId of partitions[idx] ?? []) {
356
+ const node = graph.nodes.get(nodeId);
357
+ for (const scope of mutationScopeOf(node)) {
358
+ const previous = owners.get(scope);
359
+ if (previous && previous !== agents[idx].id) {
360
+ conflicts.push(`mutation_scope_overlap:${scope}:${previous}:${agents[idx].id}`);
361
+ } else {
362
+ owners.set(scope, agents[idx].id);
363
+ }
364
+ }
365
+ }
366
+ }
367
+ return conflicts;
368
+ }
369
+
370
+ function applyWorkspaceRecord(projectRoot: string, record: WorkspaceMergeRecord): WorkspaceMergeRecord {
371
+ if (!record.root_path || record.status !== 'ready') return record;
372
+ const appliedPaths: string[] = [];
373
+ const summary = { added: 0, modified: 0, missing: 0, paths: [...record.mutation_scope] };
374
+ for (const relativePath of record.mutation_scope) {
375
+ const source = path.join(record.root_path, relativePath);
376
+ const target = path.join(projectRoot, relativePath);
377
+ if (!fs.existsSync(source) || fs.statSync(source).isDirectory()) {
378
+ summary.missing += 1;
379
+ return {
380
+ ...record,
381
+ status: 'blocked',
382
+ blocker: `missing_output:${relativePath}`,
383
+ diff_summary: summary,
384
+ next_action: `Materialize o arquivo esperado ${relativePath} no worktree do agente antes do merge.`,
385
+ };
386
+ }
387
+ if (fs.existsSync(target)) summary.modified += 1;
388
+ else summary.added += 1;
389
+ fs.mkdirSync(path.dirname(target), { recursive: true });
390
+ fs.copyFileSync(source, target);
391
+ appliedPaths.push(relativePath);
392
+ }
393
+ return {
394
+ ...record,
395
+ status: 'merged',
396
+ blocker: null,
397
+ applied_paths: appliedPaths,
398
+ diff_summary: summary,
399
+ next_action: null,
400
+ };
401
+ }
402
+
403
+ function createTrackingWorkspaceManager(
404
+ base: WorkspaceManager,
405
+ agent: AgentSpec,
406
+ graph: ExecutionGraph,
407
+ records: WorkspaceMergeRecord[]
408
+ ): WorkspaceManager & { disposeDeferred(): Promise<void> } {
409
+ const leases = new Map<string, WorkspaceLease>();
410
+ return {
411
+ isolation_level: base.isolation_level,
412
+ allocate: async (req: WorkspaceRequest) => {
413
+ const lease = await base.allocate(req);
414
+ ensureGitWorktreeLease(agent, lease);
415
+ leases.set(lease.workspace_id, lease);
416
+ const node = graph.nodes.get(req.work_item_id);
417
+ if (node) records.push(createMergeRecord(agent, node, lease));
418
+ return lease;
419
+ },
420
+ snapshot: (id: string): Promise<SnapshotRef> => base.snapshot(id),
421
+ reset: (id: string, snapRef: SnapshotRef): Promise<void> => base.reset(id, snapRef),
422
+ dispose: async () => {
423
+ // Scheduler calls dispose before the coordinator can reconcile diffs.
424
+ // Defer real cleanup until the merge report has been produced.
425
+ },
426
+ disposeDeferred: async () => {
427
+ await Promise.all([...leases.keys()].map((id) => base.dispose(id).catch(() => {})));
428
+ leases.clear();
429
+ },
430
+ };
431
+ }
432
+
433
+ async function runGraphForAgent(
434
+ graph: ExecutionGraph,
435
+ nodeIds: string[],
436
+ agent: AgentSpec,
437
+ idx: number,
438
+ opts: CoordinationOptions,
439
+ heartbeatTimeoutMs: number | null
440
+ ): Promise<{
441
+ agent_id: string;
442
+ completed: string[];
443
+ failed: string[];
444
+ timed_out: boolean;
445
+ assigned_task_ids: string[];
446
+ reassigned_task_ids: string[];
447
+ workspace_records: WorkspaceMergeRecord[];
448
+ cleanup: () => Promise<void>;
449
+ }> {
450
+ const subGraph = subGraphFor(graph, nodeIds);
451
+ const workspaceRecords: WorkspaceMergeRecord[] = [];
452
+ if (subGraph.nodes.size === 0) {
453
+ return {
454
+ agent_id: agent.id,
455
+ completed: [],
456
+ failed: [],
457
+ timed_out: false,
458
+ assigned_task_ids: nodeIds,
459
+ reassigned_task_ids: [],
460
+ workspace_records: workspaceRecords,
461
+ cleanup: async () => {},
462
+ };
463
+ }
464
+ const trackingWorkspaceManager = createTrackingWorkspaceManager(agent.workspaceManager, agent, graph, workspaceRecords);
465
+ const taskResults = new Map<string, TaskResult>();
466
+ const trackingExecutor: TaskExecutor = {
467
+ execute: async (node, lease, runId, attemptNumber) => {
468
+ const result = await agent.executor.execute(node, lease, runId, attemptNumber);
469
+ taskResults.set(node.id, result);
470
+ return result;
471
+ },
472
+ };
473
+ const ctx: SchedulerContext = {
474
+ projectRoot: opts.projectRoot,
475
+ sessionId: opts.sessionId,
476
+ runId: `${opts.runId}-agent${idx}`,
477
+ executor: trackingExecutor,
478
+ workspaceManager: trackingWorkspaceManager,
479
+ onEvent: opts.onEvent,
480
+ };
481
+ const scheduler = new Scheduler();
482
+ const work = scheduler.run(subGraph, ctx);
483
+ if (!heartbeatTimeoutMs || heartbeatTimeoutMs <= 0) {
484
+ const result = await work;
485
+ const reconciledRecords = workspaceRecords.map((record) => enrichMergeRecordWithResult(record, taskResults.get(record.work_item_id)));
486
+ return {
487
+ agent_id: agent.id,
488
+ completed: result.completed,
489
+ failed: result.failed,
490
+ timed_out: false,
491
+ assigned_task_ids: nodeIds,
492
+ reassigned_task_ids: [],
493
+ workspace_records: reconciledRecords,
494
+ cleanup: () => trackingWorkspaceManager.disposeDeferred(),
495
+ };
496
+ }
497
+ let timer: NodeJS.Timeout | null = null;
498
+ const raced: { type: 'result'; result: RunResult } | { type: 'timeout' } = await Promise.race([
499
+ work.then((result) => ({ type: 'result' as const, result })),
500
+ new Promise<{ type: 'timeout' }>((resolve) => {
501
+ timer = setTimeout(() => resolve({ type: 'timeout' }), heartbeatTimeoutMs);
502
+ }),
503
+ ]);
504
+ if (timer) clearTimeout(timer);
505
+ if (raced && raced.type === 'timeout') {
506
+ return {
507
+ agent_id: agent.id,
508
+ completed: [],
509
+ failed: [],
510
+ timed_out: true,
511
+ assigned_task_ids: nodeIds,
512
+ reassigned_task_ids: [],
513
+ workspace_records: workspaceRecords,
514
+ cleanup: () => trackingWorkspaceManager.disposeDeferred(),
515
+ };
516
+ }
517
+ const result = raced.result;
518
+ const reconciledRecords = workspaceRecords.map((record) => enrichMergeRecordWithResult(record, taskResults.get(record.work_item_id)));
519
+ return {
520
+ agent_id: agent.id,
521
+ completed: result.completed,
522
+ failed: result.failed,
523
+ timed_out: false,
524
+ assigned_task_ids: nodeIds,
525
+ reassigned_task_ids: [],
526
+ workspace_records: reconciledRecords,
527
+ cleanup: () => trackingWorkspaceManager.disposeDeferred(),
528
+ };
529
+ }
530
530
 
531
531
  // ─── Parallel mode ───────────────────────────────────────────────────────────
532
532
 
533
- async function runParallel(
534
- graph: ExecutionGraph,
535
- opts: CoordinationOptions
536
- ): Promise<CoordinationResult> {
537
- const { agents, projectRoot, sessionId, runId } = opts;
538
- ensureIsolatedAgents(agents);
539
- const heartbeatTimeoutMs = opts.heartbeatTimeoutMs ?? null;
540
-
541
- const partitions = agents.map((agent) => [...(agent.assignedTaskIds ?? [])]);
542
- if (partitions.every((partition) => partition.length === 0)) {
543
- const allIds = [...graph.nodes.keys()];
544
- allIds.forEach((id, index) => {
545
- partitions[index % agents.length].push(id);
546
- });
547
- }
548
- const mutationConflicts = detectMutationConflicts(graph, agents, partitions);
549
- if (mutationConflicts.length > 0) {
550
- const blocked = mutationConflicts;
551
- const workspaceMergeReport = buildWorkspaceMergeReport(runId, [], blocked);
552
- const state = makeState('parallel', runId, agents, partitions, [], [], [], blocked, [], [], workspaceMergeReport);
553
- persistMultiAgentArtifacts(projectRoot, runId, state, [], [], workspaceMergeReport);
554
- appendEvent(projectRoot, sessionId, {
555
- type: 'WorkItemBlocked',
556
- run_id: runId,
557
- payload: { mode: 'parallel', blockers: blocked },
558
- });
559
- return {
560
- mode: 'parallel',
561
- run_id: runId,
562
- completed: [],
563
- failed: [],
564
- blocked,
565
- agent_results: [],
566
- arbitration_results: [],
567
- workspace_merge_report: workspaceMergeReport,
568
- state,
569
- summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
570
- };
571
- }
572
- const registry = new AgentRegistry(heartbeatTimeoutMs == null ? 30_000 : heartbeatTimeoutMs);
573
- agents.forEach((agent, idx) => {
574
- registry.register(agent.id, agent.executor, agent.workspaceManager, partitions[idx] ?? []);
575
- });
576
-
577
- appendEvent(projectRoot, sessionId, {
578
- type: 'RunStarted',
579
- run_id: runId,
580
- payload: { mode: 'parallel', agent_count: agents.length, isolation_level: 'isolated' },
581
- });
582
-
583
- const initialResults = await Promise.all(
584
- agents.map(async (agent, idx) => {
585
- registry.beat(agent.id, partitions[idx][0] || null);
586
- const result = await runGraphForAgent(graph, partitions[idx], agent, idx, opts, heartbeatTimeoutMs);
587
- registry.setStatus(agent.id, result.timed_out ? 'timeout' : 'idle');
588
- return result;
589
- })
590
- );
591
-
592
- const timedOutAgents = [];
593
- const blocked: string[] = [];
594
- const orphanReassignments: Array<{ from_agent_id: string; to_agent_id: string; work_item_ids: string[] }> = [];
595
- const agentResults = initialResults.map((entry) => ({
596
- ...entry,
597
- reassigned_task_ids: entry.reassigned_task_ids || [],
598
- }));
599
- const liveAgents = agentResults.filter((entry) => !entry.timed_out);
600
- for (const timedOut of agentResults.filter((entry) => entry.timed_out)) {
601
- timedOutAgents.push({
602
- agent_id: timedOut.agent_id,
603
- work_item_ids: timedOut.assigned_task_ids,
604
- detected_at: new Date().toISOString(),
605
- });
606
- const fallback = liveAgents.find((entry) => entry.agent_id !== timedOut.agent_id);
607
- if (!fallback || timedOut.assigned_task_ids.length === 0) continue;
608
- const fallbackIdx = agents.findIndex((agent) => agent.id === fallback.agent_id);
609
- const timeoutIdx = agents.findIndex((agent) => agent.id === timedOut.agent_id);
610
- const rerun = await runGraphForAgent(graph, timedOut.assigned_task_ids, agents[fallbackIdx], fallbackIdx, opts, null);
611
- fallback.completed.push(...rerun.completed);
612
- fallback.failed.push(...rerun.failed);
613
- fallback.workspace_records.push(...rerun.workspace_records);
614
- fallback.reassigned_task_ids.push(...timedOut.assigned_task_ids);
615
- const previousCleanup = fallback.cleanup;
616
- fallback.cleanup = async () => {
617
- await previousCleanup?.();
618
- await rerun.cleanup?.();
619
- };
620
- partitions[fallbackIdx] = [...partitions[fallbackIdx], ...timedOut.assigned_task_ids];
621
- partitions[timeoutIdx] = [];
622
- orphanReassignments.push({
623
- from_agent_id: timedOut.agent_id,
624
- to_agent_id: fallback.agent_id,
625
- work_item_ids: timedOut.assigned_task_ids,
626
- });
627
- }
628
-
629
- const completed = Array.from(new Set(agentResults.flatMap((result) => result.completed)));
630
- const failed = Array.from(new Set(agentResults.flatMap((result) => result.failed)));
631
- const rawRecords = agentResults.flatMap((result) => result.workspace_records || []);
632
- const mergedRecords = opts.applyWorkspaceMerges
633
- ? rawRecords.map((record) => applyWorkspaceRecord(projectRoot, record))
634
- : rawRecords;
635
- await Promise.all(agentResults.map((result) => result.cleanup?.().catch(() => {})));
636
- const workspaceMergeReport = buildWorkspaceMergeReport(runId, mergedRecords, blocked);
637
- const state = makeState('parallel', runId, agents, partitions, agentResults, completed, failed, blocked, orphanReassignments, timedOutAgents, workspaceMergeReport);
638
- persistMultiAgentArtifacts(projectRoot, runId, state, [], [], workspaceMergeReport);
639
-
640
- appendEvent(projectRoot, sessionId, {
641
- type: 'RunCompleted',
533
+ async function runParallel(
534
+ graph: ExecutionGraph,
535
+ opts: CoordinationOptions
536
+ ): Promise<CoordinationResult> {
537
+ const { agents, projectRoot, sessionId, runId } = opts;
538
+ ensureIsolatedAgents(agents);
539
+ const heartbeatTimeoutMs = opts.heartbeatTimeoutMs ?? null;
540
+
541
+ const partitions = agents.map((agent) => [...(agent.assignedTaskIds ?? [])]);
542
+ if (partitions.every((partition) => partition.length === 0)) {
543
+ const allIds = [...graph.nodes.keys()];
544
+ allIds.forEach((id, index) => {
545
+ partitions[index % agents.length].push(id);
546
+ });
547
+ }
548
+ const mutationConflicts = detectMutationConflicts(graph, agents, partitions);
549
+ if (mutationConflicts.length > 0) {
550
+ const blocked = mutationConflicts;
551
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, [], blocked);
552
+ const state = makeState('parallel', runId, agents, partitions, [], [], [], blocked, [], [], workspaceMergeReport);
553
+ persistMultiAgentArtifacts(projectRoot, runId, state, [], [], workspaceMergeReport);
554
+ appendEvent(projectRoot, sessionId, {
555
+ type: 'WorkItemBlocked',
556
+ run_id: runId,
557
+ payload: { mode: 'parallel', blockers: blocked },
558
+ });
559
+ return {
560
+ mode: 'parallel',
561
+ run_id: runId,
562
+ completed: [],
563
+ failed: [],
564
+ blocked,
565
+ agent_results: [],
566
+ arbitration_results: [],
567
+ workspace_merge_report: workspaceMergeReport,
568
+ state,
569
+ summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
570
+ };
571
+ }
572
+ const registry = new AgentRegistry(heartbeatTimeoutMs == null ? 30_000 : heartbeatTimeoutMs);
573
+ agents.forEach((agent, idx) => {
574
+ registry.register(agent.id, agent.executor, agent.workspaceManager, partitions[idx] ?? []);
575
+ });
576
+
577
+ appendEvent(projectRoot, sessionId, {
578
+ type: 'RunStarted',
579
+ run_id: runId,
580
+ payload: { mode: 'parallel', agent_count: agents.length, isolation_level: 'isolated' },
581
+ });
582
+
583
+ const initialResults = await Promise.all(
584
+ agents.map(async (agent, idx) => {
585
+ registry.beat(agent.id, partitions[idx][0] || null);
586
+ const result = await runGraphForAgent(graph, partitions[idx], agent, idx, opts, heartbeatTimeoutMs);
587
+ registry.setStatus(agent.id, result.timed_out ? 'timeout' : 'idle');
588
+ return result;
589
+ })
590
+ );
591
+
592
+ const timedOutAgents = [];
593
+ const blocked: string[] = [];
594
+ const orphanReassignments: Array<{ from_agent_id: string; to_agent_id: string; work_item_ids: string[] }> = [];
595
+ const agentResults = initialResults.map((entry) => ({
596
+ ...entry,
597
+ reassigned_task_ids: entry.reassigned_task_ids || [],
598
+ }));
599
+ const liveAgents = agentResults.filter((entry) => !entry.timed_out);
600
+ for (const timedOut of agentResults.filter((entry) => entry.timed_out)) {
601
+ timedOutAgents.push({
602
+ agent_id: timedOut.agent_id,
603
+ work_item_ids: timedOut.assigned_task_ids,
604
+ detected_at: new Date().toISOString(),
605
+ });
606
+ const fallback = liveAgents.find((entry) => entry.agent_id !== timedOut.agent_id);
607
+ if (!fallback || timedOut.assigned_task_ids.length === 0) continue;
608
+ const fallbackIdx = agents.findIndex((agent) => agent.id === fallback.agent_id);
609
+ const timeoutIdx = agents.findIndex((agent) => agent.id === timedOut.agent_id);
610
+ const rerun = await runGraphForAgent(graph, timedOut.assigned_task_ids, agents[fallbackIdx], fallbackIdx, opts, null);
611
+ fallback.completed.push(...rerun.completed);
612
+ fallback.failed.push(...rerun.failed);
613
+ fallback.workspace_records.push(...rerun.workspace_records);
614
+ fallback.reassigned_task_ids.push(...timedOut.assigned_task_ids);
615
+ const previousCleanup = fallback.cleanup;
616
+ fallback.cleanup = async () => {
617
+ await previousCleanup?.();
618
+ await rerun.cleanup?.();
619
+ };
620
+ partitions[fallbackIdx] = [...partitions[fallbackIdx], ...timedOut.assigned_task_ids];
621
+ partitions[timeoutIdx] = [];
622
+ orphanReassignments.push({
623
+ from_agent_id: timedOut.agent_id,
624
+ to_agent_id: fallback.agent_id,
625
+ work_item_ids: timedOut.assigned_task_ids,
626
+ });
627
+ }
628
+
629
+ const completed = Array.from(new Set(agentResults.flatMap((result) => result.completed)));
630
+ const failed = Array.from(new Set(agentResults.flatMap((result) => result.failed)));
631
+ const rawRecords = agentResults.flatMap((result) => result.workspace_records || []);
632
+ const mergedRecords = opts.applyWorkspaceMerges
633
+ ? rawRecords.map((record) => applyWorkspaceRecord(projectRoot, record))
634
+ : rawRecords;
635
+ await Promise.all(agentResults.map((result) => result.cleanup?.().catch(() => {})));
636
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, mergedRecords, blocked);
637
+ const state = makeState('parallel', runId, agents, partitions, agentResults, completed, failed, blocked, orphanReassignments, timedOutAgents, workspaceMergeReport);
638
+ persistMultiAgentArtifacts(projectRoot, runId, state, [], [], workspaceMergeReport);
639
+
640
+ appendEvent(projectRoot, sessionId, {
641
+ type: 'RunCompleted',
642
642
  run_id: runId,
643
643
  payload: { mode: 'parallel', completed: completed.length, failed: failed.length },
644
644
  });
645
645
 
646
- return {
647
- mode: 'parallel',
648
- run_id: runId,
649
- completed,
650
- failed,
651
- blocked,
652
- agent_results: agentResults,
653
- arbitration_results: [],
654
- workspace_merge_report: workspaceMergeReport,
655
- state,
656
- summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
657
- };
658
- }
646
+ return {
647
+ mode: 'parallel',
648
+ run_id: runId,
649
+ completed,
650
+ failed,
651
+ blocked,
652
+ agent_results: agentResults,
653
+ arbitration_results: [],
654
+ workspace_merge_report: workspaceMergeReport,
655
+ state,
656
+ summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
657
+ };
658
+ }
659
659
 
660
660
  // ─── Competitive mode ────────────────────────────────────────────────────────
661
661
 
@@ -679,13 +679,13 @@ async function runCompetitive(
679
679
  const completed: string[] = [];
680
680
  const failed: string[] = [];
681
681
  const blocked: string[] = [];
682
- const arbitrationResults: ArbitrationRecord[] = [];
683
- const workspaceRecords: WorkspaceMergeRecord[] = [];
682
+ const arbitrationResults: ArbitrationRecord[] = [];
683
+ const workspaceRecords: WorkspaceMergeRecord[] = [];
684
684
 
685
685
  for (const wave of graph.waves) {
686
686
  for (const nodeId of wave.node_ids) {
687
687
  const node = graph.nodes.get(nodeId)!;
688
- const result = await competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults, workspaceRecords);
688
+ const result = await competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults, workspaceRecords);
689
689
  if (result.success) completed.push(nodeId);
690
690
  else failed.push(nodeId);
691
691
  if (failed.length > 0) break;
@@ -694,24 +694,24 @@ async function runCompetitive(
694
694
  }
695
695
 
696
696
  const partitions = [Array.from(graph.nodes.keys()), Array.from(graph.nodes.keys())];
697
- const workspaceMergeReport = buildWorkspaceMergeReport(runId, workspaceRecords, blocked, true);
698
- const state = makeState(
699
- 'competitive',
700
- runId,
701
- opts.agents,
702
- partitions,
703
- [
704
- { agent_id: agentA.id, completed, failed, timed_out: false, reassigned_task_ids: [] },
705
- { agent_id: agentB.id, completed: [], failed: [], timed_out: false, reassigned_task_ids: [] },
706
- ],
707
- completed,
708
- failed,
709
- blocked,
710
- [],
711
- [],
712
- workspaceMergeReport
713
- );
714
- persistMultiAgentArtifacts(projectRoot, runId, state, [], arbitrationResults, workspaceMergeReport);
697
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, workspaceRecords, blocked, true);
698
+ const state = makeState(
699
+ 'competitive',
700
+ runId,
701
+ opts.agents,
702
+ partitions,
703
+ [
704
+ { agent_id: agentA.id, completed, failed, timed_out: false, reassigned_task_ids: [] },
705
+ { agent_id: agentB.id, completed: [], failed: [], timed_out: false, reassigned_task_ids: [] },
706
+ ],
707
+ completed,
708
+ failed,
709
+ blocked,
710
+ [],
711
+ [],
712
+ workspaceMergeReport
713
+ );
714
+ persistMultiAgentArtifacts(projectRoot, runId, state, [], arbitrationResults, workspaceMergeReport);
715
715
 
716
716
  appendEvent(projectRoot, sessionId, {
717
717
  type: 'RunCompleted',
@@ -725,36 +725,36 @@ async function runCompetitive(
725
725
  completed,
726
726
  failed,
727
727
  blocked,
728
- agent_results: [
729
- { agent_id: agentA.id, completed, failed },
730
- { agent_id: agentB.id, completed: [], failed: [] },
731
- ],
732
- arbitration_results: arbitrationResults,
733
- workspace_merge_report: workspaceMergeReport,
734
- state,
735
- summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
736
- };
737
- }
728
+ agent_results: [
729
+ { agent_id: agentA.id, completed, failed },
730
+ { agent_id: agentB.id, completed: [], failed: [] },
731
+ ],
732
+ arbitration_results: arbitrationResults,
733
+ workspace_merge_report: workspaceMergeReport,
734
+ state,
735
+ summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
736
+ };
737
+ }
738
738
 
739
739
  async function competeTwoAgents(
740
740
  nodeId: string,
741
741
  node: GraphNode,
742
- agentA: AgentSpec,
743
- agentB: AgentSpec,
744
- opts: CoordinationOptions,
745
- arbitrationResults: ArbitrationRecord[],
746
- workspaceRecords: WorkspaceMergeRecord[]
747
- ): Promise<TaskResult> {
742
+ agentA: AgentSpec,
743
+ agentB: AgentSpec,
744
+ opts: CoordinationOptions,
745
+ arbitrationResults: ArbitrationRecord[],
746
+ workspaceRecords: WorkspaceMergeRecord[]
747
+ ): Promise<TaskResult> {
748
748
  const { projectRoot, sessionId, runId } = opts;
749
749
 
750
- const allocA = await agentA.workspaceManager.allocate({
751
- work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
752
- });
753
- ensureGitWorktreeLease(agentA, allocA);
754
- const allocB = await agentB.workspaceManager.allocate({
755
- work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
756
- });
757
- ensureGitWorktreeLease(agentB, allocB);
750
+ const allocA = await agentA.workspaceManager.allocate({
751
+ work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
752
+ });
753
+ ensureGitWorktreeLease(agentA, allocA);
754
+ const allocB = await agentB.workspaceManager.allocate({
755
+ work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
756
+ });
757
+ ensureGitWorktreeLease(agentB, allocB);
758
758
 
759
759
  appendEvent(projectRoot, sessionId, {
760
760
  type: 'AttemptStarted',
@@ -772,24 +772,24 @@ async function competeTwoAgents(
772
772
  })),
773
773
  ]);
774
774
 
775
- const candidates = [
776
- { agent: agentA, alloc: allocA, result: resultA },
777
- { agent: agentB, alloc: allocB, result: resultB },
778
- ].sort((left, right) => {
779
- const leftScore = (left.result.success ? 10_000 : 0) + left.result.evidence.length * 100 - String(left.result.output || '').length;
780
- const rightScore = (right.result.success ? 10_000 : 0) + right.result.evidence.length * 100 - String(right.result.output || '').length;
781
- return rightScore - leftScore;
782
- });
783
- const winnerCandidate = candidates[0];
784
- const winner = winnerCandidate.result;
785
- const winnerAgentId = winnerCandidate.agent.id;
786
- const winnerRecord = createMergeRecord(winnerCandidate.agent, node, winnerCandidate.alloc, winner);
787
- workspaceRecords.push(opts.applyWorkspaceMerges ? applyWorkspaceRecord(projectRoot, winnerRecord) : winnerRecord);
788
-
789
- await Promise.all([
790
- agentA.workspaceManager.dispose(allocA.workspace_id).catch(() => {}),
791
- agentB.workspaceManager.dispose(allocB.workspace_id).catch(() => {}),
792
- ]);
775
+ const candidates = [
776
+ { agent: agentA, alloc: allocA, result: resultA },
777
+ { agent: agentB, alloc: allocB, result: resultB },
778
+ ].sort((left, right) => {
779
+ const leftScore = (left.result.success ? 10_000 : 0) + left.result.evidence.length * 100 - String(left.result.output || '').length;
780
+ const rightScore = (right.result.success ? 10_000 : 0) + right.result.evidence.length * 100 - String(right.result.output || '').length;
781
+ return rightScore - leftScore;
782
+ });
783
+ const winnerCandidate = candidates[0];
784
+ const winner = winnerCandidate.result;
785
+ const winnerAgentId = winnerCandidate.agent.id;
786
+ const winnerRecord = createMergeRecord(winnerCandidate.agent, node, winnerCandidate.alloc, winner);
787
+ workspaceRecords.push(opts.applyWorkspaceMerges ? applyWorkspaceRecord(projectRoot, winnerRecord) : winnerRecord);
788
+
789
+ await Promise.all([
790
+ agentA.workspaceManager.dispose(allocA.workspace_id).catch(() => {}),
791
+ agentB.workspaceManager.dispose(allocB.workspace_id).catch(() => {}),
792
+ ]);
793
793
  arbitrationResults.push({
794
794
  work_item_id: nodeId,
795
795
  mode: 'competitive',
@@ -822,8 +822,8 @@ async function runCooperative(
822
822
  ensureIsolatedAgents(opts.agents);
823
823
  const [planner, executor] = opts.agents;
824
824
  const { projectRoot, sessionId, runId } = opts;
825
- const handoffs: CooperativeHandoff[] = [];
826
- const workspaceRecords: WorkspaceMergeRecord[] = [];
825
+ const handoffs: CooperativeHandoff[] = [];
826
+ const workspaceRecords: WorkspaceMergeRecord[] = [];
827
827
 
828
828
  appendEvent(projectRoot, sessionId, {
829
829
  type: 'RunStarted',
@@ -839,14 +839,14 @@ async function runCooperative(
839
839
  for (const nodeId of wave.node_ids) {
840
840
  const node = graph.nodes.get(nodeId)!;
841
841
 
842
- const planAlloc = await planner.workspaceManager.allocate({
842
+ const planAlloc = await planner.workspaceManager.allocate({
843
843
  work_item_id: nodeId,
844
844
  attempt_number: 1,
845
845
  strategy: node.workspace_strategy,
846
- mutation_scope: node.mutation_scope,
847
- });
848
- ensureGitWorktreeLease(planner, planAlloc);
849
- await planner.workspaceManager.dispose(planAlloc.workspace_id).catch(() => {});
846
+ mutation_scope: node.mutation_scope,
847
+ });
848
+ ensureGitWorktreeLease(planner, planAlloc);
849
+ await planner.workspaceManager.dispose(planAlloc.workspace_id).catch(() => {});
850
850
 
851
851
  const handoff = buildHandoff({
852
852
  from_agent_id: planner.id,
@@ -865,13 +865,13 @@ async function runCooperative(
865
865
  payload: { mode: 'cooperative', handoff_id: handoff.handoff_id },
866
866
  });
867
867
 
868
- const execAlloc = await executor.workspaceManager.allocate({
868
+ const execAlloc = await executor.workspaceManager.allocate({
869
869
  work_item_id: nodeId,
870
870
  attempt_number: 1,
871
871
  strategy: node.workspace_strategy,
872
- mutation_scope: node.mutation_scope,
873
- });
874
- ensureGitWorktreeLease(executor, execAlloc);
872
+ mutation_scope: node.mutation_scope,
873
+ });
874
+ ensureGitWorktreeLease(executor, execAlloc);
875
875
 
876
876
  let result: TaskResult;
877
877
  try {
@@ -879,9 +879,9 @@ async function runCooperative(
879
879
  } catch (error) {
880
880
  result = { success: false, failure_class: 'env', evidence: [], output: String(error) };
881
881
  }
882
- const mergeRecord = createMergeRecord(executor, node, execAlloc, result);
883
- workspaceRecords.push(opts.applyWorkspaceMerges ? applyWorkspaceRecord(projectRoot, mergeRecord) : mergeRecord);
884
- await executor.workspaceManager.dispose(execAlloc.workspace_id).catch(() => {});
882
+ const mergeRecord = createMergeRecord(executor, node, execAlloc, result);
883
+ workspaceRecords.push(opts.applyWorkspaceMerges ? applyWorkspaceRecord(projectRoot, mergeRecord) : mergeRecord);
884
+ await executor.workspaceManager.dispose(execAlloc.workspace_id).catch(() => {});
885
885
 
886
886
  if (result.success) {
887
887
  completed.push(nodeId);
@@ -896,24 +896,24 @@ async function runCooperative(
896
896
  }
897
897
 
898
898
  const partitions = [Array.from(graph.nodes.keys()), Array.from(graph.nodes.keys())];
899
- const workspaceMergeReport = buildWorkspaceMergeReport(runId, workspaceRecords, blocked);
900
- const state = makeState(
901
- 'cooperative',
902
- runId,
903
- opts.agents,
904
- partitions,
905
- [
906
- { agent_id: planner.id, completed: [], failed: [], timed_out: false, reassigned_task_ids: [] },
907
- { agent_id: executor.id, completed, failed, timed_out: false, reassigned_task_ids: [] },
908
- ],
909
- completed,
910
- failed,
911
- blocked,
912
- [],
913
- [],
914
- workspaceMergeReport
915
- );
916
- persistMultiAgentArtifacts(projectRoot, runId, state, handoffs, [], workspaceMergeReport);
899
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, workspaceRecords, blocked);
900
+ const state = makeState(
901
+ 'cooperative',
902
+ runId,
903
+ opts.agents,
904
+ partitions,
905
+ [
906
+ { agent_id: planner.id, completed: [], failed: [], timed_out: false, reassigned_task_ids: [] },
907
+ { agent_id: executor.id, completed, failed, timed_out: false, reassigned_task_ids: [] },
908
+ ],
909
+ completed,
910
+ failed,
911
+ blocked,
912
+ [],
913
+ [],
914
+ workspaceMergeReport
915
+ );
916
+ persistMultiAgentArtifacts(projectRoot, runId, state, handoffs, [], workspaceMergeReport);
917
917
 
918
918
  appendEvent(projectRoot, sessionId, {
919
919
  type: 'RunCompleted',
@@ -927,17 +927,17 @@ async function runCooperative(
927
927
  completed,
928
928
  failed,
929
929
  blocked,
930
- agent_results: [
931
- { agent_id: planner.id, completed: [], failed: [] },
932
- { agent_id: executor.id, completed, failed },
933
- ],
934
- handoffs,
935
- arbitration_results: [],
936
- workspace_merge_report: workspaceMergeReport,
937
- state,
938
- summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
939
- };
940
- }
930
+ agent_results: [
931
+ { agent_id: planner.id, completed: [], failed: [] },
932
+ { agent_id: executor.id, completed, failed },
933
+ ],
934
+ handoffs,
935
+ arbitration_results: [],
936
+ workspace_merge_report: workspaceMergeReport,
937
+ state,
938
+ summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
939
+ };
940
+ }
941
941
 
942
942
  // ─── Public API ──────────────────────────────────────────────────────────────
943
943
 
@@ -953,47 +953,47 @@ export class MultiAgentCoordinator {
953
953
  }
954
954
  }
955
955
 
956
- export function multiAgentStatePath(projectRoot: string, runId: string): string {
957
- return path.join(projectRoot, '.oxe', 'runs', runId, 'multi-agent-state.json');
958
- }
959
-
960
- export function multiAgentSummaryPath(projectRoot: string, runId: string): string {
961
- return path.join(projectRoot, '.oxe', 'runs', runId, 'multi-agent-summary.json');
962
- }
963
-
964
- export function workspaceMergeReportPath(projectRoot: string, runId: string): string {
965
- return path.join(projectRoot, '.oxe', 'runs', runId, 'workspace-merge-report.json');
966
- }
967
-
968
- export function loadMultiAgentState(projectRoot: string, runId: string): MultiAgentStatusSnapshot | null {
969
- const statePath = multiAgentStatePath(projectRoot, runId);
970
- if (!fs.existsSync(statePath)) return null;
956
+ export function multiAgentStatePath(projectRoot: string, runId: string): string {
957
+ return path.join(projectRoot, '.oxe', 'runs', runId, 'multi-agent-state.json');
958
+ }
959
+
960
+ export function multiAgentSummaryPath(projectRoot: string, runId: string): string {
961
+ return path.join(projectRoot, '.oxe', 'runs', runId, 'multi-agent-summary.json');
962
+ }
963
+
964
+ export function workspaceMergeReportPath(projectRoot: string, runId: string): string {
965
+ return path.join(projectRoot, '.oxe', 'runs', runId, 'workspace-merge-report.json');
966
+ }
967
+
968
+ export function loadMultiAgentState(projectRoot: string, runId: string): MultiAgentStatusSnapshot | null {
969
+ const statePath = multiAgentStatePath(projectRoot, runId);
970
+ if (!fs.existsSync(statePath)) return null;
971
971
  try {
972
972
  return JSON.parse(fs.readFileSync(statePath, 'utf8')) as MultiAgentStatusSnapshot;
973
- } catch {
974
- return null;
975
- }
976
- }
977
-
978
- export function loadMultiAgentSummary(projectRoot: string, runId: string): MultiAgentOperationalSummary | null {
979
- const summaryPath = multiAgentSummaryPath(projectRoot, runId);
980
- if (!fs.existsSync(summaryPath)) return null;
981
- try {
982
- return JSON.parse(fs.readFileSync(summaryPath, 'utf8')) as MultiAgentOperationalSummary;
983
- } catch {
984
- return null;
985
- }
986
- }
987
-
988
- export function loadWorkspaceMergeReport(projectRoot: string, runId: string): WorkspaceMergeReport | null {
989
- const reportPath = workspaceMergeReportPath(projectRoot, runId);
990
- if (!fs.existsSync(reportPath)) return null;
991
- try {
992
- return JSON.parse(fs.readFileSync(reportPath, 'utf8')) as WorkspaceMergeReport;
993
- } catch {
994
- return null;
995
- }
996
- }
973
+ } catch {
974
+ return null;
975
+ }
976
+ }
977
+
978
+ export function loadMultiAgentSummary(projectRoot: string, runId: string): MultiAgentOperationalSummary | null {
979
+ const summaryPath = multiAgentSummaryPath(projectRoot, runId);
980
+ if (!fs.existsSync(summaryPath)) return null;
981
+ try {
982
+ return JSON.parse(fs.readFileSync(summaryPath, 'utf8')) as MultiAgentOperationalSummary;
983
+ } catch {
984
+ return null;
985
+ }
986
+ }
987
+
988
+ export function loadWorkspaceMergeReport(projectRoot: string, runId: string): WorkspaceMergeReport | null {
989
+ const reportPath = workspaceMergeReportPath(projectRoot, runId);
990
+ if (!fs.existsSync(reportPath)) return null;
991
+ try {
992
+ return JSON.parse(fs.readFileSync(reportPath, 'utf8')) as WorkspaceMergeReport;
993
+ } catch {
994
+ return null;
995
+ }
996
+ }
997
997
 
998
998
  // ─── Helpers ─────────────────────────────────────────────────────────────────
999
999