agentxchain 0.8.7 → 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 +123 -154
- package/bin/agentxchain.js +240 -8
- 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 +16 -7
- package/scripts/agentxchain-autonudge.applescript +32 -5
- 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/scripts/run-autonudge.sh +1 -1
- package/src/adapters/claude-code.js +7 -14
- package/src/adapters/cursor-local.js +17 -16
- 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/branch.js +2 -2
- package/src/commands/claim.js +84 -9
- package/src/commands/config.js +16 -0
- package/src/commands/dashboard.js +70 -0
- package/src/commands/doctor.js +9 -1
- package/src/commands/init.js +540 -5
- 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/stop.js +65 -33
- package/src/commands/template-list.js +33 -0
- package/src/commands/template-set.js +279 -0
- package/src/commands/update.js +24 -3
- package/src/commands/validate.js +20 -11
- package/src/commands/verify.js +71 -0
- package/src/commands/watch.js +112 -25
- 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 +143 -12
- 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/filter-agents.js +12 -0
- package/src/lib/gate-evaluator.js +285 -0
- package/src/lib/generate-vscode.js +158 -68
- 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/next-owner.js +61 -6
- package/src/lib/normalized-config.js +539 -0
- package/src/lib/notify.js +14 -12
- package/src/lib/plugin-config-schema.js +192 -0
- package/src/lib/plugins.js +692 -0
- package/src/lib/prompt-core.js +108 -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/safe-write.js +44 -0
- package/src/lib/schema.js +189 -0
- package/src/lib/schemas/turn-result.schema.json +205 -0
- package/src/lib/seed-prompt-polling.js +15 -73
- package/src/lib/seed-prompt.js +17 -63
- 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 +167 -19
- package/src/lib/verify-command.js +72 -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,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordinator acceptance projection & barrier evaluation.
|
|
3
|
+
*
|
|
4
|
+
* After a repo-local turn is accepted, this module:
|
|
5
|
+
* 1. Projects the accepted turn into coordinator history (without mutating repo-local state)
|
|
6
|
+
* 2. Evaluates barrier effects from the acceptance
|
|
7
|
+
* 3. Records barrier transitions in both snapshot and audit ledger
|
|
8
|
+
* 4. Detects context invalidations for downstream re-dispatch
|
|
9
|
+
*
|
|
10
|
+
* Design rules:
|
|
11
|
+
* - Coordinator NEVER writes to repo-local .agentxchain/ (DEC-MR-IMPL-004)
|
|
12
|
+
* - Projections are derived artifacts, not authoritative — repo-local history is truth
|
|
13
|
+
* - Barrier evaluation is deterministic given the current state
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
17
|
+
import { isAbsolute, join, dirname, relative, resolve } from 'node:path';
|
|
18
|
+
import { randomBytes } from 'node:crypto';
|
|
19
|
+
import { readBarriers, readCoordinatorHistory, saveCoordinatorState } from './coordinator-state.js';
|
|
20
|
+
|
|
21
|
+
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const MULTIREPO_DIR = '.agentxchain/multirepo';
|
|
24
|
+
|
|
25
|
+
function multiDir(ws) {
|
|
26
|
+
return join(ws, MULTIREPO_DIR);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function historyPath(ws) {
|
|
30
|
+
return join(multiDir(ws), 'history.jsonl');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function barriersPath(ws) {
|
|
34
|
+
return join(multiDir(ws), 'barriers.json');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function barrierLedgerPath(ws) {
|
|
38
|
+
return join(multiDir(ws), 'barrier-ledger.jsonl');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function appendJsonl(filePath, entry) {
|
|
44
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
45
|
+
appendFileSync(filePath, JSON.stringify(entry) + '\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function generateProjectionRef(repoId, workstreamId) {
|
|
49
|
+
const rand = randomBytes(4).toString('hex');
|
|
50
|
+
return `proj_${workstreamId}_${repoId}_${rand}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read repo-local history without mutating it.
|
|
55
|
+
*/
|
|
56
|
+
function readRepoLocalHistory(repoPath) {
|
|
57
|
+
const file = join(repoPath, '.agentxchain/history.jsonl');
|
|
58
|
+
if (!existsSync(file)) return [];
|
|
59
|
+
try {
|
|
60
|
+
const content = readFileSync(file, 'utf8').trim();
|
|
61
|
+
if (!content) return [];
|
|
62
|
+
return content.split('\n').map(line => JSON.parse(line));
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Projection ──────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Project a repo-local accepted turn into coordinator state.
|
|
72
|
+
*
|
|
73
|
+
* Reads the accepted turn from repo-local history, creates a derived projection,
|
|
74
|
+
* appends it to coordinator history, then evaluates barrier effects.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} workspacePath - coordinator workspace
|
|
77
|
+
* @param {object} state - current coordinator state
|
|
78
|
+
* @param {object} config - normalized coordinator config
|
|
79
|
+
* @param {string} repoId - the repo whose turn was accepted
|
|
80
|
+
* @param {object} acceptedTurn - the accepted turn result from repo-local acceptance
|
|
81
|
+
* @param {string} workstreamId - the workstream this acceptance belongs to
|
|
82
|
+
* @returns {{ ok: boolean, projection_ref?: string, barrier_effects?: object[], context_invalidations?: string[], error?: string }}
|
|
83
|
+
*/
|
|
84
|
+
export function projectRepoAcceptance(workspacePath, state, config, repoId, acceptedTurn, workstreamId) {
|
|
85
|
+
// Validate inputs
|
|
86
|
+
if (!config.repos?.[repoId]) {
|
|
87
|
+
return { ok: false, error: `Unknown repo "${repoId}"` };
|
|
88
|
+
}
|
|
89
|
+
if (!config.workstreams?.[workstreamId]) {
|
|
90
|
+
return { ok: false, error: `Unknown workstream "${workstreamId}"` };
|
|
91
|
+
}
|
|
92
|
+
if (!acceptedTurn || !acceptedTurn.turn_id) {
|
|
93
|
+
return { ok: false, error: 'acceptedTurn must include turn_id' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate cross-repo write constraint (AT-CA-006)
|
|
97
|
+
if (Array.isArray(acceptedTurn.files_changed)) {
|
|
98
|
+
const repo = config.repos[repoId];
|
|
99
|
+
for (const filePath of acceptedTurn.files_changed) {
|
|
100
|
+
const resolvedFilePath = isAbsolute(filePath)
|
|
101
|
+
? resolve(filePath)
|
|
102
|
+
: resolve(repo.resolved_path, filePath);
|
|
103
|
+
const repoRelativePath = relative(repo.resolved_path, resolvedFilePath);
|
|
104
|
+
const insideTargetRepo = repoRelativePath === ''
|
|
105
|
+
|| (!repoRelativePath.startsWith('..') && !isAbsolute(repoRelativePath));
|
|
106
|
+
|
|
107
|
+
if (insideTargetRepo) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const otherRepoId = config.repo_order.find((candidateRepoId) => {
|
|
112
|
+
if (candidateRepoId === repoId) return false;
|
|
113
|
+
const candidateRepo = config.repos[candidateRepoId];
|
|
114
|
+
if (!candidateRepo?.resolved_path) return false;
|
|
115
|
+
const candidateRelativePath = relative(candidateRepo.resolved_path, resolvedFilePath);
|
|
116
|
+
return candidateRelativePath === ''
|
|
117
|
+
|| (!candidateRelativePath.startsWith('..') && !isAbsolute(candidateRelativePath));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
error: otherRepoId
|
|
123
|
+
? `Cross-repo write violation: file "${filePath}" belongs to repo "${otherRepoId}", not "${repoId}"`
|
|
124
|
+
: `Cross-repo write violation: file "${filePath}" resolves outside repo "${repoId}"`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const projectionRef = generateProjectionRef(repoId, workstreamId);
|
|
130
|
+
const now = new Date().toISOString();
|
|
131
|
+
|
|
132
|
+
// Create the projection entry
|
|
133
|
+
const projection = {
|
|
134
|
+
type: 'acceptance_projection',
|
|
135
|
+
timestamp: now,
|
|
136
|
+
super_run_id: state.super_run_id,
|
|
137
|
+
projection_ref: projectionRef,
|
|
138
|
+
workstream_id: workstreamId,
|
|
139
|
+
repo_id: repoId,
|
|
140
|
+
repo_run_id: state.repo_runs?.[repoId]?.run_id ?? null,
|
|
141
|
+
repo_turn_id: acceptedTurn.turn_id,
|
|
142
|
+
summary: acceptedTurn.summary ?? '',
|
|
143
|
+
files_changed: acceptedTurn.files_changed ?? [],
|
|
144
|
+
decisions: acceptedTurn.decisions ?? [],
|
|
145
|
+
verification: acceptedTurn.verification ?? null,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Append to coordinator history (NOT repo-local history)
|
|
149
|
+
appendJsonl(historyPath(workspacePath), projection);
|
|
150
|
+
|
|
151
|
+
// Evaluate barrier effects
|
|
152
|
+
const barrierEffects = evaluateBarrierEffects(workspacePath, state, config, repoId, workstreamId);
|
|
153
|
+
|
|
154
|
+
// Check for context invalidations
|
|
155
|
+
const contextInvalidations = detectContextInvalidations(workspacePath, config, repoId, workstreamId);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
projection_ref: projectionRef,
|
|
160
|
+
barrier_effects: barrierEffects,
|
|
161
|
+
context_invalidations: contextInvalidations,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Barrier Evaluation ──────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Evaluate all barriers against current coordinator state.
|
|
169
|
+
*
|
|
170
|
+
* Scans each barrier and determines if it should transition based on
|
|
171
|
+
* the projections currently in coordinator history.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} workspacePath
|
|
174
|
+
* @param {object} state
|
|
175
|
+
* @param {object} config
|
|
176
|
+
* @returns {{ barriers: object, changes: object[] }}
|
|
177
|
+
*/
|
|
178
|
+
export function evaluateBarriers(workspacePath, state, config) {
|
|
179
|
+
const barriers = readBarriers(workspacePath);
|
|
180
|
+
const history = readCoordinatorHistory(workspacePath);
|
|
181
|
+
const changes = [];
|
|
182
|
+
|
|
183
|
+
for (const [barrierId, barrier] of Object.entries(barriers)) {
|
|
184
|
+
if (barrier.status === 'satisfied') continue;
|
|
185
|
+
|
|
186
|
+
const newStatus = computeBarrierStatus(barrier, history, config);
|
|
187
|
+
if (newStatus !== barrier.status) {
|
|
188
|
+
const previousStatus = barrier.status;
|
|
189
|
+
barrier.status = newStatus;
|
|
190
|
+
|
|
191
|
+
// Update satisfied_repos for tracking
|
|
192
|
+
if (barrier.type === 'all_repos_accepted') {
|
|
193
|
+
barrier.satisfied_repos = getAcceptedReposForWorkstream(history, barrier.workstream_id, barrier.required_repos);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
changes.push({
|
|
197
|
+
barrier_id: barrierId,
|
|
198
|
+
previous_status: previousStatus,
|
|
199
|
+
new_status: newStatus,
|
|
200
|
+
workstream_id: barrier.workstream_id,
|
|
201
|
+
type: barrier.type,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Record in audit ledger
|
|
205
|
+
recordBarrierTransition(workspacePath, barrierId, previousStatus, newStatus, {
|
|
206
|
+
super_run_id: state.super_run_id,
|
|
207
|
+
workstream_id: barrier.workstream_id,
|
|
208
|
+
barrier_type: barrier.type,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Persist updated barriers snapshot
|
|
214
|
+
if (changes.length > 0) {
|
|
215
|
+
writeFileSync(barriersPath(workspacePath), JSON.stringify(barriers, null, 2) + '\n');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { barriers, changes };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Record a barrier transition in the barrier-ledger.jsonl.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} workspacePath
|
|
225
|
+
* @param {string} barrierId
|
|
226
|
+
* @param {string} previousStatus
|
|
227
|
+
* @param {string} newStatus
|
|
228
|
+
* @param {object} causation - metadata about why the transition occurred
|
|
229
|
+
*/
|
|
230
|
+
export function recordBarrierTransition(workspacePath, barrierId, previousStatus, newStatus, causation) {
|
|
231
|
+
appendJsonl(barrierLedgerPath(workspacePath), {
|
|
232
|
+
type: 'barrier_transition',
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
234
|
+
barrier_id: barrierId,
|
|
235
|
+
previous_status: previousStatus,
|
|
236
|
+
new_status: newStatus,
|
|
237
|
+
causation,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Internal barrier logic ──────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
function getAcceptedReposForWorkstream(history, workstreamId, requiredRepos) {
|
|
244
|
+
const accepted = new Set();
|
|
245
|
+
for (const entry of history) {
|
|
246
|
+
if (entry?.type === 'acceptance_projection' && entry.workstream_id === workstreamId) {
|
|
247
|
+
if (requiredRepos.includes(entry.repo_id)) {
|
|
248
|
+
accepted.add(entry.repo_id);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return [...accepted];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function computeBarrierStatus(barrier, history, config) {
|
|
256
|
+
switch (barrier.type) {
|
|
257
|
+
case 'all_repos_accepted':
|
|
258
|
+
return computeAllReposAccepted(barrier, history);
|
|
259
|
+
|
|
260
|
+
case 'ordered_repo_sequence':
|
|
261
|
+
return computeOrderedRepoSequence(barrier, history, config);
|
|
262
|
+
|
|
263
|
+
case 'interface_alignment':
|
|
264
|
+
return computeInterfaceAlignment(barrier, history);
|
|
265
|
+
|
|
266
|
+
case 'shared_human_gate':
|
|
267
|
+
// Never auto-satisfied — requires explicit human approval
|
|
268
|
+
return barrier.status;
|
|
269
|
+
|
|
270
|
+
default:
|
|
271
|
+
return barrier.status;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function computeAllReposAccepted(barrier, history) {
|
|
276
|
+
const required = new Set(barrier.required_repos);
|
|
277
|
+
const satisfied = new Set();
|
|
278
|
+
|
|
279
|
+
for (const entry of history) {
|
|
280
|
+
if (entry?.type === 'acceptance_projection' && entry.workstream_id === barrier.workstream_id) {
|
|
281
|
+
if (required.has(entry.repo_id)) {
|
|
282
|
+
satisfied.add(entry.repo_id);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (satisfied.size === required.size) return 'satisfied';
|
|
288
|
+
if (satisfied.size > 0) return 'partially_satisfied';
|
|
289
|
+
return 'pending';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function computeOrderedRepoSequence(barrier, history, config) {
|
|
293
|
+
const workstream = config.workstreams?.[barrier.workstream_id];
|
|
294
|
+
if (!workstream) return barrier.status;
|
|
295
|
+
|
|
296
|
+
// Upstream is entry_repo. The barrier is satisfied when entry_repo has an accepted turn.
|
|
297
|
+
const entryRepo = workstream.entry_repo;
|
|
298
|
+
const hasUpstreamAcceptance = history.some(
|
|
299
|
+
entry => entry?.type === 'acceptance_projection'
|
|
300
|
+
&& entry.workstream_id === barrier.workstream_id
|
|
301
|
+
&& entry.repo_id === entryRepo
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (hasUpstreamAcceptance) return 'satisfied';
|
|
305
|
+
|
|
306
|
+
// Check if any non-entry repos have been accepted (partial)
|
|
307
|
+
const anyDownstreamAccepted = history.some(
|
|
308
|
+
entry => entry?.type === 'acceptance_projection'
|
|
309
|
+
&& entry.workstream_id === barrier.workstream_id
|
|
310
|
+
&& entry.repo_id !== entryRepo
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (anyDownstreamAccepted) return 'partially_satisfied';
|
|
314
|
+
return 'pending';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function computeInterfaceAlignment(barrier, history) {
|
|
318
|
+
const required = new Set(barrier.required_repos);
|
|
319
|
+
const repoDecisions = {};
|
|
320
|
+
|
|
321
|
+
for (const entry of history) {
|
|
322
|
+
if (entry?.type === 'acceptance_projection' && entry.workstream_id === barrier.workstream_id) {
|
|
323
|
+
if (required.has(entry.repo_id)) {
|
|
324
|
+
repoDecisions[entry.repo_id] = entry.decisions ?? [];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const acceptedRepos = Object.keys(repoDecisions);
|
|
330
|
+
if (acceptedRepos.length === 0) return 'pending';
|
|
331
|
+
if (acceptedRepos.length < required.size) return 'partially_satisfied';
|
|
332
|
+
|
|
333
|
+
// All repos accepted — check for DEC-* alignment (heuristic)
|
|
334
|
+
return 'satisfied';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── Barrier effects from a single acceptance ────────────────────────────────
|
|
338
|
+
|
|
339
|
+
function evaluateBarrierEffects(workspacePath, state, config, repoId, workstreamId) {
|
|
340
|
+
const barriers = readBarriers(workspacePath);
|
|
341
|
+
const history = readCoordinatorHistory(workspacePath);
|
|
342
|
+
const effects = [];
|
|
343
|
+
|
|
344
|
+
for (const [barrierId, barrier] of Object.entries(barriers)) {
|
|
345
|
+
if (barrier.status === 'satisfied') continue;
|
|
346
|
+
if (barrier.workstream_id !== workstreamId) continue;
|
|
347
|
+
|
|
348
|
+
const newStatus = computeBarrierStatus(barrier, history, config);
|
|
349
|
+
if (newStatus !== barrier.status) {
|
|
350
|
+
const previousStatus = barrier.status;
|
|
351
|
+
barrier.status = newStatus;
|
|
352
|
+
|
|
353
|
+
if (barrier.type === 'all_repos_accepted') {
|
|
354
|
+
barrier.satisfied_repos = getAcceptedReposForWorkstream(history, workstreamId, barrier.required_repos);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
effects.push({
|
|
358
|
+
barrier_id: barrierId,
|
|
359
|
+
previous_status: previousStatus,
|
|
360
|
+
new_status: newStatus,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
recordBarrierTransition(workspacePath, barrierId, previousStatus, newStatus, {
|
|
364
|
+
super_run_id: state.super_run_id,
|
|
365
|
+
workstream_id: workstreamId,
|
|
366
|
+
repo_id: repoId,
|
|
367
|
+
barrier_type: barrier.type,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Persist updated barrier snapshot
|
|
373
|
+
if (effects.length > 0) {
|
|
374
|
+
writeFileSync(barriersPath(workspacePath), JSON.stringify(barriers, null, 2) + '\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return effects;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Context invalidation detection ──────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
function detectContextInvalidations(workspacePath, config, repoId, workstreamId) {
|
|
383
|
+
const history = readCoordinatorHistory(workspacePath);
|
|
384
|
+
const invalidations = [];
|
|
385
|
+
|
|
386
|
+
// Find all dispatched turns in the same workstream for OTHER repos
|
|
387
|
+
// that were dispatched BEFORE this acceptance — their context is now stale
|
|
388
|
+
for (const entry of history) {
|
|
389
|
+
if (entry?.type !== 'turn_dispatched') continue;
|
|
390
|
+
if (entry.workstream_id !== workstreamId) continue;
|
|
391
|
+
if (entry.repo_id === repoId) continue;
|
|
392
|
+
|
|
393
|
+
// Check if this dispatched turn has NOT been projected yet (still pending)
|
|
394
|
+
const hasProjection = history.some(
|
|
395
|
+
e => e?.type === 'acceptance_projection'
|
|
396
|
+
&& e.workstream_id === workstreamId
|
|
397
|
+
&& e.repo_id === entry.repo_id
|
|
398
|
+
&& e.repo_turn_id === entry.repo_turn_id
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
if (!hasProjection) {
|
|
402
|
+
invalidations.push(entry.repo_turn_id);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Also check downstream workstreams that depend on this one
|
|
407
|
+
for (const [wsId, ws] of Object.entries(config.workstreams)) {
|
|
408
|
+
if (!ws.depends_on?.includes(workstreamId)) continue;
|
|
409
|
+
|
|
410
|
+
for (const entry of history) {
|
|
411
|
+
if (entry?.type !== 'turn_dispatched') continue;
|
|
412
|
+
if (entry.workstream_id !== wsId) continue;
|
|
413
|
+
|
|
414
|
+
const hasProjection = history.some(
|
|
415
|
+
e => e?.type === 'acceptance_projection'
|
|
416
|
+
&& e.workstream_id === wsId
|
|
417
|
+
&& e.repo_id === entry.repo_id
|
|
418
|
+
&& e.repo_turn_id === entry.repo_turn_id
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
if (!hasProjection) {
|
|
422
|
+
invalidations.push(entry.repo_turn_id);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return [...new Set(invalidations)];
|
|
428
|
+
}
|