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