agentxchain 2.104.0 → 2.105.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.
Files changed (62) hide show
  1. package/README.md +12 -6
  2. package/bin/agentxchain.js +5 -5
  3. package/dashboard/app.js +111 -7
  4. package/dashboard/components/blocked.js +95 -11
  5. package/dashboard/components/blockers.js +85 -86
  6. package/dashboard/components/coordinator-timeouts.js +13 -0
  7. package/dashboard/components/cross-repo.js +17 -12
  8. package/dashboard/components/gate.js +31 -11
  9. package/dashboard/components/initiative.js +173 -78
  10. package/dashboard/components/ledger.js +28 -0
  11. package/dashboard/components/live-status.js +39 -0
  12. package/dashboard/components/run-history.js +76 -1
  13. package/dashboard/components/timeline.js +5 -1
  14. package/dashboard/index.html +21 -0
  15. package/dashboard/live-observer.js +91 -0
  16. package/package.json +1 -1
  17. package/scripts/release-bump.sh +26 -3
  18. package/src/commands/accept-turn.js +3 -3
  19. package/src/commands/decisions.js +98 -29
  20. package/src/commands/diff.js +27 -4
  21. package/src/commands/doctor.js +48 -16
  22. package/src/commands/history.js +21 -3
  23. package/src/commands/multi.js +223 -54
  24. package/src/commands/phase.js +11 -13
  25. package/src/commands/reject-turn.js +1 -1
  26. package/src/commands/restart.js +28 -11
  27. package/src/commands/resume.js +6 -6
  28. package/src/commands/role.js +51 -14
  29. package/src/commands/run.js +5 -11
  30. package/src/commands/status.js +145 -13
  31. package/src/commands/step.js +36 -29
  32. package/src/lib/admission-control.js +14 -12
  33. package/src/lib/blocked-state.js +150 -0
  34. package/src/lib/conflict-actions.js +17 -0
  35. package/src/lib/context-section-parser.js +2 -0
  36. package/src/lib/continuity-status.js +1 -1
  37. package/src/lib/coordinator-blocker-presentation.js +127 -0
  38. package/src/lib/coordinator-event-narrative.js +43 -0
  39. package/src/lib/coordinator-gate-approval.js +98 -0
  40. package/src/lib/coordinator-gate-evaluation-presentation.js +57 -0
  41. package/src/lib/coordinator-next-actions.js +128 -0
  42. package/src/lib/coordinator-pending-gate-presentation.js +79 -0
  43. package/src/lib/coordinator-presentation-detail.js +11 -0
  44. package/src/lib/coordinator-repo-snapshots.js +53 -0
  45. package/src/lib/coordinator-repo-status-presentation.js +134 -0
  46. package/src/lib/dashboard/actions.js +105 -29
  47. package/src/lib/dashboard/bridge-server.js +7 -0
  48. package/src/lib/dashboard/coordinator-blockers.js +17 -0
  49. package/src/lib/dashboard/coordinator-repo-status.js +50 -0
  50. package/src/lib/dashboard/coordinator-timeout-status.js +34 -11
  51. package/src/lib/dashboard/state-reader.js +36 -1
  52. package/src/lib/dispatch-bundle.js +23 -0
  53. package/src/lib/export-diff.js +70 -38
  54. package/src/lib/export-verifier.js +3 -0
  55. package/src/lib/history-diff-summary.js +249 -0
  56. package/src/lib/manual-qa-fallback.js +18 -0
  57. package/src/lib/normalized-config.js +27 -22
  58. package/src/lib/recent-event-summary.js +132 -0
  59. package/src/lib/repo-decisions.js +69 -28
  60. package/src/lib/report.js +353 -145
  61. package/src/lib/run-diff.js +4 -0
  62. package/src/lib/runtime-capabilities.js +222 -0
@@ -11,7 +11,16 @@
11
11
  */
12
12
 
13
13
  import chalk from 'chalk';
14
+ import { getCoordinatorBlockerDetails } from '../lib/coordinator-blocker-presentation.js';
15
+ import {
16
+ normalizeCoordinatorGateApprovalFailure,
17
+ normalizeCoordinatorGateApprovalSuccess,
18
+ } from '../lib/coordinator-gate-approval.js';
14
19
  import { loadCoordinatorConfig } from '../lib/coordinator-config.js';
20
+ import { deriveCoordinatorNextActions } from '../lib/coordinator-next-actions.js';
21
+ import { getCoordinatorPendingGateDetails } from '../lib/coordinator-pending-gate-presentation.js';
22
+ import { buildCoordinatorRepoStatusRows } from '../lib/coordinator-repo-status-presentation.js';
23
+ import { collectCoordinatorRepoSnapshots } from '../lib/coordinator-repo-snapshots.js';
15
24
  import {
16
25
  initializeCoordinatorRun,
17
26
  loadCoordinatorState,
@@ -99,9 +108,18 @@ export async function multiStatusCommand(options) {
99
108
 
100
109
  const status = getCoordinatorStatus(workspacePath);
101
110
  const barriers = readBarriers(workspacePath);
111
+ const configResult = loadCoordinatorConfig(workspacePath);
112
+ const repoRows = buildCoordinatorRepoStatusRows({
113
+ config: configResult.ok ? configResult.config : null,
114
+ coordinatorRepoRuns: status.repo_runs || {},
115
+ });
116
+ const nextActions = deriveCoordinatorCliNextActions(
117
+ status,
118
+ configResult.ok ? configResult.config : null,
119
+ );
102
120
 
103
121
  if (options.json) {
104
- console.log(JSON.stringify({ ...status, barriers }, null, 2));
122
+ console.log(JSON.stringify({ ...status, barriers, next_actions: nextActions }, null, 2));
105
123
  return;
106
124
  }
107
125
 
@@ -129,14 +147,13 @@ export async function multiStatusCommand(options) {
129
147
  console.log(`Blocked: ${chalk.red.bold('BLOCKED')} — ${reason}`);
130
148
  }
131
149
 
132
- // Pending gate with phase transition direction
150
+ // Pending gate details
133
151
  if (status.pending_gate) {
134
- const pg = status.pending_gate;
135
- const fromTo = pg.from && pg.to ? ` ${pg.from} → ${pg.to}` : '';
136
- console.log(`Pending Gate: ${pg.gate} (${pg.gate_type})${fromTo}`);
137
- console.log(`Action: Run ${chalk.cyan('agentxchain multi approve-gate')} to advance`);
152
+ printCoordinatorPendingGate(status.pending_gate);
138
153
  }
139
154
 
155
+ printCoordinatorNextActions(nextActions);
156
+
140
157
  // Completed state
141
158
  if (status.status === 'completed') {
142
159
  console.log('');
@@ -148,9 +165,13 @@ export async function multiStatusCommand(options) {
148
165
 
149
166
  console.log('');
150
167
  console.log('Repos:');
151
- for (const [repoId, info] of Object.entries(status.repo_runs || {})) {
152
- const phase = info.phase ? ` [${info.phase}]` : '';
153
- console.log(` ${repoId}: ${info.status || 'unknown'}${phase} (run: ${info.run_id})`);
168
+ for (const row of repoRows) {
169
+ const phase = row.phase ? ` [${row.phase}]` : '';
170
+ const details = [
171
+ `run: ${row.run_id || 'unknown'}`,
172
+ ...row.details.map((detail) => `${detail.label}: ${detail.value}`),
173
+ ];
174
+ console.log(` ${row.repo_id}: ${row.status || 'unknown'}${phase} (${details.join('; ')})`);
154
175
  }
155
176
 
156
177
  // Phase gate status
@@ -183,6 +204,121 @@ function formatCoordinatorStatus(status) {
183
204
  }
184
205
  }
185
206
 
207
+ function deriveCoordinatorCliNextActions(statusLike, config) {
208
+ const repos = config ? collectCoordinatorRepoSnapshots(config) : [];
209
+ return deriveCoordinatorNextActions({
210
+ status: statusLike?.status ?? null,
211
+ blockedReason: statusLike?.blocked_reason ?? null,
212
+ pendingGate: statusLike?.pending_gate ?? null,
213
+ repos,
214
+ coordinatorRepoRuns: statusLike?.repo_runs || {},
215
+ });
216
+ }
217
+
218
+ function printCoordinatorNextActions(nextActions, write = console.log) {
219
+ if (!Array.isArray(nextActions) || nextActions.length === 0) {
220
+ return;
221
+ }
222
+
223
+ write('');
224
+ write('Next Actions:');
225
+ for (const [index, action] of nextActions.entries()) {
226
+ write(` ${index + 1}. ${action.command}`);
227
+ if (action.reason) {
228
+ write(` ${action.reason}`);
229
+ }
230
+ }
231
+ }
232
+
233
+ function printCoordinatorPendingGate(pendingGate, write = console.log) {
234
+ const details = getCoordinatorPendingGateDetails({ pendingGate });
235
+ if (details.length === 0) {
236
+ return;
237
+ }
238
+
239
+ write('Pending Gate:');
240
+ for (const detail of details) {
241
+ write(` ${detail.label}: ${detail.value}`);
242
+ }
243
+ }
244
+
245
+ function printCoordinatorGateApprovalFailure(failure, write = console.error) {
246
+ write('');
247
+ write('Coordinator Gate Approval Failed');
248
+ write('');
249
+ if (failure.gate_type) {
250
+ write(` Gate Type: ${failure.gate_type}`);
251
+ }
252
+ if (failure.gate) {
253
+ write(` Gate: ${failure.gate}`);
254
+ }
255
+ if (failure.hook_name) {
256
+ write(` Hook: ${failure.hook_name}`);
257
+ } else if (failure.hook_phase) {
258
+ write(` Hook: ${failure.hook_phase}`);
259
+ }
260
+ write(` Error: ${failure.error || 'Coordinator gate approval failed'}`);
261
+ if (failure.recovery_summary?.typed_reason) {
262
+ write(` Reason: ${failure.recovery_summary.typed_reason}`);
263
+ }
264
+ if (failure.recovery_summary?.owner) {
265
+ write(` Owner: ${failure.recovery_summary.owner}`);
266
+ }
267
+ if (failure.recovery_summary?.recovery_action) {
268
+ write(` Action: ${failure.recovery_summary.recovery_action}`);
269
+ }
270
+ if (failure.recovery_summary?.detail) {
271
+ write(` Detail: ${failure.recovery_summary.detail}`);
272
+ }
273
+ printCoordinatorNextActions(failure.next_actions, write);
274
+ }
275
+
276
+ function normalizeCoordinatorBlockerForPresentation(blocker) {
277
+ if (!blocker || typeof blocker !== 'object' || Array.isArray(blocker)) {
278
+ return null;
279
+ }
280
+
281
+ if (blocker.code === 'repo_run_id_mismatch') {
282
+ return {
283
+ code: blocker.code,
284
+ repo_id: blocker.repo_id ?? null,
285
+ expected_run_id: blocker.expected_run_id ?? null,
286
+ actual_run_id: blocker.actual_run_id ?? null,
287
+ message: blocker.message || null,
288
+ };
289
+ }
290
+
291
+ if (blocker.type === 'run_id_mismatch') {
292
+ return {
293
+ code: 'repo_run_id_mismatch',
294
+ repo_id: blocker.repo_id ?? null,
295
+ expected_run_id: blocker.coordinator_run_id ?? blocker.expected_run_id ?? null,
296
+ actual_run_id: blocker.repo_run_id ?? blocker.actual_run_id ?? null,
297
+ message: blocker.detail || blocker.message || null,
298
+ };
299
+ }
300
+
301
+ return {
302
+ code: blocker.code || blocker.type || null,
303
+ message: blocker.message || blocker.detail || null,
304
+ };
305
+ }
306
+
307
+ function printCoordinatorBlockerDetails(blockers, write = console.error) {
308
+ for (const blocker of Array.isArray(blockers) ? blockers : []) {
309
+ const normalized = normalizeCoordinatorBlockerForPresentation(blocker);
310
+ if (!normalized?.message) {
311
+ continue;
312
+ }
313
+
314
+ const codeTag = normalized.code ? `[${normalized.code}] ` : '';
315
+ write(` - ${codeTag}${normalized.message}`);
316
+ for (const detail of getCoordinatorBlockerDetails(normalized)) {
317
+ write(` ${detail.label}: ${detail.value}`);
318
+ }
319
+ }
320
+ }
321
+
186
322
  // ── multi step ─────────────────────────────────────────────────────────────
187
323
 
188
324
  export async function multiStepCommand(options) {
@@ -214,14 +350,21 @@ export async function multiStepCommand(options) {
214
350
  // Fire on_escalation hook (advisory — cannot block, only notifies)
215
351
  fireEscalationHook(workspacePath, configResult.config, state, state.blocked_reason || 'unknown reason');
216
352
  console.error(`Coordinator is blocked: ${state.blocked_reason || 'unknown reason'}`);
217
- console.error('Resolve the blocked state, then run `agentxchain multi resume` before stepping again.');
353
+ printCoordinatorNextActions(
354
+ deriveCoordinatorCliNextActions(state, configResult.config),
355
+ console.error,
356
+ );
218
357
  process.exitCode = 1;
219
358
  return;
220
359
  }
221
360
 
222
361
  if (state.pending_gate) {
223
- console.error(`Coordinator has a pending gate: ${state.pending_gate.gate}`);
224
- console.error('Approve the gate with `agentxchain multi approve-gate` before stepping.');
362
+ console.error('Coordinator has a pending gate.');
363
+ printCoordinatorPendingGate(state.pending_gate, console.error);
364
+ printCoordinatorNextActions(
365
+ deriveCoordinatorCliNextActions(state, configResult.config),
366
+ console.error,
367
+ );
225
368
  process.exitCode = 1;
226
369
  return;
227
370
  }
@@ -235,14 +378,7 @@ export async function multiStepCommand(options) {
235
378
  // Fire on_escalation for the blocked resync
236
379
  fireEscalationHook(workspacePath, configResult.config, state, resync.blocked_reason || 'resync failure');
237
380
  console.error(`Coordinator resync entered blocked state: ${resync.blocked_reason || 'unknown reason'}`);
238
- for (const mismatch of resync.mismatch_details || []) {
239
- const codeTag = mismatch.code ? `[${mismatch.code}] ` : '';
240
- console.error(` - ${codeTag}${mismatch.message}`);
241
- if (mismatch.code === 'repo_run_id_mismatch') {
242
- console.error(` expected: ${mismatch.expected_run_id}`);
243
- console.error(` actual: ${mismatch.actual_run_id}`);
244
- }
245
- }
381
+ printCoordinatorBlockerDetails(resync.mismatch_details, console.error);
246
382
  process.exitCode = 1;
247
383
  return;
248
384
  }
@@ -308,7 +444,10 @@ export async function multiStepCommand(options) {
308
444
  } else {
309
445
  console.log(`Completion gate requested: ${gate.payload.gate}`);
310
446
  }
311
- console.log('Approve the gate with `agentxchain multi approve-gate` to continue.');
447
+ const updatedState = loadCoordinatorState(workspacePath) || state;
448
+ printCoordinatorNextActions(
449
+ deriveCoordinatorCliNextActions(updatedState, configResult.config),
450
+ );
312
451
  return;
313
452
  }
314
453
 
@@ -318,14 +457,7 @@ export async function multiStepCommand(options) {
318
457
  }
319
458
  if (gate.blockers.length > 0) {
320
459
  console.error(`Coordinator ${gate.type === 'phase_transition' ? 'phase' : 'completion'} gate is not ready:`);
321
- for (const blocker of gate.blockers) {
322
- const codeTag = blocker.code ? `[${blocker.code}] ` : '';
323
- console.error(` - ${codeTag}${blocker.message}`);
324
- if (blocker.code === 'repo_run_id_mismatch') {
325
- console.error(` expected: ${blocker.expected_run_id}`);
326
- console.error(` actual: ${blocker.actual_run_id}`);
327
- }
328
- }
460
+ printCoordinatorBlockerDetails(gate.blockers, console.error);
329
461
  }
330
462
  process.exitCode = 1;
331
463
  return;
@@ -465,6 +597,8 @@ export async function multiResumeCommand(options) {
465
597
  return;
466
598
  }
467
599
 
600
+ const nextActions = deriveCoordinatorCliNextActions(result.state, configResult.config);
601
+
468
602
  if (options.json) {
469
603
  console.log(JSON.stringify({
470
604
  ok: true,
@@ -472,6 +606,8 @@ export async function multiResumeCommand(options) {
472
606
  resumed_status: result.resumed_status,
473
607
  blocked_reason: result.blocked_reason,
474
608
  pending_gate: result.state?.pending_gate || null,
609
+ next_action: nextActions[0]?.command ?? null,
610
+ next_actions: nextActions,
475
611
  resync: result.resync,
476
612
  }, null, 2));
477
613
  return;
@@ -479,11 +615,10 @@ export async function multiResumeCommand(options) {
479
615
 
480
616
  console.log(`Coordinator resumed: ${result.resumed_status}`);
481
617
  console.log(`Previous block: ${result.blocked_reason}`);
482
- if (result.resumed_status === 'paused' && result.state?.pending_gate) {
483
- console.log(`Next action: agentxchain multi approve-gate (${result.state.pending_gate.gate})`);
484
- } else {
485
- console.log('Next action: agentxchain multi step');
618
+ if (result.state?.pending_gate) {
619
+ printCoordinatorPendingGate(result.state.pending_gate);
486
620
  }
621
+ printCoordinatorNextActions(nextActions);
487
622
  }
488
623
 
489
624
  // ── multi approve-gate ─────────────────────────────────────────────────────
@@ -523,22 +658,34 @@ export async function multiApproveGateCommand(options) {
523
658
  if (gateHook.blocked) {
524
659
  const blocker = gateHook.verdicts.find(v => v.verdict === 'block');
525
660
  const reason = blocker?.message || 'before_gate hook blocked approval';
526
- console.error(`Gate approval blocked by hook: ${reason}`);
661
+ const failure = normalizeCoordinatorGateApprovalFailure({
662
+ state,
663
+ config: configResult.config,
664
+ code: 'hook_blocked',
665
+ error: reason,
666
+ hookName: blocker?.hook_name || null,
667
+ hookPhase: 'before_gate',
668
+ });
669
+ printCoordinatorGateApprovalFailure(failure);
527
670
  if (options.json) {
528
- console.log(JSON.stringify({ blocked: true, hook_phase: 'before_gate', reason }, null, 2));
671
+ console.log(JSON.stringify(failure, null, 2));
529
672
  }
530
673
  process.exitCode = 1;
531
674
  return;
532
675
  }
533
676
 
534
677
  if (!gateHook.ok) {
535
- console.error(`Gate hook failed: ${gateHook.error || 'unknown hook failure'}`);
678
+ const failure = normalizeCoordinatorGateApprovalFailure({
679
+ state,
680
+ config: configResult.config,
681
+ code: 'hook_failed',
682
+ error: gateHook.error || 'unknown hook failure',
683
+ hookName: gateHook.results?.find((entry) => entry?.hook_name)?.hook_name || null,
684
+ hookPhase: 'before_gate',
685
+ });
686
+ printCoordinatorGateApprovalFailure(failure);
536
687
  if (options.json) {
537
- console.log(JSON.stringify({
538
- blocked: true,
539
- hook_phase: 'before_gate',
540
- reason: gateHook.error || 'unknown hook failure',
541
- }, null, 2));
688
+ console.log(JSON.stringify(failure, null, 2));
542
689
  }
543
690
  process.exitCode = 1;
544
691
  return;
@@ -558,21 +705,32 @@ export async function multiApproveGateCommand(options) {
558
705
  }
559
706
 
560
707
  if (!result.ok) {
561
- console.error(`Gate approval failed: ${result.error}`);
708
+ const failure = normalizeCoordinatorGateApprovalFailure({
709
+ state,
710
+ config: configResult.config,
711
+ code: 'approval_failed',
712
+ error: result.error || 'Coordinator gate approval failed',
713
+ });
714
+ printCoordinatorGateApprovalFailure(failure);
715
+ if (options.json) {
716
+ console.log(JSON.stringify(failure, null, 2));
717
+ }
562
718
  process.exitCode = 1;
563
719
  return;
564
720
  }
565
721
 
722
+ const success = normalizeCoordinatorGateApprovalSuccess({
723
+ result: { ...result, config: configResult.config },
724
+ gateType,
725
+ });
726
+
566
727
  if (options.json) {
567
- console.log(JSON.stringify(result, null, 2));
728
+ console.log(JSON.stringify(success, null, 2));
568
729
  return;
569
730
  }
570
731
 
571
- if (gateType === 'phase_transition') {
572
- console.log(`Phase transition approved: ${result.transition?.from} → ${result.transition?.to}`);
573
- } else {
574
- console.log('Run completion approved. Coordinator run is now complete.');
575
- }
732
+ console.log(success.message);
733
+ printCoordinatorNextActions(success.next_actions);
576
734
  }
577
735
 
578
736
  // ── multi resync ───────────────────────────────────────────────────────────
@@ -614,9 +772,7 @@ export async function multiResyncCommand(options) {
614
772
  console.log(JSON.stringify({ diverged: true, mismatches: divergence.mismatches }, null, 2));
615
773
  } else {
616
774
  console.log(`Divergence detected (${divergence.mismatches.length} mismatch(es)):`);
617
- for (const m of divergence.mismatches) {
618
- console.log(` [${m.type}] ${m.detail}`);
619
- }
775
+ printCoordinatorBlockerDetails(divergence.mismatches, console.log);
620
776
  console.log('');
621
777
  console.log('Run without --dry-run to resync.');
622
778
  }
@@ -625,9 +781,17 @@ export async function multiResyncCommand(options) {
625
781
 
626
782
  // Step 2: Resync
627
783
  const result = resyncFromRepoAuthority(workspacePath, state, configResult.config);
784
+ const updatedState = loadCoordinatorState(workspacePath) || state;
785
+ const nextActions = deriveCoordinatorCliNextActions(updatedState, configResult.config);
628
786
 
629
787
  if (options.json) {
630
- console.log(JSON.stringify(result, null, 2));
788
+ console.log(JSON.stringify({
789
+ ...result,
790
+ status: updatedState.status ?? null,
791
+ pending_gate: updatedState.pending_gate ?? null,
792
+ next_action: nextActions[0]?.command ?? null,
793
+ next_actions: nextActions,
794
+ }, null, 2));
631
795
  return;
632
796
  }
633
797
 
@@ -642,9 +806,14 @@ export async function multiResyncCommand(options) {
642
806
  console.log(` ${bc.barrier_id}: ${bc.previous_status} → ${bc.new_status}`);
643
807
  }
644
808
  }
809
+ if (updatedState.pending_gate) {
810
+ printCoordinatorPendingGate(updatedState.pending_gate);
811
+ }
812
+ printCoordinatorNextActions(nextActions);
645
813
  } else {
646
- console.error('Resync completed with blocked state:');
647
- console.error(` Reason: ${result.blocked_reason}`);
814
+ console.error(`Coordinator resync entered blocked state: ${result.blocked_reason || 'unknown reason'}`);
815
+ printCoordinatorBlockerDetails(result.mismatch_details, console.error);
816
+ printCoordinatorNextActions(nextActions, console.error);
648
817
  process.exitCode = 1;
649
818
  }
650
819
  }
@@ -12,7 +12,7 @@ export function phaseCommand(subcommand, phaseId, opts) {
12
12
  process.exit(1);
13
13
  }
14
14
 
15
- const { root, config, rawConfig, version } = context;
15
+ const { root, config, version } = context;
16
16
  if (version !== 4 || config.protocol_mode !== 'governed') {
17
17
  console.log(chalk.red(' Not a governed AgentXchain project (requires v4 config).'));
18
18
  process.exit(1);
@@ -27,14 +27,14 @@ export function phaseCommand(subcommand, phaseId, opts) {
27
27
  const state = loadProjectState(root, config);
28
28
 
29
29
  if (subcommand === 'show') {
30
- return showPhase(phaseId, { root, config, rawConfig, state, phaseIds, opts });
30
+ return showPhase(phaseId, { root, config, state, phaseIds, opts });
31
31
  }
32
32
 
33
- return listPhases({ root, config, rawConfig, state, phaseIds, opts });
33
+ return listPhases({ root, config, state, phaseIds, opts });
34
34
  }
35
35
 
36
- function listPhases({ root, config, rawConfig, state, phaseIds, opts }) {
37
- const phases = phaseIds.map((phaseId) => buildPhaseRecord(root, config, rawConfig, state, phaseId));
36
+ function listPhases({ root, config, state, phaseIds, opts }) {
37
+ const phases = phaseIds.map((phaseId) => buildPhaseRecord(root, config, state, phaseId));
38
38
 
39
39
  if (opts.json) {
40
40
  console.log(JSON.stringify({
@@ -56,7 +56,7 @@ function listPhases({ root, config, rawConfig, state, phaseIds, opts }) {
56
56
  console.log(chalk.dim(' Usage: agentxchain phase show <phase>\n'));
57
57
  }
58
58
 
59
- function showPhase(requestedPhaseId, { root, config, rawConfig, state, phaseIds, opts }) {
59
+ function showPhase(requestedPhaseId, { root, config, state, phaseIds, opts }) {
60
60
  const phaseId = requestedPhaseId || state?.phase || phaseIds[0];
61
61
  if (!config.routing?.[phaseId]) {
62
62
  console.log(chalk.red(` Unknown phase: ${phaseId}`));
@@ -64,7 +64,7 @@ function showPhase(requestedPhaseId, { root, config, rawConfig, state, phaseIds,
64
64
  process.exit(1);
65
65
  }
66
66
 
67
- const phase = buildPhaseRecord(root, config, rawConfig, state, phaseId);
67
+ const phase = buildPhaseRecord(root, config, state, phaseId);
68
68
 
69
69
  if (opts.json) {
70
70
  console.log(JSON.stringify(phase, null, 2));
@@ -112,16 +112,14 @@ function showPhase(requestedPhaseId, { root, config, rawConfig, state, phaseIds,
112
112
  console.log('');
113
113
  }
114
114
 
115
- function buildPhaseRecord(root, config, rawConfig, state, phaseId) {
115
+ function buildPhaseRecord(root, config, state, phaseId) {
116
116
  const route = config.routing?.[phaseId] || {};
117
117
  const normalizedPhaseKit = config.workflow_kit?.phases?.[phaseId] || null;
118
- const rawWorkflowKit = rawConfig.workflow_kit;
119
- const rawPhaseKit = rawWorkflowKit?.phases?.[phaseId] || null;
120
- const hasExplicitWorkflowKit = rawWorkflowKit !== undefined && rawWorkflowKit !== null;
118
+ const hasExplicitWorkflowKit = config.workflow_kit?._explicit === true;
121
119
 
122
120
  const workflowSource = !hasExplicitWorkflowKit
123
121
  ? 'default'
124
- : rawPhaseKit
122
+ : normalizedPhaseKit
125
123
  ? 'explicit'
126
124
  : 'not_declared';
127
125
 
@@ -152,7 +150,7 @@ function buildPhaseRecord(root, config, rawConfig, state, phaseId) {
152
150
  max_concurrent_turns: getMaxConcurrentTurns(config, phaseId),
153
151
  workflow_kit: {
154
152
  source: workflowSource,
155
- template: typeof rawPhaseKit?.template === 'string' ? rawPhaseKit.template : null,
153
+ template: typeof normalizedPhaseKit?.template === 'string' ? normalizedPhaseKit.template : null,
156
154
  artifacts,
157
155
  },
158
156
  };
@@ -76,7 +76,7 @@ export async function rejectTurnCommand(opts) {
76
76
  console.log('');
77
77
 
78
78
  if (result.escalated) {
79
- const recovery = deriveRecoveryDescriptor(result.state);
79
+ const recovery = deriveRecoveryDescriptor(result.state, config);
80
80
  if (recovery) {
81
81
  console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
82
82
  console.log(` ${chalk.dim('Owner:')} ${recovery.owner}`);
@@ -13,7 +13,7 @@
13
13
  import chalk from 'chalk';
14
14
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
15
15
  import { join, dirname } from 'path';
16
- import { loadProjectContext, loadProjectState } from '../lib/config.js';
16
+ import { loadProjectContext } from '../lib/config.js';
17
17
  import {
18
18
  assignGovernedTurn,
19
19
  getActiveTurns,
@@ -23,6 +23,8 @@ import {
23
23
  HISTORY_PATH,
24
24
  LEDGER_PATH,
25
25
  } from '../lib/governed-state.js';
26
+ import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
27
+ import { deriveRecommendedContinuityAction } from '../lib/continuity-status.js';
26
28
  import { readSessionCheckpoint, writeSessionCheckpoint, captureBaselineRef, SESSION_PATH } from '../lib/session-checkpoint.js';
27
29
 
28
30
  /**
@@ -30,6 +32,7 @@ import { readSessionCheckpoint, writeSessionCheckpoint, captureBaselineRef, SESS
30
32
  * so a new agent session can orient quickly.
31
33
  */
32
34
  function generateRecoveryReport(root, state, checkpoint, driftWarnings = []) {
35
+ const continuityAction = deriveRecommendedContinuityAction(state);
33
36
  const lines = [
34
37
  '# Session Recovery Report',
35
38
  '',
@@ -112,7 +115,7 @@ function generateRecoveryReport(root, state, checkpoint, driftWarnings = []) {
112
115
  `- **To**: ${pt.to}`,
113
116
  `- **Gate**: ${pt.gate}`,
114
117
  `- **Requested by**: ${pt.requested_by_turn || 'unknown'}`,
115
- `- **Action**: Run \`agentxchain approve-transition\` to approve`,
118
+ `- **Action**: Run \`${continuityAction.recommended_command}\` to continue`,
116
119
  '',
117
120
  );
118
121
  }
@@ -124,7 +127,7 @@ function generateRecoveryReport(root, state, checkpoint, driftWarnings = []) {
124
127
  '',
125
128
  `- **Gate**: ${pc.gate}`,
126
129
  `- **Requested by**: ${pc.requested_by_turn || 'unknown'}`,
127
- `- **Action**: Run \`agentxchain approve-completion\` to approve`,
130
+ `- **Action**: Run \`${continuityAction.recommended_command}\` to continue`,
128
131
  '',
129
132
  );
130
133
  }
@@ -139,10 +142,9 @@ function generateRecoveryReport(root, state, checkpoint, driftWarnings = []) {
139
142
  }
140
143
 
141
144
  lines.push('## Next Steps', '');
142
- if (state.pending_phase_transition) {
143
- lines.push('A phase transition is pending approval. Run `agentxchain approve-transition` before assigning new turns.', '');
144
- } else if (state.pending_run_completion) {
145
- lines.push('A run completion is pending approval. Run `agentxchain approve-completion` to finalize.', '');
145
+ if (continuityAction.recommended_command && !continuityAction.restart_recommended) {
146
+ const detail = continuityAction.recommended_detail ? ` Detail: ${continuityAction.recommended_detail}.` : '';
147
+ lines.push(`Run \`${continuityAction.recommended_command}\` next.${detail}`, '');
146
148
  } else {
147
149
  lines.push('The next turn has been assigned. Check the dispatch bundle for context.', '');
148
150
  }
@@ -197,10 +199,17 @@ export async function restartCommand(opts) {
197
199
 
198
200
  if (state.status === 'blocked') {
199
201
  console.log(chalk.red('Run is blocked. Resolve the blocker first.'));
200
- if (state.blocked_reason) {
202
+ const recovery = deriveRecoveryDescriptor(state, config);
203
+ if (recovery) {
204
+ console.log(chalk.dim(`Reason: ${recovery.typed_reason}`));
205
+ console.log(chalk.dim(`Owner: ${recovery.owner}`));
206
+ console.log(chalk.dim(`Action: ${recovery.recovery_action}`));
207
+ if (recovery.detail) {
208
+ console.log(chalk.dim(`Detail: ${recovery.detail}`));
209
+ }
210
+ } else if (state.blocked_reason) {
201
211
  console.log(chalk.dim(`Reason: ${typeof state.blocked_reason === 'string' ? state.blocked_reason : JSON.stringify(state.blocked_reason)}`));
202
212
  }
203
- console.log(chalk.dim('Use `agentxchain step --resume` or resolve the blocker, then try again.'));
204
213
  process.exit(1);
205
214
  }
206
215
 
@@ -227,17 +236,25 @@ export async function restartCommand(opts) {
227
236
  }
228
237
  }
229
238
 
239
+ const continuityAction = deriveRecommendedContinuityAction(state);
240
+
230
241
  // ── Pending gate / completion check ────────────────────────────────────
231
242
  if (state.pending_phase_transition) {
232
243
  const pt = state.pending_phase_transition;
233
244
  console.log(chalk.yellow(`Pending phase transition: ${pt.from} → ${pt.to} (gate: ${pt.gate})`));
234
- console.log(chalk.dim('Run `agentxchain approve-transition` to approve before assigning new turns.'));
245
+ console.log(chalk.dim(`Run \`${continuityAction.recommended_command}\` next.`));
246
+ if (continuityAction.recommended_detail) {
247
+ console.log(chalk.dim(`Detail: ${continuityAction.recommended_detail}`));
248
+ }
235
249
  }
236
250
 
237
251
  if (state.pending_run_completion) {
238
252
  const pc = state.pending_run_completion;
239
253
  console.log(chalk.yellow(`Pending run completion (gate: ${pc.gate})`));
240
- console.log(chalk.dim('Run `agentxchain approve-completion` to finalize.'));
254
+ console.log(chalk.dim(`Run \`${continuityAction.recommended_command}\` next.`));
255
+ if (continuityAction.recommended_detail) {
256
+ console.log(chalk.dim(`Detail: ${continuityAction.recommended_detail}`));
257
+ }
241
258
  }
242
259
 
243
260
  // Handle abandoned active turns (assigned but never completed)
@@ -101,7 +101,7 @@ export async function resumeCommand(opts) {
101
101
  }
102
102
 
103
103
  if (state.pending_phase_transition || state.pending_run_completion) {
104
- printRecoverySummary(state, 'This run is awaiting approval.');
104
+ printRecoverySummary(state, 'This run is awaiting approval.', config);
105
105
  process.exit(1);
106
106
  }
107
107
 
@@ -267,7 +267,7 @@ export async function resumeCommand(opts) {
267
267
  const assignResult = assignGovernedTurn(root, config, roleId);
268
268
  if (!assignResult.ok) {
269
269
  if (assignResult.error_code?.startsWith('hook_') || assignResult.error_code === 'hook_blocked') {
270
- printAssignmentHookFailure(assignResult, roleId);
270
+ printAssignmentHookFailure(assignResult, roleId, config);
271
271
  }
272
272
  console.log(chalk.red(`Failed to assign turn: ${assignResult.error}`));
273
273
  process.exit(1);
@@ -482,8 +482,8 @@ function printAssignmentWarnings(assignResult) {
482
482
  }
483
483
  }
484
484
 
485
- function printRecoverySummary(state, heading) {
486
- const recovery = deriveRecoveryDescriptor(state);
485
+ function printRecoverySummary(state, heading, config) {
486
+ const recovery = deriveRecoveryDescriptor(state, config);
487
487
  console.log(chalk.yellow(heading));
488
488
  if (!recovery) {
489
489
  return;
@@ -495,8 +495,8 @@ function printRecoverySummary(state, heading) {
495
495
  }
496
496
  }
497
497
 
498
- function printAssignmentHookFailure(result, roleId) {
499
- const recovery = deriveRecoveryDescriptor(result.state);
498
+ function printAssignmentHookFailure(result, roleId, config) {
499
+ const recovery = deriveRecoveryDescriptor(result.state, config);
500
500
  const hookName = result.hookResults?.blocker?.hook_name
501
501
  || result.hookResults?.results?.find((entry) => entry.hook_name)?.hook_name
502
502
  || '(unknown)';