@kal-elsam/kairo-runtime 0.16.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 (81) hide show
  1. package/CHANGELOG.md +50 -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 +96 -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 +10 -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/conversation/bootstrap-analyzer-adapters.js +251 -0
  22. package/src/global/conversation/cli.js +53 -0
  23. package/src/global/conversation/codex-sandbox.js +230 -0
  24. package/src/global/conversation/cursor-sandbox.js +215 -0
  25. package/src/global/conversation/project-analysis.js +204 -0
  26. package/src/global/conversation/project-profile.js +178 -0
  27. package/src/global/conversation/project-router.js +149 -0
  28. package/src/global/conversation/project-strategy-store.js +64 -0
  29. package/src/global/conversation/project-strategy.js +514 -0
  30. package/src/global/conversation/sanitized-snapshot.js +169 -0
  31. package/src/global/conversation/secret-scanner.js +71 -0
  32. package/src/global/conversation/service.js +1063 -0
  33. package/src/global/conversation/session-store.js +75 -0
  34. package/src/global/conversation/transcript-store.js +79 -0
  35. package/src/global/conversation/ui.js +195 -0
  36. package/src/global/intelligence/capability-scoring.js +480 -0
  37. package/src/global/intelligence/execution-router.js +444 -0
  38. package/src/global/intelligence/kairo-telemetry-source.js +59 -0
  39. package/src/global/intelligence/kairobench-runner.js +85 -0
  40. package/src/global/intelligence/kairobench-source.js +34 -0
  41. package/src/global/intelligence/kairobench-tasks.js +47 -0
  42. package/src/global/intelligence/model-candidate-catalog.js +456 -0
  43. package/src/global/intelligence/model-capability-registry-sources.js +145 -0
  44. package/src/global/intelligence/model-capability-registry.js +125 -0
  45. package/src/global/intelligence/model-intelligence.js +1646 -0
  46. package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
  47. package/src/global/intelligence/quick-ask.js +149 -0
  48. package/src/global/intelligence/role-profiles.js +251 -0
  49. package/src/global/intelligence/skill-catalog.js +67 -0
  50. package/src/global/intelligence/subscription-pressure-source.js +41 -0
  51. package/src/global/mcp/kairo-mcp.js +51 -18
  52. package/src/global/mcp/work-snapshot-rule.js +4 -2
  53. package/src/global/mcp/workspace-binding.js +88 -0
  54. package/src/global/mcp/workspace-mcp-entry.js +74 -0
  55. package/src/global/mcp-install.js +8 -1
  56. package/src/global/observability/artificial-analysis-models.js +118 -0
  57. package/src/global/observability/claude-models.js +31 -0
  58. package/src/global/observability/claude-usage.js +112 -0
  59. package/src/global/observability/codex-models.js +96 -0
  60. package/src/global/observability/codex-usage.js +160 -0
  61. package/src/global/observability/cursor-auth.js +88 -0
  62. package/src/global/observability/cursor-models.js +101 -0
  63. package/src/global/observability/huggingface-leaderboard.js +97 -0
  64. package/src/global/observability/opencode-models.js +101 -0
  65. package/src/global/observability/opencode-usage.js +162 -0
  66. package/src/global/paths.js +49 -2
  67. package/src/global/profile.js +23 -1
  68. package/src/global/runtime/execution-adapters/claude.js +63 -30
  69. package/src/global/runtime/execution-adapters/codex.js +9 -2
  70. package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
  71. package/src/global/runtime/execution-adapters/opencode.js +83 -18
  72. package/src/global/runtime/execution-worktree-manager.js +924 -0
  73. package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
  74. package/src/global/runtime/execution-worktree-store.js +83 -0
  75. package/src/global/runtime/execution-worktree-types.js +45 -0
  76. package/src/global/runtime/run-events.js +38 -0
  77. package/src/global/runtime/run-manager.js +22 -6
  78. package/src/global/runtime/run-supervisor.js +41 -12
  79. package/src/global/runtime/usage-manager.js +96 -0
  80. package/src/global/runtime/usage-store.js +69 -0
  81. package/src/global/runtime/usage-types.js +62 -0
@@ -0,0 +1,162 @@
1
+ // Manufacturer-published benchmark snapshots — deliberately NOT a live API
2
+ // (OpenAI/Anthropic don't expose one; their comparison tables are static
3
+ // blog posts). Hand-curated and versioned instead, refreshed only when a
4
+ // vendor publishes a new one. This is the source that finally gives Codex
5
+ // and Claude real cross-vendor evidence — Hugging Face's leaderboards have
6
+ // zero coverage of either (verified separately; see
7
+ // model-capability-registry-sources.js's HF comment), since neither is
8
+ // hosted on the HF Hub.
9
+ //
10
+ // Hard rule, per explicit decision: manufacturer-reported is never treated
11
+ // as independent. Every entry here is `verified: false` in the registry —
12
+ // OpenAI grading its own model against a competitor's public score is real
13
+ // data worth keeping, but it is not the same epistemic weight as an
14
+ // independently-run, third-party eval. Provenance (source, published date,
15
+ // exact benchmark version, and the vendor's own methodology caveat) is
16
+ // kept alongside every number specifically so this distinction survives
17
+ // into anything built on top of it.
18
+
19
+ /**
20
+ * Each entry is one benchmark's real, cross-vendor comparison table as
21
+ * published by ONE vendor at ONE point in time. `caveat` is the vendor's
22
+ * own stated methodology note — never omitted, since it changes how much
23
+ * weight the number deserves (e.g. "safeguards can zero out a task").
24
+ * Values below were verified directly (Anthropic's page, fetched live) or
25
+ * cross-checked against multiple independent third-party sources (OpenAI's
26
+ * page blocks direct fetches; corroborated via Vellum, llm-stats, and
27
+ * Artificial Analysis's own coverage of the same launch) — not copied
28
+ * from a single unverified paste.
29
+ */
30
+ export const OFFICIAL_BENCHMARK_SNAPSHOTS = [
31
+ {
32
+ source: "openai-official",
33
+ url: "https://openai.com/index/gpt-6-astra/",
34
+ published: "2026-09-03",
35
+ benchmark: "terminal-bench", benchmarkVersion: "4.0",
36
+ // Corrected from an earlier 57.7 (a transcription error introduced by
37
+ // paraphrasing a search summary instead of the source) after
38
+ // cross-checking multiple independent citations of OpenAI's own
39
+ // launch page. Other real numbers exist for other configs — Astra at
40
+ // "xhigh"/"max" reasoning effort, and Artificial Analysis's own
41
+ // independently-measured snapshot — but those are different real
42
+ // measurements, not this one; they belong in their own entries if
43
+ // ever added, never blended into this vendor's reported baseline.
44
+ caveat: "OpenAI's own reported results, run at maximum reasoning effort in an environment that may differ from production.",
45
+ scores: [
46
+ { adapterId: "codex", modelId: "gpt-6-astra", value: 57.9 },
47
+ { adapterId: "codex", modelId: "gpt-5.6-sol", value: 37.3 },
48
+ { adapterId: "claude", modelId: "claude-fable-5-1", value: 55.8 }
49
+ ]
50
+ },
51
+ {
52
+ source: "openai-official",
53
+ url: "https://openai.com/index/gpt-6-astra/",
54
+ published: "2026-09-03",
55
+ // Kept as "gpqa-diamond", deliberately distinct from AA's own "gpqa"
56
+ // metric: same underlying benchmark, but AA reports it as a 0-1
57
+ // fraction while this manufacturer table reports a 0-100 score —
58
+ // merging them under one key would let bestEvidence silently pick
59
+ // whichever is more recent and display it without a scale, which
60
+ // would misrepresent the other source's real number.
61
+ benchmark: "gpqa-diamond", benchmarkVersion: null,
62
+ caveat: "OpenAI's own reported results, run at maximum reasoning effort in an environment that may differ from production.",
63
+ scores: [
64
+ { adapterId: "codex", modelId: "gpt-6-astra", value: 96.0 },
65
+ { adapterId: "codex", modelId: "gpt-5.6-sol", value: 94.6 }
66
+ ]
67
+ },
68
+ {
69
+ source: "anthropic-official",
70
+ url: "https://www.anthropic.com/claude-fable-and-mythos-5-1",
71
+ published: "2026-09-03",
72
+ benchmark: "terminal-bench", benchmarkVersion: "4.0",
73
+ caveat: "Fable 5.1 was evaluated with its production safeguards enabled; on tasks where safeguards intervened, it scored zero, which can understate its real capability relative to models evaluated without that constraint.",
74
+ scores: [
75
+ { adapterId: "claude", modelId: "claude-fable-5-1", value: 55.8 },
76
+ { adapterId: "claude", modelId: "claude-opus-5", value: 52.3 },
77
+ { adapterId: "codex", modelId: "gpt-5.6-sol", value: 37.3 }
78
+ ]
79
+ },
80
+ {
81
+ source: "anthropic-official",
82
+ url: "https://www.anthropic.com/claude-fable-and-mythos-5-1",
83
+ published: "2026-09-03",
84
+ benchmark: "terminal-bench-science", benchmarkVersion: "0.1",
85
+ caveat: "Fable 5.1 was evaluated with its production safeguards enabled; on tasks where safeguards intervened, it scored zero.",
86
+ scores: [
87
+ { adapterId: "claude", modelId: "claude-fable-5-1", value: 52.6 },
88
+ { adapterId: "claude", modelId: "claude-opus-5", value: 29.0 },
89
+ { adapterId: "codex", modelId: "gpt-5.6-sol", value: 22.4 }
90
+ ]
91
+ },
92
+ {
93
+ source: "anthropic-official",
94
+ url: "https://www.anthropic.com/claude-fable-and-mythos-5-1",
95
+ published: "2026-09-03",
96
+ benchmark: "cursorbench", benchmarkVersion: "3.2.0",
97
+ caveat: "Anthropic's own reported results; real statistical variance applies, not stated per-model on this page.",
98
+ scores: [
99
+ { adapterId: "claude", modelId: "claude-fable-5-1", value: 73.4 },
100
+ { adapterId: "claude", modelId: "claude-opus-5", value: 70.0 },
101
+ { adapterId: "codex", modelId: "gpt-5.6-sol", value: 67.2 }
102
+ ]
103
+ }
104
+ ];
105
+
106
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
107
+
108
+ /**
109
+ * Structural integrity check for hand-curated snapshot data — this can't
110
+ * verify a number is *correct* against the live source (nothing here can
111
+ * re-fetch OpenAI's blocked page), but it can catch the failure modes that
112
+ * actually happened while authoring this file: a missing citation, a
113
+ * malformed date, an unreferenced adapter/model, or an accidental exact
114
+ * duplicate row within the same vendor snapshot. Throws on the first
115
+ * violation — fail loud, never ingest a malformed entry silently.
116
+ * @param {Array<object>} snapshots
117
+ */
118
+ export function validateSnapshotIntegrity(snapshots) {
119
+ const seen = new Set();
120
+ snapshots.forEach((snapshot, index) => {
121
+ const where = `snapshot[${index}] (${snapshot.source ?? "?"}/${snapshot.benchmark ?? "?"})`;
122
+ if (!snapshot.source) throw new Error(`${where}: missing source`);
123
+ if (!/^https:\/\//.test(snapshot.url ?? "")) throw new Error(`${where}: url must be a real https link, got ${snapshot.url}`);
124
+ if (!DATE_PATTERN.test(snapshot.published ?? "")) throw new Error(`${where}: published must be an ISO date (YYYY-MM-DD), got ${snapshot.published}`);
125
+ if (!snapshot.benchmark) throw new Error(`${where}: missing benchmark`);
126
+ if (!snapshot.caveat) throw new Error(`${where}: missing the vendor's own methodology caveat`);
127
+ if (!Array.isArray(snapshot.scores) || !snapshot.scores.length) throw new Error(`${where}: scores must be a non-empty array`);
128
+ for (const score of snapshot.scores) {
129
+ if (!score.adapterId || !score.modelId) throw new Error(`${where}: every score needs adapterId and modelId, got ${JSON.stringify(score)}`);
130
+ if (score.value != null && !Number.isFinite(score.value)) throw new Error(`${where}: score.value must be a finite number or null, got ${score.value}`);
131
+ const key = `${snapshot.source}|${snapshot.benchmark}|${snapshot.benchmarkVersion}|${score.adapterId}|${score.modelId}`;
132
+ if (seen.has(key)) throw new Error(`${where}: duplicate row for ${score.adapterId}/${score.modelId} — same vendor reporting the same benchmark/model twice is almost certainly a copy-paste mistake`);
133
+ seen.add(key);
134
+ }
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Registers every model named in a snapshot and records its real reported
140
+ * score as evidence — always `verified: false` (manufacturer-reported is
141
+ * never independent, regardless of which vendor published it), with the
142
+ * vendor's own caveat preserved as `modelConfig` so it isn't lost.
143
+ * Validates snapshot integrity first (see validateSnapshotIntegrity) —
144
+ * never ingests a structurally malformed entry.
145
+ * @param {ReturnType<import("./model-capability-registry.js").createCapabilityRegistry>} registry
146
+ * @param {Array<object>} [snapshots] - defaults to OFFICIAL_BENCHMARK_SNAPSHOTS
147
+ */
148
+ export function ingestOfficialSnapshotEvidence(registry, snapshots = OFFICIAL_BENCHMARK_SNAPSHOTS) {
149
+ validateSnapshotIntegrity(snapshots);
150
+ for (const snapshot of snapshots) {
151
+ for (const { adapterId, modelId, value } of snapshot.scores) {
152
+ if (value == null) continue;
153
+ const id = registry.registerIdentity(adapterId, modelId);
154
+ registry.addEvidence(id, {
155
+ metric: snapshot.benchmark, value, source: snapshot.source,
156
+ benchmarkVersion: snapshot.benchmarkVersion, modelConfig: snapshot.caveat,
157
+ date: snapshot.published, verified: false
158
+ });
159
+ }
160
+ }
161
+ return registry;
162
+ }
@@ -0,0 +1,149 @@
1
+ import { spawn as defaultSpawn } from "node:child_process";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { buildClaudeExecutionEnv } from "../runtime/execution-adapters/claude.js";
6
+
7
+ // A real, read-only question -> answer call — no task, no plan, no
8
+ // approval gate. This spends real provider usage (unlike the zero-cost
9
+ // /usage local_command probes), which is expected: answering a real
10
+ // question is real work. Fail-closed: any spawn/parse error yields
11
+ // `status: "error"`, never a fabricated answer.
12
+ const DEFAULT_TIMEOUT_MS = 30_000;
13
+
14
+ // The child process never inherits Kairo's own real environment
15
+ // unscrubbed — `cwd` alone (even a sanitized snapshot dir) says nothing
16
+ // about what env vars a spawned process can read, and Kairo's own
17
+ // process env can carry real secrets (provider API keys, tokens) that
18
+ // have nothing to do with the question being asked. Mirrors
19
+ // execution-adapters/claude.js's own SAFE_ENV_KEYS precedent — reused
20
+ // directly for Claude; Codex gets an analogous, separately-scoped list
21
+ // (CODEX_HOME instead of CLAUDE_CONFIG_DIR) rather than a shared
22
+ // abstraction neither adapter asked for.
23
+ const CODEX_SAFE_ENV_KEYS = Object.freeze([
24
+ "PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE",
25
+ "TMPDIR", "TERM", "CODEX_HOME", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
26
+ "http_proxy", "https_proxy", "no_proxy", "NODE_EXTRA_CA_CERTS"
27
+ ]);
28
+ function buildCodexExecutionEnv(sourceEnv = process.env) {
29
+ const env = Object.create(null);
30
+ for (const key of CODEX_SAFE_ENV_KEYS) {
31
+ if (sourceEnv[key] != null && sourceEnv[key] !== "") env[key] = sourceEnv[key];
32
+ }
33
+ return env;
34
+ }
35
+
36
+ function unknown(error) {
37
+ return { status: "error", answer: null, error: String(error) };
38
+ }
39
+
40
+ /** @param {{question:string, model:string|null, cwd:string, spawn:Function, timeoutMs:number, env:object}} args */
41
+ function askClaude({ question, model, cwd, spawn, timeoutMs, env }) {
42
+ // --restricted: removes Bash/code-execution tools and WebFetch, ignores
43
+ // project/user settings, and confines the remaining file tools to cwd
44
+ // — the closest real equivalent to Codex's --sandbox read-only, since
45
+ // plain -p alone enforces no tool restriction at all.
46
+ // --strict-mcp-config: skip MCP servers too, so --restricted's own
47
+ // isolation isn't reopened by a configured MCP server with broader access.
48
+ const args = ["-p", question, "--output-format", "json", "--restricted", "--strict-mcp-config"];
49
+ if (model) args.push("--model", model);
50
+ return new Promise((resolve) => {
51
+ let child;
52
+ try {
53
+ child = spawn("claude", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
54
+ } catch (error) {
55
+ resolve(unknown(error?.message ?? error));
56
+ return;
57
+ }
58
+ let stdout = "";
59
+ let finished = false;
60
+ const timer = setTimeout(() => finish(unknown("claude -p timed out")), timeoutMs);
61
+ function finish(result) {
62
+ if (finished) return;
63
+ finished = true;
64
+ clearTimeout(timer);
65
+ try { child.kill?.(); } catch { /* best effort */ }
66
+ resolve(result);
67
+ }
68
+ child.stdout?.on("data", (chunk) => { stdout += chunk; });
69
+ child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
70
+ child.once?.("close", () => {
71
+ let parsed;
72
+ try { parsed = JSON.parse(stdout); } catch { return finish(unknown("malformed JSON from claude -p")); }
73
+ if (typeof parsed?.result !== "string") return finish(unknown("no result text in claude -p response"));
74
+ finish({ status: "answered", answer: parsed.result, error: null });
75
+ });
76
+ });
77
+ }
78
+
79
+ /** @param {{question:string, model:string|null, cwd:string, spawn:Function, timeoutMs:number, env:object}} args */
80
+ async function askCodex({ question, model, cwd, spawn, timeoutMs, env }) {
81
+ let outDir;
82
+ try {
83
+ outDir = await mkdtemp(join(tmpdir(), "kairo-ask-codex-"));
84
+ } catch (error) {
85
+ return unknown(error?.message ?? error);
86
+ }
87
+ const outFile = join(outDir, "answer.txt");
88
+ // read-only sandbox has nothing to approve, so --approve-for-me would
89
+ // conflict with --sandbox (the real CLI rejects combining them).
90
+ // --skip-git-repo-check: `cwd` is sometimes a sanitized snapshot
91
+ // directory (see conversation/sanitized-snapshot.js), which is
92
+ // deliberately not a real git repo (it excludes .git entirely) —
93
+ // without this, codex exec would refuse to run there at all. Harmless
94
+ // when cwd genuinely is a real git repo (the plain ASK-mode case).
95
+ const args = ["exec", "--sandbox", "read-only", "--skip-git-repo-check", "-o", outFile];
96
+ if (model) args.unshift("--model", model);
97
+ args.push(question);
98
+
99
+ try {
100
+ return await new Promise((resolve) => {
101
+ let child;
102
+ try {
103
+ child = spawn("codex", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
104
+ } catch (error) {
105
+ resolve(unknown(error?.message ?? error));
106
+ return;
107
+ }
108
+ let finished = false;
109
+ const timer = setTimeout(() => finish(unknown("codex exec timed out")), timeoutMs);
110
+ function finish(result) {
111
+ if (finished) return;
112
+ finished = true;
113
+ clearTimeout(timer);
114
+ try { child.kill?.(); } catch { /* best effort */ }
115
+ resolve(result);
116
+ }
117
+ child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
118
+ child.once?.("close", async () => {
119
+ try {
120
+ const text = (await readFile(outFile, "utf8")).trim();
121
+ if (!text) return finish(unknown("codex exec produced no final message"));
122
+ finish({ status: "answered", answer: text, error: null });
123
+ } catch (error) {
124
+ finish(unknown(error?.message ?? error));
125
+ }
126
+ });
127
+ });
128
+ } finally {
129
+ await rm(outDir, { recursive: true, force: true }).catch(() => {});
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Asks the given provider a real, read-only question and returns its real
135
+ * answer text. Supports Codex and Claude today; any other provider yields
136
+ * an honest "unsupported" result rather than a guess.
137
+ * @param {object} args
138
+ * @param {"codex"|"claude"} args.provider
139
+ * @param {string} args.question
140
+ * @param {string|null} [args.model]
141
+ * @param {string} args.cwd
142
+ */
143
+ export async function askProvider({
144
+ provider, question, model = null, cwd, spawn = defaultSpawn, timeoutMs = DEFAULT_TIMEOUT_MS, sourceEnv = process.env
145
+ }) {
146
+ if (provider === "claude") return askClaude({ question, model, cwd, spawn, timeoutMs, env: buildClaudeExecutionEnv(sourceEnv) });
147
+ if (provider === "codex") return askCodex({ question, model, cwd, spawn, timeoutMs, env: buildCodexExecutionEnv(sourceEnv) });
148
+ return { status: "unsupported", answer: null, error: `ASK is not supported for provider "${provider}" yet.` };
149
+ }
@@ -0,0 +1,251 @@
1
+ // RoleProfile: the canonical definition of Kairo's six team-vocabulary
2
+ // roles (Explorer / Architect / Builder / Debugger / Tester / Reviewer)
3
+ // — what each role is FOR, what real model can do its work, what it's
4
+ // allowed to do, what it must hand back, how risky its own mistakes are,
5
+ // when it's "done", and when it should escalate to a stronger model.
6
+ // This module is the source of truth; model-intelligence.js imports
7
+ // ROLE_CAPABILITIES from here (and re-exports it, so existing external
8
+ // callers keep working) rather than the other way around — a real
9
+ // routing layer will need to import BOTH role-profiles.js (policy) and
10
+ // capability-scoring.js (scoring) without ever routing through each
11
+ // other, and role-profiles.js -> model-intelligence.js -> capability-
12
+ // scoring.js would have made that a cycle the moment routing imported
13
+ // role-profiles.js too.
14
+ //
15
+ // ROLE_CAPABILITIES answers "which real model can do this role's work";
16
+ // the rest of RoleProfile answers "what does doing this role's work
17
+ // actually mean, and under what constraints". A RoleProfile's own
18
+ // `capabilities` field is a direct reference to ROLE_CAPABILITIES[role],
19
+ // never a duplicate — the two stay in sync by construction, not by
20
+ // remembering to update two places.
21
+ //
22
+ // Economy has no RoleProfile. Per the "Convertir la guía en un equipo
23
+ // operativo real" plan, Economy stops being a 7th role competing for its
24
+ // own slot and becomes an EXECUTION POLICY any of the six real roles can
25
+ // run under (cheapest real model that still clears the role's own
26
+ // requiredRoleFit floor, instead of the role's own leader/near-
27
+ // equivalence pick) — see model-intelligence.js's own Economy handling,
28
+ // still unchanged as of this module; wiring RoleProfile-aware routing to
29
+ // actually apply that policy per-role is a later increment, not this one.
30
+
31
+ // Each role's real relevant capabilities (see capability-scoring.js),
32
+ // derived from the plan's per-role table. "Hard problem solving" folds
33
+ // into reasoning (GPQA/HLE are themselves hard-reasoning benchmarks);
34
+ // "scientific coding" folds into coding (SciCode is already one of
35
+ // coding's real component benchmarks) rather than inventing a separate
36
+ // capability neither BENCHMARK_IDENTITIES nor any real source measures
37
+ // directly. Tester/Reviewer/Explorer rows were truncated in the source
38
+ // plan — inferred from this codebase's own pre-existing role-definition
39
+ // pattern (Tester: coding + terminal execution; Reviewer: independent
40
+ // reasoning + coding review; Explorer: the same reasoning-only signal
41
+ // Architect always had) rather than guessed from nothing.
42
+ // required: a model MUST have real evidence for every one of these to
43
+ // compete for the role at all — missing evidence on even one required
44
+ // capability excludes it from the ranking entirely (see
45
+ // model-intelligence.js's buildAiTeamRoleDefinitions's compute()).
46
+ // required capabilities ALONE decide both the ranking order
47
+ // (requiredRoleFit, i.e. RoleEvaluation.capabilityPercentile computed
48
+ // only from `required`) and how close two real picks are (gapValueByRole,
49
+ // computed the same way) — optional capabilities never dilute either
50
+ // number. optional: real evidence, when present, is scored completely
51
+ // separately (optionalEvaluationsByRole) and used ONLY as a tiebreak
52
+ // among candidates already equally fit on required capabilities — it can
53
+ // never move a model up in requiredRoleFit order, and its absence never
54
+ // excludes a model. Before this split, an optional capability was folded
55
+ // into the SAME median as required ones — so a generalist's real
56
+ // requiredRoleFit gap on the capabilities that actually define the role
57
+ // could be smoothed over by an unrelated optional signal. Whether this
58
+ // alone changes a specific real pick (e.g. Muse Spark 1.3 on Architect)
59
+ // still depends on the per-role near-equivalence band
60
+ // (model-intelligence.js's ROLE_NEAR_EQUIVALENCE_BAND) — verify against
61
+ // real data, never assume.
62
+ //
63
+ // softwareExecution is optional everywhere it appears (Builder, Debugger),
64
+ // not required — verified against two independent real catalogs before
65
+ // deciding this, not assumed: the full multi-provider catalog (Codex +
66
+ // Claude + Cursor + OpenCode Go, 81 scored candidates) showed real
67
+ // evidence for only 3 of them; crm's own real candidate pool (9 scored
68
+ // candidates) showed only 2. Coverage is a fact about the CURRENT real
69
+ // catalog, never a fixed constant — these specific numbers will already
70
+ // be stale by the time this comment is read; re-measure via
71
+ // capability-scoring.js's computeCapabilityPercentile against the real
72
+ // scored pool in hand, never assume a ratio. Making softwareExecution
73
+ // required at either measured ratio would have left Builder/Debugger with
74
+ // only 2-3 real candidates system-wide, no matter how many other models
75
+ // are genuinely capable — instructionFollowing is optional everywhere for
76
+ // the same reason (never load-bearing enough for any role to gate on).
77
+ export const ROLE_CAPABILITIES = {
78
+ Explorer: { required: ["reasoning"], optional: ["instructionFollowing"] },
79
+ Architect: { required: ["reasoning", "coding"], optional: ["instructionFollowing"] },
80
+ Builder: { required: ["coding", "terminalExecution"], optional: ["softwareExecution", "instructionFollowing"] },
81
+ Debugger: { required: ["reasoning", "coding", "terminalExecution"], optional: ["softwareExecution"] },
82
+ Tester: { required: ["coding", "terminalExecution"], optional: [] },
83
+ Reviewer: { required: ["reasoning", "coding"], optional: [] }
84
+ };
85
+
86
+ // Stable, machine-checkable vocabularies for a router to branch on — the
87
+ // human-readable `allowedActions`/`escalationConditions` strings below
88
+ // are for prompts and UI copy; a router must never parse or pattern-match
89
+ // those sentences to decide anything, since wording can change for
90
+ // clarity without meaning to change behavior. `allowedActionIds`/
91
+ // `escalationSignalIds` are the actual decision surface: closed,
92
+ // versioned sets a router (or a test) can validate membership against.
93
+ // Every id used in ROLE_PROFILES below must come from one of these two
94
+ // sets — see role-profiles.test.js's own membership check.
95
+ export const ALLOWED_ACTION_IDS = [
96
+ "repo.read", "repo.search", "repo.inspect_history",
97
+ "plan.write", "repo.write", "build.run", "lint.run", "test.run", "test.write", "review.write"
98
+ ];
99
+ export const ESCALATION_SIGNAL_IDS = [
100
+ "large_scope", "architectural_intent_required", "cross_subsystem", "high_reversal_cost",
101
+ "plan_invalidated", "security_sensitive", "root_cause_not_found", "high_uncertainty"
102
+ ];
103
+
104
+ /**
105
+ * @typedef {object} RoleProfile
106
+ * @property {string} role
107
+ * @property {string} objective - what this role is trying to accomplish, in one sentence.
108
+ * @property {string} responsibility - the concrete scope of work this role owns.
109
+ * @property {{required: string[], optional: string[]}} capabilities - direct reference to ROLE_CAPABILITIES[role].
110
+ * @property {string[]} allowedActions - human-readable description of what this role may do — prompts/UI only, never evaluated mechanically. See `allowedActionIds` for the real decision surface.
111
+ * @property {string[]} allowedActionIds - stable ids from ALLOWED_ACTION_IDS; what a router actually checks.
112
+ * @property {string} deliverable - what this role must hand back when it finishes.
113
+ * @property {"low"|"medium"|"high"} riskLevel - how costly a mistake from this role is to the rest of the team; already a stable enum, safe for a router to branch on directly.
114
+ * @property {string} completionCriteria - the real, checkable condition that marks this role's work as done (human-readable; not yet machine-evaluable — see role-profiles.js's own module doc).
115
+ * @property {string[]} escalationConditions - human-readable description of when this role should escalate — prompts/UI only. See `escalationSignalIds` for the real decision surface.
116
+ * @property {string[]} escalationSignalIds - stable ids from ESCALATION_SIGNAL_IDS; what a router actually checks to decide whether to escalate.
117
+ * @property {{dependsOn: string[], independentOf: string[]}} dependencies - which other roles this one's output depends on, and which it must stay independent from.
118
+ */
119
+
120
+ /**
121
+ * The six real RoleProfiles. Order matches ROLE_CAPABILITIES's own
122
+ * declaration order (Explorer, Architect, Builder, Debugger, Tester,
123
+ * Reviewer) — not alphabetical, not risk-ordered — so a reader who
124
+ * already knows one table can find the same role in the other without
125
+ * re-deriving the order.
126
+ * @type {Record<string, RoleProfile>}
127
+ */
128
+ export const ROLE_PROFILES = {
129
+ Explorer: {
130
+ role: "Explorer",
131
+ objective: "Investigate a real question or area of the codebase and come back with real, checkable evidence — never a guess.",
132
+ responsibility: "Read-only reconnaissance: locate the relevant real files, trace how something actually works today, and summarize findings a later role (usually Architect) can act on.",
133
+ capabilities: ROLE_CAPABILITIES.Explorer,
134
+ allowedActions: ["read files", "search/grep the repository", "run read-only inspection commands (e.g. git log, git blame)"],
135
+ allowedActionIds: ["repo.read", "repo.search", "repo.inspect_history"],
136
+ deliverable: "A findings summary with real file:line references for every claim — no unverified assertion presented as fact.",
137
+ riskLevel: "low",
138
+ completionCriteria: "Every question the investigation was scoped to answer has a real, cited answer, or is explicitly listed as unresolved with why.",
139
+ escalationConditions: [
140
+ "the area under investigation is large enough that a shallow read risks missing a real contradiction elsewhere in the codebase",
141
+ "the question requires understanding real architectural intent, not just locating code"
142
+ ],
143
+ escalationSignalIds: ["large_scope", "architectural_intent_required"],
144
+ dependencies: { dependsOn: [], independentOf: ["Builder", "Reviewer"] }
145
+ },
146
+ Architect: {
147
+ role: "Architect",
148
+ objective: "Turn a real requirement (and, when available, Explorer's findings) into a concrete, buildable plan — decide the approach before any code changes.",
149
+ responsibility: "Design and sequence the real work: which files change, in what order, what the acceptance criteria are, and what tradeoffs were considered and rejected.",
150
+ capabilities: ROLE_CAPABILITIES.Architect,
151
+ allowedActions: ["read files", "search/grep the repository", "produce a written plan or design document"],
152
+ allowedActionIds: ["repo.read", "repo.search", "plan.write"],
153
+ deliverable: "A plan concrete enough for Builder to execute without re-deciding the approach — real file targets, real ordering, real acceptance criteria.",
154
+ riskLevel: "high",
155
+ completionCriteria: "The plan covers every real file the work will touch, states its acceptance criteria explicitly, and names the tradeoffs it chose between.",
156
+ escalationConditions: [
157
+ "the requirement touches multiple subsystems whose real interaction isn't already well understood",
158
+ "a wrong architectural decision here would be expensive to reverse once Builder has acted on it"
159
+ ],
160
+ escalationSignalIds: ["cross_subsystem", "high_reversal_cost"],
161
+ dependencies: { dependsOn: ["Explorer"], independentOf: [] }
162
+ },
163
+ Builder: {
164
+ role: "Builder",
165
+ objective: "Implement the real plan Architect produced — write, edit, or remove real code.",
166
+ responsibility: "Execute the approved plan faithfully: make the real file changes it calls for, following this codebase's own existing conventions rather than inventing new ones.",
167
+ capabilities: ROLE_CAPABILITIES.Builder,
168
+ allowedActions: ["read files", "write/edit files", "run build/lint commands to self-check"],
169
+ allowedActionIds: ["repo.read", "repo.write", "build.run", "lint.run"],
170
+ deliverable: "The real code change described by the plan, in a state ready for Tester/Reviewer — not a partial or half-finished implementation.",
171
+ riskLevel: "medium",
172
+ completionCriteria: "Every file target in the plan is actually changed, the change builds/lints cleanly, and no acceptance criterion from the plan is left unaddressed.",
173
+ escalationConditions: [
174
+ "the real implementation reveals the plan's approach doesn't actually work and needs Architect to reconsider it",
175
+ "the change touches a real security- or data-integrity-sensitive path"
176
+ ],
177
+ escalationSignalIds: ["plan_invalidated", "security_sensitive"],
178
+ dependencies: { dependsOn: ["Architect"], independentOf: [] }
179
+ },
180
+ Debugger: {
181
+ role: "Debugger",
182
+ objective: "Find the real root cause of a real failure and fix it — never patch the symptom without understanding why it happened.",
183
+ responsibility: "Reproduce the real failure, trace it to its real cause in the code, and make the minimal real change that actually fixes that cause.",
184
+ capabilities: ROLE_CAPABILITIES.Debugger,
185
+ allowedActions: ["read files", "write/edit files", "run tests and reproduction commands"],
186
+ // test.write, not just test.run: this role's own deliverable requires
187
+ // handing back a real regression test — per this project's own Zero
188
+ // Bugs Policy ("every bug fix requires a regression test that fails
189
+ // first, then passes with the fix"), the same actor that finds and
190
+ // fixes the real root cause writes that specific test, not a
191
+ // mandatory Debugger->Tester handoff for every fix. Debugger already
192
+ // has broad repo.write; this is a more specific, explicit grant of
193
+ // the same real capability, not an expansion of what it can touch.
194
+ allowedActionIds: ["repo.read", "repo.write", "test.write", "test.run"],
195
+ deliverable: "A fix with a real regression test that fails before the fix and passes after it, plus a stated root cause.",
196
+ riskLevel: "high",
197
+ completionCriteria: "The real failure no longer reproduces, a regression test proves it, and the stated root cause is the actual cause, not a plausible-sounding guess.",
198
+ escalationConditions: [
199
+ "the failure's real root cause isn't found after a reasonable real investigation — guessing at fixes past that point is worse than escalating",
200
+ "the failure is in a real security- or data-integrity-sensitive path"
201
+ ],
202
+ escalationSignalIds: ["root_cause_not_found", "security_sensitive"],
203
+ dependencies: { dependsOn: [], independentOf: ["Builder"] }
204
+ },
205
+ Tester: {
206
+ role: "Tester",
207
+ objective: "Verify the real behavior of a change, including its real edge cases — not just the happy path Builder already checked.",
208
+ responsibility: "Write and run real tests that would catch a real regression, deliberately probing edge cases and failure modes the implementation might have missed.",
209
+ capabilities: ROLE_CAPABILITIES.Tester,
210
+ allowedActions: ["read files", "write/edit test files", "run test commands"],
211
+ allowedActionIds: ["repo.read", "test.write", "test.run"],
212
+ deliverable: "A real, passing test suite covering the change's stated behavior and its real edge cases, with any gap found reported honestly.",
213
+ riskLevel: "medium",
214
+ completionCriteria: "The relevant real test command passes, and the edge cases specific to this change (not just a generic checklist) are actually covered.",
215
+ escalationConditions: [
216
+ "the edge cases under test require reasoning about a real, non-obvious interaction between subsystems"
217
+ ],
218
+ escalationSignalIds: ["cross_subsystem"],
219
+ dependencies: { dependsOn: ["Builder"], independentOf: [] }
220
+ },
221
+ Reviewer: {
222
+ role: "Reviewer",
223
+ objective: "Independently verify a real change is correct, safe, and consistent with this codebase's own conventions — the team's last real check before delivery.",
224
+ responsibility: "Read the real diff with an adversarial eye: look for correctness issues, security issues, and departures from established patterns Builder may have missed or rationalized.",
225
+ capabilities: ROLE_CAPABILITIES.Reviewer,
226
+ allowedActions: ["read files", "search/grep the repository", "produce a written review with real file:line findings"],
227
+ allowedActionIds: ["repo.read", "repo.search", "review.write"],
228
+ deliverable: "A review that either approves the change or lists real, concrete, file:line-anchored findings — never a vague 'looks fine' or an unfounded objection.",
229
+ riskLevel: "high",
230
+ completionCriteria: "Every real file the change touches has been read, and every finding raised is backed by a concrete file:line reference and a real failure scenario.",
231
+ escalationConditions: [
232
+ "the change touches a real security- or data-integrity-sensitive path",
233
+ "the reviewer's own uncertainty about correctness is high enough that a second, independent pass would materially change the outcome"
234
+ ],
235
+ escalationSignalIds: ["security_sensitive", "high_uncertainty"],
236
+ // Reviewer's independence from Builder isn't just a design principle
237
+ // here — model-intelligence.js's buildAiTeam enforces it mechanically
238
+ // (a Reviewer pick is barred from Builder's own chosen adapter
239
+ // whenever a real, near-equivalent or floor-clearing alternative
240
+ // exists — see passesConcentration's reviewerBuilderAdapter check).
241
+ dependencies: { dependsOn: ["Builder"], independentOf: ["Builder"] }
242
+ }
243
+ };
244
+
245
+ /**
246
+ * @param {string} role
247
+ * @returns {RoleProfile|null}
248
+ */
249
+ export function getRoleProfile(role) {
250
+ return ROLE_PROFILES[role] ?? null;
251
+ }
@@ -0,0 +1,67 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ // Reads the real project-local skill catalog (SKILL.md name + description
6
+ // per skill) from the same directories context-compiler.js already scans
7
+ // for skill *names* — this reads their real descriptions too, so the
8
+ // execution router can match a task against what a skill actually claims
9
+ // to do instead of guessing from bare folder names.
10
+ const SKILL_ROOTS = ["docs/skills", ".cursor/skills", ".codex/skills", ".claude/skills"];
11
+
12
+ /**
13
+ * Minimal frontmatter parser for SKILL.md's `---\nkey: value\n---` header —
14
+ * no YAML dependency, since skill frontmatter is flat key/value pairs (per
15
+ * the vercel-labs/skills format: required `name` and `description`).
16
+ * @param {string} text
17
+ */
18
+ export function parseSkillFrontmatter(text) {
19
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text ?? "");
20
+ if (!match) return null;
21
+ const fields = {};
22
+ for (const line of match[1].split("\n")) {
23
+ const fieldMatch = /^([\w.-]+):\s*(.*)$/.exec(line.trim());
24
+ if (!fieldMatch) continue;
25
+ fields[fieldMatch[1]] = fieldMatch[2].trim().replace(/^["']|["']$/g, "");
26
+ }
27
+ return fields;
28
+ }
29
+
30
+ /**
31
+ * Reads every SKILL.md under this project's standard skill directories and
32
+ * returns their real `name`/`description`. Skills without both fields are
33
+ * skipped rather than guessed at.
34
+ * @param {string} root - project root
35
+ * @returns {Promise<Array<{name: string, description: string, path: string}>>}
36
+ */
37
+ export async function readSkillCatalog(root) {
38
+ const skills = [];
39
+ const seenNames = new Set();
40
+ for (const skillRoot of SKILL_ROOTS) {
41
+ const dir = join(root, skillRoot);
42
+ if (!existsSync(dir)) continue;
43
+ let entries;
44
+ try {
45
+ entries = await readdir(dir, { withFileTypes: true });
46
+ } catch {
47
+ continue;
48
+ }
49
+ for (const entry of entries) {
50
+ if (!entry.isDirectory()) continue;
51
+ const skillMdPath = join(dir, entry.name, "SKILL.md");
52
+ if (!existsSync(skillMdPath)) continue;
53
+ let raw;
54
+ try {
55
+ raw = await readFile(skillMdPath, "utf8");
56
+ } catch {
57
+ continue;
58
+ }
59
+ const frontmatter = parseSkillFrontmatter(raw);
60
+ const name = frontmatter?.name ?? entry.name;
61
+ if (!frontmatter?.description || seenNames.has(name)) continue;
62
+ seenNames.add(name);
63
+ skills.push({ name, description: frontmatter.description, path: join(skillRoot, entry.name, "SKILL.md") });
64
+ }
65
+ }
66
+ return skills;
67
+ }