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.
Files changed (66) hide show
  1. package/README.md +13 -7
  2. package/bin/agentxchain.js +16 -8
  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/scripts/release-preflight.sh +82 -38
  19. package/src/commands/accept-turn.js +3 -3
  20. package/src/commands/decisions.js +98 -29
  21. package/src/commands/diff.js +27 -4
  22. package/src/commands/doctor.js +48 -16
  23. package/src/commands/generate.js +126 -1
  24. package/src/commands/history.js +21 -3
  25. package/src/commands/init.js +15 -97
  26. package/src/commands/multi.js +223 -54
  27. package/src/commands/phase.js +11 -13
  28. package/src/commands/reject-turn.js +1 -1
  29. package/src/commands/restart.js +28 -11
  30. package/src/commands/resume.js +6 -6
  31. package/src/commands/role.js +51 -14
  32. package/src/commands/run.js +5 -11
  33. package/src/commands/status.js +145 -13
  34. package/src/commands/step.js +36 -29
  35. package/src/lib/admission-control.js +14 -12
  36. package/src/lib/blocked-state.js +150 -0
  37. package/src/lib/conflict-actions.js +17 -0
  38. package/src/lib/context-section-parser.js +2 -0
  39. package/src/lib/continuity-status.js +1 -1
  40. package/src/lib/coordinator-blocker-presentation.js +127 -0
  41. package/src/lib/coordinator-event-narrative.js +43 -0
  42. package/src/lib/coordinator-gate-approval.js +98 -0
  43. package/src/lib/coordinator-gate-evaluation-presentation.js +57 -0
  44. package/src/lib/coordinator-next-actions.js +128 -0
  45. package/src/lib/coordinator-pending-gate-presentation.js +79 -0
  46. package/src/lib/coordinator-presentation-detail.js +11 -0
  47. package/src/lib/coordinator-repo-snapshots.js +53 -0
  48. package/src/lib/coordinator-repo-status-presentation.js +134 -0
  49. package/src/lib/dashboard/actions.js +105 -29
  50. package/src/lib/dashboard/bridge-server.js +7 -0
  51. package/src/lib/dashboard/coordinator-blockers.js +17 -0
  52. package/src/lib/dashboard/coordinator-repo-status.js +50 -0
  53. package/src/lib/dashboard/coordinator-timeout-status.js +34 -11
  54. package/src/lib/dashboard/state-reader.js +36 -1
  55. package/src/lib/dispatch-bundle.js +23 -0
  56. package/src/lib/export-diff.js +70 -38
  57. package/src/lib/export-verifier.js +3 -0
  58. package/src/lib/history-diff-summary.js +249 -0
  59. package/src/lib/manual-qa-fallback.js +18 -0
  60. package/src/lib/normalized-config.js +27 -22
  61. package/src/lib/planning-artifacts.js +131 -0
  62. package/src/lib/recent-event-summary.js +132 -0
  63. package/src/lib/repo-decisions.js +69 -28
  64. package/src/lib/report.js +353 -145
  65. package/src/lib/run-diff.js +4 -0
  66. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.103.0",
3
+ "version": "2.105.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- echo "[3/6] Test suite"
103
- # Install MCP example deps — tests start example servers as subprocesses
104
- for example_dir in "${CLI_DIR}/../examples/mcp-echo-agent" "${CLI_DIR}/../examples/mcp-http-echo-agent"; do
105
- if [[ -f "${example_dir}/package.json" && ! -d "${example_dir}/node_modules" ]]; then
106
- echo " Installing deps for $(basename "$example_dir")..."
107
- (cd "$example_dir" && env -u NODE_AUTH_TOKEN -u NPM_CONFIG_USERCONFIG npm install --ignore-scripts --userconfig /dev/null 2>&1) || true
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
- TEST_STATUS=$?
114
- fi
115
- TEST_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# pass / { print $3 }')"
116
- TEST_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^# fail / { print $3 }')"
117
- if [ -z "${TEST_PASS:-}" ]; then
118
- 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 } }')"
119
- NODE_PASS="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ tests / { print $3; exit }')"
120
- if [ -n "${VITEST_PASS:-}" ] && [ -n "${NODE_PASS:-}" ]; then
121
- TEST_PASS="$((VITEST_PASS + NODE_PASS))"
122
- elif [ -n "${NODE_PASS:-}" ]; then
123
- TEST_PASS="${NODE_PASS}"
124
- elif [ -n "${VITEST_PASS:-}" ]; then
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
- fi
128
- if [ -z "${TEST_FAIL:-}" ]; then
129
- NODE_FAIL="$(printf '%s\n' "$TEST_OUTPUT" | awk '/^ℹ fail / { print $3; exit }')"
130
- if [ -n "${NODE_FAIL:-}" ]; then
131
- TEST_FAIL="${NODE_FAIL}"
132
- elif printf '%s\n' "$TEST_OUTPUT" | grep -Eq '^[[:space:]]*Tests[[:space:]]+[0-9]+[[:space:]]+passed'; then
133
- TEST_FAIL=0
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
- fi
136
- if [ "$TEST_STATUS" -eq 0 ] && [ "${TEST_FAIL:-0}" = "0" ]; then
137
- if [ -n "${TEST_PASS:-}" ]; then
138
- pass "${TEST_PASS} tests passed, 0 failures"
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
- pass "npm test passed, 0 failures"
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 { readRepoDecisions, getActiveRepoDecisions, getRepoDecisionById, resolveDecisionAuthority } from '../lib/repo-decisions.js';
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(dec, null, 2));
39
+ console.log(JSON.stringify(enriched, null, 2));
31
40
  return;
32
41
  }
33
- console.log(chalk.bold(`Decision ${dec.id}`));
34
- console.log(` Category: ${dec.category}`);
35
- console.log(` Statement: ${dec.statement}`);
36
- console.log(` Rationale: ${dec.rationale}`);
37
- console.log(` Status: ${formatStatus(dec.status)}`);
38
- console.log(` Role: ${dec.role || '—'}`);
39
- console.log(` Phase: ${dec.phase || '—'}`);
40
- console.log(` Run: ${(dec.run_id || '—').slice(0, 16)}`);
41
- console.log(` Turn: ${(dec.turn_id || '—').slice(0, 16)}`);
42
- console.log(` Durability: ${dec.durability || 'repo'}`);
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
- const config = loadConfig(root);
45
- if (config && dec.role) {
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} (${dec.role})`);
57
+ console.log(` Authority: ${auth} (${enriched.role})`);
49
58
  }
50
59
  }
51
- if (dec.overrides) {
52
- console.log(` Supersedes: ${chalk.yellow(dec.overrides)}`);
60
+ if (enriched.overrides) {
61
+ console.log(` Supersedes: ${chalk.yellow(enriched.overrides)}`);
53
62
  }
54
- console.log(` Created: ${dec.created_at || '—'}`);
55
- if (dec.overridden_by) {
56
- console.log(` Overridden: ${chalk.yellow(dec.overridden_by)}`);
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 decisions = opts.all ? readRepoDecisions(root) : getActiveRepoDecisions(root);
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(decisions, null, 2));
76
+ console.log(JSON.stringify(enrichedDecisions, null, 2));
66
77
  return;
67
78
  }
68
79
 
69
- if (decisions.length === 0) {
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
- console.log(chalk.bold(`${label}: ${decisions.length}`));
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 decisions) {
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 !== '/') {
@@ -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(exportDiff.diff, null, 2));
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(diff, null, 2));
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) {