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.
- package/.cursor/commands/oxe-dashboard.md +2 -2
- package/.cursor/commands/oxe-execute.md +2 -2
- package/.cursor/commands/oxe-plan.md +2 -2
- package/.cursor/commands/oxe-quick.md +2 -2
- package/.cursor/commands/oxe-spec.md +3 -3
- package/.github/prompts/oxe-dashboard.prompt.md +2 -2
- package/.github/prompts/oxe-execute.prompt.md +2 -2
- package/.github/prompts/oxe-plan.prompt.md +2 -2
- package/.github/prompts/oxe-quick.prompt.md +2 -2
- package/.github/prompts/oxe-spec.prompt.md +3 -3
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +45 -0
- package/README.md +32 -26
- package/bin/lib/oxe-context-engine.cjs +2 -0
- package/bin/lib/oxe-operational.cjs +230 -74
- package/bin/lib/oxe-project-health.cjs +43 -9
- package/bin/lib/oxe-rationality.cjs +146 -1
- package/bin/lib/oxe-release.cjs +55 -0
- package/bin/oxe-cc.js +60 -37
- package/commands/oxe/dashboard.md +2 -2
- package/commands/oxe/execute.md +2 -2
- package/commands/oxe/plan.md +2 -2
- package/commands/oxe/quick.md +2 -2
- package/commands/oxe/spec.md +3 -3
- package/docs/RELEASE-READINESS.md +8 -1
- package/lib/runtime/scheduler/multi-agent-coordinator.d.ts +48 -0
- package/lib/runtime/scheduler/multi-agent-coordinator.js +274 -14
- package/lib/runtime/workspace/strategies/git-worktree.js +18 -9
- package/oxe/templates/REFERENCE-ANCHORS.template.md +12 -5
- package/oxe/templates/SPEC.template.md +10 -0
- package/oxe/templates/VISUAL-INPUTS.template.json +27 -0
- package/oxe/templates/VISUAL-INPUTS.template.md +36 -0
- package/oxe/workflows/execute.md +3 -0
- package/oxe/workflows/plan.md +13 -9
- package/oxe/workflows/references/workflow-runtime-contracts.json +44 -29
- package/oxe/workflows/spec.md +19 -8
- package/oxe/workflows/ui-spec.md +3 -2
- package/package.json +6 -3
- package/packages/runtime/package.json +1 -1
- package/packages/runtime/src/scheduler/multi-agent-coordinator.ts +379 -47
- 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,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:
|
|
110
|
-
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
|
|
216
|
-
|
|
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
|
|
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;
|
|
@@ -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.
|