agentxchain 0.8.8 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -142
- package/bin/agentxchain.js +186 -5
- package/dashboard/app.js +305 -0
- package/dashboard/components/blocked.js +145 -0
- package/dashboard/components/cross-repo.js +126 -0
- package/dashboard/components/gate.js +311 -0
- package/dashboard/components/hooks.js +177 -0
- package/dashboard/components/initiative.js +147 -0
- package/dashboard/components/ledger.js +165 -0
- package/dashboard/components/timeline.js +222 -0
- package/dashboard/index.html +352 -0
- package/package.json +14 -6
- package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
- package/scripts/publish-from-tag.sh +88 -0
- package/scripts/release-postflight.sh +231 -0
- package/scripts/release-preflight.sh +167 -0
- package/src/commands/accept-turn.js +160 -0
- package/src/commands/approve-completion.js +80 -0
- package/src/commands/approve-transition.js +85 -0
- package/src/commands/dashboard.js +70 -0
- package/src/commands/init.js +516 -0
- package/src/commands/migrate.js +348 -0
- package/src/commands/multi.js +549 -0
- package/src/commands/plugin.js +157 -0
- package/src/commands/reject-turn.js +204 -0
- package/src/commands/resume.js +389 -0
- package/src/commands/status.js +196 -3
- package/src/commands/step.js +947 -0
- package/src/commands/template-list.js +33 -0
- package/src/commands/template-set.js +279 -0
- package/src/commands/validate.js +20 -11
- package/src/commands/verify.js +71 -0
- package/src/lib/adapters/api-proxy-adapter.js +1076 -0
- package/src/lib/adapters/local-cli-adapter.js +337 -0
- package/src/lib/adapters/manual-adapter.js +169 -0
- package/src/lib/blocked-state.js +94 -0
- package/src/lib/config.js +97 -1
- package/src/lib/context-compressor.js +121 -0
- package/src/lib/context-section-parser.js +220 -0
- package/src/lib/coordinator-acceptance.js +428 -0
- package/src/lib/coordinator-config.js +461 -0
- package/src/lib/coordinator-dispatch.js +276 -0
- package/src/lib/coordinator-gates.js +487 -0
- package/src/lib/coordinator-hooks.js +239 -0
- package/src/lib/coordinator-recovery.js +523 -0
- package/src/lib/coordinator-state.js +365 -0
- package/src/lib/cross-repo-context.js +247 -0
- package/src/lib/dashboard/bridge-server.js +284 -0
- package/src/lib/dashboard/file-watcher.js +93 -0
- package/src/lib/dashboard/state-reader.js +96 -0
- package/src/lib/dispatch-bundle.js +568 -0
- package/src/lib/dispatch-manifest.js +252 -0
- package/src/lib/gate-evaluator.js +285 -0
- package/src/lib/governed-state.js +2139 -0
- package/src/lib/governed-templates.js +145 -0
- package/src/lib/hook-runner.js +788 -0
- package/src/lib/normalized-config.js +539 -0
- package/src/lib/plugin-config-schema.js +192 -0
- package/src/lib/plugins.js +692 -0
- package/src/lib/protocol-conformance.js +291 -0
- package/src/lib/reference-conformance-adapter.js +717 -0
- package/src/lib/repo-observer.js +597 -0
- package/src/lib/repo.js +0 -31
- package/src/lib/schema.js +121 -0
- package/src/lib/schemas/turn-result.schema.json +205 -0
- package/src/lib/token-budget.js +206 -0
- package/src/lib/token-counter.js +27 -0
- package/src/lib/turn-paths.js +67 -0
- package/src/lib/turn-result-validator.js +496 -0
- package/src/lib/validation.js +137 -0
- package/src/templates/governed/api-service.json +31 -0
- package/src/templates/governed/cli-tool.json +30 -0
- package/src/templates/governed/generic.json +10 -0
- package/src/templates/governed/web-app.json +30 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { appendFileSync, readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readBarriers, saveCoordinatorState } from './coordinator-state.js';
|
|
4
|
+
import { recordBarrierTransition } from './coordinator-acceptance.js';
|
|
5
|
+
import { safeWriteJson } from './safe-write.js';
|
|
6
|
+
|
|
7
|
+
function appendCoordinatorHistory(workspacePath, entry) {
|
|
8
|
+
appendFileSync(join(workspacePath, '.agentxchain/multirepo/history.jsonl'), JSON.stringify(entry) + '\n');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function loadRepoState(repoPath) {
|
|
12
|
+
const filePath = join(repoPath, '.agentxchain/state.json');
|
|
13
|
+
if (!existsSync(filePath)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getActiveTurnCount(repoState) {
|
|
25
|
+
if (!repoState?.active_turns || typeof repoState.active_turns !== 'object' || Array.isArray(repoState.active_turns)) {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Object.keys(repoState.active_turns).length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getPhaseOrder(config) {
|
|
33
|
+
const routingPhases = Object.keys(config.routing || {});
|
|
34
|
+
if (routingPhases.length > 0) {
|
|
35
|
+
return routingPhases;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const phases = [];
|
|
39
|
+
for (const workstreamId of config.workstream_order || []) {
|
|
40
|
+
const phase = config.workstreams?.[workstreamId]?.phase;
|
|
41
|
+
if (phase && !phases.includes(phase)) {
|
|
42
|
+
phases.push(phase);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return phases;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getNextPhase(currentPhase, config) {
|
|
49
|
+
const phases = getPhaseOrder(config);
|
|
50
|
+
const currentIndex = phases.indexOf(currentPhase);
|
|
51
|
+
if (currentIndex === -1 || currentIndex === phases.length - 1) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return phases[currentIndex + 1];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getRequiredReposForPhase(state, config) {
|
|
58
|
+
const required = new Set();
|
|
59
|
+
for (const workstreamId of config.workstream_order || []) {
|
|
60
|
+
const workstream = config.workstreams?.[workstreamId];
|
|
61
|
+
if (workstream?.phase !== state.phase) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
for (const repoId of workstream.repos || []) {
|
|
65
|
+
required.add(repoId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...required];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildRepoBlockers(config, repoIds) {
|
|
72
|
+
const blockers = [];
|
|
73
|
+
|
|
74
|
+
for (const repoId of repoIds) {
|
|
75
|
+
const repo = config.repos?.[repoId];
|
|
76
|
+
const repoState = repo?.resolved_path ? loadRepoState(repo.resolved_path) : null;
|
|
77
|
+
|
|
78
|
+
if (!repoState) {
|
|
79
|
+
blockers.push({
|
|
80
|
+
code: 'repo_state_missing',
|
|
81
|
+
repo_id: repoId,
|
|
82
|
+
message: `Repo "${repoId}" is missing .agentxchain/state.json`,
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (repoState.status === 'blocked') {
|
|
88
|
+
blockers.push({
|
|
89
|
+
code: 'repo_blocked',
|
|
90
|
+
repo_id: repoId,
|
|
91
|
+
message: `Repo "${repoId}" is blocked`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const activeTurnCount = getActiveTurnCount(repoState);
|
|
96
|
+
if (activeTurnCount > 0) {
|
|
97
|
+
blockers.push({
|
|
98
|
+
code: 'repo_active_turns',
|
|
99
|
+
repo_id: repoId,
|
|
100
|
+
active_turns: activeTurnCount,
|
|
101
|
+
message: `Repo "${repoId}" still has ${activeTurnCount} active turn(s)`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (repoState.pending_phase_transition || repoState.pending_run_completion) {
|
|
106
|
+
blockers.push({
|
|
107
|
+
code: 'repo_pending_gate',
|
|
108
|
+
repo_id: repoId,
|
|
109
|
+
message: `Repo "${repoId}" still has a pending repo-local gate`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return blockers;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildPhaseBarrierState(state, config, barriers) {
|
|
118
|
+
const workstreams = [];
|
|
119
|
+
const blockers = [];
|
|
120
|
+
const humanBarriers = [];
|
|
121
|
+
|
|
122
|
+
for (const workstreamId of config.workstream_order || []) {
|
|
123
|
+
const workstream = config.workstreams?.[workstreamId];
|
|
124
|
+
if (workstream?.phase !== state.phase) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const barrierId = `${workstreamId}_completion`;
|
|
129
|
+
const barrier = barriers[barrierId];
|
|
130
|
+
if (!barrier) {
|
|
131
|
+
blockers.push({
|
|
132
|
+
code: 'barrier_missing',
|
|
133
|
+
barrier_id: barrierId,
|
|
134
|
+
workstream_id: workstreamId,
|
|
135
|
+
message: `Barrier "${barrierId}" is missing`,
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
workstreams.push({
|
|
141
|
+
workstream_id: workstreamId,
|
|
142
|
+
barrier_id: barrierId,
|
|
143
|
+
barrier_type: barrier.type,
|
|
144
|
+
barrier_status: barrier.status,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (barrier.status === 'satisfied') {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (barrier.type === 'shared_human_gate') {
|
|
152
|
+
humanBarriers.push(barrierId);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
blockers.push({
|
|
157
|
+
code: 'barrier_unsatisfied',
|
|
158
|
+
barrier_id: barrierId,
|
|
159
|
+
workstream_id: workstreamId,
|
|
160
|
+
barrier_status: barrier.status,
|
|
161
|
+
message: `Barrier "${barrierId}" is "${barrier.status}"`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { workstreams, blockers, humanBarriers };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function applyHumanBarrierApprovals(workspacePath, state, barrierIds, gate) {
|
|
169
|
+
if (!Array.isArray(barrierIds) || barrierIds.length === 0) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const barriers = readBarriers(workspacePath);
|
|
174
|
+
let changed = false;
|
|
175
|
+
|
|
176
|
+
for (const barrierId of barrierIds) {
|
|
177
|
+
const barrier = barriers[barrierId];
|
|
178
|
+
if (!barrier || barrier.status === 'satisfied') {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const previousStatus = barrier.status;
|
|
183
|
+
barrier.status = 'satisfied';
|
|
184
|
+
barrier.approved_at = new Date().toISOString();
|
|
185
|
+
changed = true;
|
|
186
|
+
|
|
187
|
+
recordBarrierTransition(workspacePath, barrierId, previousStatus, 'satisfied', {
|
|
188
|
+
super_run_id: state.super_run_id,
|
|
189
|
+
gate_type: gate.gate_type,
|
|
190
|
+
gate: gate.gate,
|
|
191
|
+
approved_by: 'human',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (changed) {
|
|
196
|
+
safeWriteJson(join(workspacePath, '.agentxchain/multirepo/barriers.json'), barriers);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getInitiativeGate(config) {
|
|
201
|
+
if (config.gates?.initiative_ship) {
|
|
202
|
+
return {
|
|
203
|
+
gate_id: 'initiative_ship',
|
|
204
|
+
...config.gates.initiative_ship,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
gate_id: 'initiative_ship',
|
|
210
|
+
requires_human_approval: true,
|
|
211
|
+
requires_repos: config.repo_order.filter((repoId) => config.repos?.[repoId]?.required !== false),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function repoIsCompletionReady(repoState) {
|
|
216
|
+
return repoState?.status === 'completed' || Boolean(repoState?.pending_run_completion);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function evaluatePhaseGate(workspacePath, state, config, targetPhase) {
|
|
220
|
+
const nextPhase = getNextPhase(state.phase, config);
|
|
221
|
+
const resolvedTargetPhase = targetPhase ?? nextPhase;
|
|
222
|
+
const blockers = [];
|
|
223
|
+
|
|
224
|
+
if (!nextPhase) {
|
|
225
|
+
blockers.push({
|
|
226
|
+
code: 'no_next_phase',
|
|
227
|
+
message: `Current phase "${state.phase}" is already final`,
|
|
228
|
+
});
|
|
229
|
+
} else if (resolvedTargetPhase !== nextPhase) {
|
|
230
|
+
blockers.push({
|
|
231
|
+
code: 'phase_skip_forbidden',
|
|
232
|
+
message: `Requested phase "${resolvedTargetPhase}" is invalid; next phase is "${nextPhase}"`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const repoIds = getRequiredReposForPhase(state, config);
|
|
237
|
+
blockers.push(...buildRepoBlockers(config, repoIds));
|
|
238
|
+
|
|
239
|
+
const barriers = readBarriers(workspacePath);
|
|
240
|
+
const barrierState = buildPhaseBarrierState(state, config, barriers);
|
|
241
|
+
blockers.push(...barrierState.blockers);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
ready: blockers.length === 0,
|
|
245
|
+
current_phase: state.phase,
|
|
246
|
+
target_phase: resolvedTargetPhase,
|
|
247
|
+
gate_id: resolvedTargetPhase ? `phase_transition:${state.phase}->${resolvedTargetPhase}` : null,
|
|
248
|
+
required_repos: repoIds,
|
|
249
|
+
workstreams: barrierState.workstreams,
|
|
250
|
+
blockers,
|
|
251
|
+
human_barriers: barrierState.humanBarriers,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function requestPhaseTransition(workspacePath, state, config, targetPhase) {
|
|
256
|
+
if (state.status !== 'active') {
|
|
257
|
+
return { ok: false, error: `Cannot request phase transition: status is "${state.status}", expected "active"` };
|
|
258
|
+
}
|
|
259
|
+
if (state.pending_gate) {
|
|
260
|
+
return { ok: false, error: `Cannot request phase transition: pending gate "${state.pending_gate.gate}" already exists` };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const evaluation = evaluatePhaseGate(workspacePath, state, config, targetPhase);
|
|
264
|
+
if (!evaluation.ready) {
|
|
265
|
+
return { ok: false, error: 'Phase gate is not ready', evaluation };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const pendingGate = {
|
|
269
|
+
gate_type: 'phase_transition',
|
|
270
|
+
gate: evaluation.gate_id,
|
|
271
|
+
from: state.phase,
|
|
272
|
+
to: evaluation.target_phase,
|
|
273
|
+
required_repos: evaluation.required_repos,
|
|
274
|
+
human_barriers: evaluation.human_barriers,
|
|
275
|
+
requested_at: new Date().toISOString(),
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const updatedState = {
|
|
279
|
+
...state,
|
|
280
|
+
status: 'paused',
|
|
281
|
+
pending_gate: pendingGate,
|
|
282
|
+
};
|
|
283
|
+
saveCoordinatorState(workspacePath, updatedState);
|
|
284
|
+
|
|
285
|
+
appendCoordinatorHistory(workspacePath, {
|
|
286
|
+
type: 'phase_transition_requested',
|
|
287
|
+
timestamp: pendingGate.requested_at,
|
|
288
|
+
super_run_id: state.super_run_id,
|
|
289
|
+
gate: pendingGate.gate,
|
|
290
|
+
from: pendingGate.from,
|
|
291
|
+
to: pendingGate.to,
|
|
292
|
+
required_repos: pendingGate.required_repos,
|
|
293
|
+
human_barriers: pendingGate.human_barriers,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return { ok: true, state: updatedState, gate: pendingGate, evaluation };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function approveCoordinatorPhaseTransition(workspacePath, state, config) {
|
|
300
|
+
const pendingGate = state.pending_gate;
|
|
301
|
+
if (!pendingGate || pendingGate.gate_type !== 'phase_transition') {
|
|
302
|
+
return { ok: false, error: 'No pending phase transition to approve' };
|
|
303
|
+
}
|
|
304
|
+
if (!['paused', 'blocked'].includes(state.status)) {
|
|
305
|
+
return { ok: false, error: `Cannot approve phase transition: status is "${state.status}", expected "paused" or "blocked"` };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
applyHumanBarrierApprovals(workspacePath, state, pendingGate.human_barriers, pendingGate);
|
|
309
|
+
|
|
310
|
+
const updatedState = {
|
|
311
|
+
...state,
|
|
312
|
+
phase: pendingGate.to,
|
|
313
|
+
status: 'active',
|
|
314
|
+
pending_gate: null,
|
|
315
|
+
phase_gate_status: {
|
|
316
|
+
...(state.phase_gate_status || {}),
|
|
317
|
+
[pendingGate.gate]: 'passed',
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
saveCoordinatorState(workspacePath, updatedState);
|
|
321
|
+
|
|
322
|
+
appendCoordinatorHistory(workspacePath, {
|
|
323
|
+
type: 'phase_transition_approved',
|
|
324
|
+
timestamp: new Date().toISOString(),
|
|
325
|
+
super_run_id: state.super_run_id,
|
|
326
|
+
gate: pendingGate.gate,
|
|
327
|
+
from: pendingGate.from,
|
|
328
|
+
to: pendingGate.to,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return { ok: true, state: updatedState, transition: pendingGate };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function evaluateCompletionGate(workspacePath, state, config) {
|
|
335
|
+
const initiativeGate = getInitiativeGate(config);
|
|
336
|
+
const blockers = [];
|
|
337
|
+
const phaseOrder = getPhaseOrder(config);
|
|
338
|
+
|
|
339
|
+
if (phaseOrder.length > 0 && state.phase !== phaseOrder[phaseOrder.length - 1]) {
|
|
340
|
+
blockers.push({
|
|
341
|
+
code: 'not_final_phase',
|
|
342
|
+
message: `Current phase "${state.phase}" is not final`,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const barriers = readBarriers(workspacePath);
|
|
347
|
+
const humanBarriers = [];
|
|
348
|
+
for (const [barrierId, barrier] of Object.entries(barriers)) {
|
|
349
|
+
if (barrier.status === 'satisfied') {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (barrier.type === 'shared_human_gate') {
|
|
354
|
+
humanBarriers.push(barrierId);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
blockers.push({
|
|
359
|
+
code: 'barrier_unsatisfied',
|
|
360
|
+
barrier_id: barrierId,
|
|
361
|
+
barrier_status: barrier.status,
|
|
362
|
+
message: `Barrier "${barrierId}" is "${barrier.status}"`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const repoId of initiativeGate.requires_repos || []) {
|
|
367
|
+
const repo = config.repos?.[repoId];
|
|
368
|
+
const repoState = repo?.resolved_path ? loadRepoState(repo.resolved_path) : null;
|
|
369
|
+
|
|
370
|
+
if (!repoState) {
|
|
371
|
+
blockers.push({
|
|
372
|
+
code: 'repo_state_missing',
|
|
373
|
+
repo_id: repoId,
|
|
374
|
+
message: `Repo "${repoId}" is missing .agentxchain/state.json`,
|
|
375
|
+
});
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (repoState.status === 'blocked') {
|
|
380
|
+
blockers.push({
|
|
381
|
+
code: 'repo_blocked',
|
|
382
|
+
repo_id: repoId,
|
|
383
|
+
message: `Repo "${repoId}" is blocked`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const activeTurnCount = getActiveTurnCount(repoState);
|
|
388
|
+
if (activeTurnCount > 0) {
|
|
389
|
+
blockers.push({
|
|
390
|
+
code: 'repo_active_turns',
|
|
391
|
+
repo_id: repoId,
|
|
392
|
+
active_turns: activeTurnCount,
|
|
393
|
+
message: `Repo "${repoId}" still has ${activeTurnCount} active turn(s)`,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!repoIsCompletionReady(repoState)) {
|
|
398
|
+
blockers.push({
|
|
399
|
+
code: 'repo_not_completion_ready',
|
|
400
|
+
repo_id: repoId,
|
|
401
|
+
message: `Repo "${repoId}" is neither completed nor pending run completion approval`,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
ready: blockers.length === 0,
|
|
408
|
+
gate_id: initiativeGate.gate_id,
|
|
409
|
+
blockers,
|
|
410
|
+
required_repos: initiativeGate.requires_repos,
|
|
411
|
+
human_barriers: humanBarriers,
|
|
412
|
+
requires_human_approval: initiativeGate.requires_human_approval !== false,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function requestCoordinatorCompletion(workspacePath, state, config) {
|
|
417
|
+
if (state.status !== 'active') {
|
|
418
|
+
return { ok: false, error: `Cannot request initiative completion: status is "${state.status}", expected "active"` };
|
|
419
|
+
}
|
|
420
|
+
if (state.pending_gate) {
|
|
421
|
+
return { ok: false, error: `Cannot request initiative completion: pending gate "${state.pending_gate.gate}" already exists` };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const evaluation = evaluateCompletionGate(workspacePath, state, config);
|
|
425
|
+
if (!evaluation.ready) {
|
|
426
|
+
return { ok: false, error: 'Completion gate is not ready', evaluation };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const pendingGate = {
|
|
430
|
+
gate_type: 'run_completion',
|
|
431
|
+
gate: evaluation.gate_id,
|
|
432
|
+
required_repos: evaluation.required_repos,
|
|
433
|
+
human_barriers: evaluation.human_barriers,
|
|
434
|
+
requested_at: new Date().toISOString(),
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const updatedState = {
|
|
438
|
+
...state,
|
|
439
|
+
status: 'paused',
|
|
440
|
+
pending_gate: pendingGate,
|
|
441
|
+
};
|
|
442
|
+
saveCoordinatorState(workspacePath, updatedState);
|
|
443
|
+
|
|
444
|
+
appendCoordinatorHistory(workspacePath, {
|
|
445
|
+
type: 'run_completion_requested',
|
|
446
|
+
timestamp: pendingGate.requested_at,
|
|
447
|
+
super_run_id: state.super_run_id,
|
|
448
|
+
gate: pendingGate.gate,
|
|
449
|
+
required_repos: pendingGate.required_repos,
|
|
450
|
+
human_barriers: pendingGate.human_barriers,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return { ok: true, state: updatedState, gate: pendingGate, evaluation };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function approveCoordinatorCompletion(workspacePath, state, config) {
|
|
457
|
+
const pendingGate = state.pending_gate;
|
|
458
|
+
if (!pendingGate || pendingGate.gate_type !== 'run_completion') {
|
|
459
|
+
return { ok: false, error: 'No pending initiative completion to approve' };
|
|
460
|
+
}
|
|
461
|
+
if (!['paused', 'blocked'].includes(state.status)) {
|
|
462
|
+
return { ok: false, error: `Cannot approve initiative completion: status is "${state.status}", expected "paused" or "blocked"` };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
applyHumanBarrierApprovals(workspacePath, state, pendingGate.human_barriers, pendingGate);
|
|
466
|
+
|
|
467
|
+
const updatedState = {
|
|
468
|
+
...state,
|
|
469
|
+
status: 'completed',
|
|
470
|
+
completed_at: new Date().toISOString(),
|
|
471
|
+
pending_gate: null,
|
|
472
|
+
phase_gate_status: {
|
|
473
|
+
...(state.phase_gate_status || {}),
|
|
474
|
+
[pendingGate.gate]: 'passed',
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
saveCoordinatorState(workspacePath, updatedState);
|
|
478
|
+
|
|
479
|
+
appendCoordinatorHistory(workspacePath, {
|
|
480
|
+
type: 'run_completed',
|
|
481
|
+
timestamp: updatedState.completed_at,
|
|
482
|
+
super_run_id: state.super_run_id,
|
|
483
|
+
gate: pendingGate.gate,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
return { ok: true, state: updatedState, completion: pendingGate };
|
|
487
|
+
}
|