oxe-cc 1.8.3 → 1.10.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 (42) hide show
  1. package/.cursor/commands/oxe-dashboard.md +2 -2
  2. package/.cursor/commands/oxe-execute.md +2 -2
  3. package/.cursor/commands/oxe-plan.md +2 -2
  4. package/.cursor/commands/oxe-quick.md +2 -2
  5. package/.cursor/commands/oxe-spec.md +3 -3
  6. package/.github/prompts/oxe-dashboard.prompt.md +2 -2
  7. package/.github/prompts/oxe-execute.prompt.md +2 -2
  8. package/.github/prompts/oxe-plan.prompt.md +2 -2
  9. package/.github/prompts/oxe-quick.prompt.md +2 -2
  10. package/.github/prompts/oxe-spec.prompt.md +3 -3
  11. package/AGENTS.md +1 -1
  12. package/CHANGELOG.md +45 -0
  13. package/README.md +32 -26
  14. package/bin/lib/oxe-context-engine.cjs +2 -0
  15. package/bin/lib/oxe-operational.cjs +230 -74
  16. package/bin/lib/oxe-project-health.cjs +43 -9
  17. package/bin/lib/oxe-rationality.cjs +146 -1
  18. package/bin/lib/oxe-release.cjs +55 -0
  19. package/bin/oxe-cc.js +60 -37
  20. package/commands/oxe/dashboard.md +2 -2
  21. package/commands/oxe/execute.md +2 -2
  22. package/commands/oxe/plan.md +2 -2
  23. package/commands/oxe/quick.md +2 -2
  24. package/commands/oxe/spec.md +3 -3
  25. package/docs/RELEASE-READINESS.md +8 -1
  26. package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +48 -0
  27. package/lib/runtime/scheduler/multi-agent-coordinator.js +274 -14
  28. package/lib/runtime/workspace/strategies/git-worktree.js +18 -9
  29. package/oxe/templates/REFERENCE-ANCHORS.template.md +12 -5
  30. package/oxe/templates/SPEC.template.md +10 -0
  31. package/oxe/templates/VISUAL-INPUTS.template.json +27 -0
  32. package/oxe/templates/VISUAL-INPUTS.template.md +36 -0
  33. package/oxe/workflows/execute.md +3 -0
  34. package/oxe/workflows/plan.md +13 -9
  35. package/oxe/workflows/references/workflow-runtime-contracts.json +44 -29
  36. package/oxe/workflows/spec.md +19 -8
  37. package/oxe/workflows/ui-spec.md +3 -2
  38. package/package.json +6 -3
  39. package/packages/runtime/package.json +1 -1
  40. package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +379 -47
  41. package/packages/runtime/src/workspace/strategies/git-worktree.ts +24 -16
  42. package/vscode-extension/package.json +1 -1
@@ -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,42 @@ 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_refs: string[];
49
+ evidence_count: number;
50
+ verify_status: 'pass' | 'fail' | 'partial';
51
+ status: 'ready' | 'merged' | 'blocked' | 'skipped';
52
+ blocker: string | null;
53
+ applied_paths: string[];
54
+ diff_summary: {
55
+ added: number;
56
+ modified: number;
57
+ missing: number;
58
+ paths: string[];
59
+ };
60
+ next_action: string | null;
61
+ recorded_at: string;
62
+ }
63
+ export interface WorkspaceMergeReport {
64
+ schema_version: 1;
65
+ run_id: string;
66
+ generated_at: string;
67
+ workspace_isolation: 'git_worktree';
68
+ merge_readiness: 'ready' | 'blocked' | 'partial';
69
+ arbitration_required: boolean;
70
+ blockers: string[];
71
+ records: WorkspaceMergeRecord[];
72
+ }
36
73
  export interface MultiAgentStatusSnapshot {
37
74
  run_id: string;
38
75
  mode: CoordinationMode;
@@ -52,6 +89,10 @@ export interface MultiAgentStatusSnapshot {
52
89
  timed_out: boolean;
53
90
  reassigned_task_ids: string[];
54
91
  }>;
92
+ worktrees: WorkspaceMergeRecord[];
93
+ workspace_merge: WorkspaceMergeReport;
94
+ merge_blockers: string[];
95
+ arbitration_required: boolean;
55
96
  orphan_reassignments: Array<{
56
97
  from_agent_id: string;
57
98
  to_agent_id: string;
@@ -78,6 +119,10 @@ export interface MultiAgentOperationalSummary {
78
119
  orphan_reassignment_count: number;
79
120
  timeout_count: number;
80
121
  participating_agents: string[];
122
+ workspace_isolation: 'git_worktree';
123
+ merge_readiness: WorkspaceMergeReport['merge_readiness'];
124
+ arbitration_required: boolean;
125
+ merge_blocker_count: number;
81
126
  health: 'healthy' | 'degraded';
82
127
  updated_at: string;
83
128
  }
@@ -94,6 +139,7 @@ export interface CoordinationResult {
94
139
  }>;
95
140
  handoffs?: CooperativeHandoff[];
96
141
  arbitration_results?: ArbitrationRecord[];
142
+ workspace_merge_report?: WorkspaceMergeReport;
97
143
  state?: MultiAgentStatusSnapshot;
98
144
  summary?: MultiAgentOperationalSummary;
99
145
  }
@@ -102,5 +148,7 @@ export declare class MultiAgentCoordinator {
102
148
  }
103
149
  export declare function multiAgentStatePath(projectRoot: string, runId: string): string;
104
150
  export declare function multiAgentSummaryPath(projectRoot: string, runId: string): string;
151
+ export declare function workspaceMergeReportPath(projectRoot: string, runId: string): string;
105
152
  export declare function loadMultiAgentState(projectRoot: string, runId: string): MultiAgentStatusSnapshot | null;
106
153
  export declare function loadMultiAgentSummary(projectRoot: string, runId: string): MultiAgentOperationalSummary | null;
154
+ 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,173 @@ 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 evidenceRefs = Array.isArray(result?.evidence) ? result.evidence.map(String).filter(Boolean) : [];
126
+ const scope = mutationScopeOf(node);
127
+ return {
128
+ work_item_id: node.id,
129
+ agent_id: agent.id,
130
+ workspace_id: lease.workspace_id,
131
+ strategy: lease.strategy,
132
+ isolation_level: lease.isolation_level,
133
+ branch: lease.branch ?? null,
134
+ base_commit: lease.base_commit ?? null,
135
+ root_path: lease.root_path ?? null,
136
+ mutation_scope: scope,
137
+ diff_paths: scope,
138
+ evidence_refs: evidenceRefs,
139
+ evidence_count: evidenceRefs.length,
140
+ verify_status: result ? (result.success ? 'pass' : 'fail') : 'partial',
141
+ status: result ? (result.success ? 'ready' : 'blocked') : 'ready',
142
+ blocker: result && !result.success ? `verify_failed:${result.failure_class}` : null,
143
+ applied_paths: [],
144
+ diff_summary: { added: 0, modified: 0, missing: 0, paths: scope },
145
+ next_action: result && !result.success ? 'Corrija a falha de verify/evidence antes de aplicar merge do workspace.' : null,
146
+ recorded_at: new Date().toISOString(),
147
+ };
148
+ }
149
+ function enrichMergeRecordWithResult(record, result) {
150
+ if (!result)
151
+ return record;
152
+ const evidenceRefs = Array.isArray(result.evidence) ? result.evidence.map(String).filter(Boolean) : [];
153
+ return {
154
+ ...record,
155
+ evidence_refs: evidenceRefs,
156
+ evidence_count: evidenceRefs.length,
157
+ verify_status: result.success ? 'pass' : 'fail',
158
+ status: result.success ? 'ready' : 'blocked',
159
+ blocker: result.success ? null : `verify_failed:${result.failure_class}`,
160
+ next_action: result.success ? null : 'Corrija a falha de verify/evidence antes de aplicar merge do workspace.',
161
+ recorded_at: new Date().toISOString(),
162
+ };
163
+ }
164
+ function buildWorkspaceMergeReport(runId, records, extraBlockers = [], arbitrationRequired = false) {
165
+ const blockers = [
166
+ ...extraBlockers,
167
+ ...records.filter((record) => record.status === 'blocked' && record.blocker).map((record) => `${record.work_item_id}:${record.blocker}`),
168
+ ];
169
+ return {
170
+ schema_version: 1,
171
+ run_id: runId,
172
+ generated_at: new Date().toISOString(),
173
+ workspace_isolation: 'git_worktree',
174
+ merge_readiness: blockers.length > 0 ? 'blocked' : records.some((record) => record.status === 'ready') ? 'partial' : 'ready',
175
+ arbitration_required: arbitrationRequired,
176
+ blockers,
177
+ records,
178
+ };
179
+ }
180
+ function detectMutationConflicts(graph, agents, partitions) {
181
+ const owners = new Map();
182
+ const conflicts = [];
183
+ for (let idx = 0; idx < agents.length; idx += 1) {
184
+ for (const nodeId of partitions[idx] ?? []) {
185
+ const node = graph.nodes.get(nodeId);
186
+ for (const scope of mutationScopeOf(node)) {
187
+ const previous = owners.get(scope);
188
+ if (previous && previous !== agents[idx].id) {
189
+ conflicts.push(`mutation_scope_overlap:${scope}:${previous}:${agents[idx].id}`);
190
+ }
191
+ else {
192
+ owners.set(scope, agents[idx].id);
193
+ }
194
+ }
195
+ }
196
+ }
197
+ return conflicts;
198
+ }
199
+ function applyWorkspaceRecord(projectRoot, record) {
200
+ if (!record.root_path || record.status !== 'ready')
201
+ return record;
202
+ const appliedPaths = [];
203
+ const summary = { added: 0, modified: 0, missing: 0, paths: [...record.mutation_scope] };
204
+ for (const relativePath of record.mutation_scope) {
205
+ const source = path_1.default.join(record.root_path, relativePath);
206
+ const target = path_1.default.join(projectRoot, relativePath);
207
+ if (!fs_1.default.existsSync(source) || fs_1.default.statSync(source).isDirectory()) {
208
+ summary.missing += 1;
209
+ return {
210
+ ...record,
211
+ status: 'blocked',
212
+ blocker: `missing_output:${relativePath}`,
213
+ diff_summary: summary,
214
+ next_action: `Materialize o arquivo esperado ${relativePath} no worktree do agente antes do merge.`,
215
+ };
216
+ }
217
+ if (fs_1.default.existsSync(target))
218
+ summary.modified += 1;
219
+ else
220
+ summary.added += 1;
221
+ fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true });
222
+ fs_1.default.copyFileSync(source, target);
223
+ appliedPaths.push(relativePath);
224
+ }
225
+ return {
226
+ ...record,
227
+ status: 'merged',
228
+ blocker: null,
229
+ applied_paths: appliedPaths,
230
+ diff_summary: summary,
231
+ next_action: null,
232
+ };
233
+ }
234
+ function createTrackingWorkspaceManager(base, agent, graph, records) {
235
+ const leases = new Map();
236
+ return {
237
+ isolation_level: base.isolation_level,
238
+ allocate: async (req) => {
239
+ const lease = await base.allocate(req);
240
+ ensureGitWorktreeLease(agent, lease);
241
+ leases.set(lease.workspace_id, lease);
242
+ const node = graph.nodes.get(req.work_item_id);
243
+ if (node)
244
+ records.push(createMergeRecord(agent, node, lease));
245
+ return lease;
246
+ },
247
+ snapshot: (id) => base.snapshot(id),
248
+ reset: (id, snapRef) => base.reset(id, snapRef),
249
+ dispose: async () => {
250
+ // Scheduler calls dispose before the coordinator can reconcile diffs.
251
+ // Defer real cleanup until the merge report has been produced.
252
+ },
253
+ disposeDeferred: async () => {
254
+ await Promise.all([...leases.keys()].map((id) => base.dispose(id).catch(() => { })));
255
+ leases.clear();
256
+ },
257
+ };
258
+ }
93
259
  async function runGraphForAgent(graph, nodeIds, agent, idx, opts, heartbeatTimeoutMs) {
94
260
  const subGraph = subGraphFor(graph, nodeIds);
261
+ const workspaceRecords = [];
95
262
  if (subGraph.nodes.size === 0) {
96
263
  return {
97
264
  agent_id: agent.id,
@@ -100,20 +267,32 @@ async function runGraphForAgent(graph, nodeIds, agent, idx, opts, heartbeatTimeo
100
267
  timed_out: false,
101
268
  assigned_task_ids: nodeIds,
102
269
  reassigned_task_ids: [],
270
+ workspace_records: workspaceRecords,
271
+ cleanup: async () => { },
103
272
  };
104
273
  }
274
+ const trackingWorkspaceManager = createTrackingWorkspaceManager(agent.workspaceManager, agent, graph, workspaceRecords);
275
+ const taskResults = new Map();
276
+ const trackingExecutor = {
277
+ execute: async (node, lease, runId, attemptNumber) => {
278
+ const result = await agent.executor.execute(node, lease, runId, attemptNumber);
279
+ taskResults.set(node.id, result);
280
+ return result;
281
+ },
282
+ };
105
283
  const ctx = {
106
284
  projectRoot: opts.projectRoot,
107
285
  sessionId: opts.sessionId,
108
286
  runId: `${opts.runId}-agent${idx}`,
109
- executor: agent.executor,
110
- workspaceManager: agent.workspaceManager,
287
+ executor: trackingExecutor,
288
+ workspaceManager: trackingWorkspaceManager,
111
289
  onEvent: opts.onEvent,
112
290
  };
113
291
  const scheduler = new scheduler_1.Scheduler();
114
292
  const work = scheduler.run(subGraph, ctx);
115
293
  if (!heartbeatTimeoutMs || heartbeatTimeoutMs <= 0) {
116
294
  const result = await work;
295
+ const reconciledRecords = workspaceRecords.map((record) => enrichMergeRecordWithResult(record, taskResults.get(record.work_item_id)));
117
296
  return {
118
297
  agent_id: agent.id,
119
298
  completed: result.completed,
@@ -121,6 +300,8 @@ async function runGraphForAgent(graph, nodeIds, agent, idx, opts, heartbeatTimeo
121
300
  timed_out: false,
122
301
  assigned_task_ids: nodeIds,
123
302
  reassigned_task_ids: [],
303
+ workspace_records: reconciledRecords,
304
+ cleanup: () => trackingWorkspaceManager.disposeDeferred(),
124
305
  };
125
306
  }
126
307
  let timer = null;
@@ -140,9 +321,12 @@ async function runGraphForAgent(graph, nodeIds, agent, idx, opts, heartbeatTimeo
140
321
  timed_out: true,
141
322
  assigned_task_ids: nodeIds,
142
323
  reassigned_task_ids: [],
324
+ workspace_records: workspaceRecords,
325
+ cleanup: () => trackingWorkspaceManager.disposeDeferred(),
143
326
  };
144
327
  }
145
328
  const result = raced.result;
329
+ const reconciledRecords = workspaceRecords.map((record) => enrichMergeRecordWithResult(record, taskResults.get(record.work_item_id)));
146
330
  return {
147
331
  agent_id: agent.id,
148
332
  completed: result.completed,
@@ -150,6 +334,8 @@ async function runGraphForAgent(graph, nodeIds, agent, idx, opts, heartbeatTimeo
150
334
  timed_out: false,
151
335
  assigned_task_ids: nodeIds,
152
336
  reassigned_task_ids: [],
337
+ workspace_records: reconciledRecords,
338
+ cleanup: () => trackingWorkspaceManager.disposeDeferred(),
153
339
  };
154
340
  }
155
341
  // ─── Parallel mode ───────────────────────────────────────────────────────────
@@ -164,6 +350,30 @@ async function runParallel(graph, opts) {
164
350
  partitions[index % agents.length].push(id);
165
351
  });
166
352
  }
353
+ const mutationConflicts = detectMutationConflicts(graph, agents, partitions);
354
+ if (mutationConflicts.length > 0) {
355
+ const blocked = mutationConflicts;
356
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, [], blocked);
357
+ const state = makeState('parallel', runId, agents, partitions, [], [], [], blocked, [], [], workspaceMergeReport);
358
+ persistMultiAgentArtifacts(projectRoot, runId, state, [], [], workspaceMergeReport);
359
+ (0, bus_1.appendEvent)(projectRoot, sessionId, {
360
+ type: 'WorkItemBlocked',
361
+ run_id: runId,
362
+ payload: { mode: 'parallel', blockers: blocked },
363
+ });
364
+ return {
365
+ mode: 'parallel',
366
+ run_id: runId,
367
+ completed: [],
368
+ failed: [],
369
+ blocked,
370
+ agent_results: [],
371
+ arbitration_results: [],
372
+ workspace_merge_report: workspaceMergeReport,
373
+ state,
374
+ summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
375
+ };
376
+ }
167
377
  const registry = new agent_registry_1.AgentRegistry(heartbeatTimeoutMs == null ? 30000 : heartbeatTimeoutMs);
168
378
  agents.forEach((agent, idx) => {
169
379
  registry.register(agent.id, agent.executor, agent.workspaceManager, partitions[idx] ?? []);
@@ -201,7 +411,13 @@ async function runParallel(graph, opts) {
201
411
  const rerun = await runGraphForAgent(graph, timedOut.assigned_task_ids, agents[fallbackIdx], fallbackIdx, opts, null);
202
412
  fallback.completed.push(...rerun.completed);
203
413
  fallback.failed.push(...rerun.failed);
414
+ fallback.workspace_records.push(...rerun.workspace_records);
204
415
  fallback.reassigned_task_ids.push(...timedOut.assigned_task_ids);
416
+ const previousCleanup = fallback.cleanup;
417
+ fallback.cleanup = async () => {
418
+ await previousCleanup?.();
419
+ await rerun.cleanup?.();
420
+ };
205
421
  partitions[fallbackIdx] = [...partitions[fallbackIdx], ...timedOut.assigned_task_ids];
206
422
  partitions[timeoutIdx] = [];
207
423
  orphanReassignments.push({
@@ -212,8 +428,14 @@ async function runParallel(graph, opts) {
212
428
  }
213
429
  const completed = Array.from(new Set(agentResults.flatMap((result) => result.completed)));
214
430
  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);
431
+ const rawRecords = agentResults.flatMap((result) => result.workspace_records || []);
432
+ const mergedRecords = opts.applyWorkspaceMerges
433
+ ? rawRecords.map((record) => applyWorkspaceRecord(projectRoot, record))
434
+ : rawRecords;
435
+ await Promise.all(agentResults.map((result) => result.cleanup?.().catch(() => { })));
436
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, mergedRecords, blocked);
437
+ const state = makeState('parallel', runId, agents, partitions, agentResults, completed, failed, blocked, orphanReassignments, timedOutAgents, workspaceMergeReport);
438
+ persistMultiAgentArtifacts(projectRoot, runId, state, [], [], workspaceMergeReport);
217
439
  (0, bus_1.appendEvent)(projectRoot, sessionId, {
218
440
  type: 'RunCompleted',
219
441
  run_id: runId,
@@ -227,6 +449,7 @@ async function runParallel(graph, opts) {
227
449
  blocked,
228
450
  agent_results: agentResults,
229
451
  arbitration_results: [],
452
+ workspace_merge_report: workspaceMergeReport,
230
453
  state,
231
454
  summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
232
455
  };
@@ -248,10 +471,11 @@ async function runCompetitive(graph, opts) {
248
471
  const failed = [];
249
472
  const blocked = [];
250
473
  const arbitrationResults = [];
474
+ const workspaceRecords = [];
251
475
  for (const wave of graph.waves) {
252
476
  for (const nodeId of wave.node_ids) {
253
477
  const node = graph.nodes.get(nodeId);
254
- const result = await competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults);
478
+ const result = await competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults, workspaceRecords);
255
479
  if (result.success)
256
480
  completed.push(nodeId);
257
481
  else
@@ -263,11 +487,12 @@ async function runCompetitive(graph, opts) {
263
487
  break;
264
488
  }
265
489
  const partitions = [Array.from(graph.nodes.keys()), Array.from(graph.nodes.keys())];
490
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, workspaceRecords, blocked, true);
266
491
  const state = makeState('competitive', runId, opts.agents, partitions, [
267
492
  { agent_id: agentA.id, completed, failed, timed_out: false, reassigned_task_ids: [] },
268
493
  { agent_id: agentB.id, completed: [], failed: [], timed_out: false, reassigned_task_ids: [] },
269
- ], completed, failed, blocked, [], []);
270
- persistMultiAgentArtifacts(projectRoot, runId, state, [], arbitrationResults);
494
+ ], completed, failed, blocked, [], [], workspaceMergeReport);
495
+ persistMultiAgentArtifacts(projectRoot, runId, state, [], arbitrationResults, workspaceMergeReport);
271
496
  (0, bus_1.appendEvent)(projectRoot, sessionId, {
272
497
  type: 'RunCompleted',
273
498
  run_id: runId,
@@ -284,18 +509,21 @@ async function runCompetitive(graph, opts) {
284
509
  { agent_id: agentB.id, completed: [], failed: [] },
285
510
  ],
286
511
  arbitration_results: arbitrationResults,
512
+ workspace_merge_report: workspaceMergeReport,
287
513
  state,
288
514
  summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
289
515
  };
290
516
  }
291
- async function competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults) {
517
+ async function competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationResults, workspaceRecords) {
292
518
  const { projectRoot, sessionId, runId } = opts;
293
519
  const allocA = await agentA.workspaceManager.allocate({
294
520
  work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
295
521
  });
522
+ ensureGitWorktreeLease(agentA, allocA);
296
523
  const allocB = await agentB.workspaceManager.allocate({
297
524
  work_item_id: nodeId, attempt_number: 1, strategy: node.workspace_strategy, mutation_scope: node.mutation_scope,
298
525
  });
526
+ ensureGitWorktreeLease(agentB, allocB);
299
527
  (0, bus_1.appendEvent)(projectRoot, sessionId, {
300
528
  type: 'AttemptStarted',
301
529
  run_id: runId,
@@ -310,12 +538,23 @@ async function competeTwoAgents(nodeId, node, agentA, agentB, opts, arbitrationR
310
538
  success: false, failure_class: 'env', evidence: [], output: String(error),
311
539
  })),
312
540
  ]);
541
+ const candidates = [
542
+ { agent: agentA, alloc: allocA, result: resultA },
543
+ { agent: agentB, alloc: allocB, result: resultB },
544
+ ].sort((left, right) => {
545
+ const leftScore = (left.result.success ? 10000 : 0) + left.result.evidence.length * 100 - String(left.result.output || '').length;
546
+ const rightScore = (right.result.success ? 10000 : 0) + right.result.evidence.length * 100 - String(right.result.output || '').length;
547
+ return rightScore - leftScore;
548
+ });
549
+ const winnerCandidate = candidates[0];
550
+ const winner = winnerCandidate.result;
551
+ const winnerAgentId = winnerCandidate.agent.id;
552
+ const winnerRecord = createMergeRecord(winnerCandidate.agent, node, winnerCandidate.alloc, winner);
553
+ workspaceRecords.push(opts.applyWorkspaceMerges ? applyWorkspaceRecord(projectRoot, winnerRecord) : winnerRecord);
313
554
  await Promise.all([
314
555
  agentA.workspaceManager.dispose(allocA.workspace_id).catch(() => { }),
315
556
  agentB.workspaceManager.dispose(allocB.workspace_id).catch(() => { }),
316
557
  ]);
317
- const winner = resultA.success ? resultA : resultB.success ? resultB : resultA;
318
- const winnerAgentId = resultA.success ? agentA.id : resultB.success ? agentB.id : agentA.id;
319
558
  arbitrationResults.push({
320
559
  work_item_id: nodeId,
321
560
  mode: 'competitive',
@@ -343,6 +582,7 @@ async function runCooperative(graph, opts) {
343
582
  const [planner, executor] = opts.agents;
344
583
  const { projectRoot, sessionId, runId } = opts;
345
584
  const handoffs = [];
585
+ const workspaceRecords = [];
346
586
  (0, bus_1.appendEvent)(projectRoot, sessionId, {
347
587
  type: 'RunStarted',
348
588
  run_id: runId,
@@ -360,6 +600,7 @@ async function runCooperative(graph, opts) {
360
600
  strategy: node.workspace_strategy,
361
601
  mutation_scope: node.mutation_scope,
362
602
  });
603
+ ensureGitWorktreeLease(planner, planAlloc);
363
604
  await planner.workspaceManager.dispose(planAlloc.workspace_id).catch(() => { });
364
605
  const handoff = (0, agent_roles_1.buildHandoff)({
365
606
  from_agent_id: planner.id,
@@ -382,6 +623,7 @@ async function runCooperative(graph, opts) {
382
623
  strategy: node.workspace_strategy,
383
624
  mutation_scope: node.mutation_scope,
384
625
  });
626
+ ensureGitWorktreeLease(executor, execAlloc);
385
627
  let result;
386
628
  try {
387
629
  result = await executor.executor.execute(node, execAlloc, runId, 1);
@@ -389,6 +631,8 @@ async function runCooperative(graph, opts) {
389
631
  catch (error) {
390
632
  result = { success: false, failure_class: 'env', evidence: [], output: String(error) };
391
633
  }
634
+ const mergeRecord = createMergeRecord(executor, node, execAlloc, result);
635
+ workspaceRecords.push(opts.applyWorkspaceMerges ? applyWorkspaceRecord(projectRoot, mergeRecord) : mergeRecord);
392
636
  await executor.workspaceManager.dispose(execAlloc.workspace_id).catch(() => { });
393
637
  if (result.success) {
394
638
  completed.push(nodeId);
@@ -404,11 +648,12 @@ async function runCooperative(graph, opts) {
404
648
  break;
405
649
  }
406
650
  const partitions = [Array.from(graph.nodes.keys()), Array.from(graph.nodes.keys())];
651
+ const workspaceMergeReport = buildWorkspaceMergeReport(runId, workspaceRecords, blocked);
407
652
  const state = makeState('cooperative', runId, opts.agents, partitions, [
408
653
  { agent_id: planner.id, completed: [], failed: [], timed_out: false, reassigned_task_ids: [] },
409
654
  { agent_id: executor.id, completed, failed, timed_out: false, reassigned_task_ids: [] },
410
- ], completed, failed, blocked, [], []);
411
- persistMultiAgentArtifacts(projectRoot, runId, state, handoffs, []);
655
+ ], completed, failed, blocked, [], [], workspaceMergeReport);
656
+ persistMultiAgentArtifacts(projectRoot, runId, state, handoffs, [], workspaceMergeReport);
412
657
  (0, bus_1.appendEvent)(projectRoot, sessionId, {
413
658
  type: 'RunCompleted',
414
659
  run_id: runId,
@@ -426,6 +671,7 @@ async function runCooperative(graph, opts) {
426
671
  ],
427
672
  handoffs,
428
673
  arbitration_results: [],
674
+ workspace_merge_report: workspaceMergeReport,
429
675
  state,
430
676
  summary: loadMultiAgentSummary(projectRoot, runId) || undefined,
431
677
  };
@@ -449,6 +695,9 @@ function multiAgentStatePath(projectRoot, runId) {
449
695
  function multiAgentSummaryPath(projectRoot, runId) {
450
696
  return path_1.default.join(projectRoot, '.oxe', 'runs', runId, 'multi-agent-summary.json');
451
697
  }
698
+ function workspaceMergeReportPath(projectRoot, runId) {
699
+ return path_1.default.join(projectRoot, '.oxe', 'runs', runId, 'workspace-merge-report.json');
700
+ }
452
701
  function loadMultiAgentState(projectRoot, runId) {
453
702
  const statePath = multiAgentStatePath(projectRoot, runId);
454
703
  if (!fs_1.default.existsSync(statePath))
@@ -471,6 +720,17 @@ function loadMultiAgentSummary(projectRoot, runId) {
471
720
  return null;
472
721
  }
473
722
  }
723
+ function loadWorkspaceMergeReport(projectRoot, runId) {
724
+ const reportPath = workspaceMergeReportPath(projectRoot, runId);
725
+ if (!fs_1.default.existsSync(reportPath))
726
+ return null;
727
+ try {
728
+ return JSON.parse(fs_1.default.readFileSync(reportPath, 'utf8'));
729
+ }
730
+ catch {
731
+ return null;
732
+ }
733
+ }
474
734
  // ─── Helpers ─────────────────────────────────────────────────────────────────
475
735
  function subGraphFor(graph, nodeIds) {
476
736
  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;
@@ -4,18 +4,23 @@
4
4
 
5
5
  <reference_anchors version="1" ready="false" status="not_ready">
6
6
  <anchor
7
- id="RA-01"
8
- task="T1"
9
- critical="true"
10
- status="resolved"
11
- source_type="local"
7
+ id="RA-01"
8
+ task="T1"
9
+ critical="true"
10
+ status="resolved"
11
+ source_type="local"
12
12
  path=".oxe/investigations/externals/exemplo.txt"
13
+ visual_ref="not_applicable"
14
+ extraction_confidence="not_applicable"
15
+ reproducibility="local_file"
13
16
  snippet_ref="lines 10-30"
14
17
  source_ref="external-ref: exemplo">
15
18
  <relevance>Por que esta referência sustenta a tarefa.</relevance>
16
19
  <action>copy | adapt | consult</action>
17
20
  <summary>Resumo semântico curto do trecho relevante ou do contrato.</summary>
18
21
  <critical_fields>IDs, colunas, offsets, eventos ou contratos que não podem ser improvisados.</critical_fields>
22
+ <limitations>Limitações da referência. Para imagem/anexo visual, registrar se depende de inspeção do runtime hospedeiro.</limitations>
23
+ <derived_requirements>R-ID/A* derivados desta referência, quando aplicável.</derived_requirements>
19
24
  </anchor>
20
25
  </reference_anchors>
21
26
 
@@ -26,3 +31,5 @@
26
31
  - `path` deve ser reproduzível no workspace ou materializado em `.oxe/investigations/externals/`.
27
32
  - Referência solta em texto não é evidência. Se o path/range/snippet não puder ser materializado, marcar `status="missing"` e derrubar readiness.
28
33
  - Anchors podem ser locais, externos materializados, predecessor interno, contrato público, fixture ou decisão registrada.
34
+ - Para imagens/anexos visuais, usar `source_type="visual_attachment|screenshot|mockup|image_description"` e ligar `visual_ref` a `VISUAL-INPUTS.md/json`.
35
+ - Âncora visual crítica só pode ficar `resolved` quando a extração textual for suficiente para executar sem reabrir a imagem.