oxe-cc 1.8.3 → 1.9.1

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.
@@ -18,6 +18,7 @@ export interface CoordinationOptions {
18
18
  runId: string;
19
19
  onEvent?: SchedulerContext['onEvent'];
20
20
  heartbeatTimeoutMs?: number;
21
+ applyWorkspaceMerges?: boolean;
21
22
  }
22
23
  export interface ArbitrationRecord {
23
24
  work_item_id: string;
@@ -33,6 +34,33 @@ export interface MultiAgentOwnership {
33
34
  work_item_id: string;
34
35
  owner_agent_id: string;
35
36
  }
37
+ export interface WorkspaceMergeRecord {
38
+ work_item_id: string;
39
+ agent_id: string;
40
+ workspace_id: string;
41
+ strategy: string;
42
+ isolation_level: 'shared' | 'isolated';
43
+ branch: string | null;
44
+ base_commit: string | null;
45
+ root_path: string | null;
46
+ mutation_scope: string[];
47
+ diff_paths: string[];
48
+ evidence_count: number;
49
+ verify_status: 'pass' | 'fail' | 'partial';
50
+ status: 'ready' | 'merged' | 'blocked' | 'skipped';
51
+ blocker: string | null;
52
+ recorded_at: string;
53
+ }
54
+ export interface WorkspaceMergeReport {
55
+ schema_version: 1;
56
+ run_id: string;
57
+ generated_at: string;
58
+ workspace_isolation: 'git_worktree';
59
+ merge_readiness: 'ready' | 'blocked' | 'partial';
60
+ arbitration_required: boolean;
61
+ blockers: string[];
62
+ records: WorkspaceMergeRecord[];
63
+ }
36
64
  export interface MultiAgentStatusSnapshot {
37
65
  run_id: string;
38
66
  mode: CoordinationMode;
@@ -52,6 +80,10 @@ export interface MultiAgentStatusSnapshot {
52
80
  timed_out: boolean;
53
81
  reassigned_task_ids: string[];
54
82
  }>;
83
+ worktrees: WorkspaceMergeRecord[];
84
+ workspace_merge: WorkspaceMergeReport;
85
+ merge_blockers: string[];
86
+ arbitration_required: boolean;
55
87
  orphan_reassignments: Array<{
56
88
  from_agent_id: string;
57
89
  to_agent_id: string;
@@ -78,6 +110,10 @@ export interface MultiAgentOperationalSummary {
78
110
  orphan_reassignment_count: number;
79
111
  timeout_count: number;
80
112
  participating_agents: string[];
113
+ workspace_isolation: 'git_worktree';
114
+ merge_readiness: WorkspaceMergeReport['merge_readiness'];
115
+ arbitration_required: boolean;
116
+ merge_blocker_count: number;
81
117
  health: 'healthy' | 'degraded';
82
118
  updated_at: string;
83
119
  }
@@ -94,6 +130,7 @@ export interface CoordinationResult {
94
130
  }>;
95
131
  handoffs?: CooperativeHandoff[];
96
132
  arbitration_results?: ArbitrationRecord[];
133
+ workspace_merge_report?: WorkspaceMergeReport;
97
134
  state?: MultiAgentStatusSnapshot;
98
135
  summary?: MultiAgentOperationalSummary;
99
136
  }
@@ -102,5 +139,7 @@ export declare class MultiAgentCoordinator {
102
139
  }
103
140
  export declare function multiAgentStatePath(projectRoot: string, runId: string): string;
104
141
  export declare function multiAgentSummaryPath(projectRoot: string, runId: string): string;
142
+ export declare function workspaceMergeReportPath(projectRoot: string, runId: string): string;
105
143
  export declare function loadMultiAgentState(projectRoot: string, runId: string): MultiAgentStatusSnapshot | null;
106
144
  export declare function loadMultiAgentSummary(projectRoot: string, runId: string): MultiAgentOperationalSummary | null;
145
+ export declare function loadWorkspaceMergeReport(projectRoot: string, runId: string): WorkspaceMergeReport | null;
@@ -6,8 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MultiAgentCoordinator = void 0;
7
7
  exports.multiAgentStatePath = multiAgentStatePath;
8
8
  exports.multiAgentSummaryPath = multiAgentSummaryPath;
9
+ exports.workspaceMergeReportPath = workspaceMergeReportPath;
9
10
  exports.loadMultiAgentState = loadMultiAgentState;
10
11
  exports.loadMultiAgentSummary = loadMultiAgentSummary;
12
+ exports.loadWorkspaceMergeReport = loadWorkspaceMergeReport;
11
13
  const fs_1 = __importDefault(require("fs"));
12
14
  const path_1 = __importDefault(require("path"));
13
15
  const bus_1 = require("../events/bus");
@@ -19,11 +21,12 @@ function ensureRunDir(projectRoot, runId) {
19
21
  fs_1.default.mkdirSync(dir, { recursive: true });
20
22
  return dir;
21
23
  }
22
- function persistMultiAgentArtifacts(projectRoot, runId, state, handoffs = [], arbitrationResults = []) {
24
+ function persistMultiAgentArtifacts(projectRoot, runId, state, handoffs = [], arbitrationResults = [], workspaceMergeReport = emptyWorkspaceMergeReport(runId)) {
23
25
  const runDir = ensureRunDir(projectRoot, runId);
24
26
  fs_1.default.writeFileSync(path_1.default.join(runDir, 'multi-agent-state.json'), JSON.stringify(state, null, 2), 'utf8');
25
27
  fs_1.default.writeFileSync(path_1.default.join(runDir, 'handoffs.json'), JSON.stringify(handoffs, null, 2), 'utf8');
26
28
  fs_1.default.writeFileSync(path_1.default.join(runDir, 'arbitration-results.json'), JSON.stringify(arbitrationResults, null, 2), 'utf8');
29
+ fs_1.default.writeFileSync(path_1.default.join(runDir, 'workspace-merge-report.json'), JSON.stringify(workspaceMergeReport, null, 2), 'utf8');
27
30
  const summary = {
28
31
  run_id: state.run_id,
29
32
  mode: state.mode,
@@ -38,6 +41,10 @@ function persistMultiAgentArtifacts(projectRoot, runId, state, handoffs = [], ar
38
41
  orphan_reassignment_count: state.orphan_reassignments.length,
39
42
  timeout_count: state.timed_out_agents.length,
40
43
  participating_agents: state.agent_results.map((entry) => entry.agent_id),
44
+ workspace_isolation: 'git_worktree',
45
+ merge_readiness: workspaceMergeReport.merge_readiness,
46
+ arbitration_required: workspaceMergeReport.arbitration_required,
47
+ merge_blocker_count: workspaceMergeReport.blockers.length,
41
48
  health: state.timed_out_agents.length > 0 || state.failed.length > 0 ? 'degraded' : 'healthy',
42
49
  updated_at: state.updated_at,
43
50
  };
@@ -62,7 +69,7 @@ function buildOwnership(agents, partitions) {
62
69
  }
63
70
  return ownership;
64
71
  }
65
- function makeState(mode, runId, agents, partitions, agentResults, completed, failed, blocked, orphanReassignments, timedOutAgents) {
72
+ function makeState(mode, runId, agents, partitions, agentResults, completed, failed, blocked, orphanReassignments, timedOutAgents, workspaceMergeReport = emptyWorkspaceMergeReport(runId)) {
66
73
  return {
67
74
  run_id: runId,
68
75
  mode,
@@ -85,13 +92,132 @@ function makeState(mode, runId, agents, partitions, agentResults, completed, fai
85
92
  reassigned_task_ids: result?.reassigned_task_ids ?? [],
86
93
  };
87
94
  }),
95
+ worktrees: workspaceMergeReport.records,
96
+ workspace_merge: workspaceMergeReport,
97
+ merge_blockers: workspaceMergeReport.blockers,
98
+ arbitration_required: workspaceMergeReport.arbitration_required,
88
99
  orphan_reassignments: orphanReassignments,
89
100
  timed_out_agents: timedOutAgents,
90
101
  updated_at: new Date().toISOString(),
91
102
  };
92
103
  }
104
+ function emptyWorkspaceMergeReport(runId) {
105
+ return {
106
+ schema_version: 1,
107
+ run_id: runId,
108
+ generated_at: new Date().toISOString(),
109
+ workspace_isolation: 'git_worktree',
110
+ merge_readiness: 'ready',
111
+ arbitration_required: false,
112
+ blockers: [],
113
+ records: [],
114
+ };
115
+ }
116
+ function ensureGitWorktreeLease(agent, lease) {
117
+ if (lease.isolation_level !== 'isolated' || lease.strategy !== 'git_worktree') {
118
+ throw new Error(`Multi-agent real requires git_worktree isolated workspace. Agent ${agent.id} received ${lease.strategy}/${lease.isolation_level}.`);
119
+ }
120
+ }
121
+ function mutationScopeOf(node) {
122
+ return Array.isArray(node?.mutation_scope) ? node.mutation_scope.map(String).filter(Boolean) : [];
123
+ }
124
+ function createMergeRecord(agent, node, lease, result) {
125
+ const evidenceCount = Array.isArray(result?.evidence) ? result.evidence.length : 0;
126
+ return {
127
+ work_item_id: node.id,
128
+ agent_id: agent.id,
129
+ workspace_id: lease.workspace_id,
130
+ strategy: lease.strategy,
131
+ isolation_level: lease.isolation_level,
132
+ branch: lease.branch ?? null,
133
+ base_commit: lease.base_commit ?? null,
134
+ root_path: lease.root_path ?? null,
135
+ mutation_scope: mutationScopeOf(node),
136
+ diff_paths: mutationScopeOf(node),
137
+ evidence_count: evidenceCount,
138
+ verify_status: result ? (result.success ? 'pass' : 'fail') : 'partial',
139
+ status: result ? (result.success ? 'ready' : 'blocked') : 'ready',
140
+ blocker: result && !result.success ? `verify_failed:${result.failure_class}` : null,
141
+ recorded_at: new Date().toISOString(),
142
+ };
143
+ }
144
+ function buildWorkspaceMergeReport(runId, records, extraBlockers = [], arbitrationRequired = false) {
145
+ const blockers = [
146
+ ...extraBlockers,
147
+ ...records.filter((record) => record.status === 'blocked' && record.blocker).map((record) => `${record.work_item_id}:${record.blocker}`),
148
+ ];
149
+ return {
150
+ schema_version: 1,
151
+ run_id: runId,
152
+ generated_at: new Date().toISOString(),
153
+ workspace_isolation: 'git_worktree',
154
+ merge_readiness: blockers.length > 0 ? 'blocked' : records.some((record) => record.status === 'ready') ? 'partial' : 'ready',
155
+ arbitration_required: arbitrationRequired,
156
+ blockers,
157
+ records,
158
+ };
159
+ }
160
+ function detectMutationConflicts(graph, agents, partitions) {
161
+ const owners = new Map();
162
+ const conflicts = [];
163
+ for (let idx = 0; idx < agents.length; idx += 1) {
164
+ for (const nodeId of partitions[idx] ?? []) {
165
+ const node = graph.nodes.get(nodeId);
166
+ for (const scope of mutationScopeOf(node)) {
167
+ const previous = owners.get(scope);
168
+ if (previous && previous !== agents[idx].id) {
169
+ conflicts.push(`mutation_scope_overlap:${scope}:${previous}:${agents[idx].id}`);
170
+ }
171
+ else {
172
+ owners.set(scope, agents[idx].id);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ return conflicts;
178
+ }
179
+ function applyWorkspaceRecord(projectRoot, record) {
180
+ if (!record.root_path || record.status !== 'ready')
181
+ return record;
182
+ for (const relativePath of record.mutation_scope) {
183
+ const source = path_1.default.join(record.root_path, relativePath);
184
+ const target = path_1.default.join(projectRoot, relativePath);
185
+ if (!fs_1.default.existsSync(source) || fs_1.default.statSync(source).isDirectory()) {
186
+ return { ...record, status: 'blocked', blocker: `missing_output:${relativePath}` };
187
+ }
188
+ fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true });
189
+ fs_1.default.copyFileSync(source, target);
190
+ }
191
+ return { ...record, status: 'merged', blocker: null };
192
+ }
193
+ function createTrackingWorkspaceManager(base, agent, graph, records) {
194
+ const leases = new Map();
195
+ return {
196
+ isolation_level: base.isolation_level,
197
+ allocate: async (req) => {
198
+ const lease = await base.allocate(req);
199
+ ensureGitWorktreeLease(agent, lease);
200
+ leases.set(lease.workspace_id, lease);
201
+ const node = graph.nodes.get(req.work_item_id);
202
+ if (node)
203
+ records.push(createMergeRecord(agent, node, lease));
204
+ return lease;
205
+ },
206
+ snapshot: (id) => base.snapshot(id),
207
+ reset: (id, snapRef) => base.reset(id, snapRef),
208
+ dispose: async () => {
209
+ // Scheduler calls dispose before the coordinator can reconcile diffs.
210
+ // Defer real cleanup until the merge report has been produced.
211
+ },
212
+ disposeDeferred: async () => {
213
+ await Promise.all([...leases.keys()].map((id) => base.dispose(id).catch(() => { })));
214
+ leases.clear();
215
+ },
216
+ };
217
+ }
93
218
  async function runGraphForAgent(graph, nodeIds, agent, idx, opts, heartbeatTimeoutMs) {
94
219
  const subGraph = subGraphFor(graph, nodeIds);
220
+ const workspaceRecords = [];
95
221
  if (subGraph.nodes.size === 0) {
96
222
  return {
97
223
  agent_id: agent.id,
@@ -100,14 +226,17 @@ async function runGraphForAgent(graph, nodeIds, agent, idx, opts, heartbeatTimeo
100
226
  timed_out: false,
101
227
  assigned_task_ids: nodeIds,
102
228
  reassigned_task_ids: [],
229
+ workspace_records: workspaceRecords,
230
+ cleanup: async () => { },
103
231
  };
104
232
  }
233
+ const trackingWorkspaceManager = createTrackingWorkspaceManager(agent.workspaceManager, agent, graph, workspaceRecords);
105
234
  const ctx = {
106
235
  projectRoot: opts.projectRoot,
107
236
  sessionId: opts.sessionId,
108
237
  runId: `${opts.runId}-agent${idx}`,
109
238
  executor: agent.executor,
110
- workspaceManager: agent.workspaceManager,
239
+ workspaceManager: trackingWorkspaceManager,
111
240
  onEvent: opts.onEvent,
112
241
  };
113
242
  const scheduler = new scheduler_1.Scheduler();
@@ -121,6 +250,8 @@ async function runGraphForAgent(graph, nodeIds, agent, idx, opts, heartbeatTimeo
121
250
  timed_out: false,
122
251
  assigned_task_ids: nodeIds,
123
252
  reassigned_task_ids: [],
253
+ workspace_records: workspaceRecords,
254
+ cleanup: () => trackingWorkspaceManager.disposeDeferred(),
124
255
  };
125
256
  }
126
257
  let timer = null;
@@ -140,6 +271,8 @@ async function runGraphForAgent(graph, nodeIds, agent, idx, opts, heartbeatTimeo
140
271
  timed_out: true,
141
272
  assigned_task_ids: nodeIds,
142
273
  reassigned_task_ids: [],
274
+ workspace_records: workspaceRecords,
275
+ cleanup: () => trackingWorkspaceManager.disposeDeferred(),
143
276
  };
144
277
  }
145
278
  const result = raced.result;
@@ -150,6 +283,8 @@ async function runGraphForAgent(graph, nodeIds, agent, idx, opts, heartbeatTimeo
150
283
  timed_out: false,
151
284
  assigned_task_ids: nodeIds,
152
285
  reassigned_task_ids: [],
286
+ workspace_records: workspaceRecords,
287
+ cleanup: () => trackingWorkspaceManager.disposeDeferred(),
153
288
  };
154
289
  }
155
290
  // ─── Parallel mode ───────────────────────────────────────────────────────────
@@ -164,6 +299,30 @@ async function runParallel(graph, opts) {
164
299
  partitions[index % agents.length].push(id);
165
300
  });
166
301
  }
302
+ const mutationConflicts = detectMutationConflicts(graph, agents, partitions);
303
+ if (mutationConflicts.length > 0) {
304
+ const blocked = mutationConflicts;
305
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, [], blocked);
306
+ const state = makeState('parallel', runId, agents, partitions, [], [], [], blocked, [], [], workspaceMergeReport);
307
+ persistMultiAgentArtifacts(projectRoot, runId, state, [], [], workspaceMergeReport);
308
+ (0, bus_1.appendEvent)(projectRoot, sessionId, {
309
+ type: 'WorkItemBlocked',
310
+ run_id: runId,
311
+ payload: { mode: 'parallel', blockers: blocked },
312
+ });
313
+ return {
314
+ mode: 'parallel',
315
+ run_id: runId,
316
+ completed: [],
317
+ failed: [],
318
+ blocked,
319
+ agent_results: [],
320
+ arbitration_results: [],
321
+ workspace_merge_report: workspaceMergeReport,
322
+ state,
323
+ summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
324
+ };
325
+ }
167
326
  const registry = new agent_registry_1.AgentRegistry(heartbeatTimeoutMs == null ? 30000 : heartbeatTimeoutMs);
168
327
  agents.forEach((agent, idx) => {
169
328
  registry.register(agent.id, agent.executor, agent.workspaceManager, partitions[idx] ?? []);
@@ -201,7 +360,13 @@ async function runParallel(graph, opts) {
201
360
  const rerun = await runGraphForAgent(graph, timedOut.assigned_task_ids, agents[fallbackIdx], fallbackIdx, opts, null);
202
361
  fallback.completed.push(...rerun.completed);
203
362
  fallback.failed.push(...rerun.failed);
363
+ fallback.workspace_records.push(...rerun.workspace_records);
204
364
  fallback.reassigned_task_ids.push(...timedOut.assigned_task_ids);
365
+ const previousCleanup = fallback.cleanup;
366
+ fallback.cleanup = async () => {
367
+ await previousCleanup?.();
368
+ await rerun.cleanup?.();
369
+ };
205
370
  partitions[fallbackIdx] = [...partitions[fallbackIdx], ...timedOut.assigned_task_ids];
206
371
  partitions[timeoutIdx] = [];
207
372
  orphanReassignments.push({
@@ -212,8 +377,14 @@ async function runParallel(graph, opts) {
212
377
  }
213
378
  const completed = Array.from(new Set(agentResults.flatMap((result) => result.completed)));
214
379
  const failed = Array.from(new Set(agentResults.flatMap((result) => result.failed)));
215
- const state = makeState('parallel', runId, agents, partitions, agentResults, completed, failed, blocked, orphanReassignments, timedOutAgents);
216
- persistMultiAgentArtifacts(projectRoot, runId, state);
380
+ const rawRecords = agentResults.flatMap((result) => result.workspace_records || []);
381
+ const mergedRecords = opts.applyWorkspaceMerges
382
+ ? rawRecords.map((record) => applyWorkspaceRecord(projectRoot, record))
383
+ : rawRecords;
384
+ await Promise.all(agentResults.map((result) => result.cleanup?.().catch(() => { })));
385
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, mergedRecords, blocked);
386
+ const state = makeState('parallel', runId, agents, partitions, agentResults, completed, failed, blocked, orphanReassignments, timedOutAgents, workspaceMergeReport);
387
+ persistMultiAgentArtifacts(projectRoot, runId, state, [], [], workspaceMergeReport);
217
388
  (0, bus_1.appendEvent)(projectRoot, sessionId, {
218
389
  type: 'RunCompleted',
219
390
  run_id: runId,
@@ -227,6 +398,7 @@ async function runParallel(graph, opts) {
227
398
  blocked,
228
399
  agent_results: agentResults,
229
400
  arbitration_results: [],
401
+ workspace_merge_report: workspaceMergeReport,
230
402
  state,
231
403
  summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
232
404
  };
@@ -248,10 +420,11 @@ async function runCompetitive(graph, opts) {
248
420
  const failed = [];
249
421
  const blocked = [];
250
422
  const arbitrationResults = [];
423
+ const workspaceRecords = [];
251
424
  for (const wave of graph.waves) {
252
425
  for (const nodeId of wave.node_ids) {
253
426
  const node = graph.nodes.get(nodeId);
254
- const result = await competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults);
427
+ const result = await competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults, workspaceRecords);
255
428
  if (result.success)
256
429
  completed.push(nodeId);
257
430
  else
@@ -263,11 +436,12 @@ async function runCompetitive(graph, opts) {
263
436
  break;
264
437
  }
265
438
  const partitions = [Array.from(graph.nodes.keys()), Array.from(graph.nodes.keys())];
439
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, workspaceRecords, blocked, true);
266
440
  const state = makeState('competitive', runId, opts.agents, partitions, [
267
441
  { agent_id: agentA.id, completed, failed, timed_out: false, reassigned_task_ids: [] },
268
442
  { agent_id: agentB.id, completed: [], failed: [], timed_out: false, reassigned_task_ids: [] },
269
- ], completed, failed, blocked, [], []);
270
- persistMultiAgentArtifacts(projectRoot, runId, state, [], arbitrationResults);
443
+ ], completed, failed, blocked, [], [], workspaceMergeReport);
444
+ persistMultiAgentArtifacts(projectRoot, runId, state, [], arbitrationResults, workspaceMergeReport);
271
445
  (0, bus_1.appendEvent)(projectRoot, sessionId, {
272
446
  type: 'RunCompleted',
273
447
  run_id: runId,
@@ -284,18 +458,21 @@ async function runCompetitive(graph, opts) {
284
458
  { agent_id: agentB.id, completed: [], failed: [] },
285
459
  ],
286
460
  arbitration_results: arbitrationResults,
461
+ workspace_merge_report: workspaceMergeReport,
287
462
  state,
288
463
  summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
289
464
  };
290
465
  }
291
- async function competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults) {
466
+ async function competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults, workspaceRecords) {
292
467
  const { projectRoot, sessionId, runId } = opts;
293
468
  const allocA = await agentA.workspaceManager.allocate({
294
469
  work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
295
470
  });
471
+ ensureGitWorktreeLease(agentA, allocA);
296
472
  const allocB = await agentB.workspaceManager.allocate({
297
473
  work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
298
474
  });
475
+ ensureGitWorktreeLease(agentB, allocB);
299
476
  (0, bus_1.appendEvent)(projectRoot, sessionId, {
300
477
  type: 'AttemptStarted',
301
478
  run_id: runId,
@@ -310,12 +487,23 @@ async function competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationR
310
487
  success: false, failure_class: 'env', evidence: [], output: String(error),
311
488
  })),
312
489
  ]);
490
+ const candidates = [
491
+ { agent: agentA, alloc: allocA, result: resultA },
492
+ { agent: agentB, alloc: allocB, result: resultB },
493
+ ].sort((left, right) => {
494
+ const leftScore = (left.result.success ? 10000 : 0) + left.result.evidence.length * 100 - String(left.result.output || '').length;
495
+ const rightScore = (right.result.success ? 10000 : 0) + right.result.evidence.length * 100 - String(right.result.output || '').length;
496
+ return rightScore - leftScore;
497
+ });
498
+ const winnerCandidate = candidates[0];
499
+ const winner = winnerCandidate.result;
500
+ const winnerAgentId = winnerCandidate.agent.id;
501
+ const winnerRecord = createMergeRecord(winnerCandidate.agent, node, winnerCandidate.alloc, winner);
502
+ workspaceRecords.push(opts.applyWorkspaceMerges ? applyWorkspaceRecord(projectRoot, winnerRecord) : winnerRecord);
313
503
  await Promise.all([
314
504
  agentA.workspaceManager.dispose(allocA.workspace_id).catch(() => { }),
315
505
  agentB.workspaceManager.dispose(allocB.workspace_id).catch(() => { }),
316
506
  ]);
317
- const winner = resultA.success ? resultA : resultB.success ? resultB : resultA;
318
- const winnerAgentId = resultA.success ? agentA.id : resultB.success ? agentB.id : agentA.id;
319
507
  arbitrationResults.push({
320
508
  work_item_id: nodeId,
321
509
  mode: 'competitive',
@@ -343,6 +531,7 @@ async function runCooperative(graph, opts) {
343
531
  const [planner, executor] = opts.agents;
344
532
  const { projectRoot, sessionId, runId } = opts;
345
533
  const handoffs = [];
534
+ const workspaceRecords = [];
346
535
  (0, bus_1.appendEvent)(projectRoot, sessionId, {
347
536
  type: 'RunStarted',
348
537
  run_id: runId,
@@ -360,6 +549,7 @@ async function runCooperative(graph, opts) {
360
549
  strategy: node.workspace_strategy,
361
550
  mutation_scope: node.mutation_scope,
362
551
  });
552
+ ensureGitWorktreeLease(planner, planAlloc);
363
553
  await planner.workspaceManager.dispose(planAlloc.workspace_id).catch(() => { });
364
554
  const handoff = (0, agent_roles_1.buildHandoff)({
365
555
  from_agent_id: planner.id,
@@ -382,6 +572,7 @@ async function runCooperative(graph, opts) {
382
572
  strategy: node.workspace_strategy,
383
573
  mutation_scope: node.mutation_scope,
384
574
  });
575
+ ensureGitWorktreeLease(executor, execAlloc);
385
576
  let result;
386
577
  try {
387
578
  result = await executor.executor.execute(node, execAlloc, runId, 1);
@@ -389,6 +580,8 @@ async function runCooperative(graph, opts) {
389
580
  catch (error) {
390
581
  result = { success: false, failure_class: 'env', evidence: [], output: String(error) };
391
582
  }
583
+ const mergeRecord = createMergeRecord(executor, node, execAlloc, result);
584
+ workspaceRecords.push(opts.applyWorkspaceMerges ? applyWorkspaceRecord(projectRoot, mergeRecord) : mergeRecord);
392
585
  await executor.workspaceManager.dispose(execAlloc.workspace_id).catch(() => { });
393
586
  if (result.success) {
394
587
  completed.push(nodeId);
@@ -404,11 +597,12 @@ async function runCooperative(graph, opts) {
404
597
  break;
405
598
  }
406
599
  const partitions = [Array.from(graph.nodes.keys()), Array.from(graph.nodes.keys())];
600
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, workspaceRecords, blocked);
407
601
  const state = makeState('cooperative', runId, opts.agents, partitions, [
408
602
  { agent_id: planner.id, completed: [], failed: [], timed_out: false, reassigned_task_ids: [] },
409
603
  { agent_id: executor.id, completed, failed, timed_out: false, reassigned_task_ids: [] },
410
- ], completed, failed, blocked, [], []);
411
- persistMultiAgentArtifacts(projectRoot, runId, state, handoffs, []);
604
+ ], completed, failed, blocked, [], [], workspaceMergeReport);
605
+ persistMultiAgentArtifacts(projectRoot, runId, state, handoffs, [], workspaceMergeReport);
412
606
  (0, bus_1.appendEvent)(projectRoot, sessionId, {
413
607
  type: 'RunCompleted',
414
608
  run_id: runId,
@@ -426,6 +620,7 @@ async function runCooperative(graph, opts) {
426
620
  ],
427
621
  handoffs,
428
622
  arbitration_results: [],
623
+ workspace_merge_report: workspaceMergeReport,
429
624
  state,
430
625
  summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
431
626
  };
@@ -449,6 +644,9 @@ function multiAgentStatePath(projectRoot, runId) {
449
644
  function multiAgentSummaryPath(projectRoot, runId) {
450
645
  return path_1.default.join(projectRoot, '.oxe', 'runs', runId, 'multi-agent-summary.json');
451
646
  }
647
+ function workspaceMergeReportPath(projectRoot, runId) {
648
+ return path_1.default.join(projectRoot, '.oxe', 'runs', runId, 'workspace-merge-report.json');
649
+ }
452
650
  function loadMultiAgentState(projectRoot, runId) {
453
651
  const statePath = multiAgentStatePath(projectRoot, runId);
454
652
  if (!fs_1.default.existsSync(statePath))
@@ -471,6 +669,17 @@ function loadMultiAgentSummary(projectRoot, runId) {
471
669
  return null;
472
670
  }
473
671
  }
672
+ function loadWorkspaceMergeReport(projectRoot, runId) {
673
+ const reportPath = workspaceMergeReportPath(projectRoot, runId);
674
+ if (!fs_1.default.existsSync(reportPath))
675
+ return null;
676
+ try {
677
+ return JSON.parse(fs_1.default.readFileSync(reportPath, 'utf8'));
678
+ }
679
+ catch {
680
+ return null;
681
+ }
682
+ }
474
683
  // ─── Helpers ─────────────────────────────────────────────────────────────────
475
684
  function subGraphFor(graph, nodeIds) {
476
685
  const ids = new Set(nodeIds);
@@ -15,13 +15,15 @@ class GitWorktreeManager {
15
15
  this.leases = new Map();
16
16
  }
17
17
  async allocate(req) {
18
- const wsId = `ws-${req.work_item_id}-a${req.attempt_number}`;
19
- const branch = `oxe/${req.work_item_id}-attempt${req.attempt_number}`;
18
+ const suffix = crypto_1.default.randomBytes(4).toString('hex');
19
+ const safeWorkItem = String(req.work_item_id).replace(/[^A-Za-z0-9._-]/g, '-');
20
+ const wsId = `ws-${safeWorkItem}-a${req.attempt_number}-${suffix}`;
21
+ const branch = `oxe/${safeWorkItem}-attempt${req.attempt_number}-${suffix}`;
20
22
  const worktreePath = path_1.default.join(this.projectRoot, '.oxe', 'workspaces', wsId);
21
- const baseCommit = this.git(['rev-parse', 'HEAD']).trim();
23
+ const baseCommit = this.git(['rev-parse', 'HEAD'], undefined, 'git_worktree requires a git repository with at least one base commit').trim();
22
24
  fs_1.default.mkdirSync(path_1.default.dirname(worktreePath), { recursive: true });
23
25
  // Create worktree on a new branch starting from HEAD
24
- this.git(['worktree', 'add', worktreePath, '-b', branch]);
26
+ this.git(['worktree', 'add', worktreePath, '-b', branch], undefined, 'failed to create git_worktree workspace');
25
27
  const lease = {
26
28
  workspace_id: wsId,
27
29
  strategy: 'git_worktree',
@@ -71,11 +73,18 @@ class GitWorktreeManager {
71
73
  }
72
74
  this.leases.delete(id);
73
75
  }
74
- git(args, cwd) {
75
- return (0, child_process_1.execFileSync)('git', args, {
76
- cwd: cwd ?? this.projectRoot,
77
- encoding: 'utf8',
78
- });
76
+ git(args, cwd, message) {
77
+ try {
78
+ return (0, child_process_1.execFileSync)('git', args, {
79
+ cwd: cwd ?? this.projectRoot,
80
+ encoding: 'utf8',
81
+ stdio: ['ignore', 'pipe', 'pipe'],
82
+ });
83
+ }
84
+ catch (error) {
85
+ const detail = error instanceof Error ? error.message : String(error);
86
+ throw new Error(`${message || 'git command failed'}: git ${args.join(' ')} (${detail})`);
87
+ }
79
88
  }
80
89
  }
81
90
  exports.GitWorktreeManager = GitWorktreeManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxe-cc",
3
- "version": "1.8.3",
3
+ "version": "1.9.1",
4
4
  "description": "OXE — spec-driven workflows in .oxe/ with runtime enterprise, evidence-first verification and multi-runtime integrations (npx)",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -71,13 +71,16 @@
71
71
  "test:root": "node --test tests/install.test.cjs tests/oxe-project-health.test.cjs tests/oxe-dashboard.test.cjs tests/oxe-operational.test.cjs tests/oxe-azure.test.cjs tests/oxe-sdk.test.cjs tests/oxe-manifest.test.cjs tests/oxe-agent-install.test.cjs tests/oxe-install-resolve-full.test.cjs tests/oxe-health-extended.test.cjs tests/oxe-workflows-edge.test.cjs tests/oxe-sdk-edge.test.cjs tests/oxe-cli-edge.test.cjs tests/oxe-npm-version.test.cjs tests/oxe-scripts.test.cjs tests/oxe-retro-health.test.cjs tests/oxe-security-permissions.test.cjs tests/oxe-runtime-semantics.test.cjs tests/oxe-plugins.test.cjs",
72
72
  "test:runtime": "cd packages/runtime && npm test",
73
73
  "test:runtime-smoke": "node scripts/runtime-smoke-matrix.cjs",
74
+ "test:runtime-real": "node scripts/runtime-real-suite.cjs",
74
75
  "test:recovery-fixtures": "node scripts/run-recovery-fixtures.cjs",
75
76
  "test:multi-agent-soak": "node scripts/run-multi-agent-soak.cjs",
76
- "test": "npm run build:runtime && npm run test:root && npm run test:runtime && npm run test:runtime-smoke && npm run test:recovery-fixtures && npm run test:multi-agent-soak",
77
+ "test:multi-agent-real": "node scripts/run-multi-agent-real.cjs",
78
+ "test": "npm run build:runtime && npm run test:root && npm run test:runtime && npm run test:runtime-smoke && npm run test:runtime-real && npm run test:recovery-fixtures && npm run test:multi-agent-soak && npm run test:multi-agent-real",
77
79
  "test:coverage": "c8 --check-coverage --lines 82 --functions 85 --branches 58 --statements 82 npm test",
78
80
  "scan:assets": "node scripts/oxe-assets-scan.cjs",
81
+ "release:pack-check": "node scripts/release-pack-check.cjs",
79
82
  "build:vscode-ext": "cd vscode-extension && npx @vscode/vsce package --no-yarn --allow-missing-repository",
80
- "prepublishOnly": "npm test && npm run scan:assets && npm run build:vscode-ext && npm run release:manifest && node bin/oxe-cc.js --version"
83
+ "prepublishOnly": "npm test && npm run scan:assets && npm run build:vscode-ext && npm run release:manifest && npm run release:pack-check && node bin/oxe-cc.js --version"
81
84
  },
82
85
  "c8": {
83
86
  "all": true,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxe/runtime",
3
- "version": "1.8.3",
3
+ "version": "1.9.1",
4
4
  "private": true,
5
5
  "license": "MIT",
6
6
  "description": "OXE agentic execution engine — enterprise runtime core",