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
@@ -1,5 +1,9 @@
1
1
  import chalk from 'chalk';
2
2
  import { loadProjectContext } from '../lib/config.js';
3
+ import {
4
+ getRoleRuntimeCapabilityContract,
5
+ summarizeRuntimeCapabilityContract,
6
+ } from '../lib/runtime-capabilities.js';
3
7
 
4
8
  export function roleCommand(subcommand, roleId, opts) {
5
9
  const context = loadProjectContext();
@@ -8,25 +12,26 @@ export function roleCommand(subcommand, roleId, opts) {
8
12
  process.exit(1);
9
13
  }
10
14
 
11
- const { rawConfig, version } = context;
15
+ const { config, version } = context;
12
16
 
13
17
  if (version !== 4) {
14
18
  console.log(chalk.red(' Not a governed AgentXchain project (requires v4 config).'));
15
19
  process.exit(1);
16
20
  }
17
21
 
18
- const roles = rawConfig.roles || {};
22
+ const roles = config.roles || {};
23
+ const runtimes = config.runtimes || {};
19
24
  const roleIds = Object.keys(roles);
20
25
 
21
26
  if (subcommand === 'show') {
22
- return showRole(roleId, roles, roleIds, opts);
27
+ return showRole(roleId, roles, runtimes, roleIds, opts);
23
28
  }
24
29
 
25
30
  // Default: list
26
- return listRoles(roles, roleIds, opts);
31
+ return listRoles(roles, runtimes, roleIds, opts);
27
32
  }
28
33
 
29
- function listRoles(roles, roleIds, opts) {
34
+ function listRoles(roles, runtimes, roleIds, opts) {
30
35
  if (roleIds.length === 0) {
31
36
  if (opts.json) {
32
37
  console.log('[]');
@@ -38,14 +43,17 @@ function listRoles(roles, roleIds, opts) {
38
43
 
39
44
  if (opts.json) {
40
45
  const output = roleIds.map((id) => {
46
+ const runtimeId = roles[id].runtime_id;
47
+ const runtime = runtimes[runtimeId];
41
48
  const entry = {
42
49
  id,
43
50
  title: roles[id].title,
44
51
  mandate: roles[id].mandate,
45
52
  write_authority: roles[id].write_authority,
46
- runtime: roles[id].runtime,
53
+ runtime: runtimeId,
54
+ runtime_contract: runtime ? getRoleRuntimeCapabilityContract(id, roles[id], runtime).runtime_contract : null,
47
55
  };
48
- if (typeof roles[id].decision_authority === 'number') {
56
+ if (typeof roles[id]?.decision_authority === 'number') {
49
57
  entry.decision_authority = roles[id].decision_authority;
50
58
  }
51
59
  return entry;
@@ -57,19 +65,22 @@ function listRoles(roles, roleIds, opts) {
57
65
  console.log(chalk.bold(`\n Roles (${roleIds.length}):\n`));
58
66
  for (const id of roleIds) {
59
67
  const r = roles[id];
68
+ const runtime = runtimes[r.runtime_id];
69
+ const contract = runtime ? getRoleRuntimeCapabilityContract(id, r, runtime) : null;
60
70
  const authority = r.write_authority === 'authoritative'
61
71
  ? chalk.green(r.write_authority)
62
72
  : r.write_authority === 'proposed'
63
73
  ? chalk.yellow(r.write_authority)
64
74
  : chalk.dim(r.write_authority);
65
- const decAuth = typeof r.decision_authority === 'number' ? chalk.dim(` dec:${r.decision_authority}`) : '';
66
- console.log(` ${chalk.cyan(id)} ${r.title} [${authority}${decAuth}] ${chalk.dim(r.runtime)}`);
75
+ const decAuth = typeof r?.decision_authority === 'number' ? chalk.dim(` dec:${r.decision_authority}`) : '';
76
+ const capabilitySuffix = contract ? chalk.dim(` {${summarizeRuntimeCapabilityContract(contract.runtime_contract)}}`) : '';
77
+ console.log(` ${chalk.cyan(id)} — ${r.title} [${authority}${decAuth}] → ${chalk.dim(r.runtime_id)}${capabilitySuffix}`);
67
78
  }
68
79
  console.log('');
69
80
  console.log(chalk.dim(' Usage: agentxchain role show <role_id>\n'));
70
81
  }
71
82
 
72
- function showRole(roleId, roles, roleIds, opts) {
83
+ function showRole(roleId, roles, runtimes, roleIds, opts) {
73
84
  if (!roleId) {
74
85
  console.log(chalk.red(' Missing role ID.'));
75
86
  console.log(chalk.dim(` Usage: agentxchain role show <role_id>`));
@@ -86,6 +97,8 @@ function showRole(roleId, roles, roleIds, opts) {
86
97
  }
87
98
 
88
99
  const r = roles[roleId];
100
+ const runtime = runtimes[r.runtime_id];
101
+ const capability = runtime ? getRoleRuntimeCapabilityContract(roleId, r, runtime) : null;
89
102
 
90
103
  if (opts.json) {
91
104
  const entry = {
@@ -93,9 +106,19 @@ function showRole(roleId, roles, roleIds, opts) {
93
106
  title: r.title,
94
107
  mandate: r.mandate,
95
108
  write_authority: r.write_authority,
96
- runtime: r.runtime,
109
+ runtime: r.runtime_id,
110
+ runtime_contract: capability?.runtime_contract || null,
111
+ effective_runtime_contract: capability
112
+ ? {
113
+ role_id: capability.role_id,
114
+ role_write_authority: capability.role_write_authority,
115
+ effective_write_path: capability.effective_write_path,
116
+ workflow_artifact_ownership: capability.workflow_artifact_ownership,
117
+ notes: capability.notes,
118
+ }
119
+ : null,
97
120
  };
98
- if (typeof r.decision_authority === 'number') {
121
+ if (typeof r?.decision_authority === 'number') {
99
122
  entry.decision_authority = r.decision_authority;
100
123
  }
101
124
  console.log(JSON.stringify(entry, null, 2));
@@ -112,9 +135,23 @@ function showRole(roleId, roles, roleIds, opts) {
112
135
  console.log(` Title: ${r.title}`);
113
136
  console.log(` Mandate: ${r.mandate}`);
114
137
  console.log(` Authority: ${authority}`);
115
- if (typeof r.decision_authority === 'number') {
138
+ if (typeof r?.decision_authority === 'number') {
116
139
  console.log(` Decision: ${r.decision_authority}`);
117
140
  }
118
- console.log(` Runtime: ${chalk.dim(r.runtime)}`);
141
+ console.log(` Runtime: ${chalk.dim(r.runtime_id)}`);
142
+ if (capability) {
143
+ const base = capability.runtime_contract;
144
+ console.log(` Transport: ${base.transport}`);
145
+ console.log(` Writes: ${base.can_write_files}`);
146
+ console.log(` Review: ${base.review_only_behavior}`);
147
+ console.log(` Proposals: ${base.proposal_support}`);
148
+ console.log(` Binary: ${base.requires_local_binary ? 'yes' : 'no'}`);
149
+ console.log(` owned_by: ${base.workflow_artifact_ownership}`);
150
+ console.log(` Effective: ${capability.effective_write_path}`);
151
+ console.log(` Ownership: ${capability.workflow_artifact_ownership}`);
152
+ for (const note of capability.notes) {
153
+ console.log(` Note: ${note}`);
154
+ }
155
+ }
119
156
  console.log('');
120
157
  }
@@ -35,6 +35,7 @@ import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
35
35
  import { summarizeRunProvenance } from '../lib/run-provenance.js';
36
36
  import { resolveGovernedRole } from '../lib/role-resolution.js';
37
37
  import { buildInheritedContext } from '../lib/run-context-inheritance.js';
38
+ import { shouldSuggestManualQaFallback } from '../lib/manual-qa-fallback.js';
38
39
  import {
39
40
  getDispatchAssignmentPath,
40
41
  getDispatchContextPath,
@@ -56,7 +57,7 @@ export async function runCommand(opts) {
56
57
  }
57
58
 
58
59
  export async function executeGovernedRun(context, opts = {}) {
59
- const { root, config, rawConfig } = context;
60
+ const { root, config } = context;
60
61
  const log = opts.log || console.log;
61
62
 
62
63
  if (config.protocol_mode !== 'governed') {
@@ -315,11 +316,11 @@ export async function executeGovernedRun(context, opts = {}) {
315
316
 
316
317
  // Adapter failure
317
318
  if (!adapterResult.ok) {
318
- if (shouldPrintManualQaFallback({
319
+ if (shouldSuggestManualQaFallback({
319
320
  roleId,
320
321
  runtimeId,
321
322
  classified: adapterResult.classified,
322
- rawConfig,
323
+ config: cfg,
323
324
  })) {
324
325
  qaMissingCredentialsFallback = {
325
326
  roleId,
@@ -438,7 +439,7 @@ export async function executeGovernedRun(context, opts = {}) {
438
439
 
439
440
  // Recovery guidance for blocked/rejected states
440
441
  if (result.state && (result.stop_reason === 'blocked' || result.stop_reason === 'reject_exhausted' || result.stop_reason === 'dispatch_error')) {
441
- const recovery = deriveRecoveryDescriptor(result.state);
442
+ const recovery = deriveRecoveryDescriptor(result.state, config);
442
443
  if (recovery) {
443
444
  log('');
444
445
  log(chalk.yellow(` Recovery: ${recovery.typed_reason}`));
@@ -514,13 +515,6 @@ function promptUser(question) {
514
515
  });
515
516
  }
516
517
 
517
- function shouldPrintManualQaFallback({ roleId, runtimeId, classified, rawConfig }) {
518
- return classified?.error_class === 'missing_credentials'
519
- && roleId === 'qa'
520
- && runtimeId === 'api-qa'
521
- && rawConfig?.runtimes?.['manual-qa']?.type === 'manual';
522
- }
523
-
524
518
  function printManualQaFallback(log = console.log) {
525
519
  log('');
526
520
  log(chalk.dim(' No-key QA fallback:'));
@@ -1,18 +1,26 @@
1
1
  import chalk from 'chalk';
2
+ import { readFileSync } from 'fs';
2
3
  import { existsSync } from 'fs';
3
4
  import { join } from 'path';
4
- import { loadConfig, loadLock, loadProjectContext, loadProjectState, loadState } from '../lib/config.js';
5
- import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
5
+ import { findProjectRoot, loadConfig, loadLock, loadProjectContext, loadProjectState, loadState } from '../lib/config.js';
6
+ import {
7
+ deriveGovernedRunNextActions,
8
+ deriveRecoveryDescriptor,
9
+ deriveRuntimeBlockedGuidance,
10
+ } from '../lib/blocked-state.js';
6
11
  import { getActiveTurn, getActiveTurnCount, getActiveTurns } from '../lib/governed-state.js';
7
12
  import { getContinuityStatus } from '../lib/continuity-status.js';
8
13
  import { getConnectorHealth } from '../lib/connector-health.js';
14
+ import { readRepoDecisions, summarizeRepoDecisions } from '../lib/repo-decisions.js';
9
15
  import { deriveWorkflowKitArtifacts } from '../lib/workflow-kit-artifacts.js';
10
16
  import { evaluateTimeouts } from '../lib/timeout-evaluator.js';
11
17
  import { summarizeRunProvenance } from '../lib/run-provenance.js';
18
+ import { readRecentRunEventSummary } from '../lib/recent-event-summary.js';
19
+ import { deriveConflictedTurnResolutionActions } from '../lib/conflict-actions.js';
12
20
  import { getDashboardPid, getDashboardSession } from './dashboard.js';
13
21
 
14
22
  export async function statusCommand(opts) {
15
- const context = loadProjectContext();
23
+ const context = loadStatusContext();
16
24
  if (!context) {
17
25
  console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
18
26
  process.exit(1);
@@ -79,11 +87,42 @@ export async function statusCommand(opts) {
79
87
  console.log('');
80
88
  }
81
89
 
90
+ function loadStatusContext(dir = process.cwd()) {
91
+ const context = loadProjectContext(dir);
92
+ if (context) return context;
93
+
94
+ const root = findProjectRoot(dir);
95
+ if (!root) return null;
96
+
97
+ let rawConfig;
98
+ try {
99
+ rawConfig = JSON.parse(readFileSync(join(root, 'agentxchain.json'), 'utf8'));
100
+ } catch {
101
+ return null;
102
+ }
103
+
104
+ if (rawConfig?.protocol_mode !== 'governed') {
105
+ return null;
106
+ }
107
+
108
+ return {
109
+ root,
110
+ rawConfig,
111
+ config: rawConfig,
112
+ version: 4,
113
+ };
114
+ }
115
+
82
116
  function renderGovernedStatus(context, opts) {
83
117
  const { root, config, version } = context;
84
118
  const state = loadProjectState(root, config);
85
119
  const continuity = getContinuityStatus(root, state);
86
120
  const connectorHealth = getConnectorHealth(root, config, state);
121
+ const recovery = deriveRecoveryDescriptor(state, config);
122
+ const runtimeGuidance = deriveRuntimeBlockedGuidance(state, config);
123
+ const nextActions = deriveGovernedRunNextActions(state, config);
124
+ const recentEventSummary = readRecentRunEventSummary(root);
125
+ const repoDecisionSummary = summarizeRepoDecisions(readRepoDecisions(root), config);
87
126
 
88
127
  const workflowKitArtifacts = deriveWorkflowKitArtifacts(root, config, state);
89
128
 
@@ -106,8 +145,13 @@ function renderGovernedStatus(context, opts) {
106
145
  provenance: state?.provenance || null,
107
146
  inherited_context: state?.inherited_context || null,
108
147
  repo_decisions: state?.repo_decisions || null,
148
+ repo_decision_summary: repoDecisionSummary,
109
149
  continuity,
150
+ recovery,
151
+ runtime_guidance: runtimeGuidance,
152
+ next_actions: nextActions,
110
153
  connector_health: connectorHealth,
154
+ recent_event_summary: recentEventSummary,
111
155
  workflow_kit_artifacts: workflowKitArtifacts,
112
156
  dashboard_session: dashboardSessionObj,
113
157
  }, null, 2));
@@ -134,8 +178,12 @@ function renderGovernedStatus(context, opts) {
134
178
  if (state?.inherited_context?.parent_run_id) {
135
179
  console.log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`)}`);
136
180
  }
137
- if (state?.repo_decisions?.length > 0) {
138
- console.log(` ${chalk.dim('Repo decisions:')} ${chalk.yellow(`${state.repo_decisions.length} active`)}`);
181
+ if (repoDecisionSummary) {
182
+ console.log(` ${chalk.dim('Repo decisions:')} ${chalk.yellow(formatRepoDecisionHeadline(repoDecisionSummary))}`);
183
+ const carryoverDetail = formatRepoDecisionCarryover(repoDecisionSummary);
184
+ if (carryoverDetail) {
185
+ console.log(` ${chalk.dim('Carryover:')} ${carryoverDetail}`);
186
+ }
139
187
  }
140
188
  if (state?.accepted_integration_ref) {
141
189
  console.log(` ${chalk.dim('Accepted:')} ${state.accepted_integration_ref}`);
@@ -144,6 +192,7 @@ function renderGovernedStatus(context, opts) {
144
192
 
145
193
  renderContinuityStatus(continuity, state);
146
194
  renderConnectorHealthStatus(connectorHealth);
195
+ renderRecentEventSummary(recentEventSummary);
147
196
 
148
197
  const activeTurnCount = getActiveTurnCount(state);
149
198
  const activeTurns = getActiveTurns(state);
@@ -171,14 +220,15 @@ function renderGovernedStatus(context, opts) {
171
220
  const cs = turn.conflict_state;
172
221
  const files = cs.conflict_error?.conflicting_files || [];
173
222
  const count = cs.detection_count || 1;
223
+ const [reassignAction, mergeAction] = deriveConflictedTurnResolutionActions(turn.turn_id);
174
224
  console.log(` ${chalk.dim('Conflict:')} ${files.length} file(s) — detection #${count}`);
175
225
  if (cs.conflict_error?.overlap_ratio != null) {
176
226
  console.log(` ${chalk.dim('Overlap:')} ${(cs.conflict_error.overlap_ratio * 100).toFixed(0)}%`);
177
227
  }
178
228
  const suggestion = cs.conflict_error?.suggested_resolution || 'reject_and_reassign';
179
229
  console.log(` ${chalk.dim('Suggested:')} ${suggestion}`);
180
- console.log(` ${chalk.dim('Resolve:')} ${chalk.cyan(`agentxchain reject-turn --turn ${turn.turn_id} --reassign`)}`);
181
- console.log(` ${chalk.dim(' or:')} ${chalk.cyan(`agentxchain accept-turn --turn ${turn.turn_id} --resolution human_merge`)}`);
230
+ console.log(` ${chalk.dim('Resolve:')} ${chalk.cyan(reassignAction.command)}`);
231
+ console.log(` ${chalk.dim(' or:')} ${chalk.cyan(mergeAction.command)}`);
182
232
  }
183
233
  }
184
234
  } else if (singleActiveTurn) {
@@ -199,9 +249,10 @@ function renderGovernedStatus(context, opts) {
199
249
  if (singleActiveTurn.status === 'conflicted' && singleActiveTurn.conflict_state) {
200
250
  const cs = singleActiveTurn.conflict_state;
201
251
  const files = cs.conflict_error?.conflicting_files || [];
252
+ const [reassignAction, mergeAction] = deriveConflictedTurnResolutionActions(singleActiveTurn.turn_id);
202
253
  console.log(` ${chalk.dim('Conflict:')} ${chalk.red(`${files.length} file(s) conflicting`)} — detection #${cs.detection_count || 1}`);
203
- console.log(` ${chalk.dim('Resolve:')} ${chalk.cyan('agentxchain reject-turn --reassign')}`);
204
- console.log(` ${chalk.dim(' or:')} ${chalk.cyan('agentxchain accept-turn --resolution human_merge')}`);
254
+ console.log(` ${chalk.dim('Resolve:')} ${chalk.cyan(reassignAction.command)}`);
255
+ console.log(` ${chalk.dim(' or:')} ${chalk.cyan(mergeAction.command)}`);
205
256
  }
206
257
  } else {
207
258
  console.log(` ${chalk.dim('Turn:')} ${chalk.yellow('No active turn')}`);
@@ -231,7 +282,6 @@ function renderGovernedStatus(context, opts) {
231
282
  if (state?.blocked_on) {
232
283
  console.log('');
233
284
  if (state.status === 'blocked') {
234
- const recovery = deriveRecoveryDescriptor(state, config);
235
285
  const detail = recovery?.detail || state.blocked_on;
236
286
  console.log(` ${chalk.dim('Blocked:')} ${chalk.red.bold('BLOCKED')} — ${detail}`);
237
287
  } else if (state.blocked_on.startsWith('human_approval:')) {
@@ -246,7 +296,6 @@ function renderGovernedStatus(context, opts) {
246
296
  renderLastGateFailure(state.last_gate_failure, config);
247
297
  }
248
298
 
249
- const recovery = deriveRecoveryDescriptor(state, config);
250
299
  if (recovery) {
251
300
  console.log('');
252
301
  console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
@@ -258,18 +307,26 @@ function renderGovernedStatus(context, opts) {
258
307
  }
259
308
  }
260
309
 
310
+ if (runtimeGuidance.length > 0) {
311
+ console.log('');
312
+ console.log(` ${chalk.dim('Runtime guidance:')}`);
313
+ for (const entry of runtimeGuidance) {
314
+ console.log(` ${chalk.yellow('•')} ${entry.code} — ${chalk.cyan(entry.command)}`);
315
+ console.log(` ${chalk.dim(`${entry.role_id} owns ${entry.artifact_path} at ${entry.phase}/${entry.gate_id}`)}`);
316
+ console.log(` ${chalk.dim(entry.reason)}`);
317
+ }
318
+ }
319
+
261
320
  if (state?.pending_phase_transition) {
262
321
  const pt = state.pending_phase_transition;
263
322
  console.log(` ${chalk.dim('Pending:')} ${formatGovernedPhase(pt.from)} → ${formatGovernedPhase(pt.to)}`);
264
323
  console.log(` ${chalk.dim('Gate:')} ${pt.gate} (requires human approval)`);
265
- console.log(` ${chalk.dim('Action:')} Run ${chalk.cyan('agentxchain approve-transition')} to advance`);
266
324
  }
267
325
 
268
326
  if (state?.pending_run_completion) {
269
327
  const pc = state.pending_run_completion;
270
328
  console.log(` ${chalk.dim('Pending:')} ${chalk.bold('Run Completion')}`);
271
329
  console.log(` ${chalk.dim('Gate:')} ${pc.gate} (requires human approval)`);
272
- console.log(` ${chalk.dim('Action:')} Run ${chalk.cyan('agentxchain approve-completion')} to finalize`);
273
330
  }
274
331
 
275
332
  if (state?.status === 'completed') {
@@ -459,6 +516,81 @@ function renderWorkflowKitArtifactsSection(wkData) {
459
516
  }
460
517
  }
461
518
 
519
+ function renderRecentEventSummary(summary) {
520
+ if (!summary) return;
521
+
522
+ const label = summary.freshness === 'recent'
523
+ ? chalk.green('recent')
524
+ : summary.freshness === 'quiet'
525
+ ? chalk.yellow('quiet')
526
+ : summary.freshness === 'unknown'
527
+ ? chalk.yellow('unknown timing')
528
+ : chalk.dim('none recorded');
529
+ const countLabel = summary.freshness === 'no_events'
530
+ ? null
531
+ : `${summary.recent_count || 0} in last ${summary.window_minutes || 15}m`;
532
+
533
+ console.log(` ${chalk.dim('Recent events:')} ${label}${countLabel ? chalk.dim(` (${countLabel})`) : ''}`);
534
+ if (summary.latest_event) {
535
+ console.log(` ${chalk.dim('Latest:')} ${summary.latest_event.summary || summary.latest_event.event_type || 'unknown_event'}`);
536
+ console.log(` ${chalk.dim('When:')} ${summary.latest_event.timestamp || 'unknown'}`);
537
+ }
538
+ console.log('');
539
+ }
540
+
541
+ function formatRepoDecisionHeadline(summary) {
542
+ if (!summary) return 'none';
543
+ const parts = [`${summary.active_count} active`];
544
+ if (summary.overridden_count > 0) {
545
+ parts.push(`${summary.overridden_count} overridden`);
546
+ }
547
+ return parts.join(', ');
548
+ }
549
+
550
+ function formatRepoDecisionCarryover(summary) {
551
+ if (!summary?.operator_summary) {
552
+ return '';
553
+ }
554
+
555
+ const { operator_summary: operatorSummary, active_count: activeCount } = summary;
556
+ const parts = [];
557
+
558
+ if (activeCount === 0) {
559
+ parts.push('no active cross-run constraints remain');
560
+ } else if (Array.isArray(operatorSummary.active_categories) && operatorSummary.active_categories.length > 0) {
561
+ parts.push(`categories: ${operatorSummary.active_categories.join(', ')}`);
562
+ }
563
+
564
+ if (typeof operatorSummary.highest_active_authority_level === 'number') {
565
+ const roleLabel = operatorSummary.highest_active_authority_role
566
+ ? ` (${operatorSummary.highest_active_authority_role})`
567
+ : '';
568
+ parts.push(`highest authority: ${operatorSummary.highest_active_authority_level}${roleLabel}`);
569
+ }
570
+
571
+ if (operatorSummary.superseding_active_count > 0) {
572
+ parts.push(pluralizeRepoDecisionCount(
573
+ operatorSummary.superseding_active_count,
574
+ 'active superseding earlier decision',
575
+ 'active superseding earlier decisions',
576
+ ));
577
+ }
578
+
579
+ if (operatorSummary.overridden_with_successor_count > 0) {
580
+ parts.push(pluralizeRepoDecisionCount(
581
+ operatorSummary.overridden_with_successor_count,
582
+ 'overridden with recorded successor',
583
+ 'overridden with recorded successors',
584
+ ));
585
+ }
586
+
587
+ return parts.join('; ');
588
+ }
589
+
590
+ function pluralizeRepoDecisionCount(count, singular, plural) {
591
+ return `${count} ${count === 1 ? singular : plural}`;
592
+ }
593
+
462
594
  function renderLastGateFailure(failure, config) {
463
595
  const entryRole = config?.routing?.[failure.phase]?.entry_role || null;
464
596
  const suggestedCommand = entryRole ? `agentxchain step --role ${entryRole}` : 'agentxchain step --role <role>';
@@ -62,9 +62,11 @@ import {
62
62
  } from '../lib/turn-paths.js';
63
63
  import { dispatchApiProxy } from '../lib/adapters/api-proxy-adapter.js';
64
64
  import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
65
+ import { deriveConflictedTurnResolutionActions } from '../lib/conflict-actions.js';
65
66
  import { runHooks } from '../lib/hook-runner.js';
66
67
  import { finalizeDispatchManifest, verifyDispatchManifest } from '../lib/dispatch-manifest.js';
67
68
  import { resolveGovernedRole } from '../lib/role-resolution.js';
69
+ import { shouldSuggestManualQaFallback } from '../lib/manual-qa-fallback.js';
68
70
 
69
71
  export async function stepCommand(opts) {
70
72
  const context = loadProjectContext();
@@ -73,7 +75,7 @@ export async function stepCommand(opts) {
73
75
  process.exit(1);
74
76
  }
75
77
 
76
- const { root, config, rawConfig } = context;
78
+ const { root, config } = context;
77
79
 
78
80
  if (config.protocol_mode !== 'governed') {
79
81
  console.log(chalk.red('The step command is only available for governed projects.'));
@@ -132,11 +134,12 @@ export async function stepCommand(opts) {
132
134
 
133
135
  // If the target turn is conflicted, print recovery paths instead of resuming
134
136
  if (targetTurn.status === 'conflicted') {
137
+ const [reassignAction, mergeAction] = deriveConflictedTurnResolutionActions(targetTurn.turn_id);
135
138
  console.log(chalk.yellow(`Turn ${targetTurn.turn_id} is conflicted. Resolve the conflict before resuming.`));
136
139
  console.log('');
137
140
  console.log(chalk.dim('Recovery options:'));
138
- console.log(` ${chalk.cyan(`agentxchain reject-turn --turn ${targetTurn.turn_id} --reassign`)} — reject and re-dispatch with conflict context`);
139
- console.log(` ${chalk.cyan(`agentxchain accept-turn --turn ${targetTurn.turn_id} --resolution human_merge`)} — manually merge and re-accept`);
141
+ console.log(` ${chalk.cyan(reassignAction.command)} — ${reassignAction.description}`);
142
+ console.log(` ${chalk.cyan(mergeAction.command)} — ${mergeAction.description}`);
140
143
  process.exit(1);
141
144
  }
142
145
 
@@ -166,13 +169,13 @@ export async function stepCommand(opts) {
166
169
 
167
170
  if (!skipAssignment) {
168
171
  if (state.pending_phase_transition || state.pending_run_completion) {
169
- printRecoverySummary(state, 'This run is awaiting approval.');
172
+ printRecoverySummary(state, 'This run is awaiting approval.', config);
170
173
  process.exit(1);
171
174
  }
172
175
 
173
176
  if (state.status === 'blocked' && activeCount > 0) {
174
177
  if (!opts.resume) {
175
- printRecoverySummary(state, 'This run is blocked on a retained turn.');
178
+ printRecoverySummary(state, 'This run is blocked on a retained turn.', config);
176
179
  process.exit(1);
177
180
  }
178
181
 
@@ -199,11 +202,12 @@ export async function stepCommand(opts) {
199
202
 
200
203
  // If the target turn is conflicted, print recovery paths
201
204
  if (targetTurn.status === 'conflicted') {
205
+ const [reassignAction, mergeAction] = deriveConflictedTurnResolutionActions(targetTurn.turn_id);
202
206
  console.log(chalk.yellow(`Turn ${targetTurn.turn_id} is conflicted. Resolve the conflict before resuming.`));
203
207
  console.log('');
204
208
  console.log(chalk.dim('Recovery options:'));
205
- console.log(` ${chalk.cyan(`agentxchain reject-turn --turn ${targetTurn.turn_id} --reassign`)} — reject and re-dispatch with conflict context`);
206
- console.log(` ${chalk.cyan(`agentxchain accept-turn --turn ${targetTurn.turn_id} --resolution human_merge`)} — manually merge and re-accept`);
209
+ console.log(` ${chalk.cyan(reassignAction.command)} — ${reassignAction.description}`);
210
+ console.log(` ${chalk.cyan(mergeAction.command)} — ${mergeAction.description}`);
207
211
  process.exit(1);
208
212
  }
209
213
 
@@ -291,7 +295,7 @@ export async function stepCommand(opts) {
291
295
  const assignResult = assignGovernedTurn(root, config, roleId);
292
296
  if (!assignResult.ok) {
293
297
  if (assignResult.error_code?.startsWith('hook_') || assignResult.error_code === 'hook_blocked') {
294
- printAssignmentHookFailure(assignResult, roleId);
298
+ printAssignmentHookFailure(assignResult, roleId, config);
295
299
  }
296
300
  console.log(chalk.red(`Failed to assign turn: ${assignResult.error}`));
297
301
  process.exit(1);
@@ -357,6 +361,7 @@ export async function stepCommand(opts) {
357
361
  turnId: turn.turn_id,
358
362
  roleId,
359
363
  action: `Fix or reconfigure the hook, then rerun agentxchain step --resume${turn.turn_id ? ` --turn ${turn.turn_id}` : ''}`,
364
+ config,
360
365
  });
361
366
  process.exit(1);
362
367
  }
@@ -430,12 +435,12 @@ export async function stepCommand(opts) {
430
435
  console.log(chalk.dim(` Retry trace: ${apiResult.retry_trace_path}`));
431
436
  }
432
437
 
433
- if (
434
- apiResult.classified?.error_class === 'missing_credentials'
435
- && roleId === 'qa'
436
- && config.roles?.qa?.runtime_id === 'api-qa'
437
- && rawConfig?.runtimes?.['manual-qa']?.type === 'manual'
438
- ) {
438
+ if (shouldSuggestManualQaFallback({
439
+ roleId,
440
+ runtimeId,
441
+ classified: apiResult.classified,
442
+ config,
443
+ })) {
439
444
  console.log(chalk.dim(' No-key QA fallback:'));
440
445
  console.log(chalk.dim(' - Edit agentxchain.json and change roles.qa.runtime from "api-qa" to "manual-qa"'));
441
446
  console.log(chalk.dim(' - Then rerun: agentxchain step --resume'));
@@ -737,6 +742,7 @@ export async function stepCommand(opts) {
737
742
  turnId: turn.turn_id,
738
743
  roleId,
739
744
  action: `Fix or reconfigure the hook, then rerun agentxchain step --resume${turn.turn_id ? ` --turn ${turn.turn_id}` : ''}`,
745
+ config,
740
746
  });
741
747
  process.exit(1);
742
748
  }
@@ -769,6 +775,7 @@ export async function stepCommand(opts) {
769
775
  turnId: turn.turn_id,
770
776
  roleId,
771
777
  action: `Fix or reconfigure the hook, then rerun agentxchain step --resume${turn.turn_id ? ` --turn ${turn.turn_id}` : ''}`,
778
+ config,
772
779
  });
773
780
  process.exit(1);
774
781
  }
@@ -779,14 +786,14 @@ export async function stepCommand(opts) {
779
786
  const acceptResult = acceptGovernedTurn(root, config, { turnId: turn.turn_id });
780
787
  if (!acceptResult.ok) {
781
788
  if (acceptResult.accepted && acceptResult.error_code?.startsWith('hook_')) {
782
- printAcceptedHookFailure(acceptResult);
789
+ printAcceptedHookFailure(acceptResult, config);
783
790
  } else {
784
791
  console.log(chalk.red(`Acceptance failed: ${acceptResult.error}`));
785
792
  }
786
793
  process.exit(1);
787
794
  }
788
795
 
789
- printAcceptSummary(acceptResult);
796
+ printAcceptSummary(acceptResult, config);
790
797
  } else {
791
798
  // Reject and potentially retry
792
799
  console.log(chalk.yellow('Validation failed:'));
@@ -808,7 +815,7 @@ export async function stepCommand(opts) {
808
815
  }
809
816
 
810
817
  if (rejectResult.escalated) {
811
- printEscalationSummary(rejectResult);
818
+ printEscalationSummary(rejectResult, config);
812
819
  } else {
813
820
  console.log(chalk.yellow('Turn rejected for retry.'));
814
821
  console.log(` Attempt: ${rejectResult.state?.current_turn?.attempt}`);
@@ -887,8 +894,8 @@ function blockStepForHookIssue(root, state, turn, { hookResults, phase, defaultD
887
894
  };
888
895
  }
889
896
 
890
- function printLifecycleHookFailure(title, result, { turnId, roleId, action }) {
891
- const recovery = deriveRecoveryDescriptor(result.state);
897
+ function printLifecycleHookFailure(title, result, { turnId, roleId, action, config }) {
898
+ const recovery = deriveRecoveryDescriptor(result.state, config);
892
899
  const hookName = result.hookResults?.blocker?.hook_name
893
900
  || result.hookResults?.results?.find((entry) => entry.hook_name)?.hook_name
894
901
  || '(unknown)';
@@ -935,8 +942,8 @@ function resolveTargetRole(opts, state, config) {
935
942
  return resolved.roleId;
936
943
  }
937
944
 
938
- function printRecoverySummary(state, heading) {
939
- const recovery = deriveRecoveryDescriptor(state);
945
+ function printRecoverySummary(state, heading, config) {
946
+ const recovery = deriveRecoveryDescriptor(state, config);
940
947
  console.log(chalk.yellow(heading));
941
948
  if (!recovery) {
942
949
  return;
@@ -1007,8 +1014,8 @@ function printAssignmentWarnings(assignResult) {
1007
1014
  }
1008
1015
  }
1009
1016
 
1010
- function printAssignmentHookFailure(result, roleId) {
1011
- const recovery = deriveRecoveryDescriptor(result.state);
1017
+ function printAssignmentHookFailure(result, roleId, config) {
1018
+ const recovery = deriveRecoveryDescriptor(result.state, config);
1012
1019
  const hookName = result.hookResults?.blocker?.hook_name
1013
1020
  || result.hookResults?.results?.find((entry) => entry.hook_name)?.hook_name
1014
1021
  || '(unknown)';
@@ -1034,8 +1041,8 @@ function printAssignmentHookFailure(result, roleId) {
1034
1041
  console.log('');
1035
1042
  }
1036
1043
 
1037
- function printAcceptedHookFailure(result) {
1038
- const recovery = deriveRecoveryDescriptor(result.state);
1044
+ function printAcceptedHookFailure(result, config) {
1045
+ const recovery = deriveRecoveryDescriptor(result.state, config);
1039
1046
  const hookName = result.hookResults?.results?.find((entry) => entry.hook_name)?.hook_name || '(unknown)';
1040
1047
 
1041
1048
  console.log('');
@@ -1058,9 +1065,9 @@ function printAcceptedHookFailure(result) {
1058
1065
  console.log('');
1059
1066
  }
1060
1067
 
1061
- function printAcceptSummary(result) {
1068
+ function printAcceptSummary(result, config) {
1062
1069
  const accepted = result.accepted;
1063
- const recovery = deriveRecoveryDescriptor(result.state);
1070
+ const recovery = deriveRecoveryDescriptor(result.state, config);
1064
1071
  console.log('');
1065
1072
  console.log(chalk.green(' Turn Accepted'));
1066
1073
  console.log(chalk.dim(' ' + '-'.repeat(44)));
@@ -1104,8 +1111,8 @@ function printAcceptSummary(result) {
1104
1111
  console.log('');
1105
1112
  }
1106
1113
 
1107
- function printEscalationSummary(result) {
1108
- const recovery = deriveRecoveryDescriptor(result.state);
1114
+ function printEscalationSummary(result, config) {
1115
+ const recovery = deriveRecoveryDescriptor(result.state, config);
1109
1116
  console.log('');
1110
1117
  console.log(chalk.red(' Turn Escalated'));
1111
1118
  console.log(chalk.dim(' ' + '-'.repeat(44)));