@kal-elsam/kairo-runtime 0.4.3 → 0.5.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 (103) hide show
  1. package/README.md +124 -17
  2. package/global-template/components/catalog.json +63 -6
  3. package/global-template/components/sdd-core/personas/teaching.md +42 -0
  4. package/global-template/components/sdd-core/skills/sdd-apply/SKILL.md +34 -0
  5. package/global-template/components/sdd-core/skills/sdd-apply/references/contract.md +4 -0
  6. package/global-template/components/sdd-core/skills/sdd-archive/SKILL.md +34 -0
  7. package/global-template/components/sdd-core/skills/sdd-archive/references/contract.md +4 -0
  8. package/global-template/components/sdd-core/skills/sdd-design/SKILL.md +34 -0
  9. package/global-template/components/sdd-core/skills/sdd-design/references/contract.md +4 -0
  10. package/global-template/components/sdd-core/skills/sdd-explore/SKILL.md +34 -0
  11. package/global-template/components/sdd-core/skills/sdd-explore/references/contract.md +4 -0
  12. package/global-template/components/sdd-core/skills/sdd-init/SKILL.md +34 -0
  13. package/global-template/components/sdd-core/skills/sdd-init/references/contract.md +4 -0
  14. package/global-template/components/sdd-core/skills/sdd-propose/SKILL.md +35 -0
  15. package/global-template/components/sdd-core/skills/sdd-propose/references/contract.md +4 -0
  16. package/global-template/components/sdd-core/skills/sdd-spec/SKILL.md +34 -0
  17. package/global-template/components/sdd-core/skills/sdd-spec/references/contract.md +4 -0
  18. package/global-template/components/sdd-core/skills/sdd-tasks/SKILL.md +34 -0
  19. package/global-template/components/sdd-core/skills/sdd-tasks/references/contract.md +4 -0
  20. package/global-template/components/sdd-core/skills/sdd-verify/SKILL.md +34 -0
  21. package/global-template/components/sdd-core/skills/sdd-verify/references/contract.md +4 -0
  22. package/package.json +1 -1
  23. package/scripts/cockpit-smoke.mjs +29 -8
  24. package/scripts/smoke-test.sh +9 -0
  25. package/src/cli.js +54 -5
  26. package/src/global/action-planner.js +17 -4
  27. package/src/global/component-authoring.js +1 -2
  28. package/src/global/component-ecosystem-checks.js +74 -15
  29. package/src/global/component-health.js +56 -0
  30. package/src/global/component-integration-cli.js +302 -0
  31. package/src/global/component-manifest.js +214 -0
  32. package/src/global/component-registry.js +19 -2
  33. package/src/global/component-resolve.js +51 -0
  34. package/src/global/components/sdd-core.js +6 -0
  35. package/src/global/control-plane-proposals.js +229 -0
  36. package/src/global/control-plane-snapshot.js +212 -0
  37. package/src/global/dashboard-guidance.js +27 -3
  38. package/src/global/diff.js +2 -5
  39. package/src/global/global-cli.js +16 -1
  40. package/src/global/global-doctor.js +75 -2
  41. package/src/global/global-installer.js +43 -6
  42. package/src/global/governance-actions.js +212 -0
  43. package/src/global/governance-repair.js +13 -0
  44. package/src/global/ink/cockpit-changes.js +124 -0
  45. package/src/global/ink/cockpit-control-center.js +106 -0
  46. package/src/global/ink/cockpit-controller.js +17 -0
  47. package/src/global/ink/cockpit-enter.js +27 -0
  48. package/src/global/ink/cockpit-focus.js +7 -1
  49. package/src/global/ink/cockpit-models.js +98 -46
  50. package/src/global/ink/cockpit-proposals.js +82 -0
  51. package/src/global/ink/cockpit-recovery.js +120 -0
  52. package/src/global/ink/cockpit-runs.js +47 -0
  53. package/src/global/ink/cockpit-scan.js +60 -0
  54. package/src/global/ink/cockpit-views.js +144 -74
  55. package/src/global/ink/orchestrator-app.js +156 -42
  56. package/src/global/ink/orchestrator-state.js +6 -0
  57. package/src/global/ink/use-orchestrator-data.js +239 -17
  58. package/src/global/integrations/engram-apply.js +105 -0
  59. package/src/global/integrations/engram-evidence.js +218 -0
  60. package/src/global/integrations/engram-exec.js +107 -0
  61. package/src/global/integrations/engram-plan.js +60 -0
  62. package/src/global/integrations/engram-provider.js +22 -0
  63. package/src/global/integrations/engram-receipts.js +116 -0
  64. package/src/global/integrations/engram-rollback.js +131 -0
  65. package/src/global/integrations/index.js +12 -0
  66. package/src/global/integrations/provider-contract.js +83 -0
  67. package/src/global/integrations/provider-registry.js +47 -0
  68. package/src/global/integrations/sdd-apply.js +190 -0
  69. package/src/global/integrations/sdd-destinations.js +95 -0
  70. package/src/global/integrations/sdd-evidence.js +89 -0
  71. package/src/global/integrations/sdd-fs-guard.js +183 -0
  72. package/src/global/integrations/sdd-lifecycle.js +84 -0
  73. package/src/global/integrations/sdd-persona.js +71 -0
  74. package/src/global/integrations/sdd-plan.js +87 -0
  75. package/src/global/integrations/sdd-provider.js +21 -0
  76. package/src/global/integrations/sdd-receipts.js +44 -0
  77. package/src/global/integrations/sdd-rollback.js +269 -0
  78. package/src/global/integrations/sdd-skill-files.js +61 -0
  79. package/src/global/integrations/sdd-state.js +175 -0
  80. package/src/global/integrations/sdd-verify.js +122 -0
  81. package/src/global/intelligence/backends/opencode-catalog.js +212 -0
  82. package/src/global/intelligence/backends/opencode-cli.js +247 -0
  83. package/src/global/intelligence/backends/opencode-evidence.js +129 -0
  84. package/src/global/intelligence/backends/opencode-http.js +152 -0
  85. package/src/global/intelligence/backends/opencode-invoke.js +83 -0
  86. package/src/global/intelligence/backends/opencode-providers.js +67 -0
  87. package/src/global/intelligence/backends/opencode-runtime.js +242 -0
  88. package/src/global/intelligence/context-compiler.js +95 -50
  89. package/src/global/intelligence/index.js +38 -1
  90. package/src/global/intelligence/orchestrate.js +4 -2
  91. package/src/global/intelligence/registry.js +62 -6
  92. package/src/global/intelligence/router.js +188 -70
  93. package/src/global/intelligence/transport-registry.js +147 -0
  94. package/src/global/intelligence/types.js +21 -2
  95. package/src/global/intelligence-cli.js +48 -17
  96. package/src/global/intelligence-session.js +37 -0
  97. package/src/global/json-output.js +1 -0
  98. package/src/global/load-component-catalog.js +10 -28
  99. package/src/global/load-workspace-component-catalog.js +22 -63
  100. package/src/global/state-migration.js +21 -1
  101. package/src/global/state.js +13 -8
  102. package/src/global/status.js +4 -17
  103. package/src/global/sync.js +2 -5
@@ -4,6 +4,9 @@ import { resolveProjectReadiness } from "../dashboard-guidance.js";
4
4
  import { STATUS_LABELS, resolveGlyphs } from "./theme.js";
5
5
  import { windowList } from "./list-window.js";
6
6
  import { buildHomeMissionModel, formatHomeRecentRun } from "./cockpit-home.js";
7
+ import { isRunsBranchView } from "./cockpit-runs.js";
8
+ import { buildChangesFooterParts } from "./cockpit-changes.js";
9
+ import { buildRecoveryFooterParts } from "./cockpit-recovery.js";
7
10
 
8
11
  export const COCKPIT_REGIONS = {
9
12
  NAV: "nav",
@@ -14,40 +17,45 @@ export const COCKPIT_REGIONS = {
14
17
  export const COCKPIT_NAV = [
15
18
  {
16
19
  id: "overview",
17
- label: "Home",
20
+ label: "Control center",
18
21
  view: ORCHESTRATOR_VIEWS.HOME,
19
- description: "What Kairo does here, readiness, and the next useful action."
22
+ description: "Coverage, integrity, and the next governance action."
20
23
  },
21
24
  {
22
- id: "active",
23
- label: "Running now",
24
- view: ORCHESTRATOR_VIEWS.ACTIVE_RUNS,
25
- description: "Supervised runs executing right now."
25
+ id: "ides",
26
+ label: "IDEs & models",
27
+ view: ORCHESTRATOR_VIEWS.IDES,
28
+ description: "Detected agents, auth signals, capabilities, and recommended policy."
26
29
  },
27
30
  {
28
- id: "recent",
29
- label: "History",
30
- view: ORCHESTRATOR_VIEWS.RECENT_RUNS,
31
- description: "Past runs with agent, state, and readable result."
31
+ id: "modules",
32
+ label: "Harness modules",
33
+ view: ORCHESTRATOR_VIEWS.MODULES,
34
+ description: "Orchestrator, SDD/TDD, and external Engram/Graphify integrations."
32
35
  },
33
36
  {
34
- id: "providers",
35
- label: "Agents",
36
- view: ORCHESTRATOR_VIEWS.PROVIDERS,
37
- description: "Installed tools Kairo can launch and audit."
37
+ id: "changes",
38
+ label: "Changes",
39
+ view: ORCHESTRATOR_VIEWS.CHANGES,
40
+ description: "Findings, drift, and exact preview before any write."
38
41
  },
39
42
  {
40
- id: "launch",
41
- label: "New run",
42
- view: ORCHESTRATOR_VIEWS.LAUNCH,
43
- action: "launch",
44
- description: "Delegate a task to an executable agent."
43
+ id: "activity",
44
+ label: "Activity & recovery",
45
+ view: ORCHESTRATOR_VIEWS.ACTIVITY,
46
+ description: "Kairo operations, backups, and rollback readiness."
45
47
  },
46
48
  {
47
- id: "diagnostics",
48
- label: "System health",
49
- view: ORCHESTRATOR_VIEWS.DIAGNOSTICS,
50
- description: "Agents, intelligence, authentication, and configuration."
49
+ id: "profile",
50
+ label: "Profile & policy",
51
+ view: ORCHESTRATOR_VIEWS.PROFILE,
52
+ description: "Defaults, scopes, consent, and precedence."
53
+ },
54
+ {
55
+ id: "runs",
56
+ label: "Runs",
57
+ view: ORCHESTRATOR_VIEWS.RUNS,
58
+ description: "Optional supervised execution — Active, History, and New run."
51
59
  }
52
60
  ];
53
61
 
@@ -62,6 +70,10 @@ export function regionsForLayout(layoutMode) {
62
70
  }
63
71
 
64
72
  export function navIndexForView(view, items = COCKPIT_NAV) {
73
+ if (isRunsBranchView(view)) {
74
+ const runsIndex = items.findIndex((item) => item.id === "runs");
75
+ return runsIndex >= 0 ? runsIndex : 0;
76
+ }
65
77
  const index = items.findIndex((item) => item.view === view);
66
78
  return index >= 0 ? index : 0;
67
79
  }
@@ -82,31 +94,51 @@ export function buildTopBarModel({
82
94
  };
83
95
  }
84
96
 
85
- export function resolveNavStatusSummary(item, { dashboard = null, diagnostics = null } = {}) {
86
- const active = dashboard?.activeRuns?.length ?? 0;
87
- const recent = dashboard?.recentRuns?.length ?? 0;
88
- const providers = dashboard?.providers ?? [];
97
+ export function resolveNavStatusSummary(item, {
98
+ dashboard = null,
99
+ diagnostics = null,
100
+ snapshot = null
101
+ } = {}) {
102
+ const active = dashboard?.activeRuns?.length ?? snapshot?.runtime?.activeRuns ?? 0;
103
+ const providers = dashboard?.providers ?? snapshot?.runtime?.providers ?? [];
89
104
  const launchable = providers.filter((entry) => entry.launchable).length;
90
- const detected = diagnostics?.diagnostics?.detected ?? providers.filter((p) => p.available).length;
91
- const errors = diagnostics?.diagnostics?.errors ?? 0;
105
+ const detected = snapshot?.coverage?.detectedAgents
106
+ ?? diagnostics?.diagnostics?.detected
107
+ ?? providers.filter((p) => p.available).length;
108
+ const governed = snapshot?.coverage?.governedAgents ?? 0;
109
+ const changes = snapshot?.diff?.hasChanges
110
+ ? (snapshot.diff.changeCount ?? snapshot.diff.changes?.length ?? 0)
111
+ : 0;
112
+ const backups = snapshot?.backups?.count ?? 0;
92
113
 
93
114
  switch (item.id) {
94
115
  case "overview":
95
- return resolveProjectReadiness({
96
- hasGlobalState: true,
97
- diagnostics,
98
- dashboard
99
- }).label;
116
+ return snapshot?.health?.replaceAll("_", " ")
117
+ ?? resolveProjectReadiness({
118
+ hasGlobalState: true,
119
+ diagnostics,
120
+ dashboard
121
+ }).label;
122
+ case "ides":
123
+ return `${governed}/${detected} governed`;
124
+ case "modules":
125
+ return `${snapshot?.coverage?.components ?? 0} modules`;
126
+ case "changes":
127
+ return changes > 0 ? `${changes} pending` : "Clean";
128
+ case "activity":
129
+ return backups > 0 ? `${backups} backups` : "No backups";
130
+ case "profile":
131
+ return snapshot?.policy?.profile ?? "defaults";
132
+ case "runs":
133
+ return active === 0 ? "Idle" : `${active} active`;
100
134
  case "active":
101
135
  return active === 0 ? "Idle" : `${active} active`;
102
- case "recent":
103
- return recent === 0 ? "No history" : `${recent} recent`;
104
136
  case "providers":
105
137
  return `${launchable}/${providers.length || detected} ready`;
106
138
  case "launch":
107
139
  return launchable > 0 ? "Ready" : "Unavailable";
108
140
  case "diagnostics":
109
- return errors > 0 ? `${errors} issues` : "Checked";
141
+ return changes > 0 ? `${changes} pending` : "Checked";
110
142
  default:
111
143
  return "";
112
144
  }
@@ -119,27 +151,29 @@ export function buildNavModel({
119
151
  unicode = true,
120
152
  items = COCKPIT_NAV,
121
153
  dashboard = null,
122
- diagnostics = null
154
+ diagnostics = null,
155
+ snapshot = null
123
156
  } = {}) {
124
157
  const glyphs = resolveGlyphs(unicode);
125
158
  const selected = items[navIndex] ?? items[0];
126
159
  const mapped = items.map((item, index) => {
127
160
  const isSelected = index === navIndex;
128
- const isCurrent = item.view === currentView;
161
+ const isCurrent = item.view === currentView
162
+ || (item.id === "runs" && isRunsBranchView(currentView));
129
163
  return {
130
164
  ...item,
131
165
  marker: isSelected ? glyphs.focus : (isCurrent ? glyphs.bullet : " "),
132
166
  selected: isSelected,
133
167
  current: isCurrent,
134
168
  focused: focused && isSelected,
135
- statusSummary: resolveNavStatusSummary(item, { dashboard, diagnostics })
169
+ statusSummary: resolveNavStatusSummary(item, { dashboard, diagnostics, snapshot })
136
170
  };
137
171
  });
138
172
 
139
173
  return {
140
174
  title: "NAVIGATION",
141
175
  explanation: selected
142
- ? `${selected.description} (${resolveNavStatusSummary(selected, { dashboard, diagnostics })})`
176
+ ? `${selected.description} (${resolveNavStatusSummary(selected, { dashboard, diagnostics, snapshot })})`
143
177
  : "",
144
178
  items: mapped
145
179
  };
@@ -186,10 +220,13 @@ export function buildSystemStripModel({
186
220
  export function buildFooterModel({
187
221
  view = ORCHESTRATOR_VIEWS.HOME,
188
222
  region = COCKPIT_REGIONS.NAV,
223
+ navIndex = 0,
189
224
  helpOpen = false,
190
225
  canCancel = false,
191
226
  unicode = true,
192
- hasError = false
227
+ hasError = false,
228
+ changesPhase = null,
229
+ recoveryPhase = null
193
230
  } = {}) {
194
231
  const glyphs = resolveGlyphs(unicode);
195
232
  const parts = [];
@@ -213,24 +250,39 @@ export function buildFooterModel({
213
250
  return { text: parts.join(` ${glyphs.bullet} `) };
214
251
  }
215
252
 
253
+ if (view === ORCHESTRATOR_VIEWS.CHANGES) {
254
+ return { text: buildChangesFooterParts(changesPhase).join(` ${glyphs.bullet} `) };
255
+ }
256
+
257
+ if (view === ORCHESTRATOR_VIEWS.ACTIVITY) {
258
+ return { text: buildRecoveryFooterParts(recoveryPhase).join(` ${glyphs.bullet} `) };
259
+ }
260
+
216
261
  parts.push("↑↓ Navigate");
217
262
 
218
- const showTab = view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
263
+ const showTab = view === ORCHESTRATOR_VIEWS.RUNS
264
+ || view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
219
265
  || view === ORCHESTRATOR_VIEWS.RECENT_RUNS
220
- || view === ORCHESTRATOR_VIEWS.LAUNCH;
266
+ || view === ORCHESTRATOR_VIEWS.LAUNCH
267
+ || view === ORCHESTRATOR_VIEWS.ACTIVITY;
221
268
  if (showTab) {
222
269
  parts.push("Tab Region");
223
270
  }
224
271
 
225
- if (view === ORCHESTRATOR_VIEWS.HOME
272
+ const activatesControlCenterCta = view === ORCHESTRATOR_VIEWS.HOME && navIndex === 0;
273
+ if (activatesControlCenterCta) {
274
+ parts.push("Enter Activate");
275
+ } else if (view === ORCHESTRATOR_VIEWS.IDES
276
+ || view === ORCHESTRATOR_VIEWS.MODULES
277
+ || view === ORCHESTRATOR_VIEWS.PROFILE
226
278
  || view === ORCHESTRATOR_VIEWS.PROVIDERS
227
279
  || view === ORCHESTRATOR_VIEWS.DIAGNOSTICS
228
280
  || region === COCKPIT_REGIONS.NAV
281
+ || view === ORCHESTRATOR_VIEWS.RUNS
229
282
  || view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
230
283
  || view === ORCHESTRATOR_VIEWS.RECENT_RUNS) {
231
284
  parts.push("Enter Open");
232
285
  }
233
-
234
286
  if (view !== ORCHESTRATOR_VIEWS.LAUNCH) {
235
287
  parts.push("R refresh");
236
288
  }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Read-only proposal presentation for Control Center / Changes.
3
+ * Links to existing views only — never executes actions.
4
+ */
5
+
6
+ export function formatProposalLines(proposals = [], {
7
+ limit = 6,
8
+ destinationFilter = null,
9
+ budgets = null
10
+ } = {}) {
11
+ const filtered = (proposals ?? []).filter((entry) => {
12
+ if (!entry?.id || !entry?.title) return false;
13
+ if (!destinationFilter) return true;
14
+ return entry.destination === destinationFilter;
15
+ });
16
+
17
+ if (filtered.length === 0) {
18
+ return destinationFilter
19
+ ? ["No proposals targeting this view."]
20
+ : ["No evidence-backed proposals."];
21
+ }
22
+
23
+ const shown = filtered.slice(0, Math.max(1, limit));
24
+ const lines = [
25
+ `Proposals · ${shown.length}/${filtered.length} evidence-backed`
26
+ ];
27
+
28
+ const budgetLine = formatProposalBudgetLine(budgets);
29
+ if (budgetLine) lines.push(budgetLine);
30
+
31
+ for (const proposal of shown) {
32
+ const severity = String(proposal.severity ?? "info").toUpperCase();
33
+ const destination = proposal.destination ?? "control-center";
34
+ lines.push(`[${severity}] ${proposal.title} → ${destination}`);
35
+ const evidence = formatProposalEvidenceSource(proposal.evidence);
36
+ if (evidence) lines.push(` evidence: ${evidence}`);
37
+ }
38
+
39
+ if (filtered.length > shown.length) {
40
+ lines.push(`… ${filtered.length - shown.length} more (open destination views)`);
41
+ }
42
+
43
+ return lines;
44
+ }
45
+
46
+ export function formatProposalEvidenceSource(evidence = []) {
47
+ const sources = [];
48
+ const seen = new Set();
49
+ for (const item of evidence ?? []) {
50
+ const source = String(item?.source ?? "").trim();
51
+ if (!source || seen.has(source)) continue;
52
+ seen.add(source);
53
+ sources.push(source);
54
+ if (sources.length >= 3) break;
55
+ }
56
+ return sources.join(" · ") || null;
57
+ }
58
+
59
+ export function formatProposalBudgetLine(budgets) {
60
+ if (!budgets || typeof budgets !== "object") return null;
61
+ const parts = [];
62
+ if (Number.isFinite(budgets.stableUsedTokens) && Number.isFinite(budgets.stableBudgetTokens)) {
63
+ parts.push(`stable ${budgets.stableUsedTokens}/${budgets.stableBudgetTokens}`);
64
+ }
65
+ if (Number.isFinite(budgets.requestUsedTokens) && Number.isFinite(budgets.requestBudgetTokens)) {
66
+ parts.push(`request ${budgets.requestUsedTokens}/${budgets.requestBudgetTokens}`);
67
+ }
68
+ if (parts.length === 0) return null;
69
+ return `Budget · ${parts.join(" · ")}`;
70
+ }
71
+
72
+ export function proposalLimitForLayout(layoutMode = "compact") {
73
+ switch (layoutMode) {
74
+ case "wide":
75
+ return 6;
76
+ case "minimal":
77
+ return 3;
78
+ case "compact":
79
+ default:
80
+ return 4;
81
+ }
82
+ }
@@ -0,0 +1,120 @@
1
+ export const RECOVERY_PHASE = Object.freeze({
2
+ IDLE: "idle",
3
+ PREVIEWING: "previewing",
4
+ CONFIRMING: "confirming",
5
+ APPLYING: "applying",
6
+ COMPLETED: "completed",
7
+ FAILED: "failed"
8
+ });
9
+
10
+ export function createRecoveryActionState() {
11
+ return {
12
+ phase: RECOVERY_PHASE.IDLE,
13
+ preview: null,
14
+ receipt: null,
15
+ error: null,
16
+ message: null,
17
+ selectedSnapshot: null
18
+ };
19
+ }
20
+
21
+ export function reduceRecoveryAction(state, action) {
22
+ switch (action.type) {
23
+ case "reset":
24
+ return createRecoveryActionState();
25
+ case "preview-start":
26
+ return {
27
+ ...createRecoveryActionState(),
28
+ phase: RECOVERY_PHASE.PREVIEWING,
29
+ selectedSnapshot: action.snapshot ?? null,
30
+ message: `Previewing rollback · ${action.snapshot ?? "?"}`
31
+ };
32
+ case "preview-ready":
33
+ return {
34
+ phase: RECOVERY_PHASE.CONFIRMING,
35
+ preview: action.preview,
36
+ receipt: null,
37
+ error: null,
38
+ selectedSnapshot: action.preview?.snapshot ?? state.selectedSnapshot,
39
+ message: action.preview?.noop
40
+ ? "Snapshot has nothing to restore."
41
+ : "Confirm restore? Y restore · N/Esc cancel"
42
+ };
43
+ case "preview-failed":
44
+ return {
45
+ ...state,
46
+ phase: RECOVERY_PHASE.FAILED,
47
+ error: action.error ?? "preview-failed",
48
+ message: action.message ?? "Rollback preview failed.",
49
+ preview: state.preview
50
+ };
51
+ case "apply-start":
52
+ return { ...state, phase: RECOVERY_PHASE.APPLYING, message: "Restoring snapshot…" };
53
+ case "apply-done":
54
+ return {
55
+ phase: action.ok ? RECOVERY_PHASE.COMPLETED : RECOVERY_PHASE.FAILED,
56
+ preview: action.preview ?? state.preview,
57
+ receipt: action.receipt ?? null,
58
+ error: action.ok ? null : (action.reason ?? "apply-failed"),
59
+ selectedSnapshot: state.selectedSnapshot,
60
+ message: action.message ?? (action.ok ? "Rollback complete." : "Rollback failed.")
61
+ };
62
+ case "cancel":
63
+ return {
64
+ ...createRecoveryActionState(),
65
+ message: "Cancelled — previous snapshot kept, no restore written."
66
+ };
67
+ default:
68
+ return state;
69
+ }
70
+ }
71
+
72
+ export function listRecoverySnapshots(snapshot) {
73
+ return snapshot?.backups?.snapshots ?? [];
74
+ }
75
+
76
+ export function formatRecoveryLines({ snapshot, recoveryAction, listIndex = 0 }) {
77
+ const backups = listRecoverySnapshots(snapshot);
78
+ const events = snapshot?.history?.events ?? [];
79
+ const phase = recoveryAction?.phase ?? RECOVERY_PHASE.IDLE;
80
+ const lines = [
81
+ `Activity & recovery · ${phase}`,
82
+ `History: ${events.length} recent event(s)`,
83
+ ...events.slice(0, 3).map((event) => `${event.command ?? event.type ?? "event"} · ${event.action ?? ""} · ${event.timestamp ?? ""}`),
84
+ "",
85
+ `Snapshots: ${snapshot?.backups?.count ?? backups.length}`
86
+ ];
87
+
88
+ if (backups.length === 0) {
89
+ lines.push("No global snapshots yet.");
90
+ } else {
91
+ backups.forEach((entry, index) => {
92
+ const mark = index === listIndex ? "›" : " ";
93
+ lines.push(`${mark} ${entry.name} · ${entry.fileCount ?? "?"} files`);
94
+ });
95
+ }
96
+
97
+ if (recoveryAction?.message) lines.push("", recoveryAction.message);
98
+ const preview = recoveryAction?.preview;
99
+ if (preview) {
100
+ lines.push(`Snapshot · ${preview.snapshot}`);
101
+ for (const file of preview.files ?? []) lines.push(` restore · ${file.displayPath}`);
102
+ if (preview.fingerprint) lines.push(`Fingerprint · ${preview.fingerprint.slice(0, 12)}…`);
103
+ }
104
+ const receipt = recoveryAction?.receipt;
105
+ if (receipt) {
106
+ lines.push("", `Receipt · ${receipt.action}`);
107
+ if (receipt.safetyBackup) lines.push(`Safety backup · ${receipt.safetyBackup}`);
108
+ for (const path of receipt.restored ?? []) lines.push(` restored · ${path}`);
109
+ }
110
+ if (phase === RECOVERY_PHASE.IDLE || phase === RECOVERY_PHASE.COMPLETED || phase === RECOVERY_PHASE.FAILED) {
111
+ lines.push("", "Enter preview · Y restore · N/Esc cancel · R re-scan");
112
+ }
113
+ return lines;
114
+ }
115
+
116
+ export function buildRecoveryFooterParts(phase) {
117
+ if (phase === RECOVERY_PHASE.PREVIEWING || phase === RECOVERY_PHASE.APPLYING) return ["Working…", "Esc Back"];
118
+ if (phase === RECOVERY_PHASE.CONFIRMING) return ["Y Restore", "N/Esc Cancel"];
119
+ return ["↑↓ Select", "Enter Preview", "R Re-scan", "Esc Back"];
120
+ }
@@ -0,0 +1,47 @@
1
+ import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
2
+
3
+ export const RUNS_HUB_ITEMS = [
4
+ {
5
+ id: "active",
6
+ label: "Active runs",
7
+ view: ORCHESTRATOR_VIEWS.ACTIVE_RUNS,
8
+ description: "Inspect and cancel supervised runs that are currently executing."
9
+ },
10
+ {
11
+ id: "history",
12
+ label: "History",
13
+ view: ORCHESTRATOR_VIEWS.RECENT_RUNS,
14
+ description: "Review completed and failed run outcomes."
15
+ },
16
+ {
17
+ id: "launch",
18
+ label: "New run",
19
+ view: ORCHESTRATOR_VIEWS.LAUNCH,
20
+ action: "launch",
21
+ description: "Start a supervised run after governance is healthy."
22
+ }
23
+ ];
24
+
25
+ export function isRunsBranchView(view) {
26
+ return view === ORCHESTRATOR_VIEWS.RUNS
27
+ || view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
28
+ || view === ORCHESTRATOR_VIEWS.RECENT_RUNS
29
+ || view === ORCHESTRATOR_VIEWS.LAUNCH
30
+ || view === ORCHESTRATOR_VIEWS.RUN_DETAIL;
31
+ }
32
+
33
+ export function isRunsChildView(view) {
34
+ return view === ORCHESTRATOR_VIEWS.ACTIVE_RUNS
35
+ || view === ORCHESTRATOR_VIEWS.RECENT_RUNS
36
+ || view === ORCHESTRATOR_VIEWS.LAUNCH;
37
+ }
38
+
39
+ export function resolveRunsHubItem(listIndex = 0, items = RUNS_HUB_ITEMS) {
40
+ if (items.length === 0) return null;
41
+ const index = Math.min(Math.max(0, listIndex), items.length - 1);
42
+ return items[index] ?? null;
43
+ }
44
+
45
+ export function formatRunsHubLines(items = RUNS_HUB_ITEMS) {
46
+ return items.map((item) => `${item.label} — ${item.description}`);
47
+ }
@@ -0,0 +1,60 @@
1
+ export const CONTROL_PLANE_AUTO_SCAN = Object.freeze({
2
+ includeDiff: true,
3
+ includeExplain: false,
4
+ includeRuntime: false
5
+ });
6
+
7
+ export async function loadCockpitScanBundle({
8
+ homeDir,
9
+ workspaceRoot,
10
+ packageName,
11
+ packageRoot,
12
+ cliVersion,
13
+ buildDashboard,
14
+ buildDiagnostics,
15
+ buildSnapshot
16
+ }) {
17
+ const [dashboard, diagnostics, snapshot] = await Promise.all([
18
+ buildDashboard({ homeDir, workspaceRoot, cliVersion }),
19
+ buildDiagnostics({ homeDir, workspaceRoot, packageName, packageRoot, cliVersion }),
20
+ buildSnapshot({
21
+ homeDir,
22
+ workspaceRoot,
23
+ packageName,
24
+ packageRoot,
25
+ cliVersion,
26
+ ...CONTROL_PLANE_AUTO_SCAN
27
+ })
28
+ ]);
29
+ return { dashboard, diagnostics, snapshot };
30
+ }
31
+
32
+ /**
33
+ * Serialize overlapping reloads and drop stale completions.
34
+ * Callers keep prior error/loading until a non-stale success updates state.
35
+ */
36
+ export function createSerializedReloader(runLoad) {
37
+ let generation = 0;
38
+ let tail = Promise.resolve();
39
+
40
+ return function reload() {
41
+ const gen = ++generation;
42
+ const job = async () => {
43
+ try {
44
+ const result = await runLoad();
45
+ if (gen !== generation) {
46
+ return { stale: true, result: null, error: null };
47
+ }
48
+ return { stale: false, result, error: null };
49
+ } catch (error) {
50
+ if (gen !== generation) {
51
+ return { stale: true, result: null, error: null };
52
+ }
53
+ return { stale: false, result: null, error };
54
+ }
55
+ };
56
+ const next = tail.then(job, job);
57
+ tail = next.then(() => undefined, () => undefined);
58
+ return next;
59
+ };
60
+ }