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.
- package/README.md +12 -6
- package/bin/agentxchain.js +5 -5
- package/dashboard/app.js +111 -7
- package/dashboard/components/blocked.js +95 -11
- package/dashboard/components/blockers.js +85 -86
- package/dashboard/components/coordinator-timeouts.js +13 -0
- package/dashboard/components/cross-repo.js +17 -12
- package/dashboard/components/gate.js +31 -11
- package/dashboard/components/initiative.js +173 -78
- package/dashboard/components/ledger.js +28 -0
- package/dashboard/components/live-status.js +39 -0
- package/dashboard/components/run-history.js +76 -1
- package/dashboard/components/timeline.js +5 -1
- package/dashboard/index.html +21 -0
- package/dashboard/live-observer.js +91 -0
- package/package.json +1 -1
- package/scripts/release-bump.sh +26 -3
- package/src/commands/accept-turn.js +3 -3
- package/src/commands/decisions.js +98 -29
- package/src/commands/diff.js +27 -4
- package/src/commands/doctor.js +48 -16
- package/src/commands/history.js +21 -3
- package/src/commands/multi.js +223 -54
- package/src/commands/phase.js +11 -13
- package/src/commands/reject-turn.js +1 -1
- package/src/commands/restart.js +28 -11
- package/src/commands/resume.js +6 -6
- package/src/commands/role.js +51 -14
- package/src/commands/run.js +5 -11
- package/src/commands/status.js +145 -13
- package/src/commands/step.js +36 -29
- package/src/lib/admission-control.js +14 -12
- package/src/lib/blocked-state.js +150 -0
- package/src/lib/conflict-actions.js +17 -0
- package/src/lib/context-section-parser.js +2 -0
- package/src/lib/continuity-status.js +1 -1
- package/src/lib/coordinator-blocker-presentation.js +127 -0
- package/src/lib/coordinator-event-narrative.js +43 -0
- package/src/lib/coordinator-gate-approval.js +98 -0
- package/src/lib/coordinator-gate-evaluation-presentation.js +57 -0
- package/src/lib/coordinator-next-actions.js +128 -0
- package/src/lib/coordinator-pending-gate-presentation.js +79 -0
- package/src/lib/coordinator-presentation-detail.js +11 -0
- package/src/lib/coordinator-repo-snapshots.js +53 -0
- package/src/lib/coordinator-repo-status-presentation.js +134 -0
- package/src/lib/dashboard/actions.js +105 -29
- package/src/lib/dashboard/bridge-server.js +7 -0
- package/src/lib/dashboard/coordinator-blockers.js +17 -0
- package/src/lib/dashboard/coordinator-repo-status.js +50 -0
- package/src/lib/dashboard/coordinator-timeout-status.js +34 -11
- package/src/lib/dashboard/state-reader.js +36 -1
- package/src/lib/dispatch-bundle.js +23 -0
- package/src/lib/export-diff.js +70 -38
- package/src/lib/export-verifier.js +3 -0
- package/src/lib/history-diff-summary.js +249 -0
- package/src/lib/manual-qa-fallback.js +18 -0
- package/src/lib/normalized-config.js +27 -22
- package/src/lib/recent-event-summary.js +132 -0
- package/src/lib/repo-decisions.js +69 -28
- package/src/lib/report.js +353 -145
- package/src/lib/run-diff.js +4 -0
- 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,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:
|
|
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:
|
|
64
|
+
next_action: nextAction,
|
|
65
|
+
next_actions: nextActions,
|
|
45
66
|
},
|
|
46
67
|
};
|
|
47
68
|
}
|
|
48
69
|
|
|
49
|
-
function
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|