@kal-elsam/kairo-runtime 0.7.0 → 0.9.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 (42) hide show
  1. package/README.md +43 -11
  2. package/global-template/components/catalog.json +4 -1
  3. package/global-template/components/orchestrator/extensions/pi/kairo-minion.js +604 -0
  4. package/package.json +1 -1
  5. package/scripts/cockpit-smoke.mjs +4 -4
  6. package/src/cli.js +34 -2
  7. package/src/global/adapters/pi.js +1 -1
  8. package/src/global/global-doctor.js +2 -0
  9. package/src/global/ink/cockpit/primitives.js +96 -31
  10. package/src/global/ink/cockpit-alerts.js +36 -0
  11. package/src/global/ink/cockpit-changes.js +59 -35
  12. package/src/global/ink/cockpit-control-center.js +129 -51
  13. package/src/global/ink/cockpit-controller.js +58 -11
  14. package/src/global/ink/cockpit-focus.js +4 -2
  15. package/src/global/ink/cockpit-models.js +89 -47
  16. package/src/global/ink/cockpit-palette.js +98 -0
  17. package/src/global/ink/cockpit-path-label.js +19 -0
  18. package/src/global/ink/cockpit-recovery.js +78 -15
  19. package/src/global/ink/cockpit-reviews.js +14 -10
  20. package/src/global/ink/cockpit-runs.js +13 -4
  21. package/src/global/ink/cockpit-settings.js +194 -0
  22. package/src/global/ink/cockpit-views.js +88 -57
  23. package/src/global/ink/orchestrator-app.js +137 -46
  24. package/src/global/ink/orchestrator-state.js +24 -14
  25. package/src/global/ink/use-orchestrator-data.js +58 -0
  26. package/src/global/paths.js +3 -0
  27. package/src/global/runtime/alerts/alert-store.js +216 -0
  28. package/src/global/runtime/alerts/alert-types.js +59 -0
  29. package/src/global/runtime/alerts/alert-validate.js +117 -0
  30. package/src/global/runtime/execution-adapters/pi.js +12 -2
  31. package/src/global/runtime/monitor/monitor-cli.js +62 -0
  32. package/src/global/runtime/monitor/monitor-platform.js +95 -0
  33. package/src/global/runtime/monitor/monitor.js +249 -0
  34. package/src/global/runtime/orchestration/index.js +23 -0
  35. package/src/global/runtime/orchestration/orch-receipts.js +234 -0
  36. package/src/global/runtime/orchestration/orch-types.js +173 -0
  37. package/src/global/runtime/orchestration/orch-validate.js +63 -0
  38. package/src/global/runtime/run-cli.js +1 -0
  39. package/src/global/runtime/run-manager.js +59 -4
  40. package/src/global/runtime/run-strategy.js +71 -0
  41. package/src/global/runtime/run-supervisor.js +22 -2
  42. package/src/global/runtime/run-types.js +5 -1
@@ -26,7 +26,7 @@ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
26
26
  const require = createRequire(import.meta.url);
27
27
  const pkg = require(join(root, "package.json"));
28
28
 
29
- assert.equal(pkg.version, "0.7.0");
29
+ assert.equal(pkg.version, "0.9.0");
30
30
  assert.ok(pkg.dependencies["ansi-escapes"]);
31
31
 
32
32
  assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
@@ -85,7 +85,7 @@ const asciiNav = buildNavModel({
85
85
  assert.equal(asciiNav.items[1].marker, ">");
86
86
  assert.equal(asciiNav.items[0].current, true);
87
87
  assert.equal(asciiNav.items[1].selected, true);
88
- assert.match(asciiNav.explanation, /Detected agents|IDEs|models|Harness|modules/i);
88
+ assert.match(asciiNav.explanation, /Repair|Govern|Activity|Usage|Settings|Overview/i);
89
89
 
90
90
  function applyKey(state, keyAction) {
91
91
  const routed = routeCockpitKey(state, keyAction);
@@ -98,7 +98,7 @@ function smokeNavigation(layoutMode) {
98
98
  layoutMode,
99
99
  region: layoutMode === LAYOUT_MODES.MINIMAL ? COCKPIT_REGIONS.CONTENT : COCKPIT_REGIONS.NAV
100
100
  });
101
- const diagnosticsIndex = COCKPIT_NAV.findIndex((item) => item.id === "changes");
101
+ const diagnosticsIndex = COCKPIT_NAV.findIndex((item) => item.id === "governance");
102
102
  while (state.navIndex < diagnosticsIndex) {
103
103
  state = applyKey(state, { type: "arrow", direction: "down" });
104
104
  }
@@ -108,7 +108,7 @@ function smokeNavigation(layoutMode) {
108
108
 
109
109
  state = applyKey(state, { type: "arrow", direction: "up" });
110
110
  state = applyKey(state, { type: "enter" });
111
- assert.equal(state.view, ORCHESTRATOR_VIEWS.MODULES);
111
+ assert.equal(state.view, ORCHESTRATOR_VIEWS.HOME);
112
112
  assert.equal(state.region, COCKPIT_REGIONS.NAV);
113
113
 
114
114
  const footer = buildFooterModel({
package/src/cli.js CHANGED
@@ -39,6 +39,8 @@ import { runOrchestratorDiagnostics, runOrchestratorShell } from "./global/orche
39
39
  import { runIntelligenceCli } from "./global/intelligence-cli.js";
40
40
  import { runGlobalRun, runGlobalRuns } from "./global/runtime/run-cli.js";
41
41
  import { runGlobalReview, runGlobalReviews } from "./global/runtime/review/review-cli.js";
42
+ import { runGlobalMonitor } from "./global/runtime/monitor/monitor-cli.js";
43
+ import { normalizeRunStrategy } from "./global/runtime/run-strategy.js";
42
44
  import {
43
45
  LEGACY_PACKAGE_NAME,
44
46
  PACKAGE_NAME,
@@ -118,6 +120,9 @@ export async function runCli(argv) {
118
120
  case "reviews":
119
121
  await runGlobalReviews(optionsWithPolicy, packageManifest);
120
122
  return;
123
+ case "monitor":
124
+ await runGlobalMonitor(optionsWithPolicy, packageManifest, { packageRoot });
125
+ return;
121
126
  case "intelligence":
122
127
  await runIntelligenceCli(optionsWithPolicy, packageManifest);
123
128
  return;
@@ -386,6 +391,7 @@ export function parseArgs(argv) {
386
391
  agent: null,
387
392
  task: null,
388
393
  model: null,
394
+ strategy: "direct",
389
395
  intelligenceBackend: null,
390
396
  permissions: null,
391
397
  captureTranscript: false,
@@ -421,6 +427,10 @@ export function parseArgs(argv) {
421
427
  parseIntelligenceAction(args, options);
422
428
  }
423
429
 
430
+ if (command === "monitor") {
431
+ parseMonitorAction(args, options);
432
+ }
433
+
424
434
  for (let index = 0; index < args.length; index += 1) {
425
435
  const arg = args[index];
426
436
 
@@ -499,6 +509,13 @@ export function parseArgs(argv) {
499
509
  else if (arg.startsWith("--agent=")) options.agent = arg.slice("--agent=".length);
500
510
  else if (arg === "--model") options.model = args[++index];
501
511
  else if (arg.startsWith("--model=")) options.model = arg.slice("--model=".length);
512
+ else if (arg === "--strategy") {
513
+ options.strategy = normalizeRunStrategy(requireFlagValue("--strategy", args[++index]));
514
+ } else if (arg.startsWith("--strategy=")) {
515
+ options.strategy = normalizeRunStrategy(
516
+ requireFlagValue("--strategy", arg.slice("--strategy=".length))
517
+ );
518
+ }
502
519
  else if (arg === "--backend") options.intelligenceBackend = args[++index] ?? "";
503
520
  else if (arg.startsWith("--backend=")) options.intelligenceBackend = arg.slice("--backend=".length);
504
521
  else if (arg === "--permissions") options.permissions = parsePathList(args[++index]);
@@ -669,6 +686,19 @@ function parseReviewsAction(args, options) {
669
686
  }
670
687
  }
671
688
 
689
+ function parseMonitorAction(args, options) {
690
+ const action = args[0];
691
+ if (!action || action.startsWith("-")) {
692
+ options.monitorAction = "status";
693
+ return;
694
+ }
695
+ if (!new Set(["enable", "disable", "status", "tick"]).has(action)) {
696
+ throw new Error(`Unknown monitor action "${action}". Use enable, disable, status, or tick.`);
697
+ }
698
+ args.shift();
699
+ options.monitorAction = action;
700
+ }
701
+
672
702
  function parseIntelligenceAction(args, options) {
673
703
  const action = args[0];
674
704
  if (!action || action.startsWith("-")) {
@@ -727,6 +757,7 @@ function normalizeCommand(command) {
727
757
  if (command === "runs") return "runs";
728
758
  if (command === "review") return "review";
729
759
  if (command === "reviews") return "reviews";
760
+ if (command === "monitor") return "monitor";
730
761
  if (command === "intelligence" || command === "intel") return "intelligence";
731
762
  if (command === "setup") return "setup";
732
763
  if (command === "status") return "status";
@@ -797,7 +828,7 @@ Usage:
797
828
  ${cli} --dry-run Setup dry-run (scriptable)
798
829
  ${cli} --version
799
830
  ${cli} shell Operations cockpit (TTY)
800
- ${cli} run --agent <id> --task "..." [--model <name>] [--cwd <dir>] [--permissions force] [--capture-transcript] [--follow] [--no-wait] [--json]
831
+ ${cli} run --agent <id> --task "..." [--strategy direct|orchestrated] [--model <name>] [--cwd <dir>] [--permissions force] [--capture-transcript] [--follow] [--no-wait] [--json]
801
832
  ${cli} runs list [--json] [--limit <n>] [--active-only]
802
833
  ${cli} runs show <runId> [--json] [--limit <n>] [--follow]
803
834
  ${cli} runs stop <runId> [--json]
@@ -854,6 +885,7 @@ Commands:
854
885
  runs List, inspect, or cancel agent runs under ~/.harness/runs/.
855
886
  review Bounded read-only review via Codex or Pi; receipts under ~/.harness/reviews/.
856
887
  reviews List or show prior review receipts (secret-free).
888
+ monitor Opt-in anomaly monitor (enable|disable|status|tick). macOS LaunchAgent; notify shell:false.
857
889
  orchestrator Read-only capability registry diagnostics (--json supported).
858
890
  intelligence Harness Engineering layer: backends, context packs, routing, budgets.
859
891
  Local-first (Ollama). Cloud only with --cloud-consent.
@@ -882,7 +914,7 @@ Commands:
882
914
 
883
915
  JSON output (--json on supported commands):
884
916
  status, sync, doctor, adapters, explain, diff, history, history last,
885
- policy (show/set/reset), report
917
+ policy (show/set/reset), report, monitor
886
918
  Human text remains the default. See README.md for examples and field notes.
887
919
 
888
920
  Version:
@@ -7,7 +7,7 @@ export const PI_CODING_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
7
7
 
8
8
  const CUSTOM_DIR_MESSAGE =
9
9
  `Pi config writes are unsupported when ${PI_CODING_AGENT_DIR_ENV} points away from `
10
- + `~/${PI_DEFAULT_ROOT_DIR} (out of scope for Kairo 0.6.0). `
10
+ + `~/${PI_DEFAULT_ROOT_DIR} (out of scope for Kairo managed config). `
11
11
  + `Unset ${PI_CODING_AGENT_DIR_ENV} to manage the default directory; runtime launches remain available.`;
12
12
 
13
13
  /** True when PI_CODING_AGENT_DIR is set to a non-default absolute/relative path. */
@@ -11,6 +11,7 @@ import {
11
11
  BACKEND_IDS,
12
12
  inspectIntelligenceBackends
13
13
  } from "./intelligence/index.js";
14
+ import { monitorDoctorCheck } from "./runtime/monitor/monitor.js";
14
15
 
15
16
  export async function runGlobalDoctorChecks(homeDir, {
16
17
  packageRoot,
@@ -43,6 +44,7 @@ export async function runGlobalDoctorChecks(homeDir, {
43
44
  }
44
45
 
45
46
  checks.push(await intelligenceProvidersCheck({ env, fetchImpl, inspectBackends }));
47
+ checks.push(await monitorDoctorCheck(homeDir));
46
48
 
47
49
  const hasMissing = checks.some((check) => check.status === "missing");
48
50
  const hasStale = checks.some((check) => check.status === "stale");
@@ -33,6 +33,22 @@ export function CockpitPanel({ title, focused = false, width, children }) {
33
33
  );
34
34
  }
35
35
 
36
+ export function CockpitSection({ title, children }) {
37
+ return React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
38
+ title && React.createElement(Text, {
39
+ bold: true,
40
+ color: COCKPIT_COLORS.secondary
41
+ }, title),
42
+ children
43
+ );
44
+ }
45
+
46
+ export function CockpitKeyHint({ keys, label, colorEnabled = true }) {
47
+ return React.createElement(Text, {
48
+ color: colorEnabled ? COCKPIT_COLORS.muted : undefined
49
+ }, `${keys} ${label}`);
50
+ }
51
+
36
52
  export function CockpitTopBar({ model, colorEnabled = true }) {
37
53
  return React.createElement(Box, { justifyContent: "space-between", width: "100%" },
38
54
  React.createElement(Text, {
@@ -45,6 +61,7 @@ export function CockpitTopBar({ model, colorEnabled = true }) {
45
61
  );
46
62
  }
47
63
 
64
+ /** Vertical nav list (legacy / tests). Prefer CockpitNavStrip in the shell. */
48
65
  export function CockpitNav({ model, colorEnabled = true }) {
49
66
  return React.createElement(Box, { flexDirection: "column" },
50
67
  model.items.map((item) => {
@@ -68,6 +85,62 @@ export function CockpitNav({ model, colorEnabled = true }) {
68
85
  );
69
86
  }
70
87
 
88
+ function stripLabel(item, { compact }) {
89
+ if (!compact) return item.label;
90
+ return item.label.split(/[&·]/)[0].trim().split(/\s+/)[0];
91
+ }
92
+
93
+ /** Horizontal navigation strip under the top bar. */
94
+ export function CockpitNavStrip({
95
+ model,
96
+ colorEnabled = true,
97
+ layoutMode = "compact",
98
+ focused = false
99
+ }) {
100
+ const compact = layoutMode !== "wide";
101
+ const parts = model.items.map((item) => {
102
+ const label = stripLabel(item, { compact });
103
+ const suffix = item.current && !item.selected ? "*" : "";
104
+ const color = item.focused
105
+ ? (colorEnabled ? COCKPIT_COLORS.primary : undefined)
106
+ : item.selected
107
+ ? (colorEnabled ? COCKPIT_COLORS.secondary : undefined)
108
+ : item.current
109
+ ? (colorEnabled ? COCKPIT_COLORS.muted : undefined)
110
+ : (colorEnabled ? COCKPIT_COLORS.muted : undefined);
111
+ return React.createElement(Text, {
112
+ key: item.id,
113
+ bold: item.focused || item.selected,
114
+ color
115
+ }, `${item.marker}${label}${suffix}`);
116
+ });
117
+
118
+ const joined = [];
119
+ parts.forEach((node, index) => {
120
+ if (index > 0) {
121
+ joined.push(React.createElement(Text, {
122
+ key: `sep-${index}`,
123
+ color: colorEnabled ? COCKPIT_COLORS.muted : undefined
124
+ }, " · "));
125
+ }
126
+ joined.push(node);
127
+ });
128
+
129
+ return React.createElement(Box, {
130
+ flexDirection: "column",
131
+ width: "100%",
132
+ borderStyle: "single",
133
+ borderColor: focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted,
134
+ paddingX: 1
135
+ },
136
+ React.createElement(Box, { flexDirection: "row", flexWrap: "wrap" }, ...joined),
137
+ model.explanation && React.createElement(Text, {
138
+ color: COCKPIT_COLORS.muted
139
+ }, model.explanation)
140
+ );
141
+ }
142
+
143
+ /** Kept for model tests; no longer rendered in the shell. */
71
144
  export function CockpitSystemStrip({ model, colorEnabled = true }) {
72
145
  return React.createElement(Box, { flexDirection: "column" },
73
146
  model.rows.map((row) =>
@@ -83,56 +156,48 @@ export function CockpitSystemStrip({ model, colorEnabled = true }) {
83
156
  );
84
157
  }
85
158
 
86
- export function CockpitFooter({ model }) {
87
- return React.createElement(Box, { flexDirection: "column" },
159
+ export function CockpitFooter({ model, columns = 80 }) {
160
+ const width = Math.max(24, Math.min(Number(columns) || 80, 120));
161
+ const bar = Math.max(20, width - 2);
162
+ return React.createElement(Box, { flexDirection: "column", width: "100%" },
88
163
  React.createElement(Text, { color: COCKPIT_COLORS.muted },
89
- `├${"─".repeat(62)}┤`
164
+ `├${"─".repeat(bar)}┤`
90
165
  ),
91
166
  React.createElement(Text, { color: COCKPIT_COLORS.muted }, `│ ${model.text}`),
92
167
  React.createElement(Text, { color: COCKPIT_COLORS.muted },
93
- `╰${"─".repeat(62)}╯`
168
+ `╰${"─".repeat(bar)}╯`
94
169
  )
95
170
  );
96
171
  }
97
172
 
173
+ /**
174
+ * Responsive single-panel shell: TopBar → NavStrip → Main → Footer.
175
+ * SYSTEM side column removed (diagnostics via Enter/detail in later slices).
176
+ */
98
177
  export function CockpitShell({
99
178
  topBar,
100
179
  footer,
101
180
  layoutMode,
102
181
  nav,
103
- system,
104
182
  navFocused,
105
183
  contentFocused,
106
- systemFocused,
107
184
  colorEnabled = true,
185
+ columns = 80,
108
186
  children
109
187
  }) {
110
- const showNav = layoutMode === "wide" || layoutMode === "compact";
111
- const showSystem = layoutMode === "wide";
112
-
113
188
  return React.createElement(Box, { flexDirection: "column", width: "100%" },
114
189
  React.createElement(CockpitTopBar, { model: topBar, colorEnabled }),
115
- layoutMode === "minimal" && nav && React.createElement(Box, { marginY: 0 },
116
- React.createElement(Text, { color: COCKPIT_COLORS.muted }, "Nav: "),
117
- React.createElement(CockpitNav, { model: nav, colorEnabled })
118
- ),
119
- React.createElement(Box, { flexDirection: "row", width: "100%" },
120
- showNav && React.createElement(CockpitPanel, {
121
- title: nav?.title ?? "NAVIGATION",
122
- focused: navFocused,
123
- width: 22
124
- }, React.createElement(CockpitNav, { model: nav, colorEnabled })),
125
- React.createElement(CockpitPanel, {
126
- title: undefined,
127
- focused: contentFocused,
128
- width: showSystem ? 48 : showNav ? 56 : "100%"
129
- }, children),
130
- showSystem && React.createElement(CockpitPanel, {
131
- title: system?.title ?? "SYSTEM",
132
- focused: systemFocused,
133
- width: 20
134
- }, React.createElement(CockpitSystemStrip, { model: system, colorEnabled }))
135
- ),
136
- React.createElement(CockpitFooter, { model: footer })
190
+ nav && React.createElement(CockpitNavStrip, {
191
+ model: nav,
192
+ colorEnabled,
193
+ layoutMode,
194
+ focused: navFocused
195
+ }),
196
+ React.createElement(CockpitPanel, {
197
+ title: undefined,
198
+ focused: contentFocused,
199
+ width: "100%"
200
+ }, children),
201
+ React.createElement(CockpitFooter, { model: footer, columns })
137
202
  );
138
203
  }
@@ -0,0 +1,36 @@
1
+ import { ALERT_STATES } from "../runtime/alerts/alert-types.js";
2
+
3
+ export function formatAlertsHeadline(alerts = null) {
4
+ if (alerts == null) return { count: null, headline: "Alert data unavailable" };
5
+ const count = alerts.filter((alert) => alert.state === ALERT_STATES.OPEN).length;
6
+ return { count, headline: count === 0 ? "None pending" : `${count} pending` };
7
+ }
8
+
9
+ export function formatAlertListLines(alerts = null) {
10
+ if (alerts == null) return ["Alert data unavailable"];
11
+ const open = alerts.filter((alert) => alert.state === ALERT_STATES.OPEN);
12
+ if (open.length === 0) return ["No pending alerts."];
13
+ return open.map((alert) => {
14
+ const when = String(alert.createdAt ?? "").slice(0, 16).replace("T", " ");
15
+ return `${alert.severity} · ${alert.title} · ${when || "unknown time"}`;
16
+ });
17
+ }
18
+
19
+ export function formatAlertDetailLines(alert) {
20
+ if (!alert) return ["Alert not found."];
21
+ return [
22
+ "SUMMARY",
23
+ `${alert.severity} · ${alert.state} · ${alert.kind}`,
24
+ alert.title,
25
+ alert.summary || "No summary.",
26
+ "",
27
+ "DETAILS",
28
+ `Id · ${alert.alertId}`,
29
+ `Source · ${alert.source ?? "n/a"} · Created · ${alert.createdAt ?? "n/a"}`
30
+ ];
31
+ }
32
+
33
+ export function selectAlertFromList(alerts = null, index = 0) {
34
+ if (!Array.isArray(alerts)) return null;
35
+ return alerts.filter((alert) => alert.state === ALERT_STATES.OPEN)[index] ?? null;
36
+ }
@@ -1,4 +1,4 @@
1
- import { formatProposalLines, proposalLimitForLayout } from "./cockpit-proposals.js";
1
+ import { formatConfirmPath } from "./cockpit-path-label.js";
2
2
 
3
3
  export const CHANGES_PHASE = Object.freeze({
4
4
  IDLE: "idle",
@@ -62,53 +62,77 @@ export function reduceChangesAction(state, action) {
62
62
  }
63
63
  }
64
64
 
65
- export function formatChangesActionLines({ snapshot, changesAction, layoutMode = "compact" }) {
66
- const diff = snapshot?.diff;
65
+ function healthLabel(kind) {
66
+ return String(kind ?? "unknown").replaceAll("_", " ");
67
+ }
68
+
69
+ /**
70
+ * Governance surface: status, coverage, recommended action first.
71
+ * Paths only when detail=true or during confirm (home-relative / distinct).
72
+ */
73
+ export function formatChangesActionLines({
74
+ snapshot,
75
+ changesAction,
76
+ detail = false,
77
+ homeDir = null
78
+ } = {}) {
67
79
  const phase = changesAction?.phase ?? CHANGES_PHASE.IDLE;
68
- const lines = [`Changes · ${phase}`];
80
+ const coverage = snapshot?.coverage ?? {};
81
+ const diff = snapshot?.diff;
82
+ const cta = snapshot?.cta;
83
+ const pending = diff?.hasChanges
84
+ ? (diff.changeCount ?? diff.changes?.length ?? 0)
85
+ : 0;
86
+ const lines = [
87
+ "STATUS",
88
+ `${healthLabel(snapshot?.health)}${pending > 0 ? ` · ${pending} pending` : " · drift clean"}`,
89
+ "",
90
+ "COVERAGE",
91
+ `${coverage.governedAgents ?? 0}/${coverage.detectedAgents ?? 0} agents governed · ${coverage.components ?? 0} components`,
92
+ "",
93
+ "NEXT",
94
+ cta?.title ?? "Review governance when ready"
95
+ ];
96
+ if (cta?.detail) lines.push(cta.detail);
69
97
 
70
- const proposalLines = formatProposalLines(snapshot?.proposals ?? [], {
71
- limit: proposalLimitForLayout(layoutMode),
72
- destinationFilter: "changes",
73
- budgets: snapshot?.budgets ?? null
74
- });
75
- if (proposalLines[0] !== "No proposals targeting this view.") {
76
- lines.push(...proposalLines, "");
98
+ if (changesAction?.message) lines.push("", changesAction.message);
99
+ if (changesAction?.error && changesAction.error !== "setup-required") {
100
+ lines.push(`Error: ${changesAction.error}`);
77
101
  }
78
102
 
79
- if (changesAction?.message) lines.push(changesAction.message);
80
- if (changesAction?.error && changesAction.error !== "setup-required") lines.push(`Error: ${changesAction.error}`);
81
-
82
103
  const preview = changesAction?.preview;
83
- if (preview?.hasChanges) {
84
- lines.push(`${preview.changes?.length ?? 0} planned change(s)`);
85
- for (const change of preview.changes ?? []) {
86
- lines.push(`${change.action ?? change.kind} · ${change.target} · ${change.status ?? "planned"}`);
87
- }
88
- if (preview.integrations?.status && preview.integrations.status !== "skipped") {
89
- lines.push(`SDD lifecycle · ${preview.integrations.status}${preview.integrations.partial ? " (partial)" : ""}`);
90
- }
91
- if (preview.fingerprint) lines.push(`Fingerprint · ${preview.fingerprint.slice(0, 12)}…`);
92
- } else if (diff && !preview) {
93
- if (!diff.installed) lines.push(diff.summary ?? "Setup required before changes can be previewed.");
94
- else if (!diff.hasChanges) lines.push(diff.summary ?? "No pending governance changes.");
95
- else {
96
- lines.push(diff.summary ?? "Pending changes");
97
- for (const change of diff.changes ?? []) lines.push(`${change.action ?? change.kind} · ${change.target} · ${change.status}`);
104
+ const planned = preview?.hasChanges
105
+ ? (preview.changes ?? [])
106
+ : (diff?.hasChanges && !preview ? (diff.changes ?? []) : []);
107
+ if (planned.length > 0) {
108
+ lines.push("", `${planned.length} planned change(s)`);
109
+ const showDetail = detail || phase === CHANGES_PHASE.CONFIRMING;
110
+ if (showDetail) {
111
+ lines.push("DETAILS");
112
+ const limit = detail ? 12 : 3;
113
+ for (const change of planned.slice(0, limit)) {
114
+ lines.push(
115
+ `${change.action ?? change.kind} · ${formatConfirmPath(change.target, homeDir)}`
116
+ );
117
+ }
118
+ if (planned.length > limit) lines.push(`… ${planned.length - limit} more`);
98
119
  }
99
120
  } else if (!diff) {
100
- lines.push("Scan did not include diff yet. Press R to re-scan.");
121
+ lines.push("", "Scan did not include diff yet. Press R to re-scan.");
122
+ } else if (!diff.installed) {
123
+ lines.push("", diff.summary ?? "Setup required before changes can be previewed.");
124
+ } else if (!diff.hasChanges && !changesAction?.message) {
125
+ lines.push("", diff.summary ?? "No pending governance changes.");
101
126
  }
102
127
 
103
128
  const receipt = changesAction?.receipt;
104
129
  if (receipt) {
105
- lines.push("", `Receipt · ${receipt.action}`);
106
- if (receipt.backups?.length) lines.push(`Backups · ${receipt.backups.length}`);
107
- if (receipt.checksBefore && receipt.checksAfter) {
130
+ lines.push("", `Result · ${receipt.action}${receipt.partial ? " (partial)" : ""}`);
131
+ if (receipt.backups?.length) lines.push(`${receipt.backups.length} backup(s) retained`);
132
+ if (detail && receipt.checksBefore && receipt.checksAfter) {
133
+ lines.push("DETAILS");
108
134
  lines.push(`Checks · before ok=${receipt.checksBefore.ok} → after ok=${receipt.checksAfter.ok}`);
109
135
  }
110
- if (receipt.integrations) lines.push(`Integrations · ${receipt.integrations.status ?? "n/a"}`);
111
- if (receipt.partial) lines.push("Partial evidence retained — success not claimed.");
112
136
  }
113
137
 
114
138
  if (phase === CHANGES_PHASE.IDLE || phase === CHANGES_PHASE.COMPLETED || phase === CHANGES_PHASE.FAILED) {