@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,171 @@
1
+ import { isAbsolute } from "node:path";
2
+ import {
3
+ parseVersionFromOutput,
4
+ probeCommand as defaultProbeCommand
5
+ } from "../cli-probe.js";
6
+ import { normalizeProbeResult } from "./probe-contract.js";
7
+
8
+ /** Surfaces recognized from `hermes --help` only — never executed by this probe. */
9
+ export const HERMES_DIAGNOSTIC_SURFACES = Object.freeze(["version", "doctor", "status"]);
10
+ export const HERMES_MANDATORY_SURFACES = Object.freeze(["version", "doctor"]);
11
+
12
+ const WHICH_TIMEOUT_MS = 3000;
13
+ const VERSION_TIMEOUT_MS = 8000;
14
+ const HELP_TIMEOUT_MS = 8000;
15
+ const MAX_PROBE_BYTES = 65_536;
16
+
17
+ function result(partial) {
18
+ return normalizeProbeResult({
19
+ id: "hermes", version: null, contractCompatible: null,
20
+ diagnostics: [], evidence: [], error: null, ...partial
21
+ }, "hermes");
22
+ }
23
+
24
+ function boundText(raw, maxBytes = MAX_PROBE_BYTES) {
25
+ const text = String(raw ?? "");
26
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
27
+ return Buffer.from(text, "utf8").subarray(0, maxBytes).toString("utf8");
28
+ }
29
+
30
+ function whichAbsolutePath(command, env, run = defaultProbeCommand) {
31
+ const which = run("which", [command], { env, timeoutMs: WHICH_TIMEOUT_MS });
32
+ if (!which?.ok) return "";
33
+ const path = String(which.stdout ?? "").trim().split(/\r?\n/)[0] ?? "";
34
+ return isAbsolute(path) ? path : "";
35
+ }
36
+
37
+ /** Resolve absolute Hermes binary; never return a bare command name. */
38
+ export function resolveHermesBinaryPath(command = "hermes", env = process.env, {
39
+ whichCommand,
40
+ probeCommand = defaultProbeCommand
41
+ } = {}) {
42
+ const resolver = typeof whichCommand === "function"
43
+ ? whichCommand
44
+ : (cmd, e) => whichAbsolutePath(cmd, e, probeCommand);
45
+ const resolved = resolver(command, env) || null;
46
+ return resolved && isAbsolute(resolved) ? resolved : null;
47
+ }
48
+
49
+ /** Presence means the CLI *declares* the surface — not that Kairo may run it. */
50
+ export function detectHermesDiagnosticSurfaces(helpText) {
51
+ const text = boundText(helpText);
52
+ const found = Object.create(null);
53
+ for (const name of HERMES_DIAGNOSTIC_SURFACES) {
54
+ found[name] = new RegExp(`(?:^|[\\s,{|/])${name}(?=[\\s,}|/-]|$)`, "m").test(text);
55
+ }
56
+ return Object.freeze(found);
57
+ }
58
+
59
+ function softError(label, kind, evidence, extra = {}) {
60
+ return result({
61
+ state: "error",
62
+ diagnostics: [`hermes ${label} ${kind === "timeout" ? "timed out" : "failed"}`],
63
+ error: kind === "timeout" ? "timeout" : kind === "exit" ? `exit ${extra.code}` : "spawn_error",
64
+ evidence,
65
+ ...extra.fields
66
+ });
67
+ }
68
+
69
+ function spawnFailure(label, probeResult, evidence, extra = {}) {
70
+ if (probeResult?.timedOut) return softError(label, "timeout", evidence, extra);
71
+ if (probeResult?.error) return softError(label, "spawn", evidence, extra);
72
+ if (probeResult?.status !== 0) {
73
+ return softError(label, "exit", evidence, { ...extra, code: probeResult?.status ?? "unknown" });
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /** which: preserve timeout/spawn_error; exit/empty → missing (not failure). */
79
+ function whichFailure(probeResult, evidence) {
80
+ if (probeResult?.timedOut) return softError("which", "timeout", evidence);
81
+ if (probeResult?.error) return softError("which", "spawn", evidence);
82
+ return null;
83
+ }
84
+
85
+ function runProbe(probeCommand, cmd, args, opts) {
86
+ try { return { ok: true, value: probeCommand(cmd, args, opts) }; }
87
+ catch { return { ok: false }; }
88
+ }
89
+
90
+ export async function probeHermes({
91
+ env = process.env,
92
+ cwd = process.cwd(),
93
+ whichCommand,
94
+ probeCommand = defaultProbeCommand
95
+ } = {}) {
96
+ const noBinary = [{ kind: "binary", path: null }];
97
+ let path;
98
+
99
+ if (typeof whichCommand === "function") {
100
+ try { path = resolveHermesBinaryPath("hermes", env, { whichCommand, probeCommand }); }
101
+ catch { return softError("which", "spawn", noBinary); }
102
+ } else {
103
+ const inv = runProbe(probeCommand, "which", ["hermes"], { env, timeoutMs: WHICH_TIMEOUT_MS });
104
+ if (!inv.ok) return softError("which", "spawn", noBinary);
105
+ const fail = whichFailure(inv.value, noBinary);
106
+ if (fail) return fail;
107
+ const candidate = String(inv.value?.stdout ?? "").trim().split(/\r?\n/)[0] ?? "";
108
+ path = inv.value?.ok && isAbsolute(candidate) ? candidate : null;
109
+ }
110
+
111
+ if (!path) {
112
+ return result({
113
+ state: "missing",
114
+ diagnostics: [
115
+ "hermes absolute binary not resolved. Install Hermes Agent separately, then re-run the probe."
116
+ ],
117
+ evidence: noBinary
118
+ });
119
+ }
120
+
121
+ const evidence = [{ kind: "binary", path }];
122
+ const versionInv = runProbe(probeCommand, path, ["--version"], {
123
+ cwd, env, timeoutMs: VERSION_TIMEOUT_MS
124
+ });
125
+ if (!versionInv.ok) return softError("--version", "spawn", evidence);
126
+ const versionFail = spawnFailure("--version", versionInv.value, evidence);
127
+ if (versionFail) return versionFail;
128
+ const version = parseVersionFromOutput(boundText(versionInv.value.stdout));
129
+ evidence.push({ kind: "version", version, ok: true });
130
+ if (!version) {
131
+ return result({
132
+ state: "incompatible", contractCompatible: false, evidence,
133
+ diagnostics: ["hermes --version did not yield a parseable version"]
134
+ });
135
+ }
136
+
137
+ const helpInv = runProbe(probeCommand, path, ["--help"], {
138
+ cwd, env, timeoutMs: HELP_TIMEOUT_MS
139
+ });
140
+ if (!helpInv.ok) return softError("--help", "spawn", evidence, { fields: { version } });
141
+ const helpFail = spawnFailure("--help", helpInv.value, evidence, { fields: { version } });
142
+ if (helpFail) return helpFail;
143
+
144
+ const surfaces = detectHermesDiagnosticSurfaces(helpInv.value.stdout);
145
+ // Capability availability only — surfaces were not executed.
146
+ evidence.push({ kind: "diagnostic_surfaces", surfaces: { ...surfaces }, executed: false });
147
+
148
+ const missingMandatory = HERMES_MANDATORY_SURFACES.filter((name) => !surfaces[name]);
149
+ if (missingMandatory.length) {
150
+ return result({
151
+ state: "incompatible", version, contractCompatible: false, evidence,
152
+ diagnostics: missingMandatory.map((name) => `missing mandatory surface in --help: ${name}`)
153
+ });
154
+ }
155
+
156
+ return result({
157
+ state: "available", version, contractCompatible: true, evidence,
158
+ diagnostics: surfaces.status ? [] : ["optional surface absent in --help: status"]
159
+ });
160
+ }
161
+
162
+ export function createHermesProbe(deps = {}) {
163
+ return {
164
+ id: "hermes",
165
+ declaredEvents: Object.freeze([]),
166
+ declaredActions: Object.freeze([]),
167
+ async probe(context = {}) {
168
+ return probeHermes({ ...deps, ...context, env: context.env ?? deps.env ?? process.env });
169
+ }
170
+ };
171
+ }
@@ -0,0 +1,85 @@
1
+ import { createGentleProbe } from "./gentle-probe.js";
2
+ import { createGraphifyProbe } from "./graphify-probe.js";
3
+ import { createHermesProbe } from "./hermes-probe.js";
4
+ import { getObservabilityProbe, registerObservabilityProbe } from "./probe-registry.js";
5
+
6
+ export {
7
+ OBSERVABILITY_PROBE_STATES,
8
+ assertObservabilityProbeContract,
9
+ normalizeProbeResult
10
+ } from "./probe-contract.js";
11
+ export {
12
+ registerObservabilityProbe,
13
+ getObservabilityProbe,
14
+ listObservabilityProbes,
15
+ resetObservabilityProbesForTests
16
+ } from "./probe-registry.js";
17
+ export { buildObservabilitySnapshot } from "./build-observability-snapshot.js";
18
+ export {
19
+ PASSIVE_SNAPSHOT_TTL_MS,
20
+ PASSIVE_SNAPSHOT_MAX_ENTRIES,
21
+ buildPassiveSnapshotKey,
22
+ resetPassiveSnapshotFlightForTests,
23
+ passiveSnapshotInFlightSizeForTests,
24
+ runPassiveObservabilitySnapshot
25
+ } from "./passive-snapshot-flight.js";
26
+ export {
27
+ SUPPORTED_PROTOCOL, SUPPORTED_SCHEMA, SUPPORTED_CONTRACT,
28
+ SUPPORTED_MANDATORY_FEATURES, evaluateGentleCapabilities, probeGentle, createGentleProbe,
29
+ resolveGentleBinaryPath
30
+ } from "./gentle-probe.js";
31
+ export { exportGentleReviewBundle, resolveNegotiatedGentleBinary } from "./gentle-bundle-export.js";
32
+ export { importGentleReviewBundle } from "./gentle-bundle-import.js";
33
+ export {
34
+ inspectGraphArtifact, assertGraphInsideWorkspace,
35
+ resolveGraphifyBinaryPath, resolveGitHeadSha, scrubGitOverrideEnv,
36
+ probeGraphify, createGraphifyProbe
37
+ } from "./graphify-probe.js";
38
+ export {
39
+ GRAPHIFY_PARSE_TTL_MS,
40
+ GRAPHIFY_PARSE_MAX_ENTRIES,
41
+ buildGraphifyParseIdentity,
42
+ resetGraphifyParseCacheForTests,
43
+ inspectGraphArtifactCached
44
+ } from "./graphify-parse-cache.js";
45
+ export { runGraphifyOp, runGraphifyCli } from "./graphify-ops.js";
46
+ export {
47
+ HERMES_DIAGNOSTIC_SURFACES, HERMES_MANDATORY_SURFACES,
48
+ detectHermesDiagnosticSurfaces, resolveHermesBinaryPath,
49
+ probeHermes, createHermesProbe
50
+ } from "./hermes-probe.js";
51
+ export {
52
+ DEFAULT_HERMES_API_URL,
53
+ HERMES_ACTIVITY_LIMIT_DEFAULT, HERMES_ACTIVITY_LIMIT_MAX,
54
+ HERMES_ACTIVITY_TIMEOUT_MS, HERMES_ACTIVE_WINDOW_MS,
55
+ assertHermesLoopbackUrl, capabilitiesAdvertiseSessionsList,
56
+ normalizeHermesSession, loadHermesActivity
57
+ } from "./hermes-activity.js";
58
+ export {
59
+ SYSTEM_RESOURCES_TIMEOUT_MS, PROCESS_ALLOWLIST,
60
+ parseProcessTable, loadSystemResources
61
+ } from "./system-resources.js";
62
+ export { recommendSystemResources } from "./resource-advisor.js";
63
+ export {
64
+ ECOSYSTEM_UPDATES_CACHE_MS,
65
+ ECOSYSTEM_UPDATES_TIMEOUT_MS,
66
+ AGENT_SKILLS_PINNED_REV,
67
+ parseGentleUpdateOutput,
68
+ parseHermesUpdateCheck,
69
+ loadEcosystemUpdates
70
+ } from "./ecosystem-updates.js";
71
+ export {
72
+ SOFT_LINK_WINDOW_MS,
73
+ parseCompanionTimestamp,
74
+ resolveRunTimestamp,
75
+ resolveReviewTimestamp,
76
+ softLinkReviewToRun,
77
+ summarizeCompanionProbes,
78
+ buildCompanionSnapshot
79
+ } from "./build-companion-snapshot.js";
80
+
81
+ export function ensureObservabilityProbesRegistered() {
82
+ if (!getObservabilityProbe("gentle")) registerObservabilityProbe(createGentleProbe());
83
+ if (!getObservabilityProbe("graphify")) registerObservabilityProbe(createGraphifyProbe());
84
+ if (!getObservabilityProbe("hermes")) registerObservabilityProbe(createHermesProbe());
85
+ }
@@ -0,0 +1,93 @@
1
+ import { buildObservabilitySnapshot } from "./build-observability-snapshot.js";
2
+ import { listObservabilityProbes } from "./probe-registry.js";
3
+
4
+ /** Short TTL for passive Cockpit/MCP observability only — never suite authority. */
5
+ export const PASSIVE_SNAPSHOT_TTL_MS = 5_000;
6
+ export const PASSIVE_SNAPSHOT_MAX_ENTRIES = 8;
7
+
8
+ /** @type {Map<string, Promise<unknown>>} */
9
+ const flights = new Map();
10
+ /** @type {Map<string, { value: unknown, expiresAt: number }>} */
11
+ const completed = new Map();
12
+
13
+ export function resetPassiveSnapshotFlightForTests() {
14
+ flights.clear();
15
+ completed.clear();
16
+ }
17
+
18
+ export function passiveSnapshotFlightSizeForTests() {
19
+ return completed.size;
20
+ }
21
+
22
+ export function passiveSnapshotInFlightSizeForTests() {
23
+ return flights.size;
24
+ }
25
+
26
+ export function buildPassiveSnapshotKey(context = {}, {
27
+ listProviders = listObservabilityProbes
28
+ } = {}) {
29
+ const workspace = String(context.workspaceRoot ?? context.cwd ?? "");
30
+ const head = String(context.headSha ?? "");
31
+ const providers = listProviders().map((p) => p.id).sort().join(",");
32
+ return `${workspace}\0${head}\0${providers}`;
33
+ }
34
+
35
+ function touchCompleted(key, entry) {
36
+ completed.delete(key);
37
+ completed.set(key, entry);
38
+ }
39
+
40
+ function evictOldestCompleted(maxEntries) {
41
+ while (completed.size > maxEntries) {
42
+ const oldest = completed.keys().next().value;
43
+ if (oldest == null) break;
44
+ completed.delete(oldest);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Single-flight + short TTL for passive observability snapshots.
50
+ * force skips completed hits but joins identical in-flight work.
51
+ * Errors clear that key's flight and completed entry so the next call rebuilds
52
+ * (a failed force refresh must not leave a stale completed hit).
53
+ * LRU applies only to completed values — never evicts active flights.
54
+ */
55
+ export async function runPassiveObservabilitySnapshot(context = {}, {
56
+ force = false,
57
+ build = buildObservabilitySnapshot,
58
+ now = Date.now,
59
+ listProviders = listObservabilityProbes,
60
+ ttlMs = PASSIVE_SNAPSHOT_TTL_MS,
61
+ maxEntries = PASSIVE_SNAPSHOT_MAX_ENTRIES
62
+ } = {}) {
63
+ const key = buildPassiveSnapshotKey(context, { listProviders });
64
+ const inflight = flights.get(key);
65
+ if (inflight) return inflight;
66
+
67
+ const cached = completed.get(key);
68
+ if (!force && cached && cached.expiresAt > now()) {
69
+ touchCompleted(key, cached);
70
+ return cached.value;
71
+ }
72
+
73
+ const promise = Promise.resolve()
74
+ .then(() => build(context))
75
+ .then((value) => {
76
+ if (flights.get(key) === promise) {
77
+ flights.delete(key);
78
+ touchCompleted(key, { value, expiresAt: now() + ttlMs });
79
+ evictOldestCompleted(maxEntries);
80
+ }
81
+ return value;
82
+ })
83
+ .catch((err) => {
84
+ if (flights.get(key) === promise) {
85
+ flights.delete(key);
86
+ completed.delete(key);
87
+ }
88
+ throw err;
89
+ });
90
+
91
+ flights.set(key, promise);
92
+ return promise;
93
+ }
@@ -0,0 +1,38 @@
1
+ /** Observability probe contract — read-only; no lifecycle mutations. */
2
+
3
+ export const OBSERVABILITY_PROBE_STATES = Object.freeze([
4
+ "missing", "available", "incompatible", "error"
5
+ ]);
6
+
7
+ export function assertObservabilityProbeContract(probe) {
8
+ if (probe == null || typeof probe !== "object" || Array.isArray(probe)) {
9
+ throw new Error("Observability probe must be an object.");
10
+ }
11
+ if (typeof probe.id !== "string" || !probe.id) {
12
+ throw new Error("Observability probe id must be a non-empty string.");
13
+ }
14
+ if (typeof probe.probe !== "function") {
15
+ throw new Error(`Observability probe "${probe.id}" is missing probe().`);
16
+ }
17
+ if (!Array.isArray(probe.declaredEvents)) {
18
+ throw new Error(`Observability probe "${probe.id}" declaredEvents must be an array.`);
19
+ }
20
+ if (!Array.isArray(probe.declaredActions)) {
21
+ throw new Error(`Observability probe "${probe.id}" declaredActions must be an array.`);
22
+ }
23
+ return probe;
24
+ }
25
+
26
+ export function normalizeProbeResult(raw, fallbackId) {
27
+ const id = typeof raw?.id === "string" && raw.id ? raw.id : fallbackId;
28
+ const state = OBSERVABILITY_PROBE_STATES.includes(raw?.state) ? raw.state : "error";
29
+ return {
30
+ id,
31
+ state,
32
+ version: raw?.version ?? null,
33
+ contractCompatible: typeof raw?.contractCompatible === "boolean" ? raw.contractCompatible : null,
34
+ diagnostics: Array.isArray(raw?.diagnostics) ? raw.diagnostics.map(String) : [],
35
+ evidence: Array.isArray(raw?.evidence) ? raw.evidence : [],
36
+ error: raw?.error == null ? null : String(raw.error)
37
+ };
38
+ }
@@ -0,0 +1,30 @@
1
+ import { assertObservabilityProbeContract } from "./probe-contract.js";
2
+
3
+ const probes = new Map();
4
+
5
+ export function registerObservabilityProbe(probe) {
6
+ assertObservabilityProbeContract(probe);
7
+ if (probes.has(probe.id)) {
8
+ throw new Error(`Observability probe "${probe.id}" is already registered.`);
9
+ }
10
+ const frozen = Object.freeze({
11
+ id: probe.id,
12
+ probe: probe.probe.bind(probe),
13
+ declaredEvents: Object.freeze([...(probe.declaredEvents ?? [])]),
14
+ declaredActions: Object.freeze([...(probe.declaredActions ?? [])])
15
+ });
16
+ probes.set(probe.id, frozen);
17
+ return frozen;
18
+ }
19
+
20
+ export function getObservabilityProbe(id) {
21
+ return probes.get(id) ?? null;
22
+ }
23
+
24
+ export function listObservabilityProbes() {
25
+ return [...probes.values()];
26
+ }
27
+
28
+ export function resetObservabilityProbesForTests() {
29
+ probes.clear();
30
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Deterministic Local Resource Advisor — observe and recommend only.
3
+ * Never deletes, compacts, or kills. Deep-scan is explicit opt-in.
4
+ */
5
+
6
+ export function recommendSystemResources(resources, { deepScan = false } = {}) {
7
+ const recommendations = [];
8
+ if (resources == null || typeof resources !== "object") {
9
+ return { recommendations, deepScan: false };
10
+ }
11
+ const state = typeof resources.state === "string" ? resources.state : "error";
12
+ if (state === "unavailable" || state === "error" || state === "incompatible") {
13
+ return { recommendations, deepScan: Boolean(deepScan) };
14
+ }
15
+
16
+ const diskPct = resources.disk?.freePercent;
17
+ if (typeof diskPct === "number" && Number.isFinite(diskPct)) {
18
+ if (diskPct < 10) {
19
+ recommendations.push({
20
+ id: "free-disk-critical",
21
+ severity: "critical",
22
+ title: "Free disk space",
23
+ detail: "Disk free below 10%. Clear caches manually after review — Kairo performs no removals."
24
+ });
25
+ } else if (diskPct < 20) {
26
+ recommendations.push({
27
+ id: "free-disk-warning",
28
+ severity: "warning",
29
+ title: "Disk space running low",
30
+ detail: "Disk free below 20%. Prefer quitting unused apps before large downloads."
31
+ });
32
+ }
33
+ }
34
+
35
+ const ramPct = resources.memory?.freePercent;
36
+ if (typeof ramPct === "number" && Number.isFinite(ramPct) && ramPct < 15) {
37
+ recommendations.push({
38
+ id: "quit-heavy-apps",
39
+ severity: "warning",
40
+ title: "Quit heavy apps",
41
+ detail: "Memory free below 15%. Quit unused agents/browsers; Kairo never terminates processes."
42
+ });
43
+ }
44
+
45
+ const tracked = Array.isArray(resources.processes?.tracked) ? resources.processes.tracked : [];
46
+ const busy = tracked.filter((entry) =>
47
+ entry != null && typeof entry === "object"
48
+ && typeof entry.name === "string"
49
+ && Number.isInteger(entry.count) && entry.count >= 2
50
+ );
51
+ if (busy.length > 0) {
52
+ const names = busy.map((entry) => entry.name).join(", ");
53
+ recommendations.push({
54
+ id: "inspect-tracked-apps",
55
+ severity: "info",
56
+ title: "Inspect local heavy apps",
57
+ detail: `Tracked pressure: ${names}. Review in Activity Monitor — no auto-quit.`
58
+ });
59
+ }
60
+
61
+ if (deepScan === true) {
62
+ recommendations.push({
63
+ id: "deep-scan-known-caches",
64
+ severity: "info",
65
+ title: "Inspect known local caches",
66
+ detail: "Opt-in deep scan only. Lists known cache/DB locations for human review — performs no removals."
67
+ });
68
+ }
69
+
70
+ return { recommendations, deepScan: deepScan === true };
71
+ }
@@ -0,0 +1,171 @@
1
+ import { probeCommand as defaultProbeCommand } from "../cli-probe.js";
2
+
3
+ export const SYSTEM_RESOURCES_TIMEOUT_MS = 2000;
4
+ export const PROCESS_ALLOWLIST = Object.freeze([
5
+ "cursor", "codex", "chatgpt", "brave", "teams", "obsidian", "ollama"
6
+ ]);
7
+
8
+ const BIN = Object.freeze({
9
+ sysctl: "/usr/sbin/sysctl", vmStat: "/usr/bin/vm_stat", pagesize: "/usr/bin/pagesize",
10
+ df: "/bin/df", ps: "/bin/ps"
11
+ });
12
+
13
+ const emptyProcesses = () => ({ totalCount: 0, zombieCount: 0, tracked: [] });
14
+
15
+ function envelope(state, nowMs, diagnostics = [], partial = {}) {
16
+ return {
17
+ state, sampledAt: new Date(nowMs).toISOString(), diagnostics: diagnostics.map(String),
18
+ memory: null, swap: null, disk: null, processes: emptyProcesses(),
19
+ thermal: { state: "unavailable" }, ssdWear: { state: "unavailable" }, ...partial
20
+ };
21
+ }
22
+
23
+ function invoke(probeCommand, cmd, args, timeoutMs) {
24
+ try { return { ok: true, value: probeCommand(cmd, args, { timeoutMs }) }; }
25
+ catch { return { ok: false }; }
26
+ }
27
+
28
+ function classify(inv) {
29
+ if (!inv.ok) return { kind: "spawn_error" };
30
+ const r = inv.value;
31
+ if (r?.timedOut) return { kind: "timeout" };
32
+ if (r?.error) {
33
+ return /EACCES|EPERM|permission/i.test(String(r.error))
34
+ ? { kind: "permission_denied" } : { kind: "spawn_error" };
35
+ }
36
+ if (!r?.ok) {
37
+ return /permission|denied|Operation not permitted/i.test(String(r.stderr ?? ""))
38
+ ? { kind: "permission_denied" } : { kind: "exit" };
39
+ }
40
+ return { kind: "ok", stdout: String(r.stdout ?? "") };
41
+ }
42
+
43
+ function parseBytesUnit(raw) {
44
+ const m = String(raw).trim().match(/^([\d.]+)\s*([KMGTP]?)/i);
45
+ if (!m || !Number.isFinite(Number(m[1]))) return null;
46
+ const mul = { B: 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4, P: 1024 ** 5 };
47
+ return Math.round(Number(m[1]) * (mul[(m[2] || "B").toUpperCase()] ?? 1));
48
+ }
49
+
50
+ function parseSwap(stdout) {
51
+ const grab = (label) => {
52
+ const m = stdout.match(new RegExp(`${label}\\s*=\\s*([\\d.]+\\s*[KMGTP]?)`, "i"));
53
+ return m ? parseBytesUnit(m[1]) : null;
54
+ };
55
+ const totalBytes = grab("total"), usedBytes = grab("used"), freeBytes = grab("free");
56
+ return totalBytes == null || usedBytes == null || freeBytes == null
57
+ ? null : { totalBytes, usedBytes, freeBytes };
58
+ }
59
+
60
+ function parseDf(stdout) {
61
+ const lines = String(stdout).trim().split(/\r?\n/).filter(Boolean);
62
+ if (lines.length < 2) return null;
63
+ const cols = lines.at(-1).trim().split(/\s+/);
64
+ const totalKb = Number(cols[1]), availKb = Number(cols[3]);
65
+ if (!Number.isFinite(totalKb) || !Number.isFinite(availKb) || totalKb <= 0) return null;
66
+ const totalBytes = totalKb * 1024, freeBytes = availKb * 1024;
67
+ return { totalBytes, freeBytes, freePercent: Math.round((freeBytes / totalBytes) * 1000) / 10 };
68
+ }
69
+
70
+ function matchAllowlist(comm) {
71
+ const name = String(comm ?? "").toLowerCase();
72
+ return PROCESS_ALLOWLIST.find((key) => name.includes(key)) ?? null;
73
+ }
74
+
75
+ export function parseProcessTable(stdout) {
76
+ let totalCount = 0, zombieCount = 0;
77
+ const counts = Object.fromEntries(PROCESS_ALLOWLIST.map((k) => [k, 0]));
78
+ for (const line of String(stdout).split(/\r?\n/)) {
79
+ const trimmed = line.trim();
80
+ if (!trimmed) continue;
81
+ const m = trimmed.match(/^(\d+)\s+(\S+)\s+(.+)$/);
82
+ if (!m) continue;
83
+ totalCount += 1;
84
+ if (/^Z/i.test(m[2])) zombieCount += 1;
85
+ const key = matchAllowlist(m[3]);
86
+ if (key) counts[key] += 1;
87
+ }
88
+ return {
89
+ totalCount, zombieCount,
90
+ tracked: PROCESS_ALLOWLIST.filter((k) => counts[k] > 0).map((k) => ({ name: k, count: counts[k] }))
91
+ };
92
+ }
93
+
94
+ function probeMemory(probeCommand, timeoutMs) {
95
+ const mem = classify(invoke(probeCommand, BIN.sysctl, ["-n", "hw.memsize"], timeoutMs));
96
+ if (mem.kind !== "ok") return { ok: false, kind: mem.kind };
97
+ const totalBytes = Number(String(mem.stdout).trim());
98
+ if (!Number.isFinite(totalBytes) || totalBytes <= 0) return { ok: false, kind: "parse" };
99
+ const page = classify(invoke(probeCommand, BIN.pagesize, [], timeoutMs));
100
+ if (page.kind !== "ok") return { ok: false, kind: page.kind };
101
+ const pageSize = Number(String(page.stdout).trim());
102
+ if (!Number.isFinite(pageSize) || pageSize <= 0) return { ok: false, kind: "parse" };
103
+ const vm = classify(invoke(probeCommand, BIN.vmStat, [], timeoutMs));
104
+ if (vm.kind !== "ok") return { ok: false, kind: vm.kind };
105
+ const free = vm.stdout.match(/Pages free:\s+(\d+)/i);
106
+ const spec = vm.stdout.match(/Pages speculative:\s+(\d+)/i);
107
+ if (!free) return { ok: false, kind: "parse" };
108
+ const freeBytes = (Number(free[1]) + (spec ? Number(spec[1]) : 0)) * pageSize;
109
+ return { ok: true, value: { totalBytes, freePercent: Math.round((freeBytes / totalBytes) * 1000) / 10 } };
110
+ }
111
+
112
+ function runProbe(fn, probeCommand, deadlineMs) {
113
+ const left = Math.max(0, deadlineMs - Date.now());
114
+ if (left <= 0) return { ok: false, kind: "timeout" };
115
+ return fn(probeCommand, left);
116
+ }
117
+
118
+ function probeSwap(probeCommand, timeoutMs) {
119
+ const cls = classify(invoke(probeCommand, BIN.sysctl, ["-n", "vm.swapusage"], timeoutMs));
120
+ if (cls.kind !== "ok") return { ok: false, kind: cls.kind };
121
+ const value = parseSwap(cls.stdout);
122
+ return value ? { ok: true, value } : { ok: false, kind: "parse" };
123
+ }
124
+
125
+ function probeDisk(probeCommand, timeoutMs) {
126
+ let cls = classify(invoke(probeCommand, BIN.df, ["-k", "/System/Volumes/Data"], timeoutMs));
127
+ if (cls.kind !== "ok") cls = classify(invoke(probeCommand, BIN.df, ["-k", "/"], timeoutMs));
128
+ if (cls.kind !== "ok") return { ok: false, kind: cls.kind };
129
+ const value = parseDf(cls.stdout);
130
+ return value ? { ok: true, value } : { ok: false, kind: "parse" };
131
+ }
132
+
133
+ function probeProcesses(probeCommand, timeoutMs) {
134
+ const cls = classify(invoke(probeCommand, BIN.ps, ["-axc", "-o", "pid=,stat=,comm="], timeoutMs));
135
+ if (cls.kind !== "ok") return { ok: false, kind: cls.kind };
136
+ return { ok: true, value: parseProcessTable(cls.stdout) };
137
+ }
138
+
139
+ /** Read-only local system resources (macOS first). Absolute bins only; never throws. */
140
+ export async function loadSystemResources({
141
+ platform = process.platform, nowMs = Date.now(),
142
+ timeoutMs = SYSTEM_RESOURCES_TIMEOUT_MS, probeCommand = defaultProbeCommand
143
+ } = {}) {
144
+ try {
145
+ if (platform !== "darwin") {
146
+ return envelope("unavailable", nowMs, ["system resources unsupported on this platform"]);
147
+ }
148
+ const diagnostics = [];
149
+ const deadlineMs = Date.now() + timeoutMs;
150
+ const memory = runProbe(probeMemory, probeCommand, deadlineMs);
151
+ const swap = runProbe(probeSwap, probeCommand, deadlineMs);
152
+ const disk = runProbe(probeDisk, probeCommand, deadlineMs);
153
+ const processes = runProbe(probeProcesses, probeCommand, deadlineMs);
154
+ for (const [label, result] of [["memory", memory], ["swap", swap], ["disk", disk], ["processes", processes]]) {
155
+ if (!result.ok) diagnostics.push(`system ${label} ${result.kind}`);
156
+ }
157
+ const anyOk = memory.ok || swap.ok || disk.ok || processes.ok;
158
+ if (!anyOk) {
159
+ return envelope("error", nowMs, diagnostics.length ? diagnostics : ["system resources probe failed"]);
160
+ }
161
+ const available = memory.ok && swap.ok && disk.ok && processes.ok;
162
+ return envelope(available ? "available" : "partial", nowMs, diagnostics, {
163
+ memory: memory.ok ? memory.value : null,
164
+ swap: swap.ok ? swap.value : null,
165
+ disk: disk.ok ? disk.value : null,
166
+ processes: processes.ok ? processes.value : emptyProcesses()
167
+ });
168
+ } catch {
169
+ return envelope("error", nowMs, ["system resources error"]);
170
+ }
171
+ }