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
@@ -0,0 +1,128 @@
1
+ import { buildCoordinatorRepoStatusEntries } from './coordinator-repo-status-presentation.js';
2
+
3
+ function collectCoordinatorRunIdMismatches(repoStatusEntries) {
4
+ return repoStatusEntries
5
+ .filter((entry) => entry?.run_id_mismatch)
6
+ .map((entry) => entry.run_id_mismatch);
7
+ }
8
+
9
+ function collectCoordinatorStatusDrift(repoStatusEntries) {
10
+ return repoStatusEntries
11
+ .filter((entry) => entry?.status_drift)
12
+ .map((entry) => entry.status_drift);
13
+ }
14
+
15
+ export function detectCoordinatorRunIdMismatches(repos, coordinatorRepoRuns) {
16
+ return collectCoordinatorRunIdMismatches(
17
+ buildCoordinatorRepoStatusEntries({
18
+ coordinatorRepoRuns,
19
+ repoSnapshots: repos,
20
+ }),
21
+ );
22
+ }
23
+
24
+ export function detectCoordinatorRepoStatusDrift(repos, coordinatorRepoRuns) {
25
+ return collectCoordinatorStatusDrift(
26
+ buildCoordinatorRepoStatusEntries({
27
+ coordinatorRepoRuns,
28
+ repoSnapshots: repos,
29
+ }),
30
+ );
31
+ }
32
+
33
+ export function deriveCoordinatorNextActions({
34
+ status,
35
+ blockedReason,
36
+ pendingGate,
37
+ repos,
38
+ coordinatorRepoRuns,
39
+ runIdMismatches,
40
+ }) {
41
+ const nextActions = [];
42
+ if (status === 'completed') {
43
+ return nextActions;
44
+ }
45
+
46
+ const repoStatusEntries = buildCoordinatorRepoStatusEntries({
47
+ coordinatorRepoRuns,
48
+ repoSnapshots: repos,
49
+ });
50
+ const mismatches = Array.isArray(runIdMismatches)
51
+ ? runIdMismatches
52
+ : collectCoordinatorRunIdMismatches(repoStatusEntries);
53
+ const statusDrift = collectCoordinatorStatusDrift(repoStatusEntries);
54
+
55
+ if (mismatches.length > 0) {
56
+ nextActions.push({
57
+ code: 'repo_run_id_mismatch',
58
+ command: 'agentxchain multi resume',
59
+ reason: `Coordinator run identity drift detected${blockedReason ? `: ${blockedReason}` : ''}. Resume after reconciling the affected child repos.`,
60
+ });
61
+ for (const mismatch of mismatches) {
62
+ nextActions.push({
63
+ code: 'repo_run_id_mismatch',
64
+ command: 'agentxchain multi resume',
65
+ reason: `Repo "${mismatch.repo_id}" run identity drifted: coordinator expects "${mismatch.expected_run_id}" but repo has "${mismatch.actual_run_id}". Re-link the correct child run, then resume.`,
66
+ });
67
+ }
68
+ if (pendingGate) {
69
+ nextActions.push({
70
+ code: 'pending_gate',
71
+ command: 'agentxchain multi approve-gate',
72
+ reason: `After resume succeeds, approve pending gate "${pendingGate.gate}" (${pendingGate.gate_type}).`,
73
+ });
74
+ }
75
+ return nextActions;
76
+ }
77
+
78
+ if (statusDrift.length > 0) {
79
+ nextActions.push({
80
+ code: 'resync',
81
+ command: 'agentxchain multi resync',
82
+ reason: `Coordinator state disagrees with repo authority for: ${statusDrift.map((entry) => entry.repo_id).join(', ')}.`,
83
+ });
84
+ if (pendingGate) {
85
+ nextActions.push({
86
+ code: 'pending_gate',
87
+ command: 'agentxchain multi approve-gate',
88
+ reason: `If resync preserves gate "${pendingGate.gate}", approve it afterward.`,
89
+ });
90
+ }
91
+ return nextActions;
92
+ }
93
+
94
+ if (status === 'blocked') {
95
+ nextActions.push({
96
+ code: 'resume',
97
+ command: 'agentxchain multi resume',
98
+ reason: `Coordinator is blocked${blockedReason ? `: ${blockedReason}` : ''}. Resume after fixing the underlying issue.`,
99
+ });
100
+ if (pendingGate) {
101
+ nextActions.push({
102
+ code: 'pending_gate',
103
+ command: 'agentxchain multi approve-gate',
104
+ reason: `After resume succeeds, approve pending gate "${pendingGate.gate}" (${pendingGate.gate_type}).`,
105
+ });
106
+ }
107
+ return nextActions;
108
+ }
109
+
110
+ if (pendingGate) {
111
+ nextActions.push({
112
+ code: 'pending_gate',
113
+ command: 'agentxchain multi approve-gate',
114
+ reason: `Coordinator is waiting on pending gate "${pendingGate.gate}" (${pendingGate.gate_type}).`,
115
+ });
116
+ return nextActions;
117
+ }
118
+
119
+ if (status === 'active' || status === 'paused') {
120
+ nextActions.push({
121
+ code: 'step',
122
+ command: 'agentxchain multi step',
123
+ reason: 'Coordinator has no blocked state or pending gate and can continue.',
124
+ });
125
+ }
126
+
127
+ return nextActions;
128
+ }
@@ -0,0 +1,79 @@
1
+ import { pushDetail } from './coordinator-presentation-detail.js';
2
+
3
+ function isObject(value) {
4
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
5
+ }
6
+
7
+ function pickString(...values) {
8
+ for (const value of values) {
9
+ if (typeof value === 'string' && value.trim().length > 0) {
10
+ return value;
11
+ }
12
+ }
13
+ return null;
14
+ }
15
+
16
+ function pickArray(...values) {
17
+ for (const value of values) {
18
+ if (Array.isArray(value) && value.length > 0) {
19
+ return value.map((item) => String(item));
20
+ }
21
+ }
22
+ return [];
23
+ }
24
+
25
+ export function getCoordinatorPendingGateSnapshot({ pendingGate = null, active = null } = {}) {
26
+ const hasPendingGate = isObject(pendingGate);
27
+ const hasActive = isObject(active);
28
+ if (!hasPendingGate && !hasActive) {
29
+ return null;
30
+ }
31
+
32
+ const gateType = pickString(pendingGate?.gate_type, active?.gate_type);
33
+ const gateId = pickString(pendingGate?.gate, active?.gate_id);
34
+ if (!gateType && !gateId) {
35
+ return null;
36
+ }
37
+
38
+ return {
39
+ gate_type: gateType,
40
+ gate_id: gateId,
41
+ current_phase: pickString(pendingGate?.from, active?.current_phase),
42
+ target_phase: pickString(pendingGate?.to, active?.target_phase),
43
+ required_repos: pickArray(pendingGate?.required_repos, active?.required_repos),
44
+ human_barriers: pickArray(pendingGate?.human_barriers, active?.human_barriers),
45
+ approval_state: 'Awaiting human approval',
46
+ };
47
+ }
48
+
49
+ export function getCoordinatorPendingGateDetails({
50
+ pendingGate = null,
51
+ active = null,
52
+ includeType = true,
53
+ includeApprovalState = true,
54
+ includeHumanBarriers = true,
55
+ } = {}) {
56
+ const snapshot = getCoordinatorPendingGateSnapshot({ pendingGate, active });
57
+ if (!snapshot) {
58
+ return [];
59
+ }
60
+
61
+ const details = [];
62
+ if (includeType) {
63
+ pushDetail(details, 'Type', snapshot.gate_type);
64
+ }
65
+ pushDetail(details, 'Gate', snapshot.gate_id, { mono: true });
66
+ pushDetail(details, 'Current Phase', snapshot.current_phase);
67
+ pushDetail(details, 'Target Phase', snapshot.target_phase);
68
+ if (snapshot.required_repos.length > 0) {
69
+ pushDetail(details, 'Required Repos', snapshot.required_repos.join(', '));
70
+ }
71
+ if (includeApprovalState) {
72
+ pushDetail(details, 'Approval State', snapshot.approval_state);
73
+ }
74
+ if (includeHumanBarriers && snapshot.human_barriers.length > 0) {
75
+ pushDetail(details, 'Human Barriers', snapshot.human_barriers.join(', '));
76
+ }
77
+
78
+ return details;
79
+ }
@@ -0,0 +1,11 @@
1
+ export function pushDetail(details, label, value, options = {}) {
2
+ if (value == null || value === '') {
3
+ return;
4
+ }
5
+
6
+ details.push({
7
+ label,
8
+ value: String(value),
9
+ mono: options.mono === true,
10
+ });
11
+ }
@@ -0,0 +1,53 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ function readRepoStateSnapshot(repo) {
5
+ const statePath = join(repo.resolved_path, '.agentxchain', 'state.json');
6
+ if (!existsSync(statePath)) {
7
+ return {
8
+ repo_id: repo.repo_id,
9
+ ok: false,
10
+ status: null,
11
+ run_id: null,
12
+ phase: null,
13
+ };
14
+ }
15
+
16
+ try {
17
+ const state = JSON.parse(readFileSync(statePath, 'utf8'));
18
+ return {
19
+ repo_id: repo.repo_id,
20
+ ok: true,
21
+ status: state?.status ?? null,
22
+ run_id: state?.run_id ?? null,
23
+ phase: state?.phase ?? null,
24
+ };
25
+ } catch {
26
+ return {
27
+ repo_id: repo.repo_id,
28
+ ok: false,
29
+ status: null,
30
+ run_id: null,
31
+ phase: null,
32
+ };
33
+ }
34
+ }
35
+
36
+ export function collectCoordinatorRepoSnapshots(config) {
37
+ return (config?.repo_order || []).map((repoId) => {
38
+ const repo = config?.repos?.[repoId];
39
+ if (!repo?.resolved_path) {
40
+ return {
41
+ repo_id: repoId,
42
+ ok: false,
43
+ status: null,
44
+ run_id: null,
45
+ phase: null,
46
+ };
47
+ }
48
+ return readRepoStateSnapshot({
49
+ repo_id: repoId,
50
+ resolved_path: repo.resolved_path,
51
+ });
52
+ });
53
+ }
@@ -0,0 +1,134 @@
1
+ import { collectCoordinatorRepoSnapshots } from './coordinator-repo-snapshots.js';
2
+
3
+ export function normalizeCoordinatorRepoStatus(status) {
4
+ if (status === 'linked' || status === 'initialized') {
5
+ return 'active';
6
+ }
7
+ return status || null;
8
+ }
9
+
10
+ function resolveCoordinatorRepoOrder({ config, coordinatorRepoRuns, repoSnapshots }) {
11
+ if (Array.isArray(config?.repo_order) && config.repo_order.length > 0) {
12
+ return config.repo_order;
13
+ }
14
+
15
+ const repoOrder = [];
16
+ const seen = new Set();
17
+
18
+ for (const repoId of Object.keys(coordinatorRepoRuns || {})) {
19
+ if (!seen.has(repoId)) {
20
+ seen.add(repoId);
21
+ repoOrder.push(repoId);
22
+ }
23
+ }
24
+
25
+ for (const snapshot of Array.isArray(repoSnapshots) ? repoSnapshots : []) {
26
+ const repoId = snapshot?.repo_id;
27
+ if (typeof repoId === 'string' && repoId.length > 0 && !seen.has(repoId)) {
28
+ seen.add(repoId);
29
+ repoOrder.push(repoId);
30
+ }
31
+ }
32
+
33
+ return repoOrder;
34
+ }
35
+
36
+ export function buildCoordinatorRepoStatusEntries({
37
+ config,
38
+ coordinatorRepoRuns,
39
+ repoSnapshots,
40
+ }) {
41
+ const resolvedRepoSnapshots = Array.isArray(repoSnapshots)
42
+ ? repoSnapshots
43
+ : (config ? collectCoordinatorRepoSnapshots(config) : []);
44
+ const repoOrder = resolveCoordinatorRepoOrder({
45
+ config,
46
+ coordinatorRepoRuns,
47
+ repoSnapshots: resolvedRepoSnapshots,
48
+ });
49
+ const snapshotByRepoId = new Map(
50
+ resolvedRepoSnapshots
51
+ .filter((entry) => typeof entry?.repo_id === 'string' && entry.repo_id.length > 0)
52
+ .map((entry) => [entry.repo_id, entry]),
53
+ );
54
+
55
+ return repoOrder.map((repoId) => {
56
+ const coordinatorRepoRun = coordinatorRepoRuns?.[repoId] || {};
57
+ const repoSnapshot = snapshotByRepoId.get(repoId) || null;
58
+ const repoAuthorityAvailable = repoSnapshot?.ok === true;
59
+ const coordinatorStatus = coordinatorRepoRun.status || null;
60
+ const normalizedCoordinatorStatus = normalizeCoordinatorRepoStatus(coordinatorStatus);
61
+ const repoAuthorityStatus = repoAuthorityAvailable ? (repoSnapshot.status || null) : null;
62
+ const repoAuthorityRunId = repoAuthorityAvailable ? (repoSnapshot.run_id ?? null) : null;
63
+ const runIdMismatch = (
64
+ repoAuthorityAvailable
65
+ && coordinatorRepoRun.run_id
66
+ && repoAuthorityRunId
67
+ && coordinatorRepoRun.run_id !== repoAuthorityRunId
68
+ )
69
+ ? {
70
+ repo_id: repoId,
71
+ expected_run_id: coordinatorRepoRun.run_id,
72
+ actual_run_id: repoAuthorityRunId,
73
+ }
74
+ : null;
75
+ const statusDrift = (
76
+ repoAuthorityAvailable
77
+ && normalizedCoordinatorStatus
78
+ && repoAuthorityStatus
79
+ && normalizedCoordinatorStatus !== repoAuthorityStatus
80
+ )
81
+ ? {
82
+ repo_id: repoId,
83
+ coordinator_status: coordinatorStatus,
84
+ repo_status: repoAuthorityStatus,
85
+ }
86
+ : null;
87
+ const details = [];
88
+
89
+ if (coordinatorStatus === 'linked' || coordinatorStatus === 'initialized') {
90
+ details.push({
91
+ label: 'coordinator',
92
+ value: coordinatorStatus,
93
+ });
94
+ }
95
+
96
+ if (runIdMismatch) {
97
+ details.push({
98
+ label: 'expected run',
99
+ value: runIdMismatch.expected_run_id,
100
+ mono: true,
101
+ });
102
+ }
103
+
104
+ return {
105
+ repo_id: repoId,
106
+ run_id: repoAuthorityAvailable
107
+ ? (repoAuthorityRunId ?? coordinatorRepoRun.run_id ?? null)
108
+ : (coordinatorRepoRun.run_id ?? null),
109
+ status: repoAuthorityAvailable
110
+ ? (repoAuthorityStatus || 'unknown')
111
+ : (normalizedCoordinatorStatus || 'unknown'),
112
+ phase: repoAuthorityAvailable
113
+ ? (repoSnapshot.phase ?? coordinatorRepoRun.phase ?? null)
114
+ : (coordinatorRepoRun.phase ?? null),
115
+ details,
116
+ coordinator_status: coordinatorStatus,
117
+ normalized_coordinator_status: normalizedCoordinatorStatus,
118
+ repo_authority_available: repoAuthorityAvailable,
119
+ run_id_mismatch: runIdMismatch,
120
+ status_drift: statusDrift,
121
+ };
122
+ });
123
+ }
124
+
125
+ export function buildCoordinatorRepoStatusRows({ config, coordinatorRepoRuns, repoSnapshots }) {
126
+ return buildCoordinatorRepoStatusEntries({ config, coordinatorRepoRuns, repoSnapshots })
127
+ .map((entry) => ({
128
+ repo_id: entry.repo_id,
129
+ run_id: entry.run_id,
130
+ status: entry.status,
131
+ phase: entry.phase,
132
+ details: entry.details,
133
+ }));
134
+ }
@@ -1,7 +1,12 @@
1
1
  import { dirname } from 'path';
2
2
  import { loadProjectContext } from '../config.js';
3
3
  import { approvePhaseTransition, approveRunCompletion } from '../governed-state.js';
4
- import { deriveRecoveryDescriptor } from '../blocked-state.js';
4
+ import { deriveGovernedRunNextActions, deriveRecoveryDescriptor } from '../blocked-state.js';
5
+ import {
6
+ deriveCoordinatorGateNextActions,
7
+ normalizeCoordinatorGateApprovalFailure,
8
+ normalizeCoordinatorGateApprovalSuccess,
9
+ } from '../coordinator-gate-approval.js';
5
10
  import { loadCoordinatorConfig } from '../coordinator-config.js';
6
11
  import { loadCoordinatorState } from '../coordinator-state.js';
7
12
  import { buildGatePayload, fireCoordinatorHook } from '../coordinator-hooks.js';
@@ -21,6 +26,16 @@ function buildError(status, code, error, extra = {}) {
21
26
  }
22
27
 
23
28
  function normalizeRepoSuccess(result, gateType) {
29
+ const nextActions = gateType === 'phase_transition'
30
+ ? [
31
+ {
32
+ command: 'agentxchain step',
33
+ reason: `Run is active in phase "${result.state?.phase || 'unknown'}" and can continue.`,
34
+ },
35
+ ]
36
+ : [];
37
+ const nextAction = nextActions[0]?.command ?? null;
38
+
24
39
  if (gateType === 'phase_transition') {
25
40
  return {
26
41
  status: 200,
@@ -28,8 +43,11 @@ function normalizeRepoSuccess(result, gateType) {
28
43
  ok: true,
29
44
  scope: 'repo',
30
45
  gate_type: 'phase_transition',
46
+ status: result.state?.status || null,
47
+ phase: result.state?.phase || null,
31
48
  message: `Phase transition approved: ${result.transition.from} -> ${result.transition.to}`,
32
- next_action: 'agentxchain step',
49
+ next_action: nextAction,
50
+ next_actions: nextActions,
33
51
  },
34
52
  };
35
53
  }
@@ -40,17 +58,59 @@ function normalizeRepoSuccess(result, gateType) {
40
58
  ok: true,
41
59
  scope: 'repo',
42
60
  gate_type: 'run_completion',
61
+ status: result.state?.status || null,
62
+ phase: result.state?.phase || null,
43
63
  message: 'Run completion approved. Run is now completed.',
44
- next_action: null,
64
+ next_action: nextAction,
65
+ next_actions: nextActions,
45
66
  },
46
67
  };
47
68
  }
48
69
 
49
- function normalizeRepoFailure(result) {
50
- const recovery = result.state ? deriveRecoveryDescriptor(result.state) : null;
70
+ function deriveRepoHookName(result) {
71
+ return result?.hookResults?.blocker?.hook_name
72
+ || result?.hookResults?.results?.find((entry) => entry?.hook_name)?.hook_name
73
+ || null;
74
+ }
75
+
76
+ function buildRecoverySummary(recovery) {
77
+ if (!recovery || typeof recovery !== 'object') {
78
+ return null;
79
+ }
80
+
81
+ return {
82
+ typed_reason: recovery.typed_reason || 'approval_failed',
83
+ owner: recovery.owner || 'human',
84
+ recovery_action: recovery.recovery_action || null,
85
+ detail: recovery.detail ?? null,
86
+ turn_retained: typeof recovery.turn_retained === 'boolean' ? recovery.turn_retained : false,
87
+ runtime_guidance: Array.isArray(recovery.runtime_guidance) ? recovery.runtime_guidance : [],
88
+ };
89
+ }
90
+
91
+ function normalizeRepoFailure(result, config) {
92
+ const recovery = result.state ? deriveRecoveryDescriptor(result.state, config) : null;
93
+ const nextActions = result.state ? deriveGovernedRunNextActions(result.state, config) : [];
94
+ const nextAction = nextActions[0]?.command || recovery?.recovery_action || null;
51
95
  const code = result.error_code || 'approval_failed';
96
+ const gateType = result.state?.pending_phase_transition
97
+ ? 'phase_transition'
98
+ : result.state?.pending_run_completion
99
+ ? 'run_completion'
100
+ : null;
101
+ const gate = result.state?.pending_phase_transition?.gate
102
+ || result.state?.pending_run_completion?.gate
103
+ || null;
104
+ const hookPhase = code.startsWith('hook_') ? 'before_gate' : null;
52
105
  return buildError(409, code, result.error || 'Gate approval failed', {
53
- next_action: recovery?.recovery_action || null,
106
+ scope: 'repo',
107
+ gate,
108
+ gate_type: gateType,
109
+ hook_phase: hookPhase,
110
+ hook_name: hookPhase ? deriveRepoHookName(result) : null,
111
+ next_action: nextAction,
112
+ next_actions: nextActions,
113
+ recovery_summary: buildRecoverySummary(recovery),
54
114
  });
55
115
  }
56
116
 
@@ -61,34 +121,22 @@ function approveRepoGate(root, config, state) {
61
121
  : approveRunCompletion(root, config);
62
122
 
63
123
  if (!result.ok) {
64
- return normalizeRepoFailure(result);
124
+ return normalizeRepoFailure(result, config);
65
125
  }
66
126
 
67
127
  return normalizeRepoSuccess(result, gateType);
68
128
  }
69
129
 
70
- function normalizeCoordinatorSuccess(result, gateType) {
71
- if (gateType === 'phase_transition') {
72
- return {
73
- status: 200,
74
- body: {
75
- ok: true,
76
- scope: 'coordinator',
77
- gate_type: 'phase_transition',
78
- message: `Coordinator phase transition approved: ${result.transition.from} -> ${result.transition.to}`,
79
- next_action: 'agentxchain multi step',
80
- },
81
- };
82
- }
83
-
130
+ function normalizeCoordinatorSuccess(result, gateType, config) {
131
+ const payload = normalizeCoordinatorGateApprovalSuccess({
132
+ result: { ...result, config },
133
+ gateType,
134
+ });
84
135
  return {
85
136
  status: 200,
86
137
  body: {
87
- ok: true,
88
138
  scope: 'coordinator',
89
- gate_type: 'run_completion',
90
- message: 'Coordinator run completion approved. Run is now complete.',
91
- next_action: null,
139
+ ...payload,
92
140
  },
93
141
  };
94
142
  }
@@ -102,11 +150,31 @@ function approveCoordinatorGate(workspacePath, state, config) {
102
150
  if (gateHook.blocked) {
103
151
  const blocker = gateHook.verdicts.find((entry) => entry.verdict === 'block');
104
152
  const reason = blocker?.message || 'before_gate hook blocked approval';
105
- return buildError(409, 'hook_blocked', reason);
153
+ return {
154
+ status: 409,
155
+ body: normalizeCoordinatorGateApprovalFailure({
156
+ state,
157
+ config,
158
+ code: 'hook_blocked',
159
+ error: reason,
160
+ hookName: blocker?.hook_name || null,
161
+ hookPhase: 'before_gate',
162
+ }),
163
+ };
106
164
  }
107
165
 
108
166
  if (!gateHook.ok) {
109
- return buildError(409, 'hook_failed', gateHook.error || 'before_gate hook failed');
167
+ return {
168
+ status: 409,
169
+ body: normalizeCoordinatorGateApprovalFailure({
170
+ state,
171
+ config,
172
+ code: 'hook_failed',
173
+ error: gateHook.error || 'before_gate hook failed',
174
+ hookName: gateHook.results?.find((entry) => entry?.hook_name)?.hook_name || null,
175
+ hookPhase: 'before_gate',
176
+ }),
177
+ };
110
178
  }
111
179
 
112
180
  const gateType = state.pending_gate.gate_type;
@@ -121,10 +189,18 @@ function approveCoordinatorGate(workspacePath, state, config) {
121
189
  }
122
190
 
123
191
  if (!result.ok) {
124
- return buildError(409, 'approval_failed', result.error || 'Coordinator gate approval failed');
192
+ return {
193
+ status: 409,
194
+ body: normalizeCoordinatorGateApprovalFailure({
195
+ state,
196
+ config,
197
+ code: 'approval_failed',
198
+ error: result.error || 'Coordinator gate approval failed',
199
+ }),
200
+ };
125
201
  }
126
202
 
127
- return normalizeCoordinatorSuccess(result, gateType);
203
+ return normalizeCoordinatorSuccess(result, gateType, config);
128
204
  }
129
205
 
130
206
  export function approvePendingDashboardGate(agentxchainDir) {
@@ -19,6 +19,7 @@ import { FileWatcher } from './file-watcher.js';
19
19
  import { readRunEvents, RUN_EVENTS_PATH } from '../run-events.js';
20
20
  import { approvePendingDashboardGate } from './actions.js';
21
21
  import { readCoordinatorBlockerSnapshot } from './coordinator-blockers.js';
22
+ import { readCoordinatorRepoStatusRows } from './coordinator-repo-status.js';
22
23
  import { readCoordinatorTimeoutStatus } from './coordinator-timeout-status.js';
23
24
  import { readAggregatedCoordinatorEvents, watchChildRepoEvents } from './coordinator-event-aggregation.js';
24
25
  import { readWorkflowKitArtifacts } from './workflow-kit-artifacts.js';
@@ -348,6 +349,12 @@ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847,
348
349
  return;
349
350
  }
350
351
 
352
+ if (pathname === '/api/coordinator/repo-status') {
353
+ const result = readCoordinatorRepoStatusRows(workspacePath);
354
+ writeJson(res, result.status, result.body);
355
+ return;
356
+ }
357
+
351
358
  if (pathname === '/api/coordinator/timeouts') {
352
359
  const result = readCoordinatorTimeoutStatus(workspacePath);
353
360
  writeJson(res, result.status, result.body);
@@ -1,5 +1,7 @@
1
1
  import { evaluateCompletionGate, evaluatePhaseGate } from '../coordinator-gates.js';
2
2
  import { loadCoordinatorConfig } from '../coordinator-config.js';
3
+ import { deriveCoordinatorNextActions } from '../coordinator-next-actions.js';
4
+ import { collectCoordinatorRepoSnapshots } from '../coordinator-repo-snapshots.js';
3
5
  import { loadCoordinatorState } from '../coordinator-state.js';
4
6
 
5
7
  function normalizePendingGate(pendingGate) {
@@ -148,6 +150,20 @@ export function readCoordinatorBlockerSnapshot(workspacePath) {
148
150
  };
149
151
  }
150
152
 
153
+ const repos = collectCoordinatorRepoSnapshots(configResult.config);
154
+ const nextActions = deriveCoordinatorNextActions({
155
+ status: state.status ?? null,
156
+ blockedReason: state.blocked_reason ?? null,
157
+ pendingGate,
158
+ repos,
159
+ coordinatorRepoRuns: state.repo_runs || {},
160
+ runIdMismatches: active.blockers?.filter((blocker) => blocker?.code === 'repo_run_id_mismatch').map((blocker) => ({
161
+ repo_id: blocker.repo_id,
162
+ expected_run_id: blocker.expected_run_id,
163
+ actual_run_id: blocker.actual_run_id,
164
+ })),
165
+ });
166
+
151
167
  return {
152
168
  ok: true,
153
169
  status: 200,
@@ -159,6 +175,7 @@ export function readCoordinatorBlockerSnapshot(workspacePath) {
159
175
  blocked_reason: state.blocked_reason ?? null,
160
176
  pending_gate: pendingGate,
161
177
  active,
178
+ next_actions: nextActions,
162
179
  evaluations: {
163
180
  phase_transition: phaseEvaluation,
164
181
  run_completion: completionEvaluation,