@kal-elsam/kairo-runtime 0.14.0 → 0.16.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 (56) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/bin/kairo-runtime.js +0 -0
  3. package/bin/kairo.js +0 -0
  4. package/package.json +1 -1
  5. package/src/cli.js +182 -8
  6. package/src/global/check-resolutions.js +31 -0
  7. package/src/global/cli-help.js +21 -2
  8. package/src/global/component-ecosystem-checks.js +2 -0
  9. package/src/global/component-integration-cli.js +29 -10
  10. package/src/global/components-resolve-cli.js +246 -0
  11. package/src/global/connection-actions.js +147 -0
  12. package/src/global/connections.js +269 -0
  13. package/src/global/control-plane/attention.js +141 -0
  14. package/src/global/control-plane/build-report.js +146 -0
  15. package/src/global/control-plane/cli.js +36 -0
  16. package/src/global/control-plane/constants.js +38 -0
  17. package/src/global/control-plane/gentle-adapters.js +183 -0
  18. package/src/global/control-plane/provider.js +69 -0
  19. package/src/global/control-plane/review-status.js +115 -0
  20. package/src/global/control-plane/sdd-status.js +49 -0
  21. package/src/global/control-plane/team.js +63 -0
  22. package/src/global/fleet-configure-plan.js +123 -0
  23. package/src/global/fleet-configure.js +303 -0
  24. package/src/global/fleet-models.js +188 -0
  25. package/src/global/fleet-set.js +219 -0
  26. package/src/global/fleet-shared.js +38 -0
  27. package/src/global/ink/cockpit-controller.js +1 -1
  28. package/src/global/ink/cockpit-models.js +4 -1
  29. package/src/global/ink/orchestrator-app.js +21 -2
  30. package/src/global/ink/ux/live-overview.js +5 -11
  31. package/src/global/ink/ux/overview-needs.js +1 -1
  32. package/src/global/integrations/engram-evidence.js +7 -2
  33. package/src/global/integrations/sdd-apply.js +17 -7
  34. package/src/global/integrations/sdd-evidence.js +22 -3
  35. package/src/global/integrations/sdd-plan.js +21 -3
  36. package/src/global/integrations/sdd-resolutions.js +73 -0
  37. package/src/global/integrations/sdd-state.js +69 -0
  38. package/src/global/integrations/sdd-verify.js +9 -4
  39. package/src/global/mcp/kairo-mcp.js +56 -5
  40. package/src/global/mcp/resolve-mcp-workspace.js +51 -0
  41. package/src/global/mcp/work-snapshot-rule.js +89 -0
  42. package/src/global/mcp/work-snapshot-tool.js +49 -0
  43. package/src/global/mcp-install.js +239 -0
  44. package/src/global/next/next-cli.js +35 -0
  45. package/src/global/next/next-report.js +145 -0
  46. package/src/global/next/project-key.js +36 -0
  47. package/src/global/next/publish-work-snapshot.js +116 -0
  48. package/src/global/next/work-enroll.js +91 -0
  49. package/src/global/next/work-snapshot.js +216 -0
  50. package/src/global/observability/fleet-activity.js +197 -0
  51. package/src/global/observability/fleet-models-catalog.js +137 -0
  52. package/src/global/observability/fleet-platforms.js +166 -0
  53. package/src/global/observability/fleet-probe.js +229 -0
  54. package/src/global/observability/gentle-probe.js +30 -2
  55. package/src/global/observability/index.js +2 -1
  56. package/src/global/paths.js +2 -1
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Unified fleet model profile (~/.harness/fleet-models.json).
3
+ * Multi-agent platforms (Claude + OpenCode) share phase keys.
4
+ * Codex keeps a single default model. Cursor Auto stays IDE-managed.
5
+ */
6
+ import { readFile, mkdir } from "node:fs/promises";
7
+ import { dirname, join } from "node:path";
8
+ import { resolveHomeDir } from "./paths.js";
9
+ import { writeAtomicJson } from "./runtime/write-atomic-json.js";
10
+ import {
11
+ SDD_PHASES,
12
+ loadGentleClaudeAssignments
13
+ } from "./fleet-shared.js";
14
+ import { parseFrontmatterModel, parseCodexDefaultModel } from "./observability/fleet-platforms.js";
15
+
16
+ export const FLEET_MODELS_VERSION = 1;
17
+
18
+ export function fleetModelsPath(homeDir = resolveHomeDir()) {
19
+ return join(homeDir, ".harness", "fleet-models.json");
20
+ }
21
+
22
+ export function emptyFleetProfile() {
23
+ const phases = {};
24
+ for (const id of SDD_PHASES) {
25
+ phases[id] = { claude: null, opencode: null };
26
+ }
27
+ return {
28
+ version: FLEET_MODELS_VERSION,
29
+ claudeDefault: null,
30
+ codexDefault: null,
31
+ cursorAgentModel: "inherit",
32
+ phases,
33
+ note: "Multi-agent phases for Claude + OpenCode. Codex uses codexDefault only. Cursor agents use inherit (Auto is IDE-managed)."
34
+ };
35
+ }
36
+
37
+ async function readJsonSafe(path, read) {
38
+ try {
39
+ return JSON.parse(await read(path, "utf8"));
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Seed profile from Gentle + current on-disk OpenCode/Claude/Codex.
47
+ * Preserves tuned OpenCode models when present.
48
+ */
49
+ export async function seedFleetProfile({
50
+ homeDir = resolveHomeDir(),
51
+ read = readFile
52
+ } = {}) {
53
+ const profile = emptyFleetProfile();
54
+ const gentle = await loadGentleClaudeAssignments(homeDir, read);
55
+ if (gentle) {
56
+ profile.claudeDefault = gentle.default ?? "sonnet";
57
+ for (const id of SDD_PHASES) {
58
+ if (gentle[id]) profile.phases[id].claude = gentle[id];
59
+ }
60
+ }
61
+
62
+ const settings = await readJsonSafe(join(homeDir, ".claude", "settings.json"), read);
63
+ if (settings?.model && !profile.claudeDefault) profile.claudeDefault = settings.model;
64
+
65
+ for (const id of SDD_PHASES) {
66
+ try {
67
+ const raw = await read(join(homeDir, ".claude", "agents", `${id}.md`), "utf8");
68
+ const model = parseFrontmatterModel(raw).model;
69
+ if (model && model !== "inherit") profile.phases[id].claude = model;
70
+ } catch { /* missing */ }
71
+ }
72
+
73
+ const oc = await readJsonSafe(join(homeDir, ".config", "opencode", "opencode.json"), read);
74
+ const agents = oc?.agent ?? oc?.agents ?? {};
75
+ for (const id of SDD_PHASES) {
76
+ const model = agents[id]?.model;
77
+ // Only declare OpenCode models that exist on disk — never invent from Claude tiers.
78
+ if (typeof model === "string") profile.phases[id].opencode = model;
79
+ }
80
+
81
+ try {
82
+ const toml = await read(join(homeDir, ".codex", "config.toml"), "utf8");
83
+ profile.codexDefault = parseCodexDefaultModel(toml);
84
+ } catch { /* missing */ }
85
+
86
+ return profile;
87
+ }
88
+
89
+ export async function loadFleetProfile({
90
+ homeDir = resolveHomeDir(),
91
+ read = readFile,
92
+ seedIfMissing = true
93
+ } = {}) {
94
+ const path = fleetModelsPath(homeDir);
95
+ const existing = await readJsonSafe(path, read);
96
+ if (existing?.phases) {
97
+ return { profile: existing, path, seeded: false };
98
+ }
99
+ if (!seedIfMissing) {
100
+ return { profile: emptyFleetProfile(), path, seeded: false };
101
+ }
102
+ const profile = await seedFleetProfile({ homeDir, read });
103
+ return { profile, path, seeded: true };
104
+ }
105
+
106
+ export async function saveFleetProfile(profile, {
107
+ homeDir = resolveHomeDir(),
108
+ writeAtomicJsonFn = writeAtomicJson,
109
+ mkdirFn = mkdir
110
+ } = {}) {
111
+ const path = fleetModelsPath(homeDir);
112
+ await mkdirFn(dirname(path), { recursive: true });
113
+ const next = {
114
+ ...profile,
115
+ version: FLEET_MODELS_VERSION,
116
+ updatedAt: new Date().toISOString()
117
+ };
118
+ await writeAtomicJsonFn(path, next);
119
+ return { path, profile: next };
120
+ }
121
+
122
+ /** Expand profile → assignment maps used by apply. */
123
+ export function profileToPlatformAssignments(profile) {
124
+ const claude = {};
125
+ const opencode = {};
126
+ if (profile.claudeDefault) claude.default = profile.claudeDefault;
127
+ for (const id of SDD_PHASES) {
128
+ const row = profile.phases?.[id] ?? {};
129
+ if (row.claude) claude[id] = row.claude;
130
+ if (row.opencode) opencode[id] = row.opencode;
131
+ }
132
+ return {
133
+ claude,
134
+ opencode,
135
+ codex: profile.codexDefault ? { codex_default: profile.codexDefault } : {},
136
+ cursorAgentModel: profile.cursorAgentModel ?? "inherit"
137
+ };
138
+ }
139
+
140
+ export function formatFleetProfileText(profile, { path = null } = {}) {
141
+ const lines = ["Fleet profile (multi-agent)", ""];
142
+ if (path) lines.push(`Path · ${path}`);
143
+ lines.push(`Claude default · ${profile.claudeDefault ?? "—"}`);
144
+ lines.push(`Codex default · ${profile.codexDefault ?? "—"} (single-model tool)`);
145
+ lines.push(`Cursor agents · ${profile.cursorAgentModel ?? "inherit"} (Auto IDE-managed)`);
146
+ lines.push("");
147
+ for (const id of SDD_PHASES) {
148
+ const row = profile.phases?.[id] ?? {};
149
+ lines.push(`${id} · claude ${row.claude ?? "—"} · opencode ${row.opencode ?? "—"}`);
150
+ }
151
+ if (profile.note) {
152
+ lines.push("");
153
+ lines.push(profile.note);
154
+ }
155
+ return lines.join("\n").trimEnd();
156
+ }
157
+
158
+ export async function runFleetModels({
159
+ json = false,
160
+ profile = false,
161
+ homeDir = resolveHomeDir()
162
+ } = {}) {
163
+ const { printJson } = await import("./json-output.js");
164
+ const { commandHeader } = await import("./brand/index.js");
165
+ const { buildFleetModelsCatalog, formatFleetModelsText } = await import(
166
+ "./observability/fleet-models-catalog.js"
167
+ );
168
+ const catalog = await buildFleetModelsCatalog({ homeDir });
169
+ if (!profile) {
170
+ if (json) printJson(catalog);
171
+ else {
172
+ console.log(commandHeader("Fleet models"));
173
+ console.log(formatFleetModelsText(catalog));
174
+ }
175
+ return catalog;
176
+ }
177
+
178
+ const loaded = await loadFleetProfile({ homeDir, seedIfMissing: true });
179
+ const payload = { ok: true, profile: loaded.profile, path: loaded.path, catalog };
180
+ if (json) printJson(payload);
181
+ else {
182
+ console.log(commandHeader("Fleet models"));
183
+ console.log(formatFleetProfileText(loaded.profile, { path: loaded.path }));
184
+ console.log("");
185
+ console.log(formatFleetModelsText(catalog));
186
+ }
187
+ return payload;
188
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Consent-gated fleet model writes (OpenCode / Claude / Codex).
3
+ * Plan by default; --yes applies with backup. Never touches Cursor Auto / state.vscdb.
4
+ */
5
+ import { copyFile, readFile, writeFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import { resolveHomeDir } from "./paths.js";
8
+ import { writeAtomicJson } from "./runtime/write-atomic-json.js";
9
+ import { printJson } from "./json-output.js";
10
+ import { commandHeader } from "./brand/index.js";
11
+ import { formatCliCommand } from "./brand/cli.js";
12
+ import {
13
+ parseFrontmatterModel,
14
+ replaceFrontmatterModel,
15
+ replaceCodexDefaultModel
16
+ } from "./observability/fleet-platforms.js";
17
+
18
+ const PLATFORMS = new Set(["opencode", "claude", "codex"]);
19
+
20
+ function backupPath(path, stamp = Date.now()) {
21
+ return `${path}.kairo-backup.${stamp}`;
22
+ }
23
+
24
+ function requirePlatform(platform) {
25
+ const p = String(platform ?? "").toLowerCase();
26
+ if (!PLATFORMS.has(p)) {
27
+ throw new Error(`Unsupported platform "${platform}". Use opencode, claude, or codex.`);
28
+ }
29
+ return p;
30
+ }
31
+
32
+ function requireAgent(agent) {
33
+ const id = String(agent ?? "").trim();
34
+ if (!id) throw new Error("Missing --agent <id>.");
35
+ return id;
36
+ }
37
+
38
+ function requireModel(model) {
39
+ const m = String(model ?? "").trim();
40
+ if (!m) throw new Error("Missing --model <id>.");
41
+ return m;
42
+ }
43
+
44
+ export function buildOpenCodeFleetSetPlan({ config, agent, model, configPath }) {
45
+ const agents = config?.agent ?? config?.agents;
46
+ if (!agents || typeof agents !== "object" || !(agent in agents)) {
47
+ throw new Error(`OpenCode agent "${agent}" not found in ${configPath}.`);
48
+ }
49
+ const prev = agents[agent];
50
+ const previousModel = typeof prev?.model === "string" ? prev.model : (config?.model ?? null);
51
+ const nextAgents = {
52
+ ...agents,
53
+ [agent]: { ...prev, model }
54
+ };
55
+ const next = { ...config, agent: nextAgents };
56
+ if (config.agents && !config.agent) {
57
+ delete next.agent;
58
+ next.agents = nextAgents;
59
+ }
60
+ return {
61
+ platform: "opencode",
62
+ agent,
63
+ model,
64
+ previousModel,
65
+ path: configPath,
66
+ wouldWrite: previousModel !== model,
67
+ next,
68
+ note: `Set OpenCode agent.${agent}.model → ${model}`
69
+ };
70
+ }
71
+
72
+ export async function runFleetSet({
73
+ platform,
74
+ agent,
75
+ model,
76
+ yes = false,
77
+ json = false,
78
+ dryRun = false,
79
+ homeDir = resolveHomeDir(),
80
+ read = readFile,
81
+ writeText = writeFile,
82
+ copyFileFn = copyFile,
83
+ writeAtomicJsonFn = writeAtomicJson,
84
+ now = () => Date.now()
85
+ } = {}) {
86
+ const p = requirePlatform(platform);
87
+ const nextModel = requireModel(model);
88
+ const agentId = p === "codex"
89
+ ? (String(agent ?? "default").trim() || "default")
90
+ : requireAgent(agent);
91
+ const apply = yes === true && dryRun !== true;
92
+
93
+ let plan;
94
+ let path;
95
+ let applyFn;
96
+
97
+ if (p === "opencode") {
98
+ path = join(homeDir, ".config", "opencode", "opencode.json");
99
+ const raw = await read(path, "utf8");
100
+ const config = JSON.parse(raw);
101
+ plan = buildOpenCodeFleetSetPlan({
102
+ config, agent: agentId, model: nextModel, configPath: path
103
+ });
104
+ applyFn = async () => {
105
+ await writeAtomicJsonFn(path, plan.next);
106
+ };
107
+ } else if (p === "claude") {
108
+ if (agentId === "default") {
109
+ path = join(homeDir, ".claude", "settings.json");
110
+ const existing = JSON.parse(await read(path, "utf8"));
111
+ const previousModel = typeof existing.model === "string" ? existing.model : null;
112
+ plan = {
113
+ platform: "claude",
114
+ agent: agentId,
115
+ model: nextModel,
116
+ previousModel,
117
+ path,
118
+ wouldWrite: previousModel !== nextModel,
119
+ next: { ...existing, model: nextModel },
120
+ note: `Set Claude settings.model → ${nextModel}`
121
+ };
122
+ applyFn = async () => {
123
+ await writeAtomicJsonFn(path, plan.next);
124
+ };
125
+ } else {
126
+ path = join(homeDir, ".claude", "agents", `${agentId}.md`);
127
+ const raw = await read(path, "utf8");
128
+ const previousModel = parseFrontmatterModel(raw).model;
129
+ const nextText = replaceFrontmatterModel(raw, nextModel);
130
+ plan = {
131
+ platform: "claude",
132
+ agent: agentId,
133
+ model: nextModel,
134
+ previousModel,
135
+ path,
136
+ wouldWrite: previousModel !== nextModel,
137
+ nextText,
138
+ note: `Set Claude agent ${agentId} frontmatter model → ${nextModel}`
139
+ };
140
+ applyFn = async () => {
141
+ await writeText(path, plan.nextText, "utf8");
142
+ };
143
+ }
144
+ } else {
145
+ path = join(homeDir, ".codex", "config.toml");
146
+ const raw = await read(path, "utf8");
147
+ const previousMatch = raw.match(/^\s*model\s*=\s*"([^"]+)"/m);
148
+ const previousModel = previousMatch ? previousMatch[1] : null;
149
+ const nextText = replaceCodexDefaultModel(raw, nextModel);
150
+ plan = {
151
+ platform: "codex",
152
+ agent: "default",
153
+ model: nextModel,
154
+ previousModel,
155
+ path,
156
+ wouldWrite: previousModel !== nextModel,
157
+ nextText,
158
+ note: `Set Codex config.toml model → ${nextModel}`
159
+ };
160
+ applyFn = async () => {
161
+ await writeText(path, plan.nextText, "utf8");
162
+ };
163
+ }
164
+
165
+ plan.backupPath = backupPath(path, now());
166
+ plan.applyWith = formatCliCommand(
167
+ `fleet set --platform ${p} --agent ${plan.agent} --model ${nextModel} --yes`
168
+ );
169
+
170
+ if (!apply) {
171
+ const payload = {
172
+ ok: true,
173
+ applied: false,
174
+ plan: {
175
+ platform: plan.platform,
176
+ agent: plan.agent,
177
+ model: plan.model,
178
+ previousModel: plan.previousModel,
179
+ path: plan.path,
180
+ wouldWrite: plan.wouldWrite,
181
+ note: plan.note,
182
+ applyWith: plan.applyWith
183
+ }
184
+ };
185
+ if (json) printJson(payload);
186
+ else {
187
+ console.log(commandHeader("Fleet set"));
188
+ console.log(plan.note);
189
+ console.log(`Path · ${plan.path}`);
190
+ console.log(`Was · ${plan.previousModel ?? "—"}`);
191
+ console.log(`Now · ${plan.model}`);
192
+ console.log(`Apply · ${plan.applyWith}`);
193
+ }
194
+ return payload;
195
+ }
196
+
197
+ await copyFileFn(path, plan.backupPath);
198
+ await applyFn();
199
+
200
+ const receipt = {
201
+ ok: true,
202
+ applied: true,
203
+ platform: plan.platform,
204
+ agent: plan.agent,
205
+ model: plan.model,
206
+ previousModel: plan.previousModel,
207
+ path: plan.path,
208
+ backupPath: plan.backupPath,
209
+ note: "Model assignment updated. Refresh Kairo Fleet to see declared changes."
210
+ };
211
+ if (json) printJson(receipt);
212
+ else {
213
+ console.log(commandHeader("Fleet set"));
214
+ console.log(`Wrote · ${receipt.path}`);
215
+ console.log(`Backup · ${receipt.backupPath}`);
216
+ console.log(receipt.note);
217
+ }
218
+ return receipt;
219
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared fleet constants + Gentle assignment loader (no circular imports).
3
+ */
4
+ import { readFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+
7
+ export const SDD_PHASES = Object.freeze([
8
+ "sdd-apply", "sdd-archive", "sdd-design", "sdd-explore", "sdd-init",
9
+ "sdd-onboard", "sdd-propose", "sdd-spec", "sdd-tasks", "sdd-verify"
10
+ ]);
11
+
12
+ /** Map Gentle/Claude tier names → OpenCode provider/model ids. */
13
+ export const CLAUDE_TO_OPENCODE = Object.freeze({
14
+ opus: "opencode-go/deepseek-v4-pro",
15
+ sonnet: "opencode-go/qwen3.5-plus",
16
+ haiku: "opencode-go/deepseek-v4-flash"
17
+ });
18
+
19
+ export async function loadGentleClaudeAssignments(homeDir, read = readFile) {
20
+ try {
21
+ const state = JSON.parse(await read(join(homeDir, ".gentle-ai", "state.json"), "utf8"));
22
+ const map = state?.claude_model_assignments;
23
+ if (!map || typeof map !== "object") return null;
24
+ return { ...map };
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ export function mapClaudeAssignmentsToOpenCode(assignments = {}) {
31
+ const out = {};
32
+ for (const [agent, tier] of Object.entries(assignments)) {
33
+ if (agent === "default" || agent === "codex_default") continue;
34
+ const key = String(tier ?? "").toLowerCase();
35
+ out[agent] = CLAUDE_TO_OPENCODE[key] ?? CLAUDE_TO_OPENCODE.sonnet;
36
+ }
37
+ return out;
38
+ }
@@ -29,7 +29,7 @@ export {
29
29
  export function createCockpitUiState({
30
30
  layoutMode = LAYOUT_MODES.COMPACT,
31
31
  view = ORCHESTRATOR_VIEWS.HOME,
32
- region = COCKPIT_REGIONS.NAV,
32
+ region = null,
33
33
  navIndex = 0,
34
34
  listIndex = 0,
35
35
  helpOpen = false,
@@ -328,8 +328,11 @@ export function buildFooterModel({
328
328
 
329
329
  // HOME footer must stay on one line at 80 cols (frame already near 24 rows).
330
330
  if (view === ORCHESTRATOR_VIEWS.HOME) {
331
+ const homeParts = region === COCKPIT_REGIONS.NAV
332
+ ? ["↑↓ Section", "Enter Open", "? Help", "Esc Exit"]
333
+ : ["1·2 Select", "Enter Run", "? Help", "Esc Exit"];
331
334
  return {
332
- text: ["↑↓", "Enter", "Tab", "Space", "R", "?", "/", "Esc"].join(` ${glyphs.bullet} `),
335
+ text: homeParts.join(` ${glyphs.bullet} `),
333
336
  columns: footerColumns
334
337
  };
335
338
  }
@@ -63,8 +63,7 @@ export function OrchestratorApp({
63
63
  const [ui, dispatch] = useReducer(
64
64
  reduceCockpitUi,
65
65
  createCockpitUiState({
66
- layoutMode: layoutMode ?? LAYOUT_MODES.COMPACT,
67
- region: COCKPIT_REGIONS.NAV
66
+ layoutMode: layoutMode ?? LAYOUT_MODES.COMPACT
68
67
  })
69
68
  );
70
69
  const data = useOrchestratorData({
@@ -166,6 +165,26 @@ export function OrchestratorApp({
166
165
  return;
167
166
  }
168
167
 
168
+ if (ui.view === ORCHESTRATOR_VIEWS.HOME && !ui.paletteOpen) {
169
+ const digit = inputKey === "1" ? 0 : inputKey === "2" ? 1 : -1;
170
+ if (digit >= 0) {
171
+ const buttons = buildOverviewButtons({
172
+ hasGlobalState,
173
+ snapshot: data.snapshot,
174
+ diagnostics: data.diagnostics,
175
+ dashboard: data.dashboard
176
+ });
177
+ const selected = buttons[digit];
178
+ const intent = selected?.intent ?? null;
179
+ if (intent === "setup") {
180
+ finish({ cancelled: false, action: "setup" });
181
+ return;
182
+ }
183
+ if (intent && openDestination(intent)) return;
184
+ return;
185
+ }
186
+ }
187
+
169
188
  if (inputKey === " " && ui.view === ORCHESTRATOR_VIEWS.HOME && !ui.paletteOpen) {
170
189
  dispatch({ type: "toggle-overview-details" });
171
190
  return;
@@ -94,12 +94,6 @@ export function adaptControlCenterToOverview(model = {}, options = {}) {
94
94
  label: `Alerts · ${model.alerts.headline ?? `${model.alerts.count} open`}`
95
95
  });
96
96
  }
97
- if (rest.length > 0) {
98
- metrics.push({
99
- id: "more",
100
- label: `${rest.length} more in Details · Space`
101
- });
102
- }
103
97
  if (metrics.length === 0) {
104
98
  metrics.push({ id: "quiet", label: "Nothing else needs you right now" });
105
99
  }
@@ -201,13 +195,13 @@ export function SemanticOverviewPanel({
201
195
  marginBottom: index === view.buttons.length - 1 ? 0 : 1
202
196
  },
203
197
  React.createElement(Text, {
204
- bold: true,
205
- color: focused && colorEnabled ? COCKPIT_COLORS.interactive : undefined
206
- }, `${focused ? glyphs.focus : " "} ${button.label}`),
198
+ bold: selected,
199
+ color: colorEnabled && selected ? COCKPIT_COLORS.interactive : undefined
200
+ }, `${selected ? glyphs.focus : " "} [${index + 1}] ${button.label}${focused ? " ← Press Enter" : ""}`),
207
201
  button.detail
208
202
  ? React.createElement(Text, {
209
203
  color: colorEnabled ? COCKPIT_COLORS.muted : undefined
210
- }, ` ${button.detail}`)
204
+ }, ` ${button.detail}`)
211
205
  : null
212
206
  );
213
207
  })
@@ -223,7 +217,7 @@ export function SemanticOverviewPanel({
223
217
  ),
224
218
  React.createElement(Details, {
225
219
  open: detailsOpen,
226
- summary: "More info",
220
+ summary: `More info (${view.details.length})`,
227
221
  lines: view.details,
228
222
  colorEnabled,
229
223
  focused: false,
@@ -4,7 +4,7 @@
4
4
  import { CONTROL_PLANE_HEALTH } from "../../control-plane-snapshot.js";
5
5
  import { formatCliCommand } from "../../brand/cli.js";
6
6
 
7
- export const OVERVIEW_NEED_LIMIT = 3;
7
+ export const OVERVIEW_NEED_LIMIT = 2;
8
8
 
9
9
  /** Prefixes that never belong on the first screen (machine / internals). */
10
10
  export const DETAILS_ONLY_PREFIXES = [
@@ -215,12 +215,17 @@ function jsonKeyEvidence(path, keyPath, kind) {
215
215
  try {
216
216
  let cursor = JSON.parse(readFileSync(path, "utf8"));
217
217
  for (const key of keyPath) {
218
- if (cursor == null || typeof cursor !== "object") {
218
+ if (cursor == null || typeof cursor !== "object" || Array.isArray(cursor)) {
219
219
  return { path, kind, present: false, conflict: true, keyPath, detail: "invalid structure" };
220
220
  }
221
+ // Missing key = unconfigured evidence, not a conflict (file may use another MCP path).
222
+ if (!Object.prototype.hasOwnProperty.call(cursor, key)) {
223
+ return { path, kind, present: false, conflict: false, keyPath };
224
+ }
221
225
  cursor = cursor[key];
222
226
  }
223
- return { path, kind, present: cursor != null && typeof cursor === "object", conflict: false, keyPath };
227
+ const present = cursor != null && typeof cursor === "object" && !Array.isArray(cursor);
228
+ return { path, kind, present, conflict: false, keyPath };
224
229
  } catch {
225
230
  return { path, kind, present: false, conflict: true, keyPath, detail: "unreadable json" };
226
231
  }
@@ -17,7 +17,8 @@ const APPLYING_ACTIONS = new Set([SDD_PLAN_ACTIONS.CREATE, SDD_PLAN_ACTIONS.UPDA
17
17
 
18
18
  export async function applySddConfigure({
19
19
  requestedAgentIds = null, detectedAgentIds = [], homeDir, packageRoot, persona = "off",
20
- personaAgentIds = [], trackedFiles = {}, preservePersona = false, dryRun = false, yes = false,
20
+ personaAgentIds = [], trackedFiles = {}, adoptedFiles = {}, overwriteConflicts = false,
21
+ preservePersona = false, dryRun = false, yes = false,
21
22
  json = false, interactive = null, receiptId = null, plan = planSddConfigure,
22
23
  confirm = promptApplyConfirmation, saveReceipt = saveSddReceipt,
23
24
  now = () => new Date().toISOString()
@@ -28,14 +29,16 @@ export async function applySddConfigure({
28
29
 
29
30
  const planned = await plan({
30
31
  requestedAgentIds, detectedAgentIds, homeDir, packageRoot, persona, personaAgentIds,
31
- trackedFiles, preservePersona, dryRun: true
32
+ trackedFiles, adoptedFiles, overwriteConflicts, preservePersona, dryRun: true
32
33
  });
33
34
  if (dryRun) return { ...planned, applied: false, cancelled: false, receipt: null };
34
35
 
35
36
  if (shouldPromptApplyConfirmation({ applying: true, dryRun, json, confirm: yes, interactive })) {
36
37
  const accepted = await confirm({
37
38
  command: "components configure sdd-core",
38
- question: "Materialize SDD skills for the planned agents? [Y/n]: "
39
+ question: overwriteConflicts
40
+ ? "Overwrite conflicting SDD skills with canonical Kairo copies (backups first)? [Y/n]: "
41
+ : "Materialize SDD skills for the planned agents? [Y/n]: "
39
42
  });
40
43
  if (!accepted) return { ...planned, applied: false, cancelled: true, receipt: null };
41
44
  }
@@ -66,7 +69,8 @@ export async function applySddConfigure({
66
69
  try {
67
70
  const managedRoot = resolveSddSkillRoot(action.agentIds[0], homeDir);
68
71
  const outcome = await materializeOne(action, {
69
- homeDir, packageRoot, managedRoot, receiptId: resolvedReceiptId
72
+ homeDir, packageRoot, managedRoot, receiptId: resolvedReceiptId,
73
+ overwriteConflicts: Boolean(action.overwrote || overwriteConflicts)
70
74
  });
71
75
  if (outcome.conflict) {
72
76
  files.push({
@@ -78,7 +82,8 @@ export async function applySddConfigure({
78
82
  if (outcome.backup) backups.push(outcome.backup);
79
83
  files.push({
80
84
  ...record, applied: true, skipped: false, outcome: SDD_FILE_OUTCOMES.APPLIED,
81
- afterHash: outcome.afterHash, parentRealpath: outcome.parentRealpath
85
+ afterHash: outcome.afterHash, parentRealpath: outcome.parentRealpath,
86
+ overwrote: Boolean(action.overwrote)
82
87
  });
83
88
  } catch (error) {
84
89
  failed = { skillId: action.skillId, destinationPath: action.destinationPath, error: error.message };
@@ -141,7 +146,9 @@ async function readCanonicalBytes(action, packageRoot) {
141
146
  ));
142
147
  }
143
148
 
144
- async function materializeOne(action, { homeDir, packageRoot, managedRoot, receiptId }) {
149
+ async function materializeOne(action, {
150
+ homeDir, packageRoot, managedRoot, receiptId, overwriteConflicts = false
151
+ }) {
145
152
  const chain = await assertSafePathChain(action.destinationPath, managedRoot, homeDir);
146
153
  if (!chain.ok) return { conflict: chain.reason };
147
154
 
@@ -169,7 +176,10 @@ async function materializeOne(action, { homeDir, packageRoot, managedRoot, recei
169
176
  return { conflict: "Managed destination disappeared after planning; preserving byte-for-byte." };
170
177
  }
171
178
  const snap = await snapshotRegularFile(action.destinationPath);
172
- if (snap.hash !== action.trackedHash) {
179
+ const expectedHash = overwriteConflicts || action.overwrote
180
+ ? snap.hash
181
+ : action.trackedHash;
182
+ if (snap.hash !== expectedHash) {
173
183
  return { conflict: "Managed file changed after planning; preserving byte-for-byte." };
174
184
  }
175
185
  const parent = await parentRealpath(action.destinationPath);