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,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
|
|
@@ -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) {
|
package/src/commands/doctor.js
CHANGED
|
@@ -11,6 +11,11 @@ import { loadNormalizedConfig, detectConfigVersion } from '../lib/normalized-con
|
|
|
11
11
|
import { readDaemonState, evaluateDaemonStatus } from '../lib/run-schedule.js';
|
|
12
12
|
import { getGovernedVersionSurface, formatGovernedVersionLabel } from '../lib/protocol-version.js';
|
|
13
13
|
import { PLUGIN_MANIFEST_FILE } from '../lib/plugins.js';
|
|
14
|
+
import {
|
|
15
|
+
getRoleRuntimeCapabilityContract,
|
|
16
|
+
summarizeRoleRuntimeCapability,
|
|
17
|
+
summarizeRuntimeCapabilityContract,
|
|
18
|
+
} from '../lib/runtime-capabilities.js';
|
|
14
19
|
|
|
15
20
|
export async function doctorCommand(opts = {}) {
|
|
16
21
|
const root = findProjectRoot(process.cwd());
|
|
@@ -76,8 +81,9 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
76
81
|
// 3. Runtime reachable — one sub-check per runtime
|
|
77
82
|
// Use normalized runtimes if available, otherwise fall back to raw config
|
|
78
83
|
const runtimes = (normalized && normalized.runtimes) || rawConfig.runtimes || {};
|
|
84
|
+
const rolesByRuntime = buildRolesByRuntime(normalized?.roles || {});
|
|
79
85
|
for (const [rtId, rt] of Object.entries(runtimes)) {
|
|
80
|
-
const check = checkRuntimeReachable(rtId, rt);
|
|
86
|
+
const check = checkRuntimeReachable(rtId, rt, rolesByRuntime[rtId] || []);
|
|
81
87
|
checks.push(check);
|
|
82
88
|
}
|
|
83
89
|
const connectorProbe = getConnectorProbeRecommendation(runtimes);
|
|
@@ -290,6 +296,12 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
290
296
|
? chalk.dim('INFO')
|
|
291
297
|
: chalk.red('FAIL');
|
|
292
298
|
console.log(` ${badge} ${c.name.padEnd(24)} ${chalk.dim(c.detail)}`);
|
|
299
|
+
if (c.runtime_contract) {
|
|
300
|
+
console.log(` ${chalk.dim(summarizeRuntimeCapabilityContract(c.runtime_contract))}`);
|
|
301
|
+
if (Array.isArray(c.bound_roles) && c.bound_roles.length > 0) {
|
|
302
|
+
console.log(` ${chalk.dim(`roles: ${c.bound_roles.map(summarizeRoleRuntimeCapability).join(' | ')}`)}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
293
305
|
}
|
|
294
306
|
|
|
295
307
|
console.log('');
|
|
@@ -309,25 +321,45 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
309
321
|
process.exit(failCount > 0 ? 1 : 0);
|
|
310
322
|
}
|
|
311
323
|
|
|
312
|
-
function
|
|
324
|
+
function buildRolesByRuntime(roles) {
|
|
325
|
+
const grouped = {};
|
|
326
|
+
for (const [roleId, role] of Object.entries(roles || {})) {
|
|
327
|
+
if (!role?.runtime_id) continue;
|
|
328
|
+
if (!grouped[role.runtime_id]) grouped[role.runtime_id] = [];
|
|
329
|
+
grouped[role.runtime_id].push([roleId, role]);
|
|
330
|
+
}
|
|
331
|
+
return grouped;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function attachRuntimeContract(baseCheck, rtId, rt, boundRoleEntries) {
|
|
335
|
+
const bound_roles = boundRoleEntries.map(([roleId, role]) => getRoleRuntimeCapabilityContract(roleId, role, rt));
|
|
336
|
+
return {
|
|
337
|
+
...baseCheck,
|
|
338
|
+
runtime_type: rt?.type || 'unknown',
|
|
339
|
+
runtime_contract: bound_roles[0]?.runtime_contract || getRoleRuntimeCapabilityContract('__unbound__', { write_authority: 'unknown' }, rt).runtime_contract,
|
|
340
|
+
bound_roles,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function checkRuntimeReachable(rtId, rt, boundRoleEntries = []) {
|
|
313
345
|
const base = { id: `runtime_${rtId}`, name: `Runtime: ${rtId}` };
|
|
314
346
|
|
|
315
347
|
if (!rt || !rt.type) {
|
|
316
|
-
return { ...base, level: 'warn', detail: 'No runtime type specified' };
|
|
348
|
+
return attachRuntimeContract({ ...base, level: 'warn', detail: 'No runtime type specified' }, rtId, rt, boundRoleEntries);
|
|
317
349
|
}
|
|
318
350
|
|
|
319
351
|
switch (rt.type) {
|
|
320
352
|
case 'manual':
|
|
321
|
-
return { ...base, level: 'pass', detail: 'Manual runtime (no binary needed)' };
|
|
353
|
+
return attachRuntimeContract({ ...base, level: 'pass', detail: 'Manual runtime (no binary needed)' }, rtId, rt, boundRoleEntries);
|
|
322
354
|
|
|
323
355
|
case 'local_cli': {
|
|
324
356
|
const cmd = Array.isArray(rt.command) ? rt.command[0] : (typeof rt.command === 'string' ? rt.command.split(/\s+/)[0] : null);
|
|
325
|
-
if (!cmd) return { ...base, level: 'warn', detail: 'No command configured' };
|
|
357
|
+
if (!cmd) return attachRuntimeContract({ ...base, level: 'warn', detail: 'No command configured' }, rtId, rt, boundRoleEntries);
|
|
326
358
|
try {
|
|
327
359
|
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
|
|
328
|
-
return { ...base, level: 'pass', detail: `${cmd} binary found` };
|
|
360
|
+
return attachRuntimeContract({ ...base, level: 'pass', detail: `${cmd} binary found` }, rtId, rt, boundRoleEntries);
|
|
329
361
|
} catch {
|
|
330
|
-
return { ...base, level: 'fail', detail: `${cmd} not found in PATH` };
|
|
362
|
+
return attachRuntimeContract({ ...base, level: 'fail', detail: `${cmd} not found in PATH` }, rtId, rt, boundRoleEntries);
|
|
331
363
|
}
|
|
332
364
|
}
|
|
333
365
|
|
|
@@ -335,34 +367,34 @@ function checkRuntimeReachable(rtId, rt) {
|
|
|
335
367
|
const envVar = rt.auth_env;
|
|
336
368
|
if (!envVar) {
|
|
337
369
|
// ollama and similar providers may not require auth
|
|
338
|
-
return { ...base, level: 'pass', detail: `${rt.provider || 'unknown'} provider (no auth required)` };
|
|
370
|
+
return attachRuntimeContract({ ...base, level: 'pass', detail: `${rt.provider || 'unknown'} provider (no auth required)` }, rtId, rt, boundRoleEntries);
|
|
339
371
|
}
|
|
340
372
|
if (process.env[envVar]) {
|
|
341
|
-
return { ...base, level: 'pass', detail: `${envVar} is set` };
|
|
373
|
+
return attachRuntimeContract({ ...base, level: 'pass', detail: `${envVar} is set` }, rtId, rt, boundRoleEntries);
|
|
342
374
|
}
|
|
343
|
-
return { ...base, level: 'fail', detail: `${envVar} not set` };
|
|
375
|
+
return attachRuntimeContract({ ...base, level: 'fail', detail: `${envVar} not set` }, rtId, rt, boundRoleEntries);
|
|
344
376
|
}
|
|
345
377
|
|
|
346
378
|
case 'mcp': {
|
|
347
379
|
const transport = rt.transport || 'stdio';
|
|
348
380
|
if (transport === 'streamable_http') {
|
|
349
|
-
return { ...base, level: 'warn', detail: 'Remote MCP endpoint (cannot verify at doctor time)' };
|
|
381
|
+
return attachRuntimeContract({ ...base, level: 'warn', detail: 'Remote MCP endpoint (cannot verify at doctor time)' }, rtId, rt, boundRoleEntries);
|
|
350
382
|
}
|
|
351
383
|
const cmd = Array.isArray(rt.command) ? rt.command[0] : (typeof rt.command === 'string' ? rt.command.split(/\s+/)[0] : null);
|
|
352
|
-
if (!cmd) return { ...base, level: 'warn', detail: 'No MCP command configured' };
|
|
384
|
+
if (!cmd) return attachRuntimeContract({ ...base, level: 'warn', detail: 'No MCP command configured' }, rtId, rt, boundRoleEntries);
|
|
353
385
|
try {
|
|
354
386
|
execSync(`command -v ${cmd}`, { stdio: 'ignore' });
|
|
355
|
-
return { ...base, level: 'pass', detail: `${cmd} binary found` };
|
|
387
|
+
return attachRuntimeContract({ ...base, level: 'pass', detail: `${cmd} binary found` }, rtId, rt, boundRoleEntries);
|
|
356
388
|
} catch {
|
|
357
|
-
return { ...base, level: 'fail', detail: `${cmd} not found in PATH` };
|
|
389
|
+
return attachRuntimeContract({ ...base, level: 'fail', detail: `${cmd} not found in PATH` }, rtId, rt, boundRoleEntries);
|
|
358
390
|
}
|
|
359
391
|
}
|
|
360
392
|
|
|
361
393
|
case 'remote_agent':
|
|
362
|
-
return { ...base, level: 'warn', detail: 'Remote agent endpoint (cannot verify at doctor time)' };
|
|
394
|
+
return attachRuntimeContract({ ...base, level: 'warn', detail: 'Remote agent endpoint (cannot verify at doctor time)' }, rtId, rt, boundRoleEntries);
|
|
363
395
|
|
|
364
396
|
default:
|
|
365
|
-
return { ...base, level: 'warn', detail: `Unknown runtime type: ${rt.type}` };
|
|
397
|
+
return attachRuntimeContract({ ...base, level: 'warn', detail: `Unknown runtime type: ${rt.type}` }, rtId, rt, boundRoleEntries);
|
|
366
398
|
}
|
|
367
399
|
}
|
|
368
400
|
|
package/src/commands/history.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { resolve } from 'path';
|
|
8
|
-
import { existsSync
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { queryRunHistory, queryRunLineage, isInheritable } from '../lib/run-history.js';
|
|
11
|
-
import {
|
|
11
|
+
import { buildRunOutcomeSummary } from '../lib/history-diff-summary.js';
|
|
12
|
+
import { getRunTriggerLabel } from '../lib/run-provenance.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @param {object} opts - { json?: boolean, limit?: number, status?: string, dir?: string }
|
|
@@ -67,7 +68,11 @@ export async function historyCommand(opts) {
|
|
|
67
68
|
});
|
|
68
69
|
|
|
69
70
|
if (opts.json) {
|
|
70
|
-
const enriched = entries.map(e => ({
|
|
71
|
+
const enriched = entries.map((e) => ({
|
|
72
|
+
...e,
|
|
73
|
+
inheritable: isInheritable(e),
|
|
74
|
+
outcome_summary: buildRunOutcomeSummary(e),
|
|
75
|
+
}));
|
|
71
76
|
console.log(JSON.stringify(enriched, null, 2));
|
|
72
77
|
return;
|
|
73
78
|
}
|
|
@@ -85,6 +90,7 @@ export async function historyCommand(opts) {
|
|
|
85
90
|
pad('#', 4),
|
|
86
91
|
pad('Run ID', 14),
|
|
87
92
|
pad('Status', 11),
|
|
93
|
+
pad('Outcome', 11),
|
|
88
94
|
pad('Trigger', 14),
|
|
89
95
|
pad('Ctx', 4),
|
|
90
96
|
pad('Phases', 8),
|
|
@@ -102,6 +108,7 @@ export async function historyCommand(opts) {
|
|
|
102
108
|
const idx = String(i + 1);
|
|
103
109
|
const runId = (entry.run_id || '—').slice(0, 12);
|
|
104
110
|
const status = formatStatus(entry.status);
|
|
111
|
+
const outcome = buildRunOutcomeSummary(entry);
|
|
105
112
|
const trigger = getRunTriggerLabel(entry.provenance);
|
|
106
113
|
const ctx = isInheritable(entry) ? '✓' : '—';
|
|
107
114
|
const phases = String(entry.phases_completed?.length || 0);
|
|
@@ -121,6 +128,7 @@ export async function historyCommand(opts) {
|
|
|
121
128
|
pad(idx, 4),
|
|
122
129
|
pad(runId, 14),
|
|
123
130
|
pad(status, 11),
|
|
131
|
+
pad(outcome.label, 11),
|
|
124
132
|
pad(trigger, 14),
|
|
125
133
|
pad(ctx, 4),
|
|
126
134
|
pad(phases, 8),
|
|
@@ -130,6 +138,10 @@ export async function historyCommand(opts) {
|
|
|
130
138
|
pad(date, 20),
|
|
131
139
|
pad(headline, 42),
|
|
132
140
|
].join(' '));
|
|
141
|
+
|
|
142
|
+
if (outcome.next_action) {
|
|
143
|
+
console.log(chalk.dim(` next: ${truncateLine(outcome.next_action, 148)}`));
|
|
144
|
+
}
|
|
133
145
|
});
|
|
134
146
|
|
|
135
147
|
console.log(chalk.dim(`\n${entries.length} run(s) shown`));
|
|
@@ -176,3 +188,9 @@ function formatHeadline(headline) {
|
|
|
176
188
|
if (normalized.length <= 40) return normalized;
|
|
177
189
|
return `${normalized.slice(0, 39)}…`;
|
|
178
190
|
}
|
|
191
|
+
|
|
192
|
+
function truncateLine(value, max) {
|
|
193
|
+
if (typeof value !== 'string') return '';
|
|
194
|
+
if (value.length <= max) return value;
|
|
195
|
+
return `${value.slice(0, max - 1)}…`;
|
|
196
|
+
}
|