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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +31 -0
- package/README.md +15 -12
- package/bin/lib/oxe-operational.cjs +209 -69
- package/bin/lib/oxe-project-health.cjs +2 -0
- package/bin/lib/oxe-release.cjs +26 -0
- package/bin/oxe-cc.js +22 -13
- package/docs/RELEASE-READINESS.md +6 -1
- package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +39 -0
- package/lib/runtime/scheduler/multi-agent-coordinator.js +222 -13
- package/lib/runtime/workspace/strategies/git-worktree.js +18 -9
- package/package.json +6 -3
- package/packages/runtime/package.json +1 -1
- package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +320 -46
- package/packages/runtime/src/workspace/strategies/git-worktree.ts +24 -16
- 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,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:
|
|
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
|
|
216
|
-
|
|
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
|
|
19
|
-
const
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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.
|
|
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": "
|
|
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,
|