@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
@@ -0,0 +1,230 @@
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
3
+ import * as z from "zod";
4
+ import { resolveHomeDir } from "../paths.js";
5
+ import { buildControlPlaneSnapshot } from "../control-plane-snapshot.js";
6
+ import { listRunRecords } from "../runtime/run-store.js";
7
+ import { listAlerts } from "../runtime/alerts/alert-store.js";
8
+ import { listReviewReceipts } from "../runtime/review/review-receipts.js";
9
+ import { buildCompanionSnapshot } from "../observability/build-companion-snapshot.js";
10
+ import { probeGentle } from "../observability/gentle-probe.js";
11
+ import { runGraphifyOp } from "../observability/graphify-ops.js";
12
+ import { resolveGitHeadSha } from "../observability/graphify-probe.js";
13
+ import { runPassiveObservabilitySnapshot } from "../observability/passive-snapshot-flight.js";
14
+ import { inspectEngramIntegration } from "../integrations/engram-evidence.js";
15
+
16
+ export const KAIRO_MCP_TOOLS = Object.freeze([
17
+ "kairo_status", "kairo_runs", "kairo_alerts", "kairo_gentle_status",
18
+ "kairo_graph_query", "kairo_graph_path", "kairo_context_summary"
19
+ ]);
20
+
21
+ const empty = z.object({});
22
+ export const mcpSchemas = Object.freeze({
23
+ empty,
24
+ runs: z.object({ limit: z.number().int().min(1).max(20).default(20), activeOnly: z.boolean().optional() }),
25
+ alerts: z.object({
26
+ limit: z.number().int().min(1).max(50).default(50),
27
+ state: z.enum(["open", "resolved", "dismissed"]).optional()
28
+ }),
29
+ graphQuery: z.object({
30
+ graph: z.string().min(1), question: z.string().min(1),
31
+ budget: z.number().int().min(1).max(8000).default(2000)
32
+ }),
33
+ graphPath: z.object({ graph: z.string().min(1), from: z.string().min(1), to: z.string().min(1) })
34
+ });
35
+
36
+ const CODE_RE = /^(?:[a-z][a-z0-9_]{0,48}|status=\d+)$/;
37
+ export const pubCodes = (xs = []) => xs.map(String).filter((d) => CODE_RE.test(d));
38
+
39
+ export function mcpResult({ ok, code, data = null, diagnostics = [], isError = false }) {
40
+ const structuredContent = { ok, code, data, diagnostics: pubCodes(diagnostics) };
41
+ return {
42
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }],
43
+ structuredContent, ...(isError ? { isError: true } : {})
44
+ };
45
+ }
46
+
47
+ const pubRun = (r) => ({
48
+ runId: r?.runId ?? null, state: r?.state ?? null, agentId: r?.agentId ?? null,
49
+ startedAt: r?.startedAt ?? null, updatedAt: r?.updatedAt ?? null,
50
+ endedAt: r?.endedAt ?? r?.completedAt ?? null
51
+ });
52
+ const pubAlert = (a) => ({
53
+ alertId: a?.alertId ?? null, state: a?.state ?? null, kind: a?.kind ?? null,
54
+ severity: a?.severity ?? null, title: a?.title ?? null, createdAt: a?.createdAt ?? null
55
+ });
56
+ export const pubCoverage = (c) => (c == null ? null : {
57
+ detectedAgents: Number(c.detectedAgents) || 0, governedAgents: Number(c.governedAgents) || 0,
58
+ components: Number(c.components) || 0,
59
+ activeModules: Array.isArray(c.activeModules) ? c.activeModules.map(String) : []
60
+ });
61
+ export const pubGentle = (p) => ({
62
+ state: p?.state ?? "missing", error: null,
63
+ diagnostics: (p?.state === "error") ? ["provider_error"] : pubCodes(p?.diagnostics)
64
+ });
65
+ export const pubSignals = (s) => ({
66
+ gentle: { state: s?.gentle?.state ?? "missing", error: null, diagnostics: [] },
67
+ graphify: {
68
+ state: s?.graphify?.state ?? "missing", error: null, diagnostics: [],
69
+ graphStatus: s?.graphify?.graphStatus ?? null
70
+ }
71
+ });
72
+ export const pubEngram = (e) => ({ status: e?.status ?? "missing", binary: null, error: null });
73
+ const pubNext = (n) => (n == null ? null : { kind: n.kind ?? null, displayOnly: true, secondary: true });
74
+ const pubCompanion = (c) => ({
75
+ ok: Boolean(c?.ok), signals: pubSignals(c?.signals), engram: pubEngram(c?.engram),
76
+ nextSafeAction: pubNext(c?.nextSafeAction), alertsCount: c?.alertsCount ?? null,
77
+ linksCount: c?.links?.length ?? 0
78
+ });
79
+ const pubLinks = (links) => (links ?? []).map((l) => ({
80
+ kind: "soft", displayOnly: true, agentId: l.agentId ?? null,
81
+ reviewId: l.reviewId ?? null, runId: l.runId ?? null, deltaMs: l.deltaMs ?? null
82
+ }));
83
+
84
+ function graphEnvelope(result) {
85
+ return mcpResult({
86
+ ok: Boolean(result.ok), code: result.ok ? "ok" : (result.code ?? "graphify_error"),
87
+ data: {
88
+ op: result.op ?? null, text: result.ok ? (result.text ?? null) : null, truncated: Boolean(result.truncated),
89
+ graphPath: result.graphPath ?? null, graphStatus: result.graphStatus ?? null
90
+ },
91
+ diagnostics: result.diagnostics ?? [], isError: !result.ok
92
+ });
93
+ }
94
+
95
+ export function createToolHandlers(deps = {}) {
96
+ const homeDir = deps.homeDir ?? resolveHomeDir();
97
+ const cwd = deps.cwd ?? process.cwd();
98
+ const listRuns = deps.listRuns ?? ((o) => listRunRecords(homeDir, o));
99
+ const listAlertRows = deps.listAlerts ?? ((o) => listAlerts({ homeDir, ...o }));
100
+ const listReviews = deps.listReviews ?? (() => listReviewReceipts({ homeDir, limit: 20 }));
101
+ const buildStatus = deps.buildStatus ?? (() => buildControlPlaneSnapshot({
102
+ homeDir, workspaceRoot: cwd, packageRoot: deps.packageRoot, packageName: deps.packageName,
103
+ cliVersion: deps.version, includeDiff: false, includeExplain: false, includeRuntime: true
104
+ }));
105
+ const resolveHead = deps.resolveHead ?? ((dir) => resolveGitHeadSha(dir));
106
+ /** Fresh HEAD per request/snapshot — never cache across MCP tool calls. */
107
+ const requestHead = () => resolveHead(cwd);
108
+ const buildObs = deps.buildObservability
109
+ ?? ((ctx) => runPassiveObservabilitySnapshot(ctx, { force: Boolean(ctx?.force) }));
110
+ const buildCompanion = deps.buildCompanion ?? ((ctx) => buildCompanionSnapshot({
111
+ ...ctx,
112
+ inspectEngram: deps.inspectEngram ?? ((c) => inspectEngramIntegration(c)),
113
+ loadAlerts: async () => listAlertRows({ limit: 50 }),
114
+ loadReviews: async () => listReviews(),
115
+ buildObservability: buildObs,
116
+ ensureRegistered: deps.ensureRegistered,
117
+ observabilityContext: { cwd, homeDir, workspaceRoot: cwd, headSha: requestHead() }
118
+ }));
119
+ const gentleProbe = deps.probeGentle ?? ((ctx) => probeGentle(ctx));
120
+ const graphOp = deps.runGraphifyOp ?? runGraphifyOp;
121
+ const gOpts = () => ({
122
+ cwd, workspaceRoot: cwd, headSha: requestHead(), whichCommand: deps.whichCommand,
123
+ probeCommand: deps.probeCommand, containPath: deps.containPath,
124
+ inspectGraph: deps.inspectGraph, resolveHead
125
+ });
126
+ const soft = (code, data) => mcpResult({
127
+ ok: true, code, data,
128
+ diagnostics: code === "degraded" ? [`${Object.keys(data)[0]}_unavailable`] : []
129
+ });
130
+ const graphFail = () => mcpResult({
131
+ ok: false, code: "provider_error", data: null, diagnostics: ["provider_error"], isError: true
132
+ });
133
+
134
+ return {
135
+ async kairo_status() {
136
+ try {
137
+ const snap = await buildStatus();
138
+ const runs = await listRuns({ limit: 20, activeOnly: false }).catch(() => []);
139
+ const companion = await buildCompanion({ controlPlaneHealth: snap?.health ?? null, runs });
140
+ return mcpResult({
141
+ ok: true, code: "ok",
142
+ data: {
143
+ health: snap?.health ?? null, coverage: pubCoverage(snap?.coverage),
144
+ cta: snap?.cta ? { kind: snap.cta.kind ?? null } : null,
145
+ companion: pubCompanion(companion)
146
+ }
147
+ });
148
+ } catch { return soft("degraded", { health: null, coverage: null, companion: null }); }
149
+ },
150
+ async kairo_runs({ limit = 20, activeOnly = false } = {}) {
151
+ try {
152
+ return mcpResult({ ok: true, code: "ok", data: { runs: (await listRuns({ limit, activeOnly: Boolean(activeOnly) })).map(pubRun) } });
153
+ } catch { return soft("degraded", { runs: [] }); }
154
+ },
155
+ async kairo_alerts({ limit = 50, state } = {}) {
156
+ try {
157
+ return mcpResult({ ok: true, code: "ok", data: { alerts: (await listAlertRows({ limit, state: state ?? null })).map(pubAlert) } });
158
+ } catch { return soft("degraded", { alerts: [] }); }
159
+ },
160
+ async kairo_gentle_status() {
161
+ try {
162
+ const probe = pubGentle(await gentleProbe({ cwd, homeDir, workspaceRoot: cwd }));
163
+ return mcpResult({
164
+ ok: true, code: probe.state === "available" ? "ok" : "degraded", data: probe,
165
+ diagnostics: probe.state === "missing" ? ["gentle_missing"] : []
166
+ });
167
+ } catch {
168
+ return mcpResult({
169
+ ok: true, code: "degraded", data: { state: "missing", error: null, diagnostics: [] },
170
+ diagnostics: ["gentle_unavailable"]
171
+ });
172
+ }
173
+ },
174
+ async kairo_graph_query({ graph, question, budget = 2000 }) {
175
+ try {
176
+ return graphEnvelope(await graphOp({
177
+ op: "query", args: [question], graphPath: graph, budget, ...gOpts()
178
+ }));
179
+ } catch { return graphFail(); }
180
+ },
181
+ async kairo_graph_path({ graph, from, to }) {
182
+ try {
183
+ return graphEnvelope(await graphOp({
184
+ op: "path", args: [from, to], graphPath: graph, ...gOpts()
185
+ }));
186
+ } catch { return graphFail(); }
187
+ },
188
+ async kairo_context_summary() {
189
+ try {
190
+ const companion = await buildCompanion({ runs: await listRuns({ limit: 20, activeOnly: false }) });
191
+ return mcpResult({
192
+ ok: true, code: companion?.ok === false ? "degraded" : "ok",
193
+ data: {
194
+ signals: pubSignals(companion?.signals), engram: pubEngram(companion?.engram),
195
+ links: pubLinks(companion?.links), alertsCount: companion?.alertsCount ?? null,
196
+ nextSafeAction: pubNext(companion?.nextSafeAction)
197
+ },
198
+ diagnostics: companion?.ok === false ? ["companion_degraded"] : []
199
+ });
200
+ } catch {
201
+ return soft("degraded", { signals: null, engram: null, links: [], alertsCount: null, nextSafeAction: null });
202
+ }
203
+ }
204
+ };
205
+ }
206
+
207
+ export function registerKairoMcpTools(registerTool, deps = {}) {
208
+ const h = createToolHandlers(deps);
209
+ for (const [name, description, inputSchema] of [
210
+ ["kairo_status", "Read-only control-plane + companion summary", empty],
211
+ ["kairo_runs", "Read-only run list", mcpSchemas.runs],
212
+ ["kairo_alerts", "Read-only alert list", mcpSchemas.alerts],
213
+ ["kairo_gentle_status", "Gentle probe / companion gentle signal", empty],
214
+ ["kairo_graph_query", "Read-only Graphify query", mcpSchemas.graphQuery],
215
+ ["kairo_graph_path", "Read-only Graphify path", mcpSchemas.graphPath],
216
+ ["kairo_context_summary", "Companion + soft links + alerts count", empty]
217
+ ]) registerTool(name, { description, inputSchema }, h[name]);
218
+ return h;
219
+ }
220
+
221
+ export function createKairoMcpServer(deps = {}) {
222
+ const Server = deps.McpServer ?? McpServer;
223
+ const server = new Server({ name: "kairo", version: deps.version ?? "0.11.0" });
224
+ registerKairoMcpTools(deps.registerTool ?? ((n, c, h) => server.registerTool(n, c, h)), deps);
225
+ return server;
226
+ }
227
+
228
+ export function runKairoMcp(deps = {}) {
229
+ return (deps.serveStdio ?? serveStdio)(() => createKairoMcpServer(deps));
230
+ }
@@ -0,0 +1,281 @@
1
+ import { CONTROL_PLANE_HEALTH } from "../control-plane-snapshot.js";
2
+ import { buildObservabilitySnapshot as defaultObs } from "./build-observability-snapshot.js";
3
+ import { createGentleProbe } from "./gentle-probe.js";
4
+ import { createGraphifyProbe } from "./graphify-probe.js";
5
+ import { createHermesProbe } from "./hermes-probe.js";
6
+ import { loadHermesActivity as defaultHermesActivity } from "./hermes-activity.js";
7
+ import { loadSystemResources as defaultSystemResources } from "./system-resources.js";
8
+ import { recommendSystemResources } from "./resource-advisor.js";
9
+ import { loadEcosystemUpdates as defaultEcosystemUpdates } from "./ecosystem-updates.js";
10
+ import { getObservabilityProbe, registerObservabilityProbe } from "./probe-registry.js";
11
+
12
+ export const SOFT_LINK_WINDOW_MS = 60 * 60 * 1000;
13
+ const RUN_TS = ["updatedAt", "endedAt", "startedAt"];
14
+ const REVIEW_TS = ["updatedAt", "createdAt", "capturedAt", "startedAt"];
15
+ const GOV_PRIMARY = new Set([
16
+ CONTROL_PLANE_HEALTH.NOT_CONFIGURED,
17
+ CONTROL_PLANE_HEALTH.ACTION_REQUIRED,
18
+ CONTROL_PLANE_HEALTH.CHECK_FAILED
19
+ ]);
20
+
21
+ function defaultEnsure() {
22
+ if (!getObservabilityProbe("gentle")) registerObservabilityProbe(createGentleProbe());
23
+ if (!getObservabilityProbe("graphify")) registerObservabilityProbe(createGraphifyProbe());
24
+ if (!getObservabilityProbe("hermes")) registerObservabilityProbe(createHermesProbe());
25
+ }
26
+
27
+ function emptyHermesActivity() {
28
+ return {
29
+ state: "error", error: "error", diagnostics: [], baseUrl: null, sessions: [],
30
+ aggregates: { returnedCount: 0, activeCount: 0, endedCount: 0, hasMore: false, lastActiveAt: null }
31
+ };
32
+ }
33
+ function summarizeHermesActivity(a) {
34
+ if (a == null || typeof a !== "object") return emptyHermesActivity();
35
+ const empty = emptyHermesActivity();
36
+ return {
37
+ state: a.state ?? "error", error: a.error ?? null, baseUrl: a.baseUrl ?? null,
38
+ diagnostics: Array.isArray(a.diagnostics) ? a.diagnostics.map(String) : [],
39
+ sessions: Array.isArray(a.sessions) ? a.sessions : [], aggregates: a.aggregates ?? empty.aggregates
40
+ };
41
+ }
42
+
43
+ function emptyEcosystemUpdates() {
44
+ return {
45
+ state: "error", checkedAt: null, cacheHit: false, diagnostics: [],
46
+ tools: {
47
+ kairo: { id: "kairo", state: "error", updateAvailable: false },
48
+ hermes: { id: "hermes", state: "error", updateAvailable: false },
49
+ gentle: { id: "gentle", state: "error", updateAvailable: false },
50
+ skills: { id: "skills", state: "error", updateAvailable: false }
51
+ }
52
+ };
53
+ }
54
+ function summarizeEcosystemUpdates(raw) {
55
+ if (raw == null || typeof raw !== "object") return emptyEcosystemUpdates();
56
+ return {
57
+ state: raw.state ?? "error",
58
+ checkedAt: raw.checkedAt ?? null,
59
+ cacheHit: raw.cacheHit === true,
60
+ diagnostics: Array.isArray(raw.diagnostics) ? raw.diagnostics.map(String) : [],
61
+ tools: raw.tools ?? emptyEcosystemUpdates().tools
62
+ };
63
+ }
64
+
65
+ function emptySystemResources() {
66
+ return {
67
+ state: "error", sampledAt: null, diagnostics: [],
68
+ memory: null, swap: null, disk: null,
69
+ processes: { totalCount: 0, zombieCount: 0, tracked: [] },
70
+ thermal: { state: "unavailable" }, ssdWear: { state: "unavailable" }
71
+ };
72
+ }
73
+ function summarizeSystemResources(raw) {
74
+ if (raw == null || typeof raw !== "object") return emptySystemResources();
75
+ const empty = emptySystemResources();
76
+ return {
77
+ state: raw.state ?? "error",
78
+ sampledAt: raw.sampledAt ?? null,
79
+ diagnostics: Array.isArray(raw.diagnostics) ? raw.diagnostics.map(String) : [],
80
+ memory: raw.memory ?? null, swap: raw.swap ?? null, disk: raw.disk ?? null,
81
+ processes: raw.processes ?? empty.processes,
82
+ thermal: raw.thermal ?? { state: "unavailable" },
83
+ ssdWear: raw.ssdWear ?? { state: "unavailable" }
84
+ };
85
+ }
86
+
87
+ export function parseCompanionTimestamp(value) {
88
+ if (value == null || value === "") return null;
89
+ if (typeof value === "number" && Number.isFinite(value)) return value;
90
+ const ms = Date.parse(String(value));
91
+ return Number.isFinite(ms) ? ms : null;
92
+ }
93
+
94
+ function firstTs(obj, keys) {
95
+ for (const key of keys) {
96
+ const ms = parseCompanionTimestamp(obj?.[key]);
97
+ if (ms != null) return ms;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ export const resolveRunTimestamp = (run) => firstTs(run, RUN_TS);
103
+ export const resolveReviewTimestamp = (review) => firstTs(review, REVIEW_TS);
104
+
105
+ /** Soft/display-only — never authority evidence. */
106
+ export function softLinkReviewToRun(review, runs = []) {
107
+ const agentId = review?.agentId;
108
+ const reviewAt = resolveReviewTimestamp(review);
109
+ if (typeof agentId !== "string" || !agentId || reviewAt == null) return null;
110
+ const eligible = [];
111
+ for (const run of runs) {
112
+ if (run?.agentId !== agentId) continue;
113
+ const runAt = resolveRunTimestamp(run);
114
+ if (runAt == null || !(reviewAt > runAt)) continue;
115
+ const deltaMs = reviewAt - runAt;
116
+ if (deltaMs > SOFT_LINK_WINDOW_MS) continue;
117
+ const runId = String(run.runId ?? "");
118
+ if (!runId) continue;
119
+ eligible.push({ runId, runAt, deltaMs });
120
+ }
121
+ if (!eligible.length) return null;
122
+ eligible.sort((a, b) => (b.runAt - a.runAt) || a.runId.localeCompare(b.runId));
123
+ const best = eligible[0];
124
+ return {
125
+ kind: "soft", displayOnly: true, agentId,
126
+ reviewId: String(review.reviewId ?? ""), runId: best.runId,
127
+ deltaMs: best.deltaMs, runAt: best.runAt, reviewAt
128
+ };
129
+ }
130
+
131
+ export function summarizeCompanionProbes(probes = []) {
132
+ const byId = Object.fromEntries((probes ?? []).map((p) => [p.id, p]));
133
+ const gentle = byId.gentle ?? { state: "missing", diagnostics: [], error: null };
134
+ const graphify = byId.graphify ?? { state: "missing", diagnostics: [], error: null };
135
+ const graph = (graphify.evidence ?? []).find((e) => e?.kind === "graph") ?? null;
136
+ return {
137
+ gentle: { state: gentle.state, error: gentle.error ?? null, diagnostics: gentle.diagnostics ?? [] },
138
+ graphify: {
139
+ state: graphify.state, error: graphify.error ?? null,
140
+ diagnostics: graphify.diagnostics ?? [], graphStatus: graph?.status ?? null
141
+ }
142
+ };
143
+ }
144
+
145
+ function rankNext({ controlPlaneHealth, signals, engram }) {
146
+ const gentleState = signals?.gentle?.state;
147
+ const graphState = signals?.graphify?.state;
148
+ const graphStatus = signals?.graphify?.graphStatus;
149
+ const engramStatus = engram?.status ?? "missing";
150
+ let kind = "idle";
151
+ let title = "Companion signals quiet";
152
+ let detail = "No companion follow-up required.";
153
+ if (gentleState === "error" || graphState === "error" || graphStatus === "error"
154
+ || engramStatus === "error" || engramStatus === "conflict" || engramStatus === "unsupported") {
155
+ kind = "investigate";
156
+ title = "Investigate companion diagnostics";
157
+ detail = "Optional tools reported a hard failure; inspect Gentle/Graphify/Engram read-only.";
158
+ } else if (gentleState === "missing" || graphState === "missing" || engramStatus === "missing") {
159
+ kind = "missing";
160
+ title = "Optional companion tool not detected";
161
+ detail = "Governance may still be healthy; missing tools are diagnostic only.";
162
+ } else if (graphStatus === "stale") {
163
+ kind = "inform";
164
+ title = "Graphify graph looks stale";
165
+ detail = "Informational only — stale does not block read-only ops.";
166
+ }
167
+ return {
168
+ kind, title, detail, secondary: true,
169
+ governancePrimary: GOV_PRIMARY.has(controlPlaneHealth), displayOnly: true
170
+ };
171
+ }
172
+
173
+ function emptyCompanion(error = null) {
174
+ return {
175
+ ok: false, error: error == null ? null : String(error),
176
+ generatedAt: new Date().toISOString(),
177
+ signals: {
178
+ gentle: { state: "error", error: null, diagnostics: [] },
179
+ graphify: { state: "error", error: null, diagnostics: [], graphStatus: null },
180
+ hermes: { activity: emptyHermesActivity() },
181
+ system: { resources: emptySystemResources(), advice: { recommendations: [], deepScan: false } },
182
+ ecosystem: { updates: emptyEcosystemUpdates() }
183
+ },
184
+ engram: { status: "error", binary: null },
185
+ links: [], alertsCount: null,
186
+ nextSafeAction: {
187
+ kind: "investigate", title: "Investigate companion diagnostics",
188
+ detail: String(error ?? "companion snapshot failed"),
189
+ secondary: true, governancePrimary: false, displayOnly: true
190
+ }
191
+ };
192
+ }
193
+
194
+ /** Fail-soft companion snapshot for Cockpit overlay — never throws. */
195
+ export async function buildCompanionSnapshot({
196
+ controlPlaneHealth = null, runs = [], reviews = null, alerts = null,
197
+ buildObservability = defaultObs, inspectEngram = null,
198
+ ensureRegistered = defaultEnsure, loadReviews = null, loadAlerts = null,
199
+ loadHermesActivity = defaultHermesActivity,
200
+ loadSystemResources = defaultSystemResources,
201
+ loadEcosystemUpdates = defaultEcosystemUpdates,
202
+ resourceDeepScan = false,
203
+ observabilityContext = {}
204
+ } = {}) {
205
+ try {
206
+ ensureRegistered();
207
+ let obs;
208
+ try { obs = await buildObservability(observabilityContext); }
209
+ catch (err) { return emptyCompanion(err?.message ?? err); }
210
+
211
+ let engram = { status: "missing", binary: null };
212
+ if (typeof inspectEngram === "function") {
213
+ try { engram = inspectEngram(observabilityContext) ?? engram; }
214
+ catch (err) { engram = { status: "error", binary: null, error: err?.message ?? String(err) }; }
215
+ }
216
+
217
+ let hermesActivity = emptyHermesActivity();
218
+ try {
219
+ hermesActivity = summarizeHermesActivity(
220
+ await loadHermesActivity({ ...(observabilityContext ?? {}) })
221
+ );
222
+ } catch {
223
+ hermesActivity = emptyHermesActivity();
224
+ }
225
+
226
+ let systemResources = emptySystemResources();
227
+ try {
228
+ systemResources = summarizeSystemResources(
229
+ await loadSystemResources({ ...(observabilityContext ?? {}) })
230
+ );
231
+ } catch {
232
+ systemResources = emptySystemResources();
233
+ }
234
+ const systemAdvice = recommendSystemResources(systemResources, {
235
+ deepScan: resourceDeepScan === true
236
+ });
237
+
238
+ let ecosystemUpdates = emptyEcosystemUpdates();
239
+ try {
240
+ ecosystemUpdates = summarizeEcosystemUpdates(
241
+ await loadEcosystemUpdates({
242
+ ...(observabilityContext ?? {}),
243
+ packageName: observabilityContext?.packageName,
244
+ installedVersion: observabilityContext?.installedVersion ?? observabilityContext?.cliVersion,
245
+ homeDir: observabilityContext?.homeDir
246
+ })
247
+ );
248
+ } catch {
249
+ ecosystemUpdates = emptyEcosystemUpdates();
250
+ }
251
+
252
+ const reviewList = Array.isArray(reviews)
253
+ ? reviews
254
+ : (typeof loadReviews === "function" ? await loadReviews() : []);
255
+ const alertList = Array.isArray(alerts)
256
+ ? alerts
257
+ : (typeof loadAlerts === "function" ? await loadAlerts() : null);
258
+ const signals = {
259
+ ...summarizeCompanionProbes(obs?.probes ?? []),
260
+ hermes: { activity: hermesActivity },
261
+ system: { resources: systemResources, advice: systemAdvice },
262
+ ecosystem: { updates: ecosystemUpdates }
263
+ };
264
+ const links = [];
265
+ for (const review of reviewList ?? []) {
266
+ const link = softLinkReviewToRun(review, runs);
267
+ if (link) links.push(link);
268
+ }
269
+ return {
270
+ ok: true, error: null, generatedAt: new Date().toISOString(), signals,
271
+ engram: { status: engram.status ?? "missing", binary: engram.binary?.path ?? null, error: engram.error ?? null },
272
+ links,
273
+ alertsCount: Array.isArray(alertList)
274
+ ? alertList.filter((a) => a?.state === "open" || a?.state === "pending").length
275
+ : null,
276
+ nextSafeAction: rankNext({ controlPlaneHealth, signals, engram })
277
+ };
278
+ } catch (err) {
279
+ return emptyCompanion(err?.message ?? err);
280
+ }
281
+ }
@@ -0,0 +1,24 @@
1
+ import { normalizeProbeResult } from "./probe-contract.js";
2
+ import { listObservabilityProbes } from "./probe-registry.js";
3
+
4
+ /** Parallel soft snapshot — never throws for individual probe failures. */
5
+ export async function buildObservabilitySnapshot(context = {}) {
6
+ const registered = listObservabilityProbes();
7
+ const settled = await Promise.allSettled(
8
+ registered.map(async (probe) => normalizeProbeResult(await probe.probe(context), probe.id))
9
+ );
10
+ const probes = settled.map((entry, i) => {
11
+ const id = registered[i].id;
12
+ if (entry.status === "fulfilled") return entry.value;
13
+ return normalizeProbeResult({
14
+ id,
15
+ state: "error",
16
+ error: entry.reason?.message ?? String(entry.reason ?? "probe failed")
17
+ }, id);
18
+ });
19
+ return {
20
+ generatedAt: new Date().toISOString(),
21
+ probes,
22
+ byId: Object.fromEntries(probes.map((p) => [p.id, p]))
23
+ };
24
+ }