agentxchain 2.14.0 → 2.15.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.
@@ -17,6 +17,11 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } fr
17
17
  import { isAbsolute, join, dirname, relative, resolve } from 'node:path';
18
18
  import { randomBytes } from 'node:crypto';
19
19
  import { readBarriers, readCoordinatorHistory, saveCoordinatorState } from './coordinator-state.js';
20
+ import {
21
+ computeBarrierStatus as computeCoordinatorBarrierStatus,
22
+ getAcceptedReposForWorkstream,
23
+ getAlignedReposForBarrier,
24
+ } from './coordinator-barriers.js';
20
25
 
21
26
  // ── Paths ───────────────────────────────────────────────────────────────────
22
27
 
@@ -238,100 +243,8 @@ export function recordBarrierTransition(workspacePath, barrierId, previousStatus
238
243
  });
239
244
  }
240
245
 
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
246
  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';
247
+ return computeCoordinatorBarrierStatus(barrier, history, config);
335
248
  }
336
249
 
337
250
  // ── Barrier effects from a single acceptance ────────────────────────────────
@@ -340,19 +253,32 @@ function evaluateBarrierEffects(workspacePath, state, config, repoId, workstream
340
253
  const barriers = readBarriers(workspacePath);
341
254
  const history = readCoordinatorHistory(workspacePath);
342
255
  const effects = [];
256
+ let snapshotChanged = false;
343
257
 
344
258
  for (const [barrierId, barrier] of Object.entries(barriers)) {
345
259
  if (barrier.status === 'satisfied') continue;
346
260
  if (barrier.workstream_id !== workstreamId) continue;
347
261
 
348
262
  const newStatus = computeBarrierStatus(barrier, history, config);
263
+ if (barrier.type === 'all_repos_accepted') {
264
+ const satisfiedRepos = getAcceptedReposForWorkstream(history, workstreamId, barrier.required_repos);
265
+ if (JSON.stringify(barrier.satisfied_repos || []) !== JSON.stringify(satisfiedRepos)) {
266
+ barrier.satisfied_repos = satisfiedRepos;
267
+ snapshotChanged = true;
268
+ }
269
+ }
270
+ if (barrier.type === 'interface_alignment') {
271
+ const satisfiedRepos = getAlignedReposForBarrier(barrier, history);
272
+ if (JSON.stringify(barrier.satisfied_repos || []) !== JSON.stringify(satisfiedRepos)) {
273
+ barrier.satisfied_repos = satisfiedRepos;
274
+ snapshotChanged = true;
275
+ }
276
+ }
277
+
349
278
  if (newStatus !== barrier.status) {
350
279
  const previousStatus = barrier.status;
351
280
  barrier.status = newStatus;
352
-
353
- if (barrier.type === 'all_repos_accepted') {
354
- barrier.satisfied_repos = getAcceptedReposForWorkstream(history, workstreamId, barrier.required_repos);
355
- }
281
+ snapshotChanged = true;
356
282
 
357
283
  effects.push({
358
284
  barrier_id: barrierId,
@@ -370,7 +296,7 @@ function evaluateBarrierEffects(workspacePath, state, config, repoId, workstream
370
296
  }
371
297
 
372
298
  // Persist updated barrier snapshot
373
- if (effects.length > 0) {
299
+ if (snapshotChanged) {
374
300
  writeFileSync(barriersPath(workspacePath), JSON.stringify(barriers, null, 2) + '\n');
375
301
  }
376
302
 
@@ -0,0 +1,116 @@
1
+ function isProjectionEntry(entry, workstreamId) {
2
+ return entry?.type === 'acceptance_projection' && entry.workstream_id === workstreamId;
3
+ }
4
+
5
+ function normalizeDecisionId(decision) {
6
+ if (typeof decision === 'string') return decision;
7
+ if (decision && typeof decision === 'object' && typeof decision.id === 'string') return decision.id;
8
+ return null;
9
+ }
10
+
11
+ function collectAcceptedDecisionIds(history, workstreamId, requiredRepos = []) {
12
+ const required = new Set(requiredRepos);
13
+ const repoDecisionIds = {};
14
+ const acceptedRepos = new Set();
15
+
16
+ for (const entry of history) {
17
+ if (!isProjectionEntry(entry, workstreamId)) continue;
18
+ if (required.size > 0 && !required.has(entry.repo_id)) continue;
19
+
20
+ acceptedRepos.add(entry.repo_id);
21
+ if (!repoDecisionIds[entry.repo_id]) {
22
+ repoDecisionIds[entry.repo_id] = new Set();
23
+ }
24
+
25
+ for (const decision of Array.isArray(entry.decisions) ? entry.decisions : []) {
26
+ const id = normalizeDecisionId(decision);
27
+ if (id) {
28
+ repoDecisionIds[entry.repo_id].add(id);
29
+ }
30
+ }
31
+ }
32
+
33
+ return { acceptedRepos, repoDecisionIds };
34
+ }
35
+
36
+ export function getAcceptedReposForWorkstream(history, workstreamId, requiredRepos = []) {
37
+ return [
38
+ ...collectAcceptedDecisionIds(history, workstreamId, requiredRepos).acceptedRepos,
39
+ ];
40
+ }
41
+
42
+ export function getAlignedReposForBarrier(barrier, history) {
43
+ const requiredRepos = Array.isArray(barrier.required_repos) ? barrier.required_repos : [];
44
+ const alignmentDecisionIds = barrier.alignment_decision_ids || {};
45
+ const { repoDecisionIds } = collectAcceptedDecisionIds(history, barrier.workstream_id, requiredRepos);
46
+ const alignedRepos = [];
47
+
48
+ for (const repoId of requiredRepos) {
49
+ const requiredIds = Array.isArray(alignmentDecisionIds[repoId]) ? alignmentDecisionIds[repoId] : [];
50
+ if (requiredIds.length === 0) continue;
51
+
52
+ const acceptedIds = repoDecisionIds[repoId] || new Set();
53
+ if (requiredIds.every((decisionId) => acceptedIds.has(decisionId))) {
54
+ alignedRepos.push(repoId);
55
+ }
56
+ }
57
+
58
+ return alignedRepos;
59
+ }
60
+
61
+ export function computeAllReposAcceptedStatus(barrier, history) {
62
+ const requiredRepos = Array.isArray(barrier.required_repos) ? barrier.required_repos : [];
63
+ const acceptedRepos = getAcceptedReposForWorkstream(history, barrier.workstream_id, requiredRepos);
64
+
65
+ if (acceptedRepos.length === requiredRepos.length && requiredRepos.length > 0) return 'satisfied';
66
+ if (acceptedRepos.length > 0) return 'partially_satisfied';
67
+ return 'pending';
68
+ }
69
+
70
+ export function computeOrderedRepoSequenceStatus(barrier, history, config) {
71
+ const workstream = config.workstreams?.[barrier.workstream_id];
72
+ if (!workstream) return barrier.status;
73
+
74
+ const entryRepo = workstream.entry_repo;
75
+ const hasUpstreamAcceptance = history.some(
76
+ (entry) => isProjectionEntry(entry, barrier.workstream_id) && entry.repo_id === entryRepo,
77
+ );
78
+
79
+ if (hasUpstreamAcceptance) return 'satisfied';
80
+
81
+ const anyDownstreamAccepted = history.some(
82
+ (entry) => isProjectionEntry(entry, barrier.workstream_id) && entry.repo_id !== entryRepo,
83
+ );
84
+
85
+ if (anyDownstreamAccepted) return 'partially_satisfied';
86
+ return 'pending';
87
+ }
88
+
89
+ export function computeInterfaceAlignmentStatus(barrier, history) {
90
+ const requiredRepos = Array.isArray(barrier.required_repos) ? barrier.required_repos : [];
91
+ const alignedRepos = getAlignedReposForBarrier(barrier, history);
92
+ const acceptedRepos = getAcceptedReposForWorkstream(history, barrier.workstream_id, requiredRepos);
93
+
94
+ if (alignedRepos.length === requiredRepos.length && requiredRepos.length > 0) return 'satisfied';
95
+ if (acceptedRepos.length > 0) return 'partially_satisfied';
96
+ return 'pending';
97
+ }
98
+
99
+ export function computeBarrierStatus(barrier, history, config) {
100
+ switch (barrier.type) {
101
+ case 'all_repos_accepted':
102
+ return computeAllReposAcceptedStatus(barrier, history);
103
+
104
+ case 'ordered_repo_sequence':
105
+ return computeOrderedRepoSequenceStatus(barrier, history, config);
106
+
107
+ case 'interface_alignment':
108
+ return computeInterfaceAlignmentStatus(barrier, history);
109
+
110
+ case 'shared_human_gate':
111
+ return barrier.status;
112
+
113
+ default:
114
+ return barrier.status;
115
+ }
116
+ }
@@ -146,12 +146,95 @@ function validateWorkstreams(raw, repoIds, errors) {
146
146
  `workstream "${workstreamId}" completion_barrier must be one of: ${Array.from(VALID_BARRIER_TYPES).join(', ')}`,
147
147
  );
148
148
  }
149
+
150
+ validateInterfaceAlignment(workstreamId, workstream, errors);
149
151
  }
150
152
 
151
153
  detectWorkstreamCycles(raw.workstreams, errors);
152
154
  return workstreamIds;
153
155
  }
154
156
 
157
+ function validateInterfaceAlignment(workstreamId, workstream, errors) {
158
+ if (workstream.completion_barrier !== 'interface_alignment') {
159
+ return;
160
+ }
161
+
162
+ const alignment = workstream.interface_alignment;
163
+ if (!alignment || typeof alignment !== 'object' || Array.isArray(alignment)) {
164
+ pushError(
165
+ errors,
166
+ 'workstream_interface_alignment_invalid',
167
+ `workstream "${workstreamId}" with completion_barrier "interface_alignment" must declare interface_alignment.decision_ids_by_repo`,
168
+ );
169
+ return;
170
+ }
171
+
172
+ const byRepo = alignment.decision_ids_by_repo;
173
+ if (!byRepo || typeof byRepo !== 'object' || Array.isArray(byRepo)) {
174
+ pushError(
175
+ errors,
176
+ 'workstream_interface_alignment_decisions_invalid',
177
+ `workstream "${workstreamId}" interface_alignment.decision_ids_by_repo must be an object`,
178
+ );
179
+ return;
180
+ }
181
+
182
+ const repoIds = Array.isArray(workstream.repos) ? workstream.repos : [];
183
+ const repoIdSet = new Set(repoIds);
184
+
185
+ for (const repoId of repoIds) {
186
+ if (!(repoId in byRepo)) {
187
+ pushError(
188
+ errors,
189
+ 'workstream_interface_alignment_repo_missing',
190
+ `workstream "${workstreamId}" must declare interface_alignment.decision_ids_by_repo["${repoId}"]`,
191
+ );
192
+ continue;
193
+ }
194
+
195
+ const decisionIds = byRepo[repoId];
196
+ if (!Array.isArray(decisionIds) || decisionIds.length === 0) {
197
+ pushError(
198
+ errors,
199
+ 'workstream_interface_alignment_repo_invalid',
200
+ `workstream "${workstreamId}" interface_alignment.decision_ids_by_repo["${repoId}"] must be a non-empty array`,
201
+ );
202
+ continue;
203
+ }
204
+
205
+ const seen = new Set();
206
+ for (const decisionId of decisionIds) {
207
+ if (typeof decisionId !== 'string' || !/^DEC-\d+$/.test(decisionId)) {
208
+ pushError(
209
+ errors,
210
+ 'workstream_interface_alignment_decision_invalid',
211
+ `workstream "${workstreamId}" interface_alignment decision "${decisionId}" for repo "${repoId}" must match DEC-NNN`,
212
+ );
213
+ continue;
214
+ }
215
+ if (seen.has(decisionId)) {
216
+ pushError(
217
+ errors,
218
+ 'workstream_interface_alignment_decision_duplicate',
219
+ `workstream "${workstreamId}" interface_alignment decision "${decisionId}" is duplicated for repo "${repoId}"`,
220
+ );
221
+ continue;
222
+ }
223
+ seen.add(decisionId);
224
+ }
225
+ }
226
+
227
+ for (const repoId of Object.keys(byRepo)) {
228
+ if (!repoIdSet.has(repoId)) {
229
+ pushError(
230
+ errors,
231
+ 'workstream_interface_alignment_repo_unknown',
232
+ `workstream "${workstreamId}" interface_alignment references undeclared repo "${repoId}"`,
233
+ );
234
+ }
235
+ }
236
+ }
237
+
155
238
  function detectWorkstreamCycles(workstreams, errors) {
156
239
  const visiting = new Set();
157
240
  const visited = new Set();
@@ -344,6 +427,16 @@ export function normalizeCoordinatorConfig(raw) {
344
427
  entry_repo: workstream.entry_repo,
345
428
  depends_on: Array.isArray(workstream.depends_on) ? [...new Set(workstream.depends_on)] : [],
346
429
  completion_barrier: workstream.completion_barrier,
430
+ interface_alignment: workstream.interface_alignment?.decision_ids_by_repo
431
+ ? {
432
+ decision_ids_by_repo: Object.fromEntries(
433
+ Object.entries(workstream.interface_alignment.decision_ids_by_repo).map(([repoId, decisionIds]) => [
434
+ repoId,
435
+ Array.isArray(decisionIds) ? [...new Set(decisionIds)] : [],
436
+ ]),
437
+ ),
438
+ }
439
+ : null,
347
440
  },
348
441
  ]),
349
442
  ),
@@ -21,6 +21,11 @@ import {
21
21
  readBarriers,
22
22
  } from './coordinator-state.js';
23
23
  import { safeWriteJson } from './safe-write.js';
24
+ import {
25
+ computeBarrierStatus as computeCoordinatorBarrierStatus,
26
+ getAcceptedReposForWorkstream,
27
+ getAlignedReposForBarrier,
28
+ } from './coordinator-barriers.js';
24
29
 
25
30
  // ── Paths ───────────────────────────────────────────────────────────────────
26
31
 
@@ -335,6 +340,23 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
335
340
  if (barrier.type === 'shared_human_gate') continue; // Never auto-transition
336
341
 
337
342
  const newStatus = recomputeBarrierStatus(barrier, fullHistory, config);
343
+ if (barrier.type === 'all_repos_accepted') {
344
+ const satisfiedRepos = getAcceptedReposForWorkstream(
345
+ fullHistory, barrier.workstream_id, barrier.required_repos
346
+ );
347
+ if (JSON.stringify(barrier.satisfied_repos || []) !== JSON.stringify(satisfiedRepos)) {
348
+ barrier.satisfied_repos = satisfiedRepos;
349
+ barriersChanged = true;
350
+ }
351
+ }
352
+ if (barrier.type === 'interface_alignment') {
353
+ const satisfiedRepos = getAlignedReposForBarrier(barrier, fullHistory);
354
+ if (JSON.stringify(barrier.satisfied_repos || []) !== JSON.stringify(satisfiedRepos)) {
355
+ barrier.satisfied_repos = satisfiedRepos;
356
+ barriersChanged = true;
357
+ }
358
+ }
359
+
338
360
  if (newStatus !== barrier.status) {
339
361
  const previousStatus = barrier.status;
340
362
  barrierChanges.push({
@@ -347,13 +369,6 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
347
369
 
348
370
  barrier.status = newStatus;
349
371
 
350
- // Update satisfied_repos for tracking
351
- if (barrier.type === 'all_repos_accepted') {
352
- barrier.satisfied_repos = getAcceptedReposForWorkstream(
353
- fullHistory, barrier.workstream_id, barrier.required_repos
354
- );
355
- }
356
-
357
372
  barriersChanged = true;
358
373
 
359
374
  appendJsonl(barrierLedgerPath(workspacePath), {
@@ -421,75 +436,115 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
421
436
  };
422
437
  }
423
438
 
424
- // ── Internal helpers ────────────────────────────────────────────────────────
439
+ /**
440
+ * Clear a coordinator blocked state after the operator resolves the cause.
441
+ *
442
+ * Recovery always begins with repo-authority resync, then restores the
443
+ * coordinator to `active` or `paused` based on whether a pending gate still
444
+ * exists. It never mutates repo-local governed state.
445
+ *
446
+ * @param {string} workspacePath
447
+ * @param {object} state
448
+ * @param {object} config
449
+ * @returns {{ ok: boolean, state?: object, resumed_status?: string, blocked_reason?: string, blocked_repos?: string[], resync?: object, error?: string }}
450
+ */
451
+ export function resumeCoordinatorFromBlockedState(workspacePath, state, config) {
452
+ if (!state || state.status !== 'blocked') {
453
+ return {
454
+ ok: false,
455
+ error: `Cannot resume coordinator: status is "${state?.status || 'missing'}", expected "blocked"`,
456
+ };
457
+ }
425
458
 
426
- function getAcceptedReposForWorkstream(history, workstreamId, requiredRepos) {
427
- const accepted = new Set();
428
- for (const entry of history) {
429
- if (entry?.type === 'acceptance_projection' && entry.workstream_id === workstreamId) {
430
- if (requiredRepos.includes(entry.repo_id)) {
431
- accepted.add(entry.repo_id);
432
- }
433
- }
459
+ const previousBlockedReason = state.blocked_reason || 'unknown blocked reason';
460
+ const expectedSuperRunId = state.super_run_id;
461
+ const resync = resyncFromRepoAuthority(workspacePath, state, config);
462
+ const refreshedState = loadCoordinatorState(workspacePath);
463
+
464
+ if (!refreshedState) {
465
+ return {
466
+ ok: false,
467
+ error: 'Coordinator state could not be reloaded after resync',
468
+ blocked_reason: previousBlockedReason,
469
+ resync,
470
+ };
434
471
  }
435
- return [...accepted];
436
- }
437
472
 
438
- function recomputeBarrierStatus(barrier, history, config) {
439
- switch (barrier.type) {
440
- case 'all_repos_accepted': {
441
- const required = new Set(barrier.required_repos);
442
- const satisfied = new Set();
443
- for (const entry of history) {
444
- if (entry?.type === 'acceptance_projection' && entry.workstream_id === barrier.workstream_id) {
445
- if (required.has(entry.repo_id)) {
446
- satisfied.add(entry.repo_id);
447
- }
448
- }
449
- }
450
- if (satisfied.size === required.size) return 'satisfied';
451
- if (satisfied.size > 0) return 'partially_satisfied';
452
- return 'pending';
453
- }
473
+ if (refreshedState.super_run_id !== expectedSuperRunId) {
474
+ return {
475
+ ok: false,
476
+ error: `Cannot resume coordinator: super_run_id changed from "${expectedSuperRunId}" to "${refreshedState.super_run_id}"`,
477
+ blocked_reason: previousBlockedReason,
478
+ resync,
479
+ };
480
+ }
454
481
 
455
- case 'ordered_repo_sequence': {
456
- const workstream = config.workstreams?.[barrier.workstream_id];
457
- if (!workstream) return barrier.status;
458
- const entryRepo = workstream.entry_repo;
459
- const hasUpstream = history.some(
460
- e => e?.type === 'acceptance_projection'
461
- && e.workstream_id === barrier.workstream_id
462
- && e.repo_id === entryRepo
463
- );
464
- if (hasUpstream) return 'satisfied';
465
- const anyDownstream = history.some(
466
- e => e?.type === 'acceptance_projection'
467
- && e.workstream_id === barrier.workstream_id
468
- && e.repo_id !== entryRepo
469
- );
470
- if (anyDownstream) return 'partially_satisfied';
471
- return 'pending';
472
- }
482
+ if (!resync.ok) {
483
+ return {
484
+ ok: false,
485
+ error: `Coordinator remains blocked: ${resync.blocked_reason || refreshedState.blocked_reason || previousBlockedReason}`,
486
+ blocked_reason: resync.blocked_reason || refreshedState.blocked_reason || previousBlockedReason,
487
+ resync,
488
+ state: refreshedState,
489
+ };
490
+ }
491
+
492
+ const blockedRepos = Object.entries(refreshedState.repo_runs || {})
493
+ .filter(([, repoRun]) => repoRun?.status === 'blocked')
494
+ .map(([repoId]) => repoId);
495
+
496
+ if (blockedRepos.length > 0) {
497
+ const blockedReason = `child repos remain blocked: ${blockedRepos.join(', ')}`;
498
+ const blockedState = {
499
+ ...refreshedState,
500
+ status: 'blocked',
501
+ blocked_reason: blockedReason,
502
+ };
503
+ saveCoordinatorState(workspacePath, blockedState);
504
+ return {
505
+ ok: false,
506
+ error: `Cannot resume coordinator: ${blockedReason}`,
507
+ blocked_reason: blockedReason,
508
+ blocked_repos: blockedRepos,
509
+ resync,
510
+ state: blockedState,
511
+ };
512
+ }
513
+
514
+ const resumedStatus = refreshedState.pending_gate ? 'paused' : 'active';
515
+ const resumedState = {
516
+ ...refreshedState,
517
+ status: resumedStatus,
518
+ };
519
+ delete resumedState.blocked_reason;
473
520
 
474
- case 'interface_alignment': {
475
- const required = new Set(barrier.required_repos);
476
- const acceptedRepos = new Set();
477
- for (const entry of history) {
478
- if (entry?.type === 'acceptance_projection' && entry.workstream_id === barrier.workstream_id) {
479
- if (required.has(entry.repo_id)) acceptedRepos.add(entry.repo_id);
480
- }
521
+ saveCoordinatorState(workspacePath, resumedState);
522
+ appendJsonl(historyPath(workspacePath), {
523
+ type: 'blocked_resolved',
524
+ timestamp: new Date().toISOString(),
525
+ super_run_id: refreshedState.super_run_id,
526
+ from: 'blocked',
527
+ to: resumedStatus,
528
+ blocked_reason: previousBlockedReason,
529
+ pending_gate: refreshedState.pending_gate
530
+ ? {
531
+ gate: refreshedState.pending_gate.gate,
532
+ gate_type: refreshedState.pending_gate.gate_type,
481
533
  }
482
- if (acceptedRepos.size === 0) return 'pending';
483
- if (acceptedRepos.size < required.size) return 'partially_satisfied';
484
- return 'satisfied';
485
- }
534
+ : null,
535
+ });
486
536
 
487
- case 'shared_human_gate':
488
- return barrier.status; // Never auto-transition
537
+ return {
538
+ ok: true,
539
+ state: resumedState,
540
+ resumed_status: resumedStatus,
541
+ blocked_reason: previousBlockedReason,
542
+ resync,
543
+ };
544
+ }
489
545
 
490
- default:
491
- return barrier.status;
492
- }
546
+ function recomputeBarrierStatus(barrier, history, config) {
547
+ return computeCoordinatorBarrierStatus(barrier, history, config);
493
548
  }
494
549
 
495
550
  /**
@@ -97,6 +97,7 @@ function bootstrapBarriers(config) {
97
97
  status: 'pending',
98
98
  required_repos: [...workstream.repos],
99
99
  satisfied_repos: [],
100
+ alignment_decision_ids: workstream.interface_alignment?.decision_ids_by_repo || null,
100
101
  created_at: new Date().toISOString(),
101
102
  };
102
103
  }