agentxchain 2.103.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 +13 -7
- package/bin/agentxchain.js +16 -8
- 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/scripts/release-preflight.sh +82 -38
- 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/generate.js +126 -1
- package/src/commands/history.js +21 -3
- package/src/commands/init.js +15 -97
- 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/planning-artifacts.js +131 -0
- 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,91 @@
|
|
|
1
|
+
export const LIVE_OBSERVER_STALE_MS = 15_000;
|
|
2
|
+
|
|
3
|
+
function formatEventScope(scope) {
|
|
4
|
+
return scope === 'coordinator' ? 'coordinator' : 'run';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function resolveTimestamp(event) {
|
|
8
|
+
if (typeof event?.timestamp === 'string' && event.timestamp.trim()) {
|
|
9
|
+
return event.timestamp;
|
|
10
|
+
}
|
|
11
|
+
if (typeof event?.observedAt === 'string' && event.observedAt.trim()) {
|
|
12
|
+
return `${event.observedAt} (observed)`;
|
|
13
|
+
}
|
|
14
|
+
return 'unknown time';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildLiveMeta({
|
|
18
|
+
connected,
|
|
19
|
+
lastRefreshAt,
|
|
20
|
+
lastEvent,
|
|
21
|
+
scope = 'run',
|
|
22
|
+
now = Date.now(),
|
|
23
|
+
staleAfterMs = LIVE_OBSERVER_STALE_MS,
|
|
24
|
+
} = {}) {
|
|
25
|
+
const refreshMs = typeof lastRefreshAt === 'string' ? new Date(lastRefreshAt).getTime() : Number.NaN;
|
|
26
|
+
const hasRefresh = Number.isFinite(refreshMs);
|
|
27
|
+
const scopeLabel = formatEventScope(scope);
|
|
28
|
+
|
|
29
|
+
let freshnessState = 'connecting';
|
|
30
|
+
let freshnessLabel = 'Connecting';
|
|
31
|
+
|
|
32
|
+
if (!connected) {
|
|
33
|
+
freshnessState = 'disconnected';
|
|
34
|
+
freshnessLabel = 'Disconnected';
|
|
35
|
+
} else if (hasRefresh) {
|
|
36
|
+
freshnessState = (now - refreshMs) > staleAfterMs ? 'stale' : 'live';
|
|
37
|
+
freshnessLabel = freshnessState === 'live' ? 'Live' : 'Stale';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const refreshDetail = hasRefresh
|
|
41
|
+
? `Updated ${lastRefreshAt}`
|
|
42
|
+
: connected
|
|
43
|
+
? 'Waiting for first dashboard refresh'
|
|
44
|
+
: 'Waiting for dashboard reconnect';
|
|
45
|
+
|
|
46
|
+
const connectionDetail = connected ? 'WebSocket connected' : 'WebSocket disconnected';
|
|
47
|
+
|
|
48
|
+
let eventDetail = `No ${scopeLabel} events observed yet`;
|
|
49
|
+
if (lastEvent) {
|
|
50
|
+
const repoSuffix = scope === 'coordinator' && lastEvent.repoId ? ` from ${lastEvent.repoId}` : '';
|
|
51
|
+
eventDetail = `Last ${scopeLabel} event: ${lastEvent.type || 'unknown_event'}${repoSuffix} at ${resolveTimestamp(lastEvent)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
title: scope === 'coordinator' ? 'Live Coordinator Feed' : 'Live Run Feed',
|
|
56
|
+
freshness_state: freshnessState,
|
|
57
|
+
freshness_label: freshnessLabel,
|
|
58
|
+
refresh_detail: refreshDetail,
|
|
59
|
+
connection_detail: connectionDetail,
|
|
60
|
+
event_detail: eventDetail,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createLiveEventFromMessage(message, observedAt = new Date().toISOString()) {
|
|
65
|
+
if (message?.type === 'event') {
|
|
66
|
+
return {
|
|
67
|
+
type: message.event?.event_type || 'unknown_event',
|
|
68
|
+
timestamp: message.event?.timestamp || null,
|
|
69
|
+
observedAt,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (message?.type === 'coordinator_event') {
|
|
74
|
+
return {
|
|
75
|
+
type: message.event?.event_type || 'unknown_event',
|
|
76
|
+
timestamp: message.event?.timestamp || null,
|
|
77
|
+
observedAt,
|
|
78
|
+
repoId: message.repo_id || message.event?.repo_id || null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function shouldRefreshViewForLiveMessage(viewName, messageType) {
|
|
86
|
+
if (messageType === 'invalidate') return true;
|
|
87
|
+
if (messageType === 'coordinator_event') {
|
|
88
|
+
return viewName === 'cross-repo' || viewName === 'gate';
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
package/package.json
CHANGED
package/scripts/release-bump.sh
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Release identity creation — replaces raw `npm version <semver>`.
|
|
3
3
|
# Creates version bump commit + annotated tag with fail-closed verification.
|
|
4
|
-
# Usage: bash scripts/release-bump.sh --target-version <semver> [--skip-preflight]
|
|
4
|
+
# Usage: bash scripts/release-bump.sh --target-version <semver> --coauthored-by "Name <email>" [--skip-preflight]
|
|
5
5
|
set -euo pipefail
|
|
6
6
|
|
|
7
7
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
@@ -10,10 +10,11 @@ REPO_ROOT="$(cd "${CLI_DIR}/.." && pwd)"
|
|
|
10
10
|
cd "$CLI_DIR"
|
|
11
11
|
|
|
12
12
|
TARGET_VERSION=""
|
|
13
|
+
COAUTHORED_BY=""
|
|
13
14
|
SKIP_PREFLIGHT=0
|
|
14
15
|
|
|
15
16
|
usage() {
|
|
16
|
-
echo "Usage: bash scripts/release-bump.sh --target-version <semver> [--skip-preflight]" >&2
|
|
17
|
+
echo "Usage: bash scripts/release-bump.sh --target-version <semver> --coauthored-by \"Name <email>\" [--skip-preflight]" >&2
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
while [[ $# -gt 0 ]]; do
|
|
@@ -32,6 +33,15 @@ while [[ $# -gt 0 ]]; do
|
|
|
32
33
|
TARGET_VERSION="$2"
|
|
33
34
|
shift 2
|
|
34
35
|
;;
|
|
36
|
+
--coauthored-by)
|
|
37
|
+
if [[ -z "${2:-}" ]]; then
|
|
38
|
+
echo "Error: --coauthored-by requires a trailer value like \"Name <email>\"" >&2
|
|
39
|
+
usage
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
COAUTHORED_BY="$2"
|
|
43
|
+
shift 2
|
|
44
|
+
;;
|
|
35
45
|
--skip-preflight)
|
|
36
46
|
SKIP_PREFLIGHT=1
|
|
37
47
|
shift
|
|
@@ -49,6 +59,12 @@ if [[ -z "$TARGET_VERSION" ]]; then
|
|
|
49
59
|
exit 1
|
|
50
60
|
fi
|
|
51
61
|
|
|
62
|
+
if [[ -z "$COAUTHORED_BY" ]]; then
|
|
63
|
+
echo "Error: --coauthored-by is required so the release commit carries the mandated trailer" >&2
|
|
64
|
+
usage
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
52
68
|
echo "AgentXchain Release Identity: ${TARGET_VERSION}"
|
|
53
69
|
echo "============================================="
|
|
54
70
|
|
|
@@ -280,13 +296,20 @@ echo " OK: version files and allowed release surfaces staged"
|
|
|
280
296
|
|
|
281
297
|
# 9. Create release commit
|
|
282
298
|
echo "[9/10] Creating release commit..."
|
|
283
|
-
git commit -m "${TARGET_VERSION}
|
|
299
|
+
git commit -m "${TARGET_VERSION}
|
|
300
|
+
|
|
301
|
+
Co-Authored-By: ${COAUTHORED_BY}"
|
|
284
302
|
RELEASE_SHA=$(git rev-parse HEAD)
|
|
285
303
|
COMMIT_MSG=$(git log -1 --format=%s)
|
|
286
304
|
if [[ "$COMMIT_MSG" != "$TARGET_VERSION" ]]; then
|
|
287
305
|
echo "FAIL: commit message is '${COMMIT_MSG}', expected '${TARGET_VERSION}'" >&2
|
|
288
306
|
exit 1
|
|
289
307
|
fi
|
|
308
|
+
COMMIT_BODY=$(git log -1 --format=%B)
|
|
309
|
+
if [[ "$COMMIT_BODY" != *"Co-Authored-By: ${COAUTHORED_BY}"* ]]; then
|
|
310
|
+
echo "FAIL: release commit body is missing the required Co-Authored-By trailer" >&2
|
|
311
|
+
exit 1
|
|
312
|
+
fi
|
|
290
313
|
echo " OK: commit ${RELEASE_SHA:0:7} with message '${TARGET_VERSION}'"
|
|
291
314
|
|
|
292
315
|
# 9.5. Inline preflight gate — tests, pack, and docs build must pass before tag
|
|
@@ -9,10 +9,12 @@ CLI_DIR="${SCRIPT_DIR}/.."
|
|
|
9
9
|
cd "$CLI_DIR"
|
|
10
10
|
|
|
11
11
|
STRICT_MODE=0
|
|
12
|
+
PUBLISH_GATE=0
|
|
12
13
|
TARGET_VERSION="2.0.0"
|
|
13
14
|
|
|
14
15
|
usage() {
|
|
15
|
-
echo "Usage: bash scripts/release-preflight.sh [--strict] [--target-version <semver>]" >&2
|
|
16
|
+
echo "Usage: bash scripts/release-preflight.sh [--strict] [--publish-gate] [--target-version <semver>]" >&2
|
|
17
|
+
echo " --publish-gate Run only release-critical checks (no full test suite). Use in CI publish workflows." >&2
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
while [[ $# -gt 0 ]]; do
|
|
@@ -21,6 +23,11 @@ while [[ $# -gt 0 ]]; do
|
|
|
21
23
|
STRICT_MODE=1
|
|
22
24
|
shift
|
|
23
25
|
;;
|
|
26
|
+
--publish-gate)
|
|
27
|
+
PUBLISH_GATE=1
|
|
28
|
+
STRICT_MODE=1
|
|
29
|
+
shift
|
|
30
|
+
;;
|
|
24
31
|
--target-version)
|
|
25
32
|
if [[ -z "${2:-}" ]]; then
|
|
26
33
|
echo "Error: --target-version requires a semver argument" >&2
|
|
@@ -99,49 +106,86 @@ else
|
|
|
99
106
|
fi
|
|
100
107
|
|
|
101
108
|
# 3. Tests
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
if [[ "$PUBLISH_GATE" -eq 1 ]]; then
|
|
110
|
+
echo "[3/6] Release-gate tests (targeted subset)"
|
|
111
|
+
# In publish-gate mode, run only release-critical tests to avoid CI hangs.
|
|
112
|
+
# The full test suite is a pre-tag responsibility, not a publish-time gate.
|
|
113
|
+
GATE_TESTS=(
|
|
114
|
+
test/release-preflight.test.js
|
|
115
|
+
test/release-docs-content.test.js
|
|
116
|
+
test/release-notes-gate.test.js
|
|
117
|
+
test/release-identity-hardening.test.js
|
|
118
|
+
test/normalized-config.test.js
|
|
119
|
+
test/conformance.test.js
|
|
120
|
+
)
|
|
121
|
+
GATE_TEST_ARGS=()
|
|
122
|
+
for t in "${GATE_TESTS[@]}"; do
|
|
123
|
+
if [[ -f "$t" ]]; then
|
|
124
|
+
GATE_TEST_ARGS+=("$t")
|
|
125
|
+
fi
|
|
126
|
+
done
|
|
127
|
+
if [[ ${#GATE_TEST_ARGS[@]} -eq 0 ]]; then
|
|
128
|
+
fail "No release-gate test files found"
|
|
129
|
+
else
|
|
130
|
+
if run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 node --test "${GATE_TEST_ARGS[@]}"; then
|
|
131
|
+
TEST_STATUS=0
|
|
132
|
+
else
|
|
133
|
+
TEST_STATUS=$?
|
|
134
|
+
fi
|
|
135
|
+
NODE_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ tests / { print $3; exit }')"
|
|
136
|
+
NODE_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ fail / { print $3; exit }')"
|
|
137
|
+
if [ "$TEST_STATUS" -eq 0 ] && [ "${NODE_FAIL:-0}" = "0" ]; then
|
|
138
|
+
pass "${NODE_PASS:-?} release-gate tests passed, 0 failures"
|
|
139
|
+
else
|
|
140
|
+
fail "Release-gate tests failed"
|
|
141
|
+
printf '%s\n' "$TEST_OUTPUT" | tail -20
|
|
142
|
+
fi
|
|
108
143
|
fi
|
|
109
|
-
done
|
|
110
|
-
if run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test; then
|
|
111
|
-
TEST_STATUS=0
|
|
112
144
|
else
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
TEST_PASS="${VITEST_PASS}"
|
|
145
|
+
echo "[3/6] Test suite"
|
|
146
|
+
# Install MCP example deps — tests start example servers as subprocesses
|
|
147
|
+
for example_dir in "${CLI_DIR}/../examples/mcp-echo-agent" "${CLI_DIR}/../examples/mcp-http-echo-agent"; do
|
|
148
|
+
if [[ -f "${example_dir}/package.json" && ! -d "${example_dir}/node_modules" ]]; then
|
|
149
|
+
echo " Installing deps for $(basename "$example_dir")..."
|
|
150
|
+
(cd "$example_dir" && env -u NODE_AUTH_TOKEN -u NPM_CONFIG_USERCONFIG npm install --ignore-scripts --userconfig /dev/null 2>&1) || true
|
|
151
|
+
fi
|
|
152
|
+
done
|
|
153
|
+
if run_and_capture TEST_OUTPUT env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test; then
|
|
154
|
+
TEST_STATUS=0
|
|
155
|
+
else
|
|
156
|
+
TEST_STATUS=$?
|
|
126
157
|
fi
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
TEST_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# pass / { print $3 }')"
|
|
159
|
+
TEST_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# fail / { print $3 }')"
|
|
160
|
+
if [ -z "${TEST_PASS:-}" ]; then
|
|
161
|
+
VITEST_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^[[:space:]]*Tests[[:space:]]+[0-9]+[[:space:]]+passed/ { for (i = 1; i <= NF; i++) if ($i ~ /^[0-9]+$/) { print $i; exit } }')"
|
|
162
|
+
NODE_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ tests / { print $3; exit }')"
|
|
163
|
+
if [ -n "${VITEST_PASS:-}" ] && [ -n "${NODE_PASS:-}" ]; then
|
|
164
|
+
TEST_PASS="$((VITEST_PASS + NODE_PASS))"
|
|
165
|
+
elif [ -n "${NODE_PASS:-}" ]; then
|
|
166
|
+
TEST_PASS="${NODE_PASS}"
|
|
167
|
+
elif [ -n "${VITEST_PASS:-}" ]; then
|
|
168
|
+
TEST_PASS="${VITEST_PASS}"
|
|
169
|
+
fi
|
|
134
170
|
fi
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
171
|
+
if [ -z "${TEST_FAIL:-}" ]; then
|
|
172
|
+
NODE_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ fail / { print $3; exit }')"
|
|
173
|
+
if [ -n "${NODE_FAIL:-}" ]; then
|
|
174
|
+
TEST_FAIL="${NODE_FAIL}"
|
|
175
|
+
elif printf '%s\n' "$TEST_OUTPUT" | grep -Eq '^[[:space:]]*Tests[[:space:]]+[0-9]+[[:space:]]+passed'; then
|
|
176
|
+
TEST_FAIL=0
|
|
177
|
+
fi
|
|
178
|
+
fi
|
|
179
|
+
if [ "$TEST_STATUS" -eq 0 ] && [ "${TEST_FAIL:-0}" = "0" ]; then
|
|
180
|
+
if [ -n "${TEST_PASS:-}" ]; then
|
|
181
|
+
pass "${TEST_PASS} tests passed, 0 failures"
|
|
182
|
+
else
|
|
183
|
+
pass "npm test passed, 0 failures"
|
|
184
|
+
fi
|
|
139
185
|
else
|
|
140
|
-
|
|
186
|
+
fail "npm test failed"
|
|
187
|
+
printf '%s\n' "$TEST_OUTPUT" | tail -20
|
|
141
188
|
fi
|
|
142
|
-
else
|
|
143
|
-
fail "npm test failed"
|
|
144
|
-
printf '%s\n' "$TEST_OUTPUT" | tail -20
|
|
145
189
|
fi
|
|
146
190
|
|
|
147
191
|
# 4. CHANGELOG has target version
|
|
@@ -54,7 +54,7 @@ export async function acceptTurnCommand(opts = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
if (result.error_code?.startsWith('hook_') || result.error_code === 'hook_blocked') {
|
|
57
|
-
const recovery = deriveRecoveryDescriptor(result.state);
|
|
57
|
+
const recovery = deriveRecoveryDescriptor(result.state, config);
|
|
58
58
|
const activeTurn = result.state?.current_turn;
|
|
59
59
|
const hookName = result.hookResults?.blocker?.hook_name
|
|
60
60
|
|| result.hookResults?.results?.find((entry) => entry.hook_name)?.hook_name
|
|
@@ -94,7 +94,7 @@ export async function acceptTurnCommand(opts = {}) {
|
|
|
94
94
|
console.log(` ${chalk.dim('Overlap:')} ${(result.conflict.overlap_ratio * 100).toFixed(0)}%`);
|
|
95
95
|
console.log(` ${chalk.dim('Suggest:')} ${result.conflict.suggested_resolution}`);
|
|
96
96
|
if (result.state?.status === 'blocked') {
|
|
97
|
-
const recovery = deriveRecoveryDescriptor(result.state);
|
|
97
|
+
const recovery = deriveRecoveryDescriptor(result.state, config);
|
|
98
98
|
if (recovery) {
|
|
99
99
|
console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
|
|
100
100
|
}
|
|
@@ -177,7 +177,7 @@ export async function acceptTurnCommand(opts = {}) {
|
|
|
177
177
|
}
|
|
178
178
|
console.log('');
|
|
179
179
|
|
|
180
|
-
const recovery = deriveRecoveryDescriptor(result.state);
|
|
180
|
+
const recovery = deriveRecoveryDescriptor(result.state, config);
|
|
181
181
|
if (recovery) {
|
|
182
182
|
console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
|
|
183
183
|
console.log(` ${chalk.dim('Owner:')} ${recovery.owner}`);
|
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
import { resolve } from 'path';
|
|
8
8
|
import { existsSync, readFileSync } from 'fs';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
readRepoDecisions,
|
|
12
|
+
getActiveRepoDecisions,
|
|
13
|
+
getRepoDecisionById,
|
|
14
|
+
resolveDecisionAuthority,
|
|
15
|
+
getDecisionAuthorityMetadata,
|
|
16
|
+
summarizeRepoDecisions,
|
|
17
|
+
} from '../lib/repo-decisions.js';
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* @param {object} opts - { json?: boolean, all?: boolean, show?: string, dir?: string }
|
|
@@ -18,6 +25,7 @@ export async function decisionsCommand(opts) {
|
|
|
18
25
|
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
19
26
|
process.exit(1);
|
|
20
27
|
}
|
|
28
|
+
const config = loadConfig(root);
|
|
21
29
|
|
|
22
30
|
// ── Show single decision ───────────────────────────────────────────────
|
|
23
31
|
if (opts.show) {
|
|
@@ -26,59 +34,68 @@ export async function decisionsCommand(opts) {
|
|
|
26
34
|
console.error(chalk.red(`Decision ${opts.show} not found in repo decisions.`));
|
|
27
35
|
process.exit(1);
|
|
28
36
|
}
|
|
37
|
+
const enriched = enrichDecision(dec, config);
|
|
29
38
|
if (opts.json) {
|
|
30
|
-
console.log(JSON.stringify(
|
|
39
|
+
console.log(JSON.stringify(enriched, null, 2));
|
|
31
40
|
return;
|
|
32
41
|
}
|
|
33
|
-
console.log(chalk.bold(`Decision ${
|
|
34
|
-
console.log(` Category: ${
|
|
35
|
-
console.log(` Statement: ${
|
|
36
|
-
console.log(` Rationale: ${
|
|
37
|
-
console.log(` Status: ${formatStatus(
|
|
38
|
-
console.log(`
|
|
39
|
-
console.log(`
|
|
40
|
-
console.log(`
|
|
41
|
-
console.log(`
|
|
42
|
-
console.log(`
|
|
42
|
+
console.log(chalk.bold(`Decision ${enriched.id}`));
|
|
43
|
+
console.log(` Category: ${enriched.category || '—'}`);
|
|
44
|
+
console.log(` Statement: ${enriched.statement || '—'}`);
|
|
45
|
+
console.log(` Rationale: ${enriched.rationale || '—'}`);
|
|
46
|
+
console.log(` Status: ${formatStatus(enriched.status)}`);
|
|
47
|
+
console.log(` Binding: ${formatBindingState(enriched.binding_state)}`);
|
|
48
|
+
console.log(` Role: ${enriched.role || '—'}`);
|
|
49
|
+
console.log(` Phase: ${enriched.phase || '—'}`);
|
|
50
|
+
console.log(` Run: ${(enriched.run_id || '—').slice(0, 16)}`);
|
|
51
|
+
console.log(` Turn: ${(enriched.turn_id || '—').slice(0, 16)}`);
|
|
52
|
+
console.log(` Durability: ${enriched.durability || 'repo'}`);
|
|
43
53
|
// Show decision authority if config has it
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const auth = resolveDecisionAuthority(dec.role, config);
|
|
54
|
+
if (config && enriched.role) {
|
|
55
|
+
const auth = resolveDecisionAuthority(enriched.role, config);
|
|
47
56
|
if (auth !== null && !(typeof auth === 'object' && auth.unknown)) {
|
|
48
|
-
console.log(` Authority: ${auth} (${
|
|
57
|
+
console.log(` Authority: ${auth} (${enriched.role})`);
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
|
-
if (
|
|
52
|
-
console.log(` Supersedes: ${chalk.yellow(
|
|
60
|
+
if (enriched.overrides) {
|
|
61
|
+
console.log(` Supersedes: ${chalk.yellow(enriched.overrides)}`);
|
|
53
62
|
}
|
|
54
|
-
console.log(` Created: ${
|
|
55
|
-
if (
|
|
56
|
-
console.log(` Overridden: ${chalk.yellow(
|
|
63
|
+
console.log(` Created: ${enriched.created_at || '—'}`);
|
|
64
|
+
if (enriched.overridden_by) {
|
|
65
|
+
console.log(` Overridden: ${chalk.yellow(enriched.overridden_by)}`);
|
|
57
66
|
}
|
|
58
67
|
return;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
// ── List decisions ─────────────────────────────────────────────────────
|
|
62
|
-
const
|
|
71
|
+
const allDecisions = readRepoDecisions(root);
|
|
72
|
+
const decisions = opts.all ? allDecisions : getActiveRepoDecisions(root);
|
|
73
|
+
const enrichedDecisions = decisions.map((dec) => enrichDecision(dec, config));
|
|
63
74
|
|
|
64
75
|
if (opts.json) {
|
|
65
|
-
console.log(JSON.stringify(
|
|
76
|
+
console.log(JSON.stringify(enrichedDecisions, null, 2));
|
|
66
77
|
return;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
if (
|
|
70
|
-
console.log(chalk.dim('No repo-level decisions found.'));
|
|
71
|
-
if (!opts.all) {
|
|
80
|
+
if (enrichedDecisions.length === 0) {
|
|
81
|
+
console.log(chalk.dim(opts.all ? 'No repo-level decisions found.' : 'No active repo decisions found.'));
|
|
82
|
+
if (!opts.all && allDecisions.length > 0) {
|
|
72
83
|
console.log(chalk.dim('Use --all to include overridden decisions.'));
|
|
84
|
+
return;
|
|
73
85
|
}
|
|
74
86
|
return;
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
const label = opts.all ? 'Repo Decisions (all)' : 'Active Repo Decisions';
|
|
78
|
-
|
|
90
|
+
const summary = buildDecisionListSummary(summarizeRepoDecisions(allDecisions, config), opts.all);
|
|
91
|
+
console.log(chalk.bold(`${label}: ${enrichedDecisions.length}`));
|
|
92
|
+
console.log(chalk.dim(summary.binding_line));
|
|
93
|
+
if (summary.category_line) console.log(chalk.dim(summary.category_line));
|
|
94
|
+
if (summary.history_line) console.log(chalk.dim(summary.history_line));
|
|
95
|
+
if (summary.authority_line) console.log(chalk.dim(summary.authority_line));
|
|
79
96
|
console.log('');
|
|
80
97
|
|
|
81
|
-
for (const dec of
|
|
98
|
+
for (const dec of enrichedDecisions) {
|
|
82
99
|
const status = formatStatus(dec.status);
|
|
83
100
|
const runShort = (dec.run_id || '').slice(0, 12);
|
|
84
101
|
const override = dec.overridden_by
|
|
@@ -86,8 +103,9 @@ export async function decisionsCommand(opts) {
|
|
|
86
103
|
: dec.overrides
|
|
87
104
|
? chalk.dim(` ← supersedes ${dec.overrides}`)
|
|
88
105
|
: '';
|
|
106
|
+
const authority = formatAuthority(dec);
|
|
89
107
|
console.log(` ${chalk.cyan(dec.id)} ${status} ${chalk.dim(dec.category)} ${dec.statement}${override}`);
|
|
90
|
-
console.log(` ${chalk.dim(`by ${dec.role || '?'} in ${runShort || '?'}`)}`);
|
|
108
|
+
console.log(` ${chalk.dim(`by ${dec.role || '?'} in ${runShort || '?'}${authority}`)}`);
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
111
|
|
|
@@ -97,6 +115,57 @@ function formatStatus(status) {
|
|
|
97
115
|
return chalk.dim(status || '—');
|
|
98
116
|
}
|
|
99
117
|
|
|
118
|
+
function enrichDecision(decision, config) {
|
|
119
|
+
const authority = getDecisionAuthorityMetadata(decision.role, config);
|
|
120
|
+
return {
|
|
121
|
+
...decision,
|
|
122
|
+
binding_state: decision.status === 'active'
|
|
123
|
+
? 'binding'
|
|
124
|
+
: decision.overridden_by
|
|
125
|
+
? 'replaced'
|
|
126
|
+
: 'historical',
|
|
127
|
+
authority_level: authority?.level ?? null,
|
|
128
|
+
authority_source: authority?.source ?? null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildDecisionListSummary(summary, includeAll) {
|
|
133
|
+
const categories = summary?.operator_summary?.active_categories || [];
|
|
134
|
+
const highestAuthority = summary?.operator_summary?.highest_active_authority_level;
|
|
135
|
+
const highestAuthorityRole = summary?.operator_summary?.highest_active_authority_role;
|
|
136
|
+
const activeCount = summary?.active_count || 0;
|
|
137
|
+
const overriddenCount = summary?.overridden_count || 0;
|
|
138
|
+
return {
|
|
139
|
+
binding_line: `binding now: ${activeCount} active decision${activeCount === 1 ? '' : 's'}`,
|
|
140
|
+
category_line: categories.length > 0
|
|
141
|
+
? `categories: ${categories.join(', ')}`
|
|
142
|
+
: null,
|
|
143
|
+
history_line: overriddenCount > 0
|
|
144
|
+
? includeAll
|
|
145
|
+
? `history: ${overriddenCount} overridden decision${overriddenCount === 1 ? '' : 's'} recorded`
|
|
146
|
+
: `history: ${overriddenCount} overridden decision${overriddenCount === 1 ? '' : 's'} hidden (use --all)`
|
|
147
|
+
: null,
|
|
148
|
+
authority_line: typeof highestAuthority === 'number'
|
|
149
|
+
? `highest active authority: ${highestAuthority} (${highestAuthorityRole || 'unknown'})`
|
|
150
|
+
: null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatAuthority(decision) {
|
|
155
|
+
if (typeof decision.authority_level === 'number') {
|
|
156
|
+
if (decision.authority_source === 'unknown_role') return ' • authority unknown';
|
|
157
|
+
return ` • authority ${decision.authority_level}`;
|
|
158
|
+
}
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatBindingState(bindingState) {
|
|
163
|
+
if (bindingState === 'binding') return chalk.green('binding');
|
|
164
|
+
if (bindingState === 'replaced') return chalk.yellow('replaced');
|
|
165
|
+
if (bindingState === 'historical') return chalk.dim('historical');
|
|
166
|
+
return chalk.dim('—');
|
|
167
|
+
}
|
|
168
|
+
|
|
100
169
|
function findProjectRoot(dir) {
|
|
101
170
|
let current = resolve(dir);
|
|
102
171
|
while (current !== '/') {
|
package/src/commands/diff.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
|
|
3
3
|
import { findProjectRoot } from '../lib/config.js';
|
|
4
4
|
import { buildExportDiff, resolveExportArtifact } from '../lib/export-diff.js';
|
|
5
|
+
import { buildExportDiffSummary, buildRunDiffSummary } from '../lib/history-diff-summary.js';
|
|
5
6
|
import { buildRunDiff, resolveRunHistoryReference } from '../lib/run-diff.js';
|
|
6
7
|
|
|
7
8
|
export async function diffCommand(leftRef, rightRef, opts) {
|
|
@@ -28,7 +29,10 @@ export async function diffCommand(leftRef, rightRef, opts) {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
if (opts.json) {
|
|
31
|
-
console.log(JSON.stringify(
|
|
32
|
+
console.log(JSON.stringify({
|
|
33
|
+
...exportDiff.diff,
|
|
34
|
+
summary: buildExportDiffSummary(exportDiff.diff),
|
|
35
|
+
}, null, 2));
|
|
32
36
|
return;
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -56,7 +60,10 @@ export async function diffCommand(leftRef, rightRef, opts) {
|
|
|
56
60
|
|
|
57
61
|
const diff = buildRunDiff(leftResult.entry, rightResult.entry);
|
|
58
62
|
if (opts.json) {
|
|
59
|
-
console.log(JSON.stringify(
|
|
63
|
+
console.log(JSON.stringify({
|
|
64
|
+
...diff,
|
|
65
|
+
summary: buildRunDiffSummary(diff),
|
|
66
|
+
}, null, 2));
|
|
60
67
|
return;
|
|
61
68
|
}
|
|
62
69
|
|
|
@@ -65,12 +72,13 @@ export async function diffCommand(leftRef, rightRef, opts) {
|
|
|
65
72
|
|
|
66
73
|
function formatRunDiffText(diff) {
|
|
67
74
|
const lines = [];
|
|
75
|
+
const summary = buildRunDiffSummary(diff);
|
|
68
76
|
lines.push(chalk.bold('Run Diff'));
|
|
69
77
|
lines.push(`${chalk.dim('Left:')} ${formatRunHeader(diff.left)}`);
|
|
70
78
|
lines.push(`${chalk.dim('Right:')} ${formatRunHeader(diff.right)}`);
|
|
79
|
+
appendComparisonSummary(lines, summary);
|
|
71
80
|
|
|
72
81
|
if (!diff.changed) {
|
|
73
|
-
lines.push('');
|
|
74
82
|
lines.push(chalk.green('No differences.'));
|
|
75
83
|
return lines.join('\n');
|
|
76
84
|
}
|
|
@@ -96,12 +104,13 @@ function formatRunDiffText(diff) {
|
|
|
96
104
|
|
|
97
105
|
function formatExportDiffText(diff) {
|
|
98
106
|
const lines = [];
|
|
107
|
+
const summary = buildExportDiffSummary(diff);
|
|
99
108
|
lines.push(chalk.bold('Export Diff'));
|
|
100
109
|
lines.push(`${chalk.dim('Left:')} ${formatExportHeader(diff.left_ref, diff.left)}`);
|
|
101
110
|
lines.push(`${chalk.dim('Right:')} ${formatExportHeader(diff.right_ref, diff.right)}`);
|
|
111
|
+
appendComparisonSummary(lines, summary);
|
|
102
112
|
|
|
103
113
|
if (!diff.changed) {
|
|
104
|
-
lines.push('');
|
|
105
114
|
lines.push(chalk.green('No differences.'));
|
|
106
115
|
return lines.join('\n');
|
|
107
116
|
}
|
|
@@ -158,6 +167,20 @@ function appendRegressionSection(lines, regressions) {
|
|
|
158
167
|
}
|
|
159
168
|
}
|
|
160
169
|
|
|
170
|
+
function appendComparisonSummary(lines, summary) {
|
|
171
|
+
lines.push('');
|
|
172
|
+
lines.push(chalk.bold('Comparison Summary'));
|
|
173
|
+
lines.push(`- Outcome: ${summary.outcome}`);
|
|
174
|
+
lines.push(`- Risk: ${summary.risk_level}`);
|
|
175
|
+
if (Array.isArray(summary.highlights) && summary.highlights.length > 0) {
|
|
176
|
+
lines.push('- Highlights:');
|
|
177
|
+
for (const item of summary.highlights) {
|
|
178
|
+
lines.push(` - ${item}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
lines.push('');
|
|
182
|
+
}
|
|
183
|
+
|
|
161
184
|
function listChangeItems(entry) {
|
|
162
185
|
const items = [];
|
|
163
186
|
if (entry.added.length > 0) {
|