@kal-elsam/kairo-runtime 0.15.0 → 0.17.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 (92) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/package.json +2 -1
  3. package/scripts/cockpit-smoke.mjs +1 -1
  4. package/scripts/ux-smoke-test.sh +3 -3
  5. package/src/cli.js +106 -11
  6. package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
  7. package/src/global/architect/architect-cli.js +76 -0
  8. package/src/global/architect/architect-codex.js +146 -0
  9. package/src/global/architect/architect-manager.js +125 -0
  10. package/src/global/architect/architect-store.js +377 -0
  11. package/src/global/architect/architect-types.js +47 -0
  12. package/src/global/cli-help.js +12 -1
  13. package/src/global/cockpit/app.js +475 -0
  14. package/src/global/cockpit/card.js +111 -0
  15. package/src/global/cockpit/cli.js +33 -0
  16. package/src/global/cockpit/gauge.js +31 -0
  17. package/src/global/cockpit/project-overlay.js +683 -0
  18. package/src/global/cockpit/rows.js +148 -0
  19. package/src/global/cockpit/theme.js +118 -0
  20. package/src/global/cockpit/view.js +1263 -0
  21. package/src/global/control-plane/attention.js +141 -0
  22. package/src/global/control-plane/build-report.js +146 -0
  23. package/src/global/control-plane/cli.js +36 -0
  24. package/src/global/control-plane/constants.js +38 -0
  25. package/src/global/control-plane/gentle-adapters.js +183 -0
  26. package/src/global/control-plane/provider.js +69 -0
  27. package/src/global/control-plane/review-status.js +115 -0
  28. package/src/global/control-plane/sdd-status.js +49 -0
  29. package/src/global/control-plane/team.js +63 -0
  30. package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
  31. package/src/global/conversation/cli.js +53 -0
  32. package/src/global/conversation/codex-sandbox.js +230 -0
  33. package/src/global/conversation/cursor-sandbox.js +215 -0
  34. package/src/global/conversation/project-analysis.js +204 -0
  35. package/src/global/conversation/project-profile.js +178 -0
  36. package/src/global/conversation/project-router.js +149 -0
  37. package/src/global/conversation/project-strategy-store.js +64 -0
  38. package/src/global/conversation/project-strategy.js +514 -0
  39. package/src/global/conversation/sanitized-snapshot.js +169 -0
  40. package/src/global/conversation/secret-scanner.js +71 -0
  41. package/src/global/conversation/service.js +1063 -0
  42. package/src/global/conversation/session-store.js +75 -0
  43. package/src/global/conversation/transcript-store.js +79 -0
  44. package/src/global/conversation/ui.js +195 -0
  45. package/src/global/intelligence/capability-scoring.js +480 -0
  46. package/src/global/intelligence/execution-router.js +444 -0
  47. package/src/global/intelligence/kairo-telemetry-source.js +59 -0
  48. package/src/global/intelligence/kairobench-runner.js +85 -0
  49. package/src/global/intelligence/kairobench-source.js +34 -0
  50. package/src/global/intelligence/kairobench-tasks.js +47 -0
  51. package/src/global/intelligence/model-candidate-catalog.js +456 -0
  52. package/src/global/intelligence/model-capability-registry-sources.js +145 -0
  53. package/src/global/intelligence/model-capability-registry.js +125 -0
  54. package/src/global/intelligence/model-intelligence.js +1646 -0
  55. package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
  56. package/src/global/intelligence/quick-ask.js +149 -0
  57. package/src/global/intelligence/role-profiles.js +251 -0
  58. package/src/global/intelligence/skill-catalog.js +67 -0
  59. package/src/global/intelligence/subscription-pressure-source.js +41 -0
  60. package/src/global/mcp/kairo-mcp.js +51 -18
  61. package/src/global/mcp/work-snapshot-rule.js +4 -2
  62. package/src/global/mcp/workspace-binding.js +88 -0
  63. package/src/global/mcp/workspace-mcp-entry.js +74 -0
  64. package/src/global/mcp-install.js +8 -1
  65. package/src/global/observability/artificial-analysis-models.js +118 -0
  66. package/src/global/observability/claude-models.js +31 -0
  67. package/src/global/observability/claude-usage.js +112 -0
  68. package/src/global/observability/codex-models.js +96 -0
  69. package/src/global/observability/codex-usage.js +160 -0
  70. package/src/global/observability/cursor-auth.js +88 -0
  71. package/src/global/observability/cursor-models.js +101 -0
  72. package/src/global/observability/gentle-probe.js +30 -2
  73. package/src/global/observability/huggingface-leaderboard.js +97 -0
  74. package/src/global/observability/index.js +2 -1
  75. package/src/global/observability/opencode-models.js +101 -0
  76. package/src/global/observability/opencode-usage.js +162 -0
  77. package/src/global/paths.js +49 -2
  78. package/src/global/profile.js +23 -1
  79. package/src/global/runtime/execution-adapters/claude.js +63 -30
  80. package/src/global/runtime/execution-adapters/codex.js +9 -2
  81. package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
  82. package/src/global/runtime/execution-adapters/opencode.js +83 -18
  83. package/src/global/runtime/execution-worktree-manager.js +924 -0
  84. package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
  85. package/src/global/runtime/execution-worktree-store.js +83 -0
  86. package/src/global/runtime/execution-worktree-types.js +45 -0
  87. package/src/global/runtime/run-events.js +38 -0
  88. package/src/global/runtime/run-manager.js +22 -6
  89. package/src/global/runtime/run-supervisor.js +41 -12
  90. package/src/global/runtime/usage-manager.js +96 -0
  91. package/src/global/runtime/usage-store.js +69 -0
  92. package/src/global/runtime/usage-types.js +62 -0
@@ -0,0 +1,96 @@
1
+ import { spawn as defaultSpawn } from "node:child_process";
2
+
3
+ // Real per-account model catalog via Codex's app-server JSON-RPC protocol
4
+ // (`model/list`, confirmed present in `codex app-server generate-json-schema`
5
+ // output and verified live) — never the static, possibly-stale documented
6
+ // list. Same fail-closed shape as codex-usage.js: any error yields
7
+ // `unknown`, never a fabricated model.
8
+ const DEFAULT_TIMEOUT_MS = 2500;
9
+ const SOURCE = "codex app-server model/list";
10
+
11
+ function unknown(error = null) {
12
+ return { status: "unknown", source: SOURCE, models: [], error: error ? String(error) : null };
13
+ }
14
+
15
+ function writeRequest(child, id, method, params) {
16
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
17
+ }
18
+
19
+ function normalizeModel(entry) {
20
+ return {
21
+ id: entry.id,
22
+ displayName: entry.displayName ?? entry.id,
23
+ isDefault: entry.isDefault === true,
24
+ hidden: entry.hidden === true
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Reads Codex's real, currently-available model catalog for this
30
+ * authenticated account — not a static documented list. Excludes hidden
31
+ * models by default (mirrors what Codex's own picker would offer).
32
+ */
33
+ export async function readCodexModels({
34
+ spawn = defaultSpawn,
35
+ cwd = process.cwd(),
36
+ env = process.env,
37
+ timeoutMs = DEFAULT_TIMEOUT_MS,
38
+ includeHidden = false
39
+ } = {}) {
40
+ let child;
41
+ try {
42
+ child = spawn("codex", ["app-server", "--listen", "stdio://"], {
43
+ cwd,
44
+ env,
45
+ stdio: ["pipe", "pipe", "pipe"]
46
+ });
47
+ } catch (error) {
48
+ return unknown(error?.message ?? error);
49
+ }
50
+
51
+ return new Promise((resolve) => {
52
+ let buffer = "";
53
+ let listRequestId = null;
54
+ let finished = false;
55
+ const timer = setTimeout(() => finish(unknown("codex app-server timeout")), timeoutMs);
56
+
57
+ function finish(result) {
58
+ if (finished) return;
59
+ finished = true;
60
+ clearTimeout(timer);
61
+ try { child.kill?.(); } catch { /* best effort */ }
62
+ resolve(result);
63
+ }
64
+
65
+ function onLine(line) {
66
+ let message;
67
+ try { message = JSON.parse(line); } catch { return finish(unknown("malformed codex app-server output")); }
68
+ if (!message || typeof message !== "object" || message.id == null) return;
69
+ if (message.error) return finish(unknown(`codex app-server error: ${message.error.message ?? "request failed"}`));
70
+ if (message.id === 1) {
71
+ listRequestId = 2;
72
+ writeRequest(child, listRequestId, "model/list", { includeHidden });
73
+ return;
74
+ }
75
+ if (message.id === listRequestId) {
76
+ const data = Array.isArray(message.result?.data) ? message.result.data : null;
77
+ if (!data) return finish(unknown("codex returned no model list"));
78
+ finish({ status: "measured", source: SOURCE, models: data.map(normalizeModel), error: null });
79
+ }
80
+ }
81
+
82
+ child.stdout?.on("data", (chunk) => {
83
+ buffer += String(chunk);
84
+ const lines = buffer.split(/\r?\n/);
85
+ buffer = lines.pop() ?? "";
86
+ for (const line of lines) if (line.trim()) onLine(line.trim());
87
+ });
88
+ child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
89
+ child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before model list")); });
90
+
91
+ writeRequest(child, 1, "initialize", {
92
+ clientInfo: { name: "kairo", title: "Kairo", version: "0.17.0" },
93
+ capabilities: {}
94
+ });
95
+ });
96
+ }
@@ -0,0 +1,160 @@
1
+ import { spawn as defaultSpawn } from "node:child_process";
2
+
3
+ const DEFAULT_TIMEOUT_MS = 2500;
4
+ const SOURCE = "codex app-server account/rateLimits/read";
5
+
6
+ function clampPercent(value) {
7
+ const number = Number(value);
8
+ if (!Number.isFinite(number)) return null;
9
+ return Math.max(0, Math.min(100, number));
10
+ }
11
+
12
+ function epochSecondsToIso(value) {
13
+ try {
14
+ const date = new Date(Number(value) * 1000);
15
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ function normalizeWindow(name, value) {
22
+ if (!value || typeof value !== "object") return null;
23
+ const usedPercent = clampPercent(value.usedPercent ?? value.used_percentage);
24
+ if (usedPercent == null) return null;
25
+ const windowDurationMins = Number(value.windowDurationMins ?? value.window_duration_mins);
26
+ const resetsAt = value.resetsAt ?? value.resets_at ?? null;
27
+ const numericReset = (typeof resetsAt === "number" && Number.isFinite(resetsAt))
28
+ || (typeof resetsAt === "string" && resetsAt.trim() !== "" && Number.isFinite(Number(resetsAt)));
29
+ const resetsAtIso = numericReset
30
+ ? epochSecondsToIso(resetsAt)
31
+ : (typeof resetsAt === "string" ? resetsAt : null);
32
+ return {
33
+ name,
34
+ usedPercent,
35
+ remainingPercent: 100 - usedPercent,
36
+ // Short aliases keep the normalized contract convenient for consumers
37
+ // while the explicit Percent fields make units unambiguous in the UI.
38
+ used: usedPercent,
39
+ remaining: 100 - usedPercent,
40
+ windowDurationMins: Number.isFinite(windowDurationMins) ? windowDurationMins : null,
41
+ // The app-server contract uses epoch seconds. Preserve that value and
42
+ // provide an ISO projection for human-facing clients.
43
+ resetsAt: numericReset ? Number(resetsAt) : (typeof resetsAt === "string" ? resetsAt : null),
44
+ resetsAtIso
45
+ };
46
+ }
47
+
48
+ /** Normalize only safe, displayable rate-limit fields. Never returns account ids/credits. */
49
+ export function normalizeCodexRateLimits(payload) {
50
+ const root = payload?.rateLimits ?? payload?.rate_limits ?? payload ?? {};
51
+ const primary = normalizeWindow("5h", root.primary);
52
+ const secondary = normalizeWindow("weekly", root.secondary);
53
+ // Current Codex returns this beside `rateLimits`; accept the older nested
54
+ // placement only as a compatibility fallback.
55
+ const ordinaryUsageAllowed = payload?.ordinaryUsageAllowed
56
+ ?? payload?.ordinary_usage_allowed
57
+ ?? root.ordinaryUsageAllowed
58
+ ?? root.ordinary_usage_allowed;
59
+ const allowed = typeof ordinaryUsageAllowed === "boolean" ? ordinaryUsageAllowed : null;
60
+ if (!primary && !secondary && allowed == null) return null;
61
+ return {
62
+ status: allowed === false ? "exhausted" : "measured",
63
+ source: SOURCE,
64
+ ordinaryUsageAllowed: allowed,
65
+ windows: [primary, secondary].filter(Boolean),
66
+ primary,
67
+ secondary
68
+ };
69
+ }
70
+
71
+ function unknown(error = null) {
72
+ return {
73
+ status: "unknown",
74
+ source: SOURCE,
75
+ ordinaryUsageAllowed: null,
76
+ windows: [],
77
+ primary: null,
78
+ secondary: null,
79
+ error: error ? String(error) : null
80
+ };
81
+ }
82
+
83
+ function writeRequest(child, id, method, params) {
84
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
85
+ }
86
+
87
+ /**
88
+ * Read Codex subscription rate limits without starting a model turn.
89
+ * This is deliberately fail-closed: malformed output, unavailable auth, or a
90
+ * timeout produces `unknown`, never a fabricated quota or a PAYG fallback.
91
+ */
92
+ export async function readCodexUsage({
93
+ spawn = defaultSpawn,
94
+ cwd = process.cwd(),
95
+ env = process.env,
96
+ timeoutMs = DEFAULT_TIMEOUT_MS
97
+ } = {}) {
98
+ let child;
99
+ try {
100
+ child = spawn("codex", ["app-server", "--listen", "stdio://"], {
101
+ cwd,
102
+ env,
103
+ stdio: ["pipe", "pipe", "pipe"]
104
+ });
105
+ } catch (error) {
106
+ return unknown(error?.message ?? error);
107
+ }
108
+
109
+ return new Promise((resolve) => {
110
+ let buffer = "";
111
+ let nextId = 2;
112
+ let rateRequestId = null;
113
+ let finished = false;
114
+ const timer = setTimeout(() => finish(unknown("codex app-server timeout")), timeoutMs);
115
+
116
+ function finish(result) {
117
+ if (finished) return;
118
+ finished = true;
119
+ clearTimeout(timer);
120
+ try { child.kill?.(); } catch { /* best effort */ }
121
+ resolve(result);
122
+ }
123
+
124
+ function onLine(line) {
125
+ let message;
126
+ try { message = JSON.parse(line); } catch { return finish(unknown("malformed codex app-server output")); }
127
+ if (!message || typeof message !== "object") return;
128
+ // Notifications have no id and must not affect the request state.
129
+ if (message.id == null) return;
130
+ if (message.error) return finish(unknown(`codex app-server error: ${message.error.message ?? "request failed"}`));
131
+ if (message.id === 1) {
132
+ rateRequestId = nextId++;
133
+ writeRequest(child, rateRequestId, "account/rateLimits/read", {
134
+ excludeResetCreditDetails: true,
135
+ supportsLunaReserve: false
136
+ });
137
+ return;
138
+ }
139
+ if (message.id === rateRequestId) {
140
+ finish(normalizeCodexRateLimits(message.result) ?? unknown("codex returned unusable rate limits"));
141
+ }
142
+ }
143
+
144
+ child.stdout?.on("data", (chunk) => {
145
+ buffer += String(chunk);
146
+ const lines = buffer.split(/\r?\n/);
147
+ buffer = lines.pop() ?? "";
148
+ for (const line of lines) if (line.trim()) onLine(line.trim());
149
+ });
150
+ child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
151
+ child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before rate limits")); });
152
+
153
+ writeRequest(child, 1, "initialize", {
154
+ clientInfo: { name: "kairo", title: "Kairo", version: "0.17.0" },
155
+ capabilities: {}
156
+ });
157
+ });
158
+ }
159
+
160
+ export const CODEX_USAGE_SOURCE = SOURCE;
@@ -0,0 +1,88 @@
1
+ import { spawn as defaultSpawn } from "node:child_process";
2
+ import { tmpdir } from "node:os";
3
+
4
+ // `cursor-agent status`/`whoami` do NOT reliably reflect whether a real
5
+ // invocation will actually work — verified empirically: on this machine
6
+ // `status` reports "Logged in" (exit 0) while a real `cursor-agent -p`
7
+ // call fails outright with "Authentication required. Please run 'agent
8
+ // login' first, or set CURSOR_API_KEY environment variable." `models`
9
+ // behaves the same way (a clean "No models available for this account."
10
+ // even while the session is actually unusable for real invocation).
11
+ //
12
+ // So the only decisive signal for "can Cursor actually be invoked right
13
+ // now" is a real invocation attempt. This probe uses the cheapest one
14
+ // available: a harmless -p call whose failure mode, when unauthenticated,
15
+ // is a local, near-instant CLI rejection (no network round trip) — it
16
+ // has NOT been confirmed whether a genuinely authenticated probe call
17
+ // itself consumes real usage, so this should only be called where a real
18
+ // eligibility decision is actually needed, not on a hot path.
19
+ //
20
+ // --trust is required here too (verified empirically — the probe's cwd,
21
+ // an OS tmpdir cursor-agent has never seen, otherwise triggers an
22
+ // interactive "Workspace Trust Required" prompt that blocks non-
23
+ // interactive use, exactly like a real Bootstrap Analysis snapshot dir
24
+ // would without it).
25
+ // A real -p call to a real model has been observed taking just over
26
+ // 20s — verified empirically, not assumed — so this needs real margin,
27
+ // not a tight bound.
28
+ const DEFAULT_TIMEOUT_MS = 45_000;
29
+ const SOURCE = "cursor-agent -p (probe)";
30
+ const AUTH_REQUIRED_PATTERN = /authentication required/i;
31
+
32
+ function unknown(error = null) {
33
+ return { authenticated: false, status: "unknown", source: SOURCE, reason: error ? String(error) : null };
34
+ }
35
+
36
+ /**
37
+ * Real, decisive probe for whether Cursor can actually be invoked right
38
+ * now — NOT whether `status`/`whoami` merely claim it can.
39
+ * @returns {Promise<{authenticated: boolean, status: "measured"|"unknown", source: string, reason: string|null}>}
40
+ */
41
+ export async function probeCursorAuth({
42
+ spawn = defaultSpawn,
43
+ cwd = tmpdir(),
44
+ env = process.env,
45
+ timeoutMs = DEFAULT_TIMEOUT_MS
46
+ } = {}) {
47
+ const args = [
48
+ "-p", "Reply with the single word: ok", "--output-format", "json",
49
+ "--mode", "ask", "--sandbox", "enabled", "--workspace", cwd, "--trust"
50
+ ];
51
+ let child;
52
+ try {
53
+ child = spawn("cursor-agent", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
54
+ } catch (error) {
55
+ return unknown(error?.message ?? error);
56
+ }
57
+
58
+ return new Promise((resolve) => {
59
+ let stdout = "";
60
+ let stderr = "";
61
+ let finished = false;
62
+ const timer = setTimeout(() => finish(unknown("cursor-agent -p probe timed out")), timeoutMs);
63
+
64
+ function finish(result) {
65
+ if (finished) return;
66
+ finished = true;
67
+ clearTimeout(timer);
68
+ try { child.kill?.(); } catch { /* best effort */ }
69
+ resolve(result);
70
+ }
71
+
72
+ child.stdout?.on("data", (chunk) => { stdout += chunk; });
73
+ child.stderr?.on("data", (chunk) => { stderr += chunk; });
74
+ child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
75
+ child.once?.("close", (code, signal) => {
76
+ const combined = `${stdout}\n${stderr}`;
77
+ if (AUTH_REQUIRED_PATTERN.test(combined)) {
78
+ return finish({
79
+ authenticated: false, status: "measured", source: SOURCE,
80
+ reason: "cursor-agent reports \"Authentication required\" — the CLI's session/keychain is not actually usable for a real invocation, regardless of what `status`/`models` claim."
81
+ });
82
+ }
83
+ if (signal) return finish(unknown(`cursor-agent -p probe was killed by signal ${signal}${stderr ? `: ${stderr.trim()}` : ""}`));
84
+ if (code !== 0) return finish(unknown(`cursor-agent -p probe exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
85
+ finish({ authenticated: true, status: "measured", source: SOURCE, reason: null });
86
+ });
87
+ });
88
+ }
@@ -0,0 +1,101 @@
1
+ import { spawn as defaultSpawn } from "node:child_process";
2
+
3
+ // Real per-account model list via `cursor-agent models`. The populated
4
+ // shape below was captured from the real CLI against a real authenticated
5
+ // account with real models enabled (one `<id> - <Display Name>` entry per
6
+ // line, plus a leading "Available models" header and a trailing "Tip: use
7
+ // --model <id>..." line) — not assumed, not the earlier placeholder
8
+ // parser that predated ever seeing a real populated catalog.
9
+ //
10
+ // A process crash (killed by a signal) or a non-zero exit is never
11
+ // reinterpreted as a clean "no models" answer — an empty/partial stdout
12
+ // from a crash mid-run parses identically to a genuine empty catalog, so
13
+ // exit status is the only real signal that tells them apart. Only a clean
14
+ // exit (code 0, no signal) is trusted; anything else yields `status:
15
+ // "unknown"` with the real stderr, never a fabricated empty catalog.
16
+ const DEFAULT_TIMEOUT_MS = 8_000;
17
+ const SOURCE = "cursor-agent models";
18
+ const ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]/g;
19
+ const NO_MODELS_SENTINEL = /no models available/i;
20
+ const LOADING_LINE = /^loading models/i;
21
+ // Real model line shape: "<id> - <Display Name>", id has no spaces (every
22
+ // real id observed is a bare slug like "gpt-5.3-codex-low" or "auto").
23
+ // Header ("Available models") and the trailing "Tip: ..." line never
24
+ // match this — no line-anchored " - " separator — so they're dropped
25
+ // without needing to special-case their exact text.
26
+ const MODEL_LINE_PATTERN = /^(\S+)\s-\s(.+)$/;
27
+
28
+ function unknown(error = null) {
29
+ return { status: "unknown", source: SOURCE, models: [], error: error ? String(error) : null };
30
+ }
31
+
32
+ /**
33
+ * Strips the spinner's ANSI control codes and non-model lines (loading
34
+ * status, the "Available models" header, the trailing "Tip: ..." line),
35
+ * returning either an empty array (the account has no models — a real,
36
+ * confirmed answer) or the real `{id, displayName}` models — the same
37
+ * shape codex-models.js/claude-models.js already use, so callers never
38
+ * need to branch on catalog source.
39
+ */
40
+ export function parseCursorModelsOutput(raw) {
41
+ const clean = String(raw ?? "").replace(ANSI_PATTERN, "");
42
+ const lines = clean.split("\n").map((line) => line.trim()).filter(Boolean).filter((line) => !LOADING_LINE.test(line));
43
+ if (lines.some((line) => NO_MODELS_SENTINEL.test(line))) return [];
44
+ const models = [];
45
+ for (const line of lines) {
46
+ const match = line.match(MODEL_LINE_PATTERN);
47
+ if (match) models.push({ id: match[1], displayName: match[2].trim() });
48
+ }
49
+ return models;
50
+ }
51
+
52
+ /**
53
+ * Reads Cursor Agent's real, currently-available model list for this
54
+ * account. `status: "measured"` with an empty `models` array is itself a
55
+ * real answer (this account has none) — distinct from `"unknown"`, which
56
+ * means the read itself failed.
57
+ */
58
+ export async function readCursorModels({
59
+ spawn = defaultSpawn,
60
+ cwd = process.cwd(),
61
+ env = process.env,
62
+ timeoutMs = DEFAULT_TIMEOUT_MS
63
+ } = {}) {
64
+ let child;
65
+ try {
66
+ child = spawn("cursor-agent", ["models"], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
67
+ } catch (error) {
68
+ return unknown(error?.message ?? error);
69
+ }
70
+
71
+ return new Promise((resolve) => {
72
+ let stdout = "";
73
+ let stderr = "";
74
+ let finished = false;
75
+ const timer = setTimeout(() => finish(unknown("cursor-agent models timed out")), timeoutMs);
76
+
77
+ function finish(result) {
78
+ if (finished) return;
79
+ finished = true;
80
+ clearTimeout(timer);
81
+ try { child.kill?.(); } catch { /* best effort */ }
82
+ resolve(result);
83
+ }
84
+
85
+ child.stdout?.on("data", (chunk) => { stdout += chunk; });
86
+ child.stderr?.on("data", (chunk) => { stderr += chunk; });
87
+ child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
88
+ child.once?.("close", (code, signal) => {
89
+ // A crash (killed by a signal, e.g. a real SIGSEGV) or a non-zero
90
+ // exit must never be silently reinterpreted as a clean, real
91
+ // "no models" answer — an empty/partial stdout from a crash mid-run
92
+ // parses identically to a genuine empty catalog, so exit status is
93
+ // the only real signal that tells them apart. Only a clean exit
94
+ // (code 0, no signal) is trusted to mean the CLI actually finished
95
+ // and its stdout is a real, complete answer.
96
+ if (signal) return finish(unknown(`cursor-agent models was killed by signal ${signal}${stderr ? `: ${stderr.trim()}` : ""}`));
97
+ if (code !== 0) return finish(unknown(`cursor-agent models exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
98
+ finish({ status: "measured", source: SOURCE, models: parseCursorModelsOutput(stdout), error: null });
99
+ });
100
+ });
101
+ }
@@ -7,8 +7,15 @@ import {
7
7
  import { normalizeProbeResult } from "./probe-contract.js";
8
8
 
9
9
  export const SUPPORTED_PROTOCOL = Object.freeze({ major: 2, minor: 0 });
10
+ export const SUPPORTED_PROTOCOL_MINORS = Object.freeze([0, 1]);
10
11
  export const SUPPORTED_SCHEMA = "gentle-ai.review-integration.capabilities/v2";
12
+ export const SUPPORTED_SCHEMA_V21 = "gentle-ai.review-integration.capabilities/v2.1";
13
+ export const SUPPORTED_CAPABILITY_SCHEMAS = Object.freeze([
14
+ SUPPORTED_SCHEMA,
15
+ SUPPORTED_SCHEMA_V21
16
+ ]);
11
17
  export const SUPPORTED_CONTRACT = "gentle-ai.review-integration/v2";
18
+ export const ADDITIVE_MINOR_POLICY = "optional-fields-only";
12
19
  export const SUPPORTED_MANDATORY_FEATURES = Object.freeze([
13
20
  "compact_v2_authority", "exact_receipt_replay", "five_delivery_gates",
14
21
  "immutable_snapshot", "legacy_v1_target_scoped_read_only",
@@ -51,7 +58,7 @@ export function evaluateGentleCapabilities(payload) {
51
58
  diagnostics: ["Capabilities payload is not an object."]
52
59
  });
53
60
  }
54
- if (payload.schema !== SUPPORTED_SCHEMA) {
61
+ if (!SUPPORTED_CAPABILITY_SCHEMAS.includes(payload.schema)) {
55
62
  diagnostics.push(`schema mismatch: got ${String(payload.schema)}`);
56
63
  }
57
64
  if (payload.contract !== SUPPORTED_CONTRACT) {
@@ -60,9 +67,30 @@ export function evaluateGentleCapabilities(payload) {
60
67
  if (payload.protocol?.major !== SUPPORTED_PROTOCOL.major) {
61
68
  diagnostics.push(`protocol.major mismatch: got ${String(payload.protocol?.major)}`);
62
69
  }
63
- if (payload.protocol?.minor !== SUPPORTED_PROTOCOL.minor) {
70
+ if (!SUPPORTED_PROTOCOL_MINORS.includes(payload.protocol?.minor)) {
64
71
  diagnostics.push(`protocol.minor mismatch: got ${String(payload.protocol?.minor)}`);
65
72
  }
73
+ const additivePolicy = payload.compatibility?.additive_minor_policy;
74
+ if (additivePolicy != null && additivePolicy !== ADDITIVE_MINOR_POLICY) {
75
+ diagnostics.push(`additive_minor_policy mismatch: got ${String(additivePolicy)}`);
76
+ }
77
+ if (typeof payload.bootstrap?.command === "string" && payload.bootstrap.command) {
78
+ evidence.push({
79
+ kind: "bootstrap",
80
+ command: payload.bootstrap.command,
81
+ required_feature: payload.bootstrap.required_feature ?? null
82
+ });
83
+ }
84
+ const requiredFeature = payload.bootstrap?.required_feature;
85
+ if (typeof requiredFeature === "string" && requiredFeature) {
86
+ const named = [
87
+ ...(Array.isArray(payload.features?.mandatory) ? payload.features.mandatory : []),
88
+ ...(Array.isArray(payload.features?.optional) ? payload.features.optional : [])
89
+ ];
90
+ if (!named.some((feature) => feature?.name === requiredFeature && feature.supported === true)) {
91
+ diagnostics.push(`bootstrap required_feature not supported: ${requiredFeature}`);
92
+ }
93
+ }
66
94
  const mandatory = payload.features?.mandatory;
67
95
  if (!Array.isArray(mandatory)) {
68
96
  diagnostics.push("features.mandatory must be an array.");
@@ -0,0 +1,97 @@
1
+ // Real per-benchmark leaderboard data from Hugging Face's official Datasets
2
+ // API — verified live against https://huggingface.co/api/datasets/{id}/leaderboard
3
+ // (public, no API key needed for a public benchmark dataset). One dataset
4
+ // covers exactly one benchmark (e.g. cais/hle for HLE, Idavidrein/gpqa for
5
+ // GPQA); there is no cross-benchmark aggregate endpoint here. Fails closed
6
+ // like every other observability probe: a failed fetch never fabricates a
7
+ // result, it falls back to the last successfully cached snapshot (marked
8
+ // "cached", with its real age) or "unknown" if there's no cache either.
9
+
10
+ import { mkdir, readFile } from "node:fs/promises";
11
+ import { dirname } from "node:path";
12
+ import { harnessHomePaths } from "../paths.js";
13
+ import { writeAtomicJson } from "../runtime/write-atomic-json.js";
14
+
15
+ export const SOURCE = "huggingface datasets api (leaderboard)";
16
+ const DEFAULT_TIMEOUT_MS = 8000;
17
+
18
+ function apiUrl(datasetId) {
19
+ return `https://huggingface.co/api/datasets/${datasetId}/leaderboard`;
20
+ }
21
+
22
+ function normalizeEntry(entry) {
23
+ return {
24
+ modelId: entry.modelId ?? null,
25
+ value: typeof entry.value === "number" ? entry.value : null,
26
+ rank: entry.rank ?? null,
27
+ verified: entry.verified === true,
28
+ notes: entry.notes ?? null
29
+ };
30
+ }
31
+
32
+ function ageLabel(fetchedAtIso) {
33
+ const fetchedAt = new Date(fetchedAtIso ?? "").getTime();
34
+ if (!Number.isFinite(fetchedAt)) return null;
35
+ const hours = (Date.now() - fetchedAt) / 3_600_000;
36
+ if (hours < 1) return "<1h";
37
+ if (hours < 48) return `${Math.round(hours)}h`;
38
+ return `${Math.round(hours / 24)}d`;
39
+ }
40
+
41
+ async function readAllCaches(homeDir, deps) {
42
+ const read = deps.readFile ?? readFile;
43
+ try {
44
+ const raw = await read(harnessHomePaths(homeDir).huggingfaceLeaderboardPath, "utf8");
45
+ const doc = JSON.parse(raw);
46
+ return doc && typeof doc === "object" ? doc : {};
47
+ } catch {
48
+ return {};
49
+ }
50
+ }
51
+
52
+ async function writeCache(homeDir, datasetId, snapshot, deps) {
53
+ const mkdirImpl = deps.mkdir ?? mkdir;
54
+ const writeJson = deps.writeAtomicJson ?? writeAtomicJson;
55
+ const path = harnessHomePaths(homeDir).huggingfaceLeaderboardPath;
56
+ const all = await readAllCaches(homeDir, deps);
57
+ all[datasetId] = snapshot;
58
+ await mkdirImpl(dirname(path), { recursive: true });
59
+ await writeJson(path, all);
60
+ }
61
+
62
+ function fromCache(cache, error) {
63
+ if (!cache || !Array.isArray(cache.entries) || typeof cache.fetchedAt !== "string") {
64
+ return { status: "unknown", source: SOURCE, fetchedAt: null, age: null, entries: [], error };
65
+ }
66
+ return { status: "cached", source: SOURCE, fetchedAt: cache.fetchedAt, age: ageLabel(cache.fetchedAt), entries: cache.entries, error };
67
+ }
68
+
69
+ /**
70
+ * @param {object} options
71
+ * @param {string} options.datasetId - e.g. "cais/hle", "SWE-bench/SWE-bench_Verified", "Idavidrein/gpqa"
72
+ * @param {string} options.homeDir - required; the cache lives under this harness home
73
+ * @param {typeof fetch} [options.fetchImpl]
74
+ * @param {number} [options.timeoutMs]
75
+ */
76
+ export async function readHuggingFaceLeaderboard({
77
+ datasetId, homeDir, fetchImpl = fetch, timeoutMs = DEFAULT_TIMEOUT_MS, ...deps
78
+ } = {}) {
79
+ if (!datasetId) return { status: "unknown", source: SOURCE, fetchedAt: null, age: null, entries: [], error: "datasetId is required" };
80
+
81
+ const controller = new AbortController();
82
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
83
+ try {
84
+ const response = await fetchImpl(apiUrl(datasetId), { signal: controller.signal });
85
+ if (!response.ok) throw new Error(`huggingface leaderboard api returned ${response.status}`);
86
+ const payload = await response.json();
87
+ const entries = Array.isArray(payload) ? payload.map(normalizeEntry) : [];
88
+ const fetchedAt = new Date().toISOString();
89
+ await writeCache(homeDir, datasetId, { fetchedAt, entries }, deps).catch(() => {});
90
+ return { status: "live", source: SOURCE, fetchedAt, age: "<1h", entries, error: null };
91
+ } catch (error) {
92
+ const all = await readAllCaches(homeDir, deps);
93
+ return fromCache(all[datasetId], error?.message ?? String(error));
94
+ } finally {
95
+ clearTimeout(timer);
96
+ }
97
+ }
@@ -24,7 +24,8 @@ export {
24
24
  runPassiveObservabilitySnapshot
25
25
  } from "./passive-snapshot-flight.js";
26
26
  export {
27
- SUPPORTED_PROTOCOL, SUPPORTED_SCHEMA, SUPPORTED_CONTRACT,
27
+ SUPPORTED_PROTOCOL, SUPPORTED_PROTOCOL_MINORS, SUPPORTED_SCHEMA, SUPPORTED_SCHEMA_V21,
28
+ SUPPORTED_CAPABILITY_SCHEMAS, SUPPORTED_CONTRACT, ADDITIVE_MINOR_POLICY,
28
29
  SUPPORTED_MANDATORY_FEATURES, evaluateGentleCapabilities, probeGentle, createGentleProbe,
29
30
  resolveGentleBinaryPath
30
31
  } from "./gentle-probe.js";