@kal-elsam/kairo-runtime 0.16.0 → 0.18.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.
- package/CHANGELOG.md +79 -0
- package/package.json +2 -1
- package/scripts/cockpit-smoke.mjs +1 -1
- package/scripts/ux-smoke-test.sh +3 -3
- package/src/cli.js +96 -11
- package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
- package/src/global/architect/architect-cli.js +76 -0
- package/src/global/architect/architect-codex.js +146 -0
- package/src/global/architect/architect-manager.js +125 -0
- package/src/global/architect/architect-store.js +377 -0
- package/src/global/architect/architect-types.js +47 -0
- package/src/global/cli-help.js +10 -1
- package/src/global/cockpit/app.js +493 -0
- package/src/global/cockpit/card.js +111 -0
- package/src/global/cockpit/cli.js +33 -0
- package/src/global/cockpit/gauge.js +31 -0
- package/src/global/cockpit/project-overlay.js +693 -0
- package/src/global/cockpit/rows.js +148 -0
- package/src/global/cockpit/theme.js +118 -0
- package/src/global/cockpit/view.js +1298 -0
- package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
- package/src/global/conversation/cli.js +53 -0
- package/src/global/conversation/codex-sandbox.js +230 -0
- package/src/global/conversation/cursor-sandbox.js +215 -0
- package/src/global/conversation/project-analysis.js +204 -0
- package/src/global/conversation/project-profile.js +178 -0
- package/src/global/conversation/project-router.js +149 -0
- package/src/global/conversation/project-strategy-store.js +64 -0
- package/src/global/conversation/project-strategy.js +514 -0
- package/src/global/conversation/sanitized-snapshot.js +169 -0
- package/src/global/conversation/secret-scanner.js +71 -0
- package/src/global/conversation/service.js +1090 -0
- package/src/global/conversation/session-store.js +75 -0
- package/src/global/conversation/transcript-store.js +79 -0
- package/src/global/conversation/ui.js +195 -0
- package/src/global/intelligence/capability-scoring.js +480 -0
- package/src/global/intelligence/execution-router.js +466 -0
- package/src/global/intelligence/kairo-telemetry-source.js +59 -0
- package/src/global/intelligence/kairobench-runner.js +85 -0
- package/src/global/intelligence/kairobench-source.js +34 -0
- package/src/global/intelligence/kairobench-tasks.js +47 -0
- package/src/global/intelligence/model-candidate-catalog.js +456 -0
- package/src/global/intelligence/model-capability-registry-sources.js +145 -0
- package/src/global/intelligence/model-capability-registry.js +125 -0
- package/src/global/intelligence/model-intelligence.js +1646 -0
- package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
- package/src/global/intelligence/quick-ask.js +149 -0
- package/src/global/intelligence/role-profiles.js +251 -0
- package/src/global/intelligence/skill-catalog.js +67 -0
- package/src/global/intelligence/subscription-pressure-source.js +41 -0
- package/src/global/mcp/kairo-mcp.js +51 -18
- package/src/global/mcp/work-snapshot-rule.js +4 -2
- package/src/global/mcp/workspace-binding.js +88 -0
- package/src/global/mcp/workspace-mcp-entry.js +74 -0
- package/src/global/mcp-install.js +8 -1
- package/src/global/observability/artificial-analysis-models.js +118 -0
- package/src/global/observability/claude-models.js +31 -0
- package/src/global/observability/claude-usage.js +112 -0
- package/src/global/observability/codex-models.js +96 -0
- package/src/global/observability/codex-usage.js +160 -0
- package/src/global/observability/cursor-auth.js +88 -0
- package/src/global/observability/cursor-models.js +101 -0
- package/src/global/observability/huggingface-leaderboard.js +97 -0
- package/src/global/observability/opencode-models.js +101 -0
- package/src/global/observability/opencode-usage.js +162 -0
- package/src/global/paths.js +49 -2
- package/src/global/profile.js +23 -1
- package/src/global/runtime/execution-adapters/claude.js +63 -30
- package/src/global/runtime/execution-adapters/codex.js +9 -2
- package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
- package/src/global/runtime/execution-adapters/opencode.js +83 -18
- package/src/global/runtime/execution-worktree-manager.js +924 -0
- package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
- package/src/global/runtime/execution-worktree-store.js +83 -0
- package/src/global/runtime/execution-worktree-types.js +45 -0
- package/src/global/runtime/run-events.js +38 -0
- package/src/global/runtime/run-manager.js +22 -6
- package/src/global/runtime/run-supervisor.js +41 -12
- package/src/global/runtime/usage-manager.js +96 -0
- package/src/global/runtime/usage-store.js +69 -0
- 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
|
+
}
|