@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
@@ -19,6 +19,11 @@ import {
19
19
  normalizeGlobalState
20
20
  } from "./state-migration.js";
21
21
  import { formatCliCommand } from "./brand/cli.js";
22
+ import {
23
+ aggregateSessionRefreshRequired,
24
+ applySddLifecycle,
25
+ nextSddState
26
+ } from "./integrations/sdd-lifecycle.js";
22
27
 
23
28
  export async function installGlobalHarness({
24
29
  packageRoot,
@@ -32,6 +37,7 @@ export async function installGlobalHarness({
32
37
  dryRun = false
33
38
  }) {
34
39
  const paths = harnessHomePaths(homeDir);
40
+ const priorState = await readGlobalState(paths.statePath);
35
41
  const timestamp = backupTimestamp();
36
42
  const targetComponents = resolveTargetComponents({ components, noDefaultComponents, workspaceRoot });
37
43
  const context = buildAdapterContext({
@@ -58,7 +64,9 @@ export async function installGlobalHarness({
58
64
  configsRepaired: [],
59
65
  configsUnchanged: [],
60
66
  backups: [],
61
- driftDetected: false
67
+ driftDetected: false,
68
+ integrations: {},
69
+ sessionRefreshRequired: false
62
70
  };
63
71
 
64
72
  const componentFiles = await installComponentAssets({
@@ -72,6 +80,9 @@ export async function installGlobalHarness({
72
80
  result.assetsRepaired.push(...Object.keys(componentFiles));
73
81
 
74
82
  await applyAdapterPlans({ context, targetAdapters, result });
83
+ await attachSddLifecycle({
84
+ result, homeDir, packageRoot, targetAdapters, targetComponents, dryRun, priorState
85
+ });
75
86
 
76
87
  if (!dryRun) {
77
88
  await persistGlobalState({
@@ -82,7 +93,8 @@ export async function installGlobalHarness({
82
93
  targetComponents,
83
94
  homeDir,
84
95
  componentFiles,
85
- backups: result.backups
96
+ backups: result.backups,
97
+ sdd: nextSddState(priorState?.sdd, result.integrations.sdd)
86
98
  });
87
99
  }
88
100
 
@@ -144,7 +156,9 @@ export async function syncGlobalHarness({
144
156
  configsUnchanged: [],
145
157
  backups: [],
146
158
  driftDetected: hasRepairableDrift(driftChecks),
147
- repairs: driftChecks.filter((check) => check.status === "missing" || check.status === "stale")
159
+ repairs: driftChecks.filter((check) => check.status === "missing" || check.status === "stale"),
160
+ integrations: {},
161
+ sessionRefreshRequired: false
148
162
  };
149
163
 
150
164
  const { coreFiles, repaired, unchanged } = await repairComponentAssets({
@@ -184,6 +198,10 @@ export async function syncGlobalHarness({
184
198
  if (applied.action !== "unchanged") result.configsRepaired.push(applied.configFile);
185
199
  }
186
200
 
201
+ await attachSddLifecycle({
202
+ result, homeDir, packageRoot, targetAdapters, targetComponents, dryRun, priorState: state
203
+ });
204
+
187
205
  if (!dryRun) {
188
206
  await persistGlobalState({
189
207
  paths,
@@ -194,7 +212,8 @@ export async function syncGlobalHarness({
194
212
  homeDir,
195
213
  componentFiles: coreFiles,
196
214
  backups: [...(state.backups ?? []), ...result.backups],
197
- installedAt: state.installedAt
215
+ installedAt: state.installedAt,
216
+ sdd: nextSddState(state?.sdd, result.integrations.sdd)
198
217
  });
199
218
  }
200
219
 
@@ -244,6 +263,22 @@ export async function uninstallGlobalHarness({ homeDir, dryRun = false }) {
244
263
  return result;
245
264
  }
246
265
 
266
+ async function attachSddLifecycle({
267
+ result, homeDir, packageRoot, targetAdapters, targetComponents, dryRun, priorState
268
+ }) {
269
+ const sdd = await applySddLifecycle({
270
+ homeDir,
271
+ packageRoot,
272
+ agentIds: targetAdapters.map((adapter) => adapter.id),
273
+ componentIds: targetComponents.map((component) => component.id),
274
+ dryRun,
275
+ priorSdd: priorState?.sdd ?? null,
276
+ yes: true
277
+ });
278
+ result.integrations.sdd = sdd;
279
+ result.sessionRefreshRequired = aggregateSessionRefreshRequired(result, sdd);
280
+ }
281
+
247
282
  async function applyAdapterPlans({ context, targetAdapters, result }) {
248
283
  for (const adapter of targetAdapters) {
249
284
  const plan = adapter.plan(context);
@@ -266,7 +301,8 @@ async function persistGlobalState({
266
301
  homeDir,
267
302
  componentFiles,
268
303
  backups,
269
- installedAt
304
+ installedAt,
305
+ sdd
270
306
  }) {
271
307
  const existingState = installedAt ? { installedAt } : await readGlobalState(paths.statePath);
272
308
  const adapterEntries = targetAdapters.map((adapter) => buildAdapterStateEntry(adapter, homeDir));
@@ -279,6 +315,7 @@ async function persistGlobalState({
279
315
  components: componentEntries,
280
316
  coreFiles: componentFiles,
281
317
  backups,
282
- installedAt: existingState?.installedAt
318
+ installedAt: existingState?.installedAt,
319
+ sdd
283
320
  }));
284
321
  }
@@ -0,0 +1,212 @@
1
+ import { createHash } from "node:crypto";
2
+ import { buildChangesFromPlan } from "./diff.js";
3
+ import { buildCheckSnapshot, recordRollbackHistory, recordSyncHistory } from "./history.js";
4
+ import { harnessHomePaths } from "./paths.js";
5
+ import { applyRollback, previewRollback } from "./rollback.js";
6
+ import { buildStatusReport } from "./status.js";
7
+ import { runHarnessSync } from "./sync.js";
8
+ import { needsManagedRepair } from "./governance-repair.js";
9
+
10
+ function stableJson(value) {
11
+ if (value == null || typeof value !== "object") return JSON.stringify(value);
12
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
13
+ const keys = Object.keys(value).sort();
14
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
15
+ }
16
+
17
+ export function fingerprintGovernancePreview(payload) {
18
+ return createHash("sha256").update(stableJson(payload)).digest("hex");
19
+ }
20
+
21
+ function summarizeIntegrations(result) {
22
+ const sdd = result?.integrations?.sdd ?? null;
23
+ if (!sdd) return null;
24
+ return {
25
+ status: sdd.status ?? null,
26
+ partial: Boolean(sdd.partial),
27
+ conflicts: sdd.conflicts ?? [],
28
+ receiptId: sdd.receipt?.id ?? null,
29
+ sessionRefreshRequired: Boolean(result?.sessionRefreshRequired ?? sdd.sessionRefreshRequired)
30
+ };
31
+ }
32
+
33
+ function buildSyncPreviewBody({ action, report, result }) {
34
+ const changes = result ? buildChangesFromPlan(result) : [];
35
+ const integrations = summarizeIntegrations(result);
36
+ const hasIntegrationWork = Boolean(
37
+ integrations
38
+ && integrations.status
39
+ && integrations.status !== "skipped"
40
+ && integrations.status !== "noop"
41
+ );
42
+ return {
43
+ kind: "sync",
44
+ action,
45
+ setupRequired: action === "setup-required",
46
+ hasChanges: changes.length > 0 || hasIntegrationWork || needsManagedRepair(report),
47
+ changes,
48
+ integrations,
49
+ checksBefore: buildCheckSnapshot(report?.counts),
50
+ overall: report?.overall ?? null
51
+ };
52
+ }
53
+
54
+ async function captureSyncPreview(options) {
55
+ const outcome = await runHarnessSync({
56
+ ...options,
57
+ dryRun: true,
58
+ yes: false,
59
+ confirm: false,
60
+ json: true,
61
+ interactive: false,
62
+ preflight: false
63
+ });
64
+ const body = buildSyncPreviewBody({
65
+ action: outcome.action,
66
+ report: outcome.report,
67
+ result: outcome.result
68
+ });
69
+ const fingerprint = fingerprintGovernancePreview(body);
70
+ return { ...body, fingerprint, wrote: false };
71
+ }
72
+
73
+ export async function previewGovernanceSync(options) {
74
+ return captureSyncPreview(options);
75
+ }
76
+
77
+ export async function applyGovernanceSync({
78
+ preview,
79
+ packageRoot,
80
+ packageName,
81
+ cliVersion,
82
+ homeDir,
83
+ workspaceRoot = null
84
+ }) {
85
+ if (!preview?.fingerprint) {
86
+ return { ok: false, reason: "missing-preview", wrote: false, receipt: null };
87
+ }
88
+
89
+ const paths = harnessHomePaths(homeDir);
90
+ const checksBefore = preview.checksBefore
91
+ ?? buildCheckSnapshot((await buildStatusReport(homeDir, { packageRoot, workspaceRoot }))?.counts);
92
+
93
+ const fresh = await captureSyncPreview({
94
+ packageRoot, packageName, cliVersion, homeDir, workspaceRoot
95
+ });
96
+
97
+ if (fresh.setupRequired) {
98
+ return { ok: false, reason: "setup-required", wrote: false, receipt: null, preview: fresh };
99
+ }
100
+
101
+ if (fresh.fingerprint !== preview.fingerprint) {
102
+ return { ok: false, reason: "stale-preview", wrote: false, receipt: null, preview: fresh };
103
+ }
104
+
105
+ if (!fresh.hasChanges) {
106
+ return {
107
+ ok: true,
108
+ reason: "noop",
109
+ wrote: false,
110
+ receipt: {
111
+ action: "noop",
112
+ backups: [],
113
+ checksBefore,
114
+ checksAfter: fresh.checksBefore,
115
+ integrations: fresh.integrations
116
+ }
117
+ };
118
+ }
119
+
120
+ const outcome = await runHarnessSync({
121
+ packageRoot,
122
+ packageName,
123
+ cliVersion,
124
+ homeDir,
125
+ workspaceRoot,
126
+ dryRun: false,
127
+ yes: true,
128
+ confirm: false,
129
+ json: true,
130
+ interactive: false,
131
+ preflight: false
132
+ });
133
+
134
+ await recordSyncHistory(homeDir, {
135
+ cliVersion,
136
+ options: { dryRun: false, yes: true, json: true, interactive: false, preflight: false },
137
+ outcome,
138
+ checksBefore,
139
+ packageRoot,
140
+ workspaceRoot
141
+ });
142
+
143
+ const afterReport = outcome.report
144
+ ?? await buildStatusReport(homeDir, { packageRoot, workspaceRoot });
145
+ const receipt = {
146
+ action: outcome.action,
147
+ backups: outcome.result?.backups ?? [],
148
+ checksBefore,
149
+ checksAfter: buildCheckSnapshot(afterReport?.counts),
150
+ integrations: summarizeIntegrations(outcome.result),
151
+ sessionRefreshRequired: Boolean(outcome.result?.sessionRefreshRequired),
152
+ historyPath: paths.historyPath
153
+ };
154
+
155
+ return {
156
+ ok: outcome.action === "repaired" || outcome.action === "noop",
157
+ reason: outcome.action,
158
+ wrote: Boolean(outcome.wrote),
159
+ receipt,
160
+ partial: Boolean(outcome.result?.integrations?.sdd?.partial)
161
+ };
162
+ }
163
+
164
+ function buildRollbackPreviewBody(preview) {
165
+ return {
166
+ kind: "rollback",
167
+ snapshot: preview.snapshot,
168
+ noop: Boolean(preview.noop),
169
+ files: (preview.plans ?? []).map((plan) => ({
170
+ backupName: plan.backupName,
171
+ displayPath: plan.displayPath
172
+ }))
173
+ };
174
+ }
175
+
176
+ export async function previewGovernanceRollback({ homeDir, snapshot }) {
177
+ const raw = await previewRollback({ homeDir, snapshot });
178
+ const body = buildRollbackPreviewBody(raw);
179
+ return { ...body, fingerprint: fingerprintGovernancePreview(body), wrote: false };
180
+ }
181
+
182
+ export async function applyGovernanceRollback({
183
+ preview,
184
+ homeDir,
185
+ cliVersion
186
+ }) {
187
+ if (!preview?.fingerprint || !preview?.snapshot) {
188
+ return { ok: false, reason: "missing-preview", wrote: false, receipt: null };
189
+ }
190
+
191
+ const fresh = await previewGovernanceRollback({ homeDir, snapshot: preview.snapshot });
192
+ if (fresh.fingerprint !== preview.fingerprint) {
193
+ return { ok: false, reason: "stale-preview", wrote: false, receipt: null, preview: fresh };
194
+ }
195
+
196
+ const result = await applyRollback({ homeDir, snapshot: preview.snapshot });
197
+ await recordRollbackHistory(homeDir, { cliVersion, snapshot: preview.snapshot, result });
198
+
199
+ const afterReport = await buildStatusReport(homeDir, {});
200
+ return {
201
+ ok: true,
202
+ reason: result.noop ? "noop" : "applied",
203
+ wrote: !result.noop,
204
+ receipt: {
205
+ action: result.noop ? "noop" : "rollback",
206
+ snapshot: preview.snapshot,
207
+ restored: result.restored ?? [],
208
+ safetyBackup: result.safetyBackup ?? null,
209
+ checksAfter: buildCheckSnapshot(afterReport?.counts)
210
+ }
211
+ };
212
+ }
@@ -0,0 +1,13 @@
1
+ /** Shared “repair needed” rule for sync, diff/snapshot, and cockpit actions. */
2
+ export function needsManagedRepair(statusReport) {
3
+ if (!statusReport) return false;
4
+ const needsSddRepair = (statusReport.checks ?? []).some((check) =>
5
+ check.componentId === "sdd-core"
6
+ && check.category === "integration"
7
+ && check.status === "warning"
8
+ );
9
+ return statusReport.overall === "drift"
10
+ || (statusReport.counts?.missing ?? 0) > 0
11
+ || (statusReport.counts?.stale ?? 0) > 0
12
+ || needsSddRepair;
13
+ }
@@ -0,0 +1,124 @@
1
+ import { formatProposalLines, proposalLimitForLayout } from "./cockpit-proposals.js";
2
+
3
+ export const CHANGES_PHASE = Object.freeze({
4
+ IDLE: "idle",
5
+ PREVIEWING: "previewing",
6
+ CONFIRMING: "confirming",
7
+ APPLYING: "applying",
8
+ COMPLETED: "completed",
9
+ FAILED: "failed"
10
+ });
11
+
12
+ export function createChangesActionState() {
13
+ return { phase: CHANGES_PHASE.IDLE, preview: null, receipt: null, error: null, message: null };
14
+ }
15
+
16
+ export function reduceChangesAction(state, action) {
17
+ switch (action.type) {
18
+ case "reset":
19
+ return createChangesActionState();
20
+ case "preview-start":
21
+ return { ...createChangesActionState(), phase: CHANGES_PHASE.PREVIEWING, message: "Building exact preview…" };
22
+ case "preview-ready":
23
+ if (action.preview?.setupRequired) {
24
+ return {
25
+ ...createChangesActionState(),
26
+ phase: CHANGES_PHASE.FAILED,
27
+ error: "setup-required",
28
+ message: "Not configured — open Control center and run setup. Changes never installs silently."
29
+ };
30
+ }
31
+ if (!action.preview?.hasChanges) {
32
+ return {
33
+ ...createChangesActionState(),
34
+ phase: CHANGES_PHASE.IDLE,
35
+ preview: action.preview,
36
+ message: "No pending governance changes."
37
+ };
38
+ }
39
+ return {
40
+ phase: CHANGES_PHASE.CONFIRMING,
41
+ preview: action.preview,
42
+ receipt: null,
43
+ error: null,
44
+ message: "Confirm apply? Y apply · N/Esc cancel"
45
+ };
46
+ case "preview-failed":
47
+ return { ...state, phase: CHANGES_PHASE.FAILED, error: action.error ?? "preview-failed", message: action.message ?? "Preview failed." };
48
+ case "apply-start":
49
+ return { ...state, phase: CHANGES_PHASE.APPLYING, message: "Applying confirmed repairs…" };
50
+ case "apply-done":
51
+ return {
52
+ phase: action.ok ? CHANGES_PHASE.COMPLETED : CHANGES_PHASE.FAILED,
53
+ preview: action.preview ?? state.preview,
54
+ receipt: action.receipt ?? null,
55
+ error: action.ok ? null : (action.reason ?? "apply-failed"),
56
+ message: action.message ?? (action.ok ? "Apply complete." : "Apply failed.")
57
+ };
58
+ case "cancel":
59
+ return { ...createChangesActionState(), message: "Cancelled — no files written." };
60
+ default:
61
+ return state;
62
+ }
63
+ }
64
+
65
+ export function formatChangesActionLines({ snapshot, changesAction, layoutMode = "compact" }) {
66
+ const diff = snapshot?.diff;
67
+ const phase = changesAction?.phase ?? CHANGES_PHASE.IDLE;
68
+ const lines = [`Changes · ${phase}`];
69
+
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, "");
77
+ }
78
+
79
+ if (changesAction?.message) lines.push(changesAction.message);
80
+ if (changesAction?.error && changesAction.error !== "setup-required") lines.push(`Error: ${changesAction.error}`);
81
+
82
+ 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}`);
98
+ }
99
+ } else if (!diff) {
100
+ lines.push("Scan did not include diff yet. Press R to re-scan.");
101
+ }
102
+
103
+ const receipt = changesAction?.receipt;
104
+ 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) {
108
+ lines.push(`Checks · before ok=${receipt.checksBefore.ok} → after ok=${receipt.checksAfter.ok}`);
109
+ }
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
+ }
113
+
114
+ if (phase === CHANGES_PHASE.IDLE || phase === CHANGES_PHASE.COMPLETED || phase === CHANGES_PHASE.FAILED) {
115
+ lines.push("", "A preview · Y confirm apply · N/Esc cancel · R re-scan");
116
+ }
117
+ return lines;
118
+ }
119
+
120
+ export function buildChangesFooterParts(phase) {
121
+ if (phase === CHANGES_PHASE.PREVIEWING || phase === CHANGES_PHASE.APPLYING) return ["Working…", "Esc Back"];
122
+ if (phase === CHANGES_PHASE.CONFIRMING) return ["Y Apply", "N/Esc Cancel"];
123
+ return ["A Preview", "R Re-scan", "Esc Back"];
124
+ }
@@ -0,0 +1,106 @@
1
+ import { CONTROL_PLANE_HEALTH } from "../control-plane-snapshot.js";
2
+ import { formatProposalLines, proposalLimitForLayout } from "./cockpit-proposals.js";
3
+
4
+ export function buildControlCenterModel({
5
+ projectName = "project",
6
+ snapshot = null,
7
+ layoutMode = "compact"
8
+ } = {}) {
9
+ if (!snapshot) {
10
+ return {
11
+ title: `CONTROL CENTER — ${projectName}`,
12
+ purpose: "Kairo aligns installed IDEs and agents with this project's architecture, rules, tools, and workflows.",
13
+ health: {
14
+ kind: CONTROL_PLANE_HEALTH.CHECK_FAILED,
15
+ label: "CHECK FAILED",
16
+ summaryLine: "Control-plane scan not available yet."
17
+ },
18
+ coverageLines: [],
19
+ cta: {
20
+ title: "NEXT",
21
+ actionTitle: "Retry scan",
22
+ actionDetail: "Press R to reload the read-only governance scan.",
23
+ enterHint: "R Retry",
24
+ kind: "verify",
25
+ destination: null
26
+ },
27
+ notes: [],
28
+ proposalLines: ["No evidence-backed proposals."],
29
+ includeEmbeddedStatus: layoutMode !== "wide"
30
+ };
31
+ }
32
+
33
+ const healthLabel = formatHealthLabel(snapshot.health);
34
+ const coverage = snapshot.coverage ?? {};
35
+ const policy = snapshot.policy;
36
+ const warnings = snapshot.status?.counts?.warning ?? 0;
37
+
38
+ return {
39
+ title: `CONTROL CENTER — ${projectName}`,
40
+ purpose: "Kairo aligns installed IDEs and agents with this project's architecture, rules, tools, and workflows.",
41
+ health: {
42
+ kind: snapshot.health,
43
+ label: healthLabel,
44
+ summaryLine: [
45
+ `${coverage.governedAgents ?? 0}/${coverage.detectedAgents ?? 0} agents governed`,
46
+ `${coverage.components ?? 0} modules`,
47
+ `${snapshot.backups?.count ?? 0} backups`
48
+ ].join(" · ")
49
+ },
50
+ coverageLines: [
51
+ `Active modules: ${(coverage.activeModules ?? []).join(", ") || "none"}`,
52
+ `Policy: ${policy?.profile ?? "none"} · applyMode ${policy?.applyMode ?? "n/a"}`,
53
+ warnings > 0
54
+ ? `Notes: ${warnings} non-blocking check warning(s)`
55
+ : "Notes: none",
56
+ snapshot.diff?.hasChanges
57
+ ? `Drift: ${snapshot.diff.changeCount ?? snapshot.diff.changes?.length ?? 0} change(s) pending review`
58
+ : "Drift: clean"
59
+ ],
60
+ cta: {
61
+ title: "NEXT",
62
+ actionTitle: snapshot.cta?.title ?? "Review control plane",
63
+ actionDetail: snapshot.cta?.detail ?? "",
64
+ enterHint: "Enter again →",
65
+ kind: snapshot.cta?.kind ?? null,
66
+ destination: snapshot.cta?.destination ?? null
67
+ },
68
+ notes: buildNotes(snapshot),
69
+ proposalLines: formatProposalLines(snapshot.proposals ?? [], {
70
+ limit: proposalLimitForLayout(layoutMode),
71
+ budgets: snapshot.budgets ?? null
72
+ }),
73
+ includeEmbeddedStatus: layoutMode !== "wide",
74
+ runsSecondaryHint: "Runs remain available as a secondary capability after setup and repairs."
75
+ };
76
+ }
77
+
78
+ function formatHealthLabel(kind) {
79
+ switch (kind) {
80
+ case CONTROL_PLANE_HEALTH.NOT_CONFIGURED:
81
+ return "NOT CONFIGURED";
82
+ case CONTROL_PLANE_HEALTH.ACTION_REQUIRED:
83
+ return "ACTION REQUIRED";
84
+ case CONTROL_PLANE_HEALTH.HEALTHY_WITH_NOTES:
85
+ return "HEALTHY WITH NOTES";
86
+ case CONTROL_PLANE_HEALTH.HEALTHY:
87
+ return "HEALTHY";
88
+ case CONTROL_PLANE_HEALTH.CHECK_FAILED:
89
+ return "CHECK FAILED";
90
+ default:
91
+ return String(kind ?? "UNKNOWN");
92
+ }
93
+ }
94
+
95
+ function buildNotes(snapshot) {
96
+ const notes = [];
97
+ for (const check of snapshot.status?.checks ?? []) {
98
+ if (check.status === "warning") {
99
+ notes.push(`${check.name}: ${check.detail ?? check.status}`);
100
+ }
101
+ }
102
+ if (notes.length === 0 && snapshot.health === CONTROL_PLANE_HEALTH.HEALTHY_WITH_NOTES) {
103
+ notes.push("Non-blocking notes are present. Open IDEs & models or Harness modules for detail.");
104
+ }
105
+ return notes.slice(0, 6);
106
+ }
@@ -12,6 +12,8 @@ import {
12
12
  regionsForLayout,
13
13
  routeCockpitKey
14
14
  } from "./cockpit-focus.js";
15
+ import { isRunsChildView } from "./cockpit-runs.js";
16
+ import { navIndexForView } from "./cockpit-models.js";
15
17
 
16
18
  export {
17
19
  canTabBetweenRegions,
@@ -142,6 +144,9 @@ export function reduceCockpitUi(state, action) {
142
144
  returnView: null
143
145
  };
144
146
  }
147
+ if (isRunsChildView(state.view)) {
148
+ return goRunsHub(state);
149
+ }
145
150
  if (state.view !== ORCHESTRATOR_VIEWS.HOME) {
146
151
  return goOverview(state);
147
152
  }
@@ -169,3 +174,15 @@ function goOverview(state) {
169
174
  returnView: null
170
175
  };
171
176
  }
177
+
178
+ function goRunsHub(state) {
179
+ return {
180
+ ...state,
181
+ helpOpen: false,
182
+ view: ORCHESTRATOR_VIEWS.RUNS,
183
+ navIndex: navIndexForView(ORCHESTRATOR_VIEWS.RUNS),
184
+ listIndex: 0,
185
+ region: defaultRegionForView(ORCHESTRATOR_VIEWS.RUNS, state.layoutMode),
186
+ returnView: null
187
+ };
188
+ }
@@ -0,0 +1,27 @@
1
+ import { ORCHESTRATOR_VIEWS } from "./orchestrator-state.js";
2
+
3
+ /**
4
+ * Separate nav open from Control center CTA activation.
5
+ * Enter opens Control center first; Enter again while there activates the CTA.
6
+ */
7
+ export function resolveEnterNavIntent({
8
+ currentView,
9
+ navItem,
10
+ ctaDestination = null
11
+ } = {}) {
12
+ if (!navItem) return { kind: "noop" };
13
+
14
+ const isOverview = navItem.id === "overview" || navItem.view === ORCHESTRATOR_VIEWS.HOME;
15
+ if (isOverview) {
16
+ if (currentView === ORCHESTRATOR_VIEWS.HOME && ctaDestination) {
17
+ return { kind: "activate-cta", destination: ctaDestination };
18
+ }
19
+ return { kind: "open-nav", view: ORCHESTRATOR_VIEWS.HOME };
20
+ }
21
+
22
+ if (navItem.action === "launch") {
23
+ return { kind: "launch" };
24
+ }
25
+
26
+ return { kind: "open-nav", view: navItem.view };
27
+ }
@@ -4,16 +4,22 @@ import { LAYOUT_MODES } from "./layout.js";
4
4
 
5
5
  const NAV_FOCUSED_VIEWS = new Set([
6
6
  ORCHESTRATOR_VIEWS.HOME,
7
+ ORCHESTRATOR_VIEWS.IDES,
8
+ ORCHESTRATOR_VIEWS.MODULES,
9
+ ORCHESTRATOR_VIEWS.CHANGES,
10
+ ORCHESTRATOR_VIEWS.PROFILE,
7
11
  ORCHESTRATOR_VIEWS.PROVIDERS,
8
12
  ORCHESTRATOR_VIEWS.DIAGNOSTICS,
9
13
  ORCHESTRATOR_VIEWS.HELP
10
14
  ]);
11
15
 
12
16
  const CONTENT_INTERACTIVE_VIEWS = new Set([
17
+ ORCHESTRATOR_VIEWS.RUNS,
13
18
  ORCHESTRATOR_VIEWS.ACTIVE_RUNS,
14
19
  ORCHESTRATOR_VIEWS.RECENT_RUNS,
15
20
  ORCHESTRATOR_VIEWS.RUN_DETAIL,
16
- ORCHESTRATOR_VIEWS.LAUNCH
21
+ ORCHESTRATOR_VIEWS.LAUNCH,
22
+ ORCHESTRATOR_VIEWS.ACTIVITY
17
23
  ]);
18
24
 
19
25
  export function isNavFocusedView(view) {