@kal-elsam/kairo-runtime 0.10.0 → 0.12.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 (76) hide show
  1. package/global-template/components/agent-skills/LICENSE +21 -0
  2. package/global-template/components/agent-skills/PROVENANCE.md +26 -0
  3. package/global-template/components/agent-skills/skills/context-engineering/SKILL.md +289 -0
  4. package/global-template/components/agent-skills/skills/frontend-ui-engineering/SKILL.md +328 -0
  5. package/global-template/components/agent-skills/skills/observability-and-instrumentation/SKILL.md +203 -0
  6. package/global-template/components/agent-skills/skills/performance-optimization/SKILL.md +396 -0
  7. package/global-template/components/agent-skills/skills/source-driven-development/SKILL.md +194 -0
  8. package/global-template/components/catalog.json +29 -0
  9. package/package.json +5 -2
  10. package/scripts/cockpit-smoke.mjs +1 -1
  11. package/src/cli.js +136 -8
  12. package/src/global/component-builders.js +3 -1
  13. package/src/global/components/agent-skills.js +27 -0
  14. package/src/global/ink/brand/wordmark.js +50 -0
  15. package/src/global/ink/cockpit/primitives.js +45 -35
  16. package/src/global/ink/cockpit-control-center.js +104 -4
  17. package/src/global/ink/cockpit-scan.js +20 -2
  18. package/src/global/ink/cockpit-views.js +2 -1
  19. package/src/global/ink/ecosystem-updates-display.js +37 -0
  20. package/src/global/ink/launch-input.js +32 -1
  21. package/src/global/ink/orchestrator-app.js +14 -5
  22. package/src/global/ink/orchestrator-state.js +17 -2
  23. package/src/global/ink/system-resources-display.js +109 -0
  24. package/src/global/ink/theme.js +13 -8
  25. package/src/global/ink/use-orchestrator-data.js +40 -4
  26. package/src/global/ink/ux/live-activity.js +4 -7
  27. package/src/global/ink/ux/live-alerts.js +2 -5
  28. package/src/global/ink/ux/live-governance.js +2 -6
  29. package/src/global/ink/ux/live-orchestration.js +2 -5
  30. package/src/global/ink/ux/live-overview.js +88 -28
  31. package/src/global/ink/ux/live-settings.js +3 -6
  32. package/src/global/ink/ux/live-usage.js +5 -7
  33. package/src/global/ink/ux/semantic.js +23 -6
  34. package/src/global/mcp/kairo-mcp.js +230 -0
  35. package/src/global/observability/build-companion-snapshot.js +281 -0
  36. package/src/global/observability/build-observability-snapshot.js +24 -0
  37. package/src/global/observability/ecosystem-updates.js +224 -0
  38. package/src/global/observability/gentle-bundle-export.js +71 -0
  39. package/src/global/observability/gentle-bundle-import.js +122 -0
  40. package/src/global/observability/gentle-probe.js +155 -0
  41. package/src/global/observability/graphify-ops.js +133 -0
  42. package/src/global/observability/graphify-parse-cache.js +90 -0
  43. package/src/global/observability/graphify-probe.js +185 -0
  44. package/src/global/observability/hermes-activity.js +163 -0
  45. package/src/global/observability/hermes-probe.js +171 -0
  46. package/src/global/observability/index.js +85 -0
  47. package/src/global/observability/passive-snapshot-flight.js +93 -0
  48. package/src/global/observability/probe-contract.js +38 -0
  49. package/src/global/observability/probe-registry.js +30 -0
  50. package/src/global/observability/resource-advisor.js +71 -0
  51. package/src/global/observability/system-resources.js +171 -0
  52. package/src/global/runtime/alerts/alert-cli.js +31 -0
  53. package/src/global/runtime/alerts/alert-store.js +29 -6
  54. package/src/global/runtime/alerts/alert-validate.js +25 -1
  55. package/src/global/runtime/alerts/controlled-alert-actions.js +56 -0
  56. package/src/global/runtime/execution-adapters/claude.js +2 -1
  57. package/src/global/runtime/execution-adapters/codex.js +2 -1
  58. package/src/global/runtime/execution-adapters/create-execution-adapter.js +3 -14
  59. package/src/global/runtime/execution-adapters/cursor.js +2 -1
  60. package/src/global/runtime/execution-adapters/opencode.js +2 -1
  61. package/src/global/runtime/execution-adapters/pi.js +2 -1
  62. package/src/global/runtime/review/index.js +1 -1
  63. package/src/global/runtime/review/review-cli.js +113 -3
  64. package/src/global/runtime/review/review-git.js +142 -11
  65. package/src/global/runtime/review/review-patch.js +2 -0
  66. package/src/global/runtime/review/review-receipts.js +12 -7
  67. package/src/global/runtime/review/review-runner.js +2 -2
  68. package/src/global/runtime/review/review-types.js +8 -5
  69. package/src/global/runtime/review/review-validate.js +5 -1
  70. package/src/global/runtime/run-cli.js +2 -0
  71. package/src/global/runtime/run-manager.js +39 -18
  72. package/src/global/runtime/run-permissions.js +231 -0
  73. package/src/global/runtime/run-profile.js +2 -0
  74. package/src/global/runtime/run-supervisor.js +77 -37
  75. package/src/global/runtime/run-types.js +2 -0
  76. package/src/global/updates-cli.js +41 -0
@@ -12,7 +12,7 @@ export function CockpitEmptyState({ title, message, hint, colorEnabled = true })
12
12
  return React.createElement(Box, { flexDirection: "column", marginY: 1 },
13
13
  title && React.createElement(Text, {
14
14
  bold: true,
15
- color: resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
15
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.brand)
16
16
  }, title),
17
17
  message && React.createElement(Text, null, message),
18
18
  hint && React.createElement(Text, {
@@ -21,23 +21,20 @@ export function CockpitEmptyState({ title, message, hint, colorEnabled = true })
21
21
  );
22
22
  }
23
23
 
24
+ /** Content region — no border; focus is conveyed by interactive children. */
24
25
  export function CockpitPanel({ title, focused = false, width, children, colorEnabled = true }) {
25
26
  return React.createElement(Box, {
26
27
  flexDirection: "column",
27
28
  width,
28
- borderStyle: "single",
29
- borderColor: resolveInkColor(
30
- colorEnabled,
31
- focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted
32
- ),
33
- paddingX: 1,
29
+ paddingX: 0,
30
+ marginY: 1,
34
31
  flexGrow: 1
35
32
  },
36
33
  title && React.createElement(Text, {
37
34
  bold: true,
38
35
  color: resolveInkColor(
39
36
  colorEnabled,
40
- focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.secondary
37
+ focused ? COCKPIT_COLORS.interactive : COCKPIT_COLORS.brand
41
38
  )
42
39
  }, title),
43
40
  children
@@ -48,7 +45,7 @@ export function CockpitSection({ title, children, colorEnabled = true }) {
48
45
  return React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
49
46
  title && React.createElement(Text, {
50
47
  bold: true,
51
- color: resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
48
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.brand)
52
49
  }, title),
53
50
  children
54
51
  );
@@ -60,15 +57,29 @@ export function CockpitKeyHint({ keys, label, colorEnabled = true }) {
60
57
  }, `${keys} ${label}`);
61
58
  }
62
59
 
60
+ /** Compact header: brand · status · project — no box-drawing. */
63
61
  export function CockpitTopBar({ model, colorEnabled = true }) {
62
+ const statusKind = String(model.status ?? "").toUpperCase() === "ONLINE"
63
+ ? "online"
64
+ : "muted";
64
65
  return React.createElement(Box, { justifyContent: "space-between", width: "100%" },
65
- React.createElement(Text, {
66
- bold: true,
67
- color: resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
68
- }, `╭─ ${model.brand} ─ ${model.status}`),
66
+ React.createElement(Box, null,
67
+ React.createElement(Text, {
68
+ bold: true,
69
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.brand)
70
+ }, model.brand),
71
+ React.createElement(Text, {
72
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
73
+ }, " · "),
74
+ React.createElement(CockpitBadge, {
75
+ label: model.status,
76
+ kind: statusKind,
77
+ colorEnabled
78
+ })
79
+ ),
69
80
  React.createElement(Text, {
70
81
  color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
71
- }, `${model.projectLabel} ─╮`)
82
+ }, model.projectLabel)
72
83
  );
73
84
  }
74
85
 
@@ -78,9 +89,9 @@ export function CockpitNav({ model, colorEnabled = true }) {
78
89
  model.items.map((item) => {
79
90
  const suffix = item.current && !item.selected ? " (open)" : "";
80
91
  const color = item.focused
81
- ? resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
92
+ ? resolveInkColor(colorEnabled, COCKPIT_COLORS.interactive)
82
93
  : item.selected
83
- ? resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
94
+ ? resolveInkColor(colorEnabled, COCKPIT_COLORS.brand)
84
95
  : item.current
85
96
  ? resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
86
97
  : undefined;
@@ -101,7 +112,7 @@ function stripLabel(item, { compact }) {
101
112
  return item.label.split(/[&·]/)[0].trim().split(/\s+/)[0];
102
113
  }
103
114
 
104
- /** Horizontal navigation strip under the top bar. */
115
+ /** Segmented horizontal nav no border; active segment uses interactive. */
105
116
  export function CockpitNavStrip({
106
117
  model,
107
118
  colorEnabled = true,
@@ -112,16 +123,20 @@ export function CockpitNavStrip({
112
123
  const parts = model.items.map((item) => {
113
124
  const label = stripLabel(item, { compact });
114
125
  const suffix = item.current && !item.selected ? "*" : "";
115
- const color = item.focused
116
- ? resolveInkColor(colorEnabled, COCKPIT_COLORS.primary)
126
+ const active = item.focused || item.selected;
127
+ const color = item.focused || (focused && item.selected)
128
+ ? resolveInkColor(colorEnabled, COCKPIT_COLORS.interactive)
117
129
  : item.selected
118
- ? resolveInkColor(colorEnabled, COCKPIT_COLORS.secondary)
130
+ ? resolveInkColor(colorEnabled, COCKPIT_COLORS.interactive)
119
131
  : resolveInkColor(colorEnabled, COCKPIT_COLORS.muted);
132
+ const text = focused && item.selected
133
+ ? `[${label}]${suffix}`
134
+ : `${item.marker}${label}${suffix}`;
120
135
  return React.createElement(Text, {
121
136
  key: item.id,
122
- bold: item.focused || item.selected,
137
+ bold: active,
123
138
  color
124
- }, `${item.marker}${label}${suffix}`);
139
+ }, text);
125
140
  });
126
141
 
127
142
  const joined = [];
@@ -138,12 +153,8 @@ export function CockpitNavStrip({
138
153
  return React.createElement(Box, {
139
154
  flexDirection: "column",
140
155
  width: "100%",
141
- borderStyle: "single",
142
- borderColor: resolveInkColor(
143
- colorEnabled,
144
- focused ? COCKPIT_COLORS.primary : COCKPIT_COLORS.muted
145
- ),
146
- paddingX: 1
156
+ marginTop: 0,
157
+ marginBottom: 0
147
158
  },
148
159
  React.createElement(Box, { flexDirection: "row", flexWrap: "wrap" }, ...joined),
149
160
  model.explanation && React.createElement(Text, {
@@ -170,20 +181,19 @@ export function CockpitSystemStrip({ model, colorEnabled = true }) {
170
181
  );
171
182
  }
172
183
 
184
+ /** Single-line shortcut bar — no framed box. */
173
185
  export function CockpitFooter({ model, columns = 80, colorEnabled = true }) {
174
186
  const width = Math.max(24, Math.min(Number(columns) || 80, 120));
175
- const bar = Math.max(20, width - 2);
176
- const muted = resolveInkColor(colorEnabled, COCKPIT_COLORS.muted);
177
- return React.createElement(Box, { flexDirection: "column", width: "100%" },
178
- React.createElement(Text, { color: muted }, `├${"".repeat(bar)}┤`),
179
- React.createElement(Text, { color: muted }, `│ ${model.text}`),
180
- React.createElement(Text, { color: muted }, `╰${"─".repeat(bar)}╯`)
187
+ return React.createElement(Box, { width: "100%", marginTop: 1 },
188
+ React.createElement(Text, {
189
+ color: resolveInkColor(colorEnabled, COCKPIT_COLORS.muted)
190
+ }, String(model.text ?? "").slice(0, width))
181
191
  );
182
192
  }
183
193
 
184
194
  /**
185
195
  * Responsive single-panel shell: TopBar → NavStrip → Main → Footer.
186
- * SYSTEM side column removed (diagnostics via Enter/detail in later slices).
196
+ * No nested borders hierarchy via type and spacing.
187
197
  */
188
198
  export function CockpitShell({
189
199
  topBar,
@@ -5,13 +5,30 @@ import {
5
5
  formatMeasuredBudgets,
6
6
  formatUsageLinesFromModel
7
7
  } from "./cockpit-usage.js";
8
+ import { LAYOUT_MODES } from "./layout.js";
9
+ import {
10
+ formatSystemResourcesLines,
11
+ formatResourceAdviceLines
12
+ } from "./system-resources-display.js";
13
+ import { formatEcosystemUpdateLines } from "./ecosystem-updates-display.js";
14
+
15
+ const HERMES_WIDE_SESSION_LIMIT = 3;
16
+ const HERMES_TITLE_MAX = 48;
17
+
18
+ export {
19
+ diskFreeTone,
20
+ formatSystemResourcesLines,
21
+ formatResourceAdviceLines
22
+ } from "./system-resources-display.js";
23
+ export { formatEcosystemUpdateLines } from "./ecosystem-updates-display.js";
8
24
 
9
25
  export function buildControlCenterModel({
10
26
  projectName = "project",
11
27
  snapshot = null,
12
28
  dashboard = null,
13
- layoutMode = "compact",
14
- alerts = null
29
+ layoutMode = LAYOUT_MODES.COMPACT,
30
+ alerts = null,
31
+ companion = null
15
32
  } = {}) {
16
33
  if (!snapshot) {
17
34
  return {
@@ -49,7 +66,9 @@ export function buildControlCenterModel({
49
66
  activity: { headline: "No activity yet" },
50
67
  alerts: formatAlertsHeadline(alerts),
51
68
  tokens: { headline: "Data unavailable" },
52
- includeEmbeddedStatus: layoutMode !== "wide",
69
+ companion: null,
70
+ companionNextAction: null,
71
+ includeEmbeddedStatus: layoutMode !== LAYOUT_MODES.WIDE,
53
72
  runsSecondaryHint: "Detail via Enter · / actions"
54
73
  };
55
74
  }
@@ -94,11 +113,92 @@ export function buildControlCenterModel({
94
113
  },
95
114
  alerts: formatAlertsHeadline(alerts),
96
115
  tokens: { headline: formatTokenHeadline(snapshot.budgets) },
97
- includeEmbeddedStatus: layoutMode !== "wide",
116
+ companion: formatCompanionOverlay(companion, layoutMode),
117
+ companionNextAction: companion?.nextSafeAction ?? null,
118
+ includeEmbeddedStatus: layoutMode !== LAYOUT_MODES.WIDE,
98
119
  runsSecondaryHint: "Detail via Enter · / actions"
99
120
  };
100
121
  }
101
122
 
123
+ /**
124
+ * Observe-only Hermes activity lines for Cockpit overlay.
125
+ * Never includes session ids, baseUrl, diagnostics dumps, or control affordances.
126
+ */
127
+ export function formatHermesActivityLines(activity, layoutMode = LAYOUT_MODES.COMPACT) {
128
+ if (activity == null || typeof activity !== "object") {
129
+ return ["Hermes · unavailable"];
130
+ }
131
+ const state = typeof activity.state === "string" && activity.state.length > 0
132
+ ? activity.state
133
+ : "error";
134
+ if (state !== "available" && state !== "partial") {
135
+ return [`Hermes · ${state}`];
136
+ }
137
+
138
+ const agg = activity.aggregates && typeof activity.aggregates === "object"
139
+ ? activity.aggregates
140
+ : {};
141
+ const active = Number.isInteger(agg.activeCount) ? agg.activeCount : null;
142
+ const ended = Number.isInteger(agg.endedCount) ? agg.endedCount : null;
143
+ const bits = [`Hermes · ${state}`];
144
+ if (active != null) bits.push(`${active} active`);
145
+ if (layoutMode !== LAYOUT_MODES.MINIMAL && ended != null) {
146
+ bits.push(`${ended} ended`);
147
+ }
148
+ const lines = [bits.join(" · ")];
149
+
150
+ if (layoutMode !== LAYOUT_MODES.WIDE) return lines;
151
+
152
+ const sessions = Array.isArray(activity.sessions) ? activity.sessions : [];
153
+ for (const session of sessions.slice(0, HERMES_WIDE_SESSION_LIMIT)) {
154
+ const label = hermesSessionLabel(session);
155
+ if (label) lines.push(` · ${label}`);
156
+ }
157
+ if (agg.hasMore === true || sessions.length > HERMES_WIDE_SESSION_LIMIT) {
158
+ lines.push(" · … more sessions");
159
+ }
160
+ return lines;
161
+ }
162
+
163
+ function hermesSessionLabel(session) {
164
+ if (session == null || typeof session !== "object" || Array.isArray(session)) return null;
165
+ const title = typeof session.title === "string" && session.title.trim()
166
+ ? session.title.trim()
167
+ : null;
168
+ const source = typeof session.source === "string" && session.source.trim()
169
+ ? session.source.trim()
170
+ : null;
171
+ const head = (title ?? source ?? "untitled")
172
+ .replace(/[\r\n\t]+/g, " ")
173
+ .slice(0, HERMES_TITLE_MAX);
174
+ const flag = session.active === true ? "active" : "ended";
175
+ return `${head} · ${flag}`;
176
+ }
177
+
178
+ function formatCompanionOverlay(companion, layoutMode = LAYOUT_MODES.COMPACT) {
179
+ if (!companion) return null;
180
+ const g = companion.signals?.gentle?.state ?? "unknown";
181
+ const gy = companion.signals?.graphify;
182
+ const graphBit = gy?.graphStatus ? `/${gy.graphStatus}` : "";
183
+ const en = companion.engram?.status ?? "unknown";
184
+ const links = companion.links?.length ?? 0;
185
+ return {
186
+ ok: companion.ok !== false,
187
+ lines: [
188
+ `Gentle · ${g}`,
189
+ `Graphify · ${gy?.state ?? "unknown"}${graphBit}`,
190
+ `Engram · ${en}`,
191
+ `Soft links · ${links}`,
192
+ ...formatHermesActivityLines(companion.signals?.hermes?.activity, layoutMode),
193
+ ...formatSystemResourcesLines(companion.signals?.system?.resources, layoutMode),
194
+ ...formatResourceAdviceLines(companion.signals?.system?.advice, layoutMode),
195
+ ...formatEcosystemUpdateLines(companion.signals?.ecosystem?.updates, layoutMode)
196
+ ],
197
+ links: companion.links ?? [],
198
+ error: companion.error ?? null
199
+ };
200
+ }
201
+
102
202
  function formatHealthLabel(kind) {
103
203
  switch (kind) {
104
204
  case CONTROL_PLANE_HEALTH.NOT_CONFIGURED:
@@ -12,7 +12,8 @@ export async function loadCockpitScanBundle({
12
12
  cliVersion,
13
13
  buildDashboard,
14
14
  buildDiagnostics,
15
- buildSnapshot
15
+ buildSnapshot,
16
+ buildCompanion = null
16
17
  }) {
17
18
  const [dashboard, diagnostics, snapshot] = await Promise.all([
18
19
  buildDashboard({ homeDir, workspaceRoot, cliVersion }),
@@ -26,7 +27,24 @@ export async function loadCockpitScanBundle({
26
27
  ...CONTROL_PLANE_AUTO_SCAN
27
28
  })
28
29
  ]);
29
- return { dashboard, diagnostics, snapshot };
30
+ let companion = null;
31
+ if (typeof buildCompanion === "function") {
32
+ try {
33
+ companion = await buildCompanion({
34
+ homeDir, workspaceRoot, packageName, packageRoot, cliVersion, dashboard, snapshot
35
+ });
36
+ } catch (error) {
37
+ const detail = error instanceof Error ? error.message : String(error);
38
+ companion = {
39
+ ok: false, error: detail, signals: null, engram: null, links: [], alertsCount: null,
40
+ nextSafeAction: {
41
+ kind: "investigate", title: "Investigate companion diagnostics",
42
+ detail, secondary: true, displayOnly: true
43
+ }
44
+ };
45
+ }
46
+ }
47
+ return { dashboard, diagnostics, snapshot, companion };
30
48
  }
31
49
 
32
50
  /**
@@ -111,7 +111,8 @@ export function renderCockpitView({
111
111
  model: controlCenter,
112
112
  detailsOpen: overviewDetailsOpen,
113
113
  colorEnabled,
114
- unicode
114
+ unicode,
115
+ layoutMode
115
116
  });
116
117
  case ORCHESTRATOR_VIEWS.USAGE:
117
118
  return React.createElement(SemanticUsagePanel, {
@@ -0,0 +1,37 @@
1
+ import { LAYOUT_MODES } from "./layout.js";
2
+
3
+ /** Display-only ecosystem update lines for Cockpit companion overlay. */
4
+ export function formatEcosystemUpdateLines(updates, layoutMode = LAYOUT_MODES.COMPACT) {
5
+ if (updates == null || typeof updates !== "object") return ["Updates · unavailable"];
6
+ const state = typeof updates.state === "string" && updates.state ? updates.state : "error";
7
+ const tools = updates.tools && typeof updates.tools === "object" ? updates.tools : {};
8
+ const pending = ["kairo", "hermes", "gentle", "skills"]
9
+ .map((id) => tools[id])
10
+ .filter((t) => t && t.updateAvailable === true);
11
+ const cache = updates.cacheHit === true ? " · cache" : "";
12
+
13
+ if (state !== "available" && state !== "partial" && pending.length === 0) {
14
+ return [`Updates · ${state}${cache}`];
15
+ }
16
+
17
+ const lines = [
18
+ pending.length > 0
19
+ ? `Updates · ${pending.length} available${cache}`
20
+ : `Updates · current${cache}`
21
+ ];
22
+ if (layoutMode === LAYOUT_MODES.MINIMAL) return lines;
23
+
24
+ for (const tool of pending.slice(0, layoutMode === LAYOUT_MODES.WIDE ? 4 : 2)) {
25
+ const inst = tool.installed ?? "—";
26
+ const latest = tool.latest ?? "—";
27
+ lines.push(` · ${tool.id} ${inst} → ${latest}`);
28
+ }
29
+ if (pending.length === 0 && layoutMode === LAYOUT_MODES.WIDE) {
30
+ for (const id of ["kairo", "hermes", "gentle", "skills"]) {
31
+ const tool = tools[id];
32
+ if (!tool) continue;
33
+ lines.push(` · ${id} · ${tool.state ?? "unknown"}`);
34
+ }
35
+ }
36
+ return lines;
37
+ }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  LAUNCH_PERMISSION_OPTIONS,
3
3
  LAUNCH_WIZARD_STEPS,
4
+ advanceLaunchWizardStep,
4
5
  retreatLaunchWizardStep
5
6
  } from "./orchestrator-state.js";
6
7
 
@@ -92,7 +93,19 @@ export function handleLaunchInput(ctx) {
92
93
 
93
94
  if (launchStep === LAUNCH_WIZARD_STEPS.CONFIRM) {
94
95
  if (key.return) {
95
- handleLaunch({ ...launchDraft, permissionIndex: launchPermissionIndex });
96
+ const next = advanceLaunchWizardStep(LAUNCH_WIZARD_STEPS.CONFIRM, {
97
+ permissionIndex: launchPermissionIndex
98
+ });
99
+ if (next === LAUNCH_WIZARD_STEPS.UNSAFE_CONFIRM) {
100
+ setLaunchStep(LAUNCH_WIZARD_STEPS.UNSAFE_CONFIRM);
101
+ return true;
102
+ }
103
+ handleLaunch({
104
+ ...launchDraft,
105
+ permissionIndex: launchPermissionIndex,
106
+ allowUnsafePermissions: false,
107
+ permissionSource: "cockpit"
108
+ });
96
109
  return true;
97
110
  }
98
111
  if (key.escape) {
@@ -101,6 +114,24 @@ export function handleLaunchInput(ctx) {
101
114
  }
102
115
  }
103
116
 
117
+ if (launchStep === LAUNCH_WIZARD_STEPS.UNSAFE_CONFIRM) {
118
+ const answer = String(inputKey ?? "").toLowerCase();
119
+ if (answer === "y") {
120
+ handleLaunch({
121
+ ...launchDraft,
122
+ permissionIndex: launchPermissionIndex,
123
+ allowUnsafePermissions: true,
124
+ permissionSource: "cockpit"
125
+ });
126
+ return true;
127
+ }
128
+ if (answer === "n" || key.escape) {
129
+ setLaunchStep(LAUNCH_WIZARD_STEPS.CONFIRM);
130
+ return allowEscapeRetreat && key.escape ? "retreated" : true;
131
+ }
132
+ return true;
133
+ }
134
+
104
135
  if (inputKey.toLowerCase() === "r") {
105
136
  reload().catch(() => {});
106
137
  return true;
@@ -41,7 +41,7 @@ import { handleLaunchInput } from "./launch-input.js";
41
41
  import { useTerminalSize } from "./use-terminal-size.js";
42
42
  import { useOrchestratorData } from "./use-orchestrator-data.js";
43
43
  import { resolveTerminalCapabilities } from "./terminal-capabilities.js";
44
- import { COCKPIT_COLORS } from "./theme.js";
44
+ import { COCKPIT_COLORS, resolveInkColor } from "./theme.js";
45
45
  import { LAYOUT_MODES } from "./layout.js";
46
46
  import { CHANGES_PHASE } from "./cockpit-changes.js";
47
47
  import { RECOVERY_PHASE, listRecoverySnapshots } from "./cockpit-recovery.js";
@@ -466,14 +466,22 @@ export function OrchestratorApp({
466
466
 
467
467
  if (data.loading) {
468
468
  return React.createElement(Box, { flexDirection: "column" },
469
- React.createElement(Text, { bold: true, color: COCKPIT_COLORS.primary }, "KAIRO"),
470
- React.createElement(Text, { color: COCKPIT_COLORS.muted }, "Loading cockpit…")
469
+ React.createElement(Text, {
470
+ bold: true,
471
+ color: resolveInkColor(caps.color, COCKPIT_COLORS.brand)
472
+ }, "KAIRO"),
473
+ React.createElement(Text, {
474
+ color: resolveInkColor(caps.color, COCKPIT_COLORS.muted)
475
+ }, "Loading cockpit…")
471
476
  );
472
477
  }
473
478
 
474
479
  if (data.error) {
475
480
  return React.createElement(Box, { flexDirection: "column" },
476
- React.createElement(Text, { bold: true, color: COCKPIT_COLORS.danger }, "Runtime error"),
481
+ React.createElement(Text, {
482
+ bold: true,
483
+ color: resolveInkColor(caps.color, COCKPIT_COLORS.danger)
484
+ }, "Runtime error"),
477
485
  React.createElement(Text, null, data.error),
478
486
  React.createElement(Text, { dimColor: true },
479
487
  data.retrying ? "Retrying read-only scan…" : "R Retry · Esc to exit")
@@ -494,7 +502,8 @@ export function OrchestratorApp({
494
502
  snapshot: data.snapshot,
495
503
  dashboard: data.dashboard,
496
504
  layoutMode: mode,
497
- alerts: data.alerts
505
+ alerts: data.alerts,
506
+ companion: data.companion
498
507
  });
499
508
  const systemOnline = data.snapshot
500
509
  ? data.snapshot.health !== CONTROL_PLANE_HEALTH.NOT_CONFIGURED
@@ -47,7 +47,8 @@ export const LAUNCH_WIZARD_STEPS = {
47
47
  TASK: "task",
48
48
  MODEL: "model",
49
49
  PERMISSIONS: "permissions",
50
- CONFIRM: "confirm"
50
+ CONFIRM: "confirm",
51
+ UNSAFE_CONFIRM: "unsafe-confirm"
51
52
  };
52
53
 
53
54
  export const LAUNCH_PERMISSION_OPTIONS = [
@@ -75,7 +76,7 @@ export function resolveLaunchPermissions(draft) {
75
76
  return LAUNCH_PERMISSION_OPTIONS[draft.permissionIndex]?.permissions ?? [];
76
77
  }
77
78
 
78
- export function advanceLaunchWizardStep(currentStep) {
79
+ export function advanceLaunchWizardStep(currentStep, { permissionIndex = 0 } = {}) {
79
80
  switch (currentStep) {
80
81
  case LAUNCH_WIZARD_STEPS.AGENT:
81
82
  return LAUNCH_WIZARD_STEPS.TASK;
@@ -85,6 +86,10 @@ export function advanceLaunchWizardStep(currentStep) {
85
86
  return LAUNCH_WIZARD_STEPS.PERMISSIONS;
86
87
  case LAUNCH_WIZARD_STEPS.PERMISSIONS:
87
88
  return LAUNCH_WIZARD_STEPS.CONFIRM;
89
+ case LAUNCH_WIZARD_STEPS.CONFIRM: {
90
+ const perms = resolveLaunchPermissions({ permissionIndex });
91
+ return perms.length > 0 ? LAUNCH_WIZARD_STEPS.UNSAFE_CONFIRM : LAUNCH_WIZARD_STEPS.CONFIRM;
92
+ }
88
93
  default:
89
94
  return LAUNCH_WIZARD_STEPS.CONFIRM;
90
95
  }
@@ -92,6 +97,8 @@ export function advanceLaunchWizardStep(currentStep) {
92
97
 
93
98
  export function retreatLaunchWizardStep(currentStep) {
94
99
  switch (currentStep) {
100
+ case LAUNCH_WIZARD_STEPS.UNSAFE_CONFIRM:
101
+ return LAUNCH_WIZARD_STEPS.CONFIRM;
95
102
  case LAUNCH_WIZARD_STEPS.CONFIRM:
96
103
  return LAUNCH_WIZARD_STEPS.PERMISSIONS;
97
104
  case LAUNCH_WIZARD_STEPS.PERMISSIONS:
@@ -147,6 +154,14 @@ export function formatLaunchWizardLines({
147
154
  return lines;
148
155
  }
149
156
 
157
+ if (step === LAUNCH_WIZARD_STEPS.UNSAFE_CONFIRM) {
158
+ const label = LAUNCH_PERMISSION_OPTIONS[permissionIndex]?.label ?? "unsafe";
159
+ lines.push(`Unsafe mode selected: ${label}`);
160
+ lines.push("This skips agent permission prompts for this run only.");
161
+ lines.push("Press Y to confirm unsafe launch · N/Esc to go back");
162
+ return lines;
163
+ }
164
+
150
165
  lines.push(`Agent: ${draft.agentId ?? "—"}`);
151
166
  lines.push(`Task length: ${draft.task.length} chars (content not stored)`);
152
167
  lines.push(`Model: ${draft.model || "default"}`);
@@ -0,0 +1,109 @@
1
+ import { LAYOUT_MODES } from "./layout.js";
2
+
3
+ /** Disk free% tone — never invents a percent. */
4
+ export function diskFreeTone(freePercent) {
5
+ if (typeof freePercent !== "number" || !Number.isFinite(freePercent)) return null;
6
+ if (freePercent < 10) return "critical";
7
+ if (freePercent < 20) return "warning";
8
+ return "healthy";
9
+ }
10
+
11
+ function formatBytesShort(bytes) {
12
+ if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) return null;
13
+ const gib = bytes / (1024 ** 3);
14
+ if (gib >= 1) return `${Math.round(gib * 10) / 10}G`;
15
+ const mib = bytes / (1024 ** 2);
16
+ return `${Math.round(mib)}M`;
17
+ }
18
+
19
+ function pctLabel(value, suffix = "% free") {
20
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
21
+ return `${value}${suffix}`;
22
+ }
23
+
24
+ /**
25
+ * Display-only System resource lines for Cockpit companion overlay.
26
+ * Never invents metrics; never includes paths, args, or control affordances.
27
+ */
28
+ export function formatSystemResourcesLines(resources, layoutMode = LAYOUT_MODES.COMPACT) {
29
+ if (resources == null || typeof resources !== "object") {
30
+ return ["System · unavailable"];
31
+ }
32
+ const state = typeof resources.state === "string" && resources.state.length > 0
33
+ ? resources.state
34
+ : "error";
35
+ if (state !== "available" && state !== "partial") {
36
+ return [`System · ${state}`];
37
+ }
38
+
39
+ const ram = pctLabel(resources.memory?.freePercent);
40
+ const swapUsed = formatBytesShort(resources.swap?.usedBytes);
41
+ const diskPct = resources.disk?.freePercent;
42
+ const disk = pctLabel(diskPct);
43
+ const tone = diskFreeTone(diskPct);
44
+
45
+ const bits = ["System"];
46
+ bits.push(ram ? `RAM ${ram}` : "RAM n/a");
47
+ bits.push(swapUsed ? `Swap ${swapUsed} used` : "Swap n/a");
48
+ bits.push(disk ? `Disk ${disk}` : "Disk n/a");
49
+ if (tone && tone !== "healthy") bits.push(tone);
50
+
51
+ const lines = [bits.join(" · ")];
52
+ if (layoutMode === LAYOUT_MODES.MINIMAL) return lines;
53
+
54
+ const ramFree = resources.memory?.freePercent;
55
+ const memoryPressure = typeof ramFree === "number" && Number.isFinite(ramFree) && ramFree < 15;
56
+ if (memoryPressure) {
57
+ lines.push(" · Memory pressure · swap elevated relevance");
58
+ } else {
59
+ lines.push(" · Swap informational");
60
+ }
61
+
62
+ const proc = resources.processes && typeof resources.processes === "object"
63
+ ? resources.processes
64
+ : null;
65
+ if (proc) {
66
+ const total = Number.isInteger(proc.totalCount) ? proc.totalCount : null;
67
+ const zombies = Number.isInteger(proc.zombieCount) ? proc.zombieCount : null;
68
+ if (total != null && zombies != null) {
69
+ lines.push(` · Processes ${total} · zombies ${zombies}`);
70
+ }
71
+ }
72
+
73
+ if (layoutMode !== LAYOUT_MODES.WIDE) return lines;
74
+
75
+ const tracked = Array.isArray(proc?.tracked) ? proc.tracked : [];
76
+ for (const entry of tracked.slice(0, 4)) {
77
+ if (entry == null || typeof entry !== "object") continue;
78
+ if (typeof entry.name !== "string" || !Number.isInteger(entry.count)) continue;
79
+ lines.push(` · ${entry.name} ×${entry.count}`);
80
+ }
81
+ const thermal = resources.thermal?.state ?? "unavailable";
82
+ const ssd = resources.ssdWear?.state ?? "unavailable";
83
+ lines.push(` · Thermal · ${thermal}`);
84
+ lines.push(` · SSD wear · ${ssd}`);
85
+ return lines;
86
+ }
87
+
88
+ /** Display-only advisor lines from deterministic recommendations. */
89
+ export function formatResourceAdviceLines(advice, layoutMode = LAYOUT_MODES.COMPACT) {
90
+ const list = Array.isArray(advice?.recommendations) ? advice.recommendations : [];
91
+ if (list.length === 0) return ["Advisor · quiet"];
92
+ const top = list[0];
93
+ const severity = typeof top.severity === "string" ? top.severity : "info";
94
+ const title = typeof top.title === "string" ? top.title : "recommendation";
95
+ const lines = [`Advisor · ${severity} · ${title}`];
96
+ if (layoutMode === LAYOUT_MODES.MINIMAL) return lines;
97
+ if (typeof top.detail === "string" && top.detail.length > 0) {
98
+ lines.push(` · ${top.detail.slice(0, 96)}`);
99
+ }
100
+ if (layoutMode === LAYOUT_MODES.WIDE) {
101
+ for (const item of list.slice(1, 3)) {
102
+ if (item == null || typeof item !== "object") continue;
103
+ const sev = typeof item.severity === "string" ? item.severity : "info";
104
+ const t = typeof item.title === "string" ? item.title : "recommendation";
105
+ lines.push(` · ${sev} · ${t}`);
106
+ }
107
+ }
108
+ return lines;
109
+ }