@kal-elsam/kairo-runtime 0.23.2 → 0.25.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 +41 -0
- package/package.json +1 -1
- package/src/global/conversation/project-strategy.js +8 -3
- package/src/global/conversation/service.js +30 -2
- package/src/global/intelligence/quick-ask.js +73 -3
- package/src/global/observability/codex-models.js +1 -1
- package/src/global/observability/codex-usage.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.25.0 — 2026-09-19 (Kairo Runtime)
|
|
9
|
+
|
|
10
|
+
Minor release. Cursor joins ASK.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- ASK can now call Cursor, via its real, documented read-only mode
|
|
15
|
+
(`cursor-agent --mode ask`, verified live) — never combined with
|
|
16
|
+
`--force`/`--yolo`. PROJECT TEAM roles assigned to Cursor (Architect/
|
|
17
|
+
Builder/Debugger in this project's own team) are now real ASK
|
|
18
|
+
candidates, not just execution candidates.
|
|
19
|
+
- OpenCode (Go/Zen) stays excluded from ASK: verified its CLI has no
|
|
20
|
+
portable, flag-driven read-only mode — its permission model is
|
|
21
|
+
project/user-config-driven, so a read-only call can't be guaranteed
|
|
22
|
+
safe across different installs.
|
|
23
|
+
|
|
24
|
+
## 0.24.0 — 2026-09-18 (Kairo Runtime)
|
|
25
|
+
|
|
26
|
+
Minor release. ASK mode joins PROJECT TEAM.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- ASK mode now checks PROJECT TEAM's Explorer role first (read-only
|
|
31
|
+
investigation is exactly Explorer's job), via the same routing real
|
|
32
|
+
execution already uses — instead of always going through a fully
|
|
33
|
+
separate, generic quota/effort heuristic. Falls back to that
|
|
34
|
+
heuristic only when there's no active team yet, or Explorer's real
|
|
35
|
+
assignment isn't one ASK can actually invoke (Cursor/OpenCode
|
|
36
|
+
Go/Zen aren't ask-capable yet). Role is always Explorer, never
|
|
37
|
+
inferred from the question's text.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- A flaky timing test in `quick-ask.test.js` (from 0.23.2) used
|
|
42
|
+
margins too tight to reliably survive full-suite load; widened them.
|
|
43
|
+
|
|
44
|
+
Not yet in this release: PLAN mode routing through the Orchestrator
|
|
45
|
+
role (plan creation is currently structurally Codex-only, a bigger
|
|
46
|
+
change than a routing choice), and AGENT mode (an open product
|
|
47
|
+
question on whether it should skip the plan→approve gate).
|
|
48
|
+
|
|
8
49
|
## 0.23.2 — 2026-09-18 (Kairo Runtime)
|
|
9
50
|
|
|
10
51
|
Patch release.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Kal-elSam/harness#readme",
|
|
@@ -21,10 +21,15 @@ import { ROLE_CAPABILITIES } from "../intelligence/role-profiles.js";
|
|
|
21
21
|
import { computeRoleEvaluations } from "../intelligence/capability-scoring.js";
|
|
22
22
|
|
|
23
23
|
// The Bootstrap Analyst investigates read-only via askProvider
|
|
24
|
-
// (intelligence/quick-ask.js), which only actually supports these
|
|
24
|
+
// (intelligence/quick-ask.js), which only actually supports these
|
|
25
25
|
// providers today — offering any other real candidate as an "alternative"
|
|
26
|
-
// here would be a menu item Kairo can't actually run.
|
|
27
|
-
|
|
26
|
+
// here would be a menu item Kairo can't actually run. Exported: ASK mode's
|
|
27
|
+
// own real-time routing (service.js's planAsk) needs this exact same real
|
|
28
|
+
// constraint when it tries to route a plain question through a PROJECT
|
|
29
|
+
// TEAM role — a role assigned to, say, opencode-go is a real, valid team
|
|
30
|
+
// assignment, just not one askProvider can call (its real CLI has no
|
|
31
|
+
// portable read-only mode — see quick-ask.js's own askProvider doc).
|
|
32
|
+
export const ASK_SUPPORTED_ADAPTERS = new Set(["codex", "claude", "cursor"]);
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
35
|
* The Bootstrap Analyst as a temporary, read-only WORKFLOW — deliberately
|
|
@@ -26,8 +26,8 @@ import { appendTranscriptEntry, clearTranscript, readTranscript } from "./transc
|
|
|
26
26
|
import { readSession, writeSessionMode } from "./session-store.js";
|
|
27
27
|
import { computeProjectProfile } from "./project-profile.js";
|
|
28
28
|
import {
|
|
29
|
-
buildProjectStrategy, computeBootstrapAnalystAlternatives, computeBootstrapAnalystCatalog,
|
|
30
|
-
computeProjectTeamEditCatalog, applyProjectTeamOverride, resetProjectTeamAssignment
|
|
29
|
+
ASK_SUPPORTED_ADAPTERS, buildProjectStrategy, computeBootstrapAnalystAlternatives, computeBootstrapAnalystCatalog,
|
|
30
|
+
isStrategyStale, computeProjectTeamEditCatalog, applyProjectTeamOverride, resetProjectTeamAssignment
|
|
31
31
|
} from "./project-strategy.js";
|
|
32
32
|
import { buildAnalystPrompt, deriveRoleRequirements, parseProjectAnalysis } from "./project-analysis.js";
|
|
33
33
|
import { buildSanitizedSnapshot } from "./sanitized-snapshot.js";
|
|
@@ -662,6 +662,34 @@ export function createConversationService(deps = {}) {
|
|
|
662
662
|
*/
|
|
663
663
|
async planAsk({ cwd, task }) {
|
|
664
664
|
const projectRoot = await root(cwd);
|
|
665
|
+
// PROJECT TEAM's own Explorer role first — a real, approved,
|
|
666
|
+
// project-specific assignment beats the generic heuristic below,
|
|
667
|
+
// same principle as real execution routing. Explorer, never a
|
|
668
|
+
// guess from the question's text: resolveProjectRoute's whole
|
|
669
|
+
// design is that a role is always the caller's own explicit fixed
|
|
670
|
+
// choice, never inferred per-call — ASK questions are read-only
|
|
671
|
+
// investigation, which is exactly Explorer's job.
|
|
672
|
+
const teamRoute = await this.routeProjectExecution("Explorer", projectRoot);
|
|
673
|
+
const teamModel = teamRoute.decision === "ROUTED" ? teamRoute.model
|
|
674
|
+
: teamRoute.decision === "WAIT_FOR_PROJECT_TEAM" ? teamRoute.suggestedAlternative?.model ?? null
|
|
675
|
+
: null;
|
|
676
|
+
// Only when that real assignment is one askProvider can actually
|
|
677
|
+
// call (see ASK_SUPPORTED_ADAPTERS's own doc) — a role can be
|
|
678
|
+
// validly assigned to Cursor/OpenCode Go/Zen, which ASK simply
|
|
679
|
+
// can't invoke yet, so that's a real reason to fall through below,
|
|
680
|
+
// never an error.
|
|
681
|
+
if (teamModel && ASK_SUPPORTED_ADAPTERS.has(teamModel.adapterId)) {
|
|
682
|
+
return {
|
|
683
|
+
decision: {
|
|
684
|
+
decision: "ROUTED", provider: teamModel.adapterId, model: teamModel.modelId,
|
|
685
|
+
why: `Explorer (PROJECT TEAM): ${teamRoute.why}`
|
|
686
|
+
},
|
|
687
|
+
projectRoot
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// No active team, or Explorer's real assignment isn't ask-capable —
|
|
691
|
+
// fall back to the generic quota/capability heuristic so ASK stays
|
|
692
|
+
// useful even before a team is approved.
|
|
665
693
|
const adapters = inspectAdapters({ cwd: projectRoot });
|
|
666
694
|
let codexUsage = null;
|
|
667
695
|
let claudeUsage = null;
|
|
@@ -33,6 +33,22 @@ function buildCodexExecutionEnv(sourceEnv = process.env) {
|
|
|
33
33
|
return env;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Same real scrubbing principle as Codex/Claude above — CURSOR_API_KEY/
|
|
37
|
+
// CURSOR_API_ENDPOINT are cursor-agent's own documented real auth env
|
|
38
|
+
// vars (verified via `cursor-agent -p --help`), never a guess.
|
|
39
|
+
const CURSOR_SAFE_ENV_KEYS = Object.freeze([
|
|
40
|
+
"PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE",
|
|
41
|
+
"TMPDIR", "TERM", "CURSOR_API_KEY", "CURSOR_API_ENDPOINT", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY",
|
|
42
|
+
"http_proxy", "https_proxy", "no_proxy", "NODE_EXTRA_CA_CERTS"
|
|
43
|
+
]);
|
|
44
|
+
function buildCursorExecutionEnv(sourceEnv = process.env) {
|
|
45
|
+
const env = Object.create(null);
|
|
46
|
+
for (const key of CURSOR_SAFE_ENV_KEYS) {
|
|
47
|
+
if (sourceEnv[key] != null && sourceEnv[key] !== "") env[key] = sourceEnv[key];
|
|
48
|
+
}
|
|
49
|
+
return env;
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
function unknown(error) {
|
|
37
53
|
return { status: "error", answer: null, error: String(error) };
|
|
38
54
|
}
|
|
@@ -102,6 +118,54 @@ function askClaude({ question, model, cwd, spawn, timeoutMs, env }) {
|
|
|
102
118
|
});
|
|
103
119
|
}
|
|
104
120
|
|
|
121
|
+
/**
|
|
122
|
+
* cursor-agent's own real, documented read-only mode (verified via
|
|
123
|
+
* `cursor-agent -p --help`): "ask: Q&A style for explanations and
|
|
124
|
+
* questions (read-only)" — never combined with --force/--yolo, which
|
|
125
|
+
* would grant real write/shell access. A real invalid-model failure
|
|
126
|
+
* (verified live) exits non-zero with a plain-text error on stderr, no
|
|
127
|
+
* JSON at all — unlike Claude's/Codex's own failure shapes, so a failed
|
|
128
|
+
* JSON parse here reports the real stderr text, never a generic guess.
|
|
129
|
+
* @param {{question:string, model:string|null, cwd:string, spawn:Function, timeoutMs:number, env:object}} args
|
|
130
|
+
*/
|
|
131
|
+
function askCursor({ question, model, cwd, spawn, timeoutMs, env }) {
|
|
132
|
+
const args = ["-p", question, "--mode", "ask", "--output-format", "json"];
|
|
133
|
+
if (model) args.push("--model", model);
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
let child;
|
|
136
|
+
try {
|
|
137
|
+
child = spawn("cursor-agent", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
resolve(unknown(error?.message ?? error));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
let stdout = "";
|
|
143
|
+
let stderr = "";
|
|
144
|
+
let finished = false;
|
|
145
|
+
const clearIdleTimer = armIdleTimeout(child, timeoutMs, () => finish(unknown(`cursor-agent idle-timed out after ${timeoutMs}ms with no output`)));
|
|
146
|
+
function finish(result) {
|
|
147
|
+
if (finished) return;
|
|
148
|
+
finished = true;
|
|
149
|
+
clearIdleTimer();
|
|
150
|
+
try { child.kill?.(); } catch { /* best effort */ }
|
|
151
|
+
resolve(result);
|
|
152
|
+
}
|
|
153
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
154
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk; });
|
|
155
|
+
child.once?.("error", (error) => finish(unknown(error?.message ?? error)));
|
|
156
|
+
child.once?.("close", (code) => {
|
|
157
|
+
let parsed;
|
|
158
|
+
try { parsed = JSON.parse(stdout); } catch {
|
|
159
|
+
return finish(unknown(stderr.trim() || `cursor-agent exited ${code} with no parseable output`));
|
|
160
|
+
}
|
|
161
|
+
if (parsed?.is_error === true || typeof parsed?.result !== "string") {
|
|
162
|
+
return finish(unknown(parsed?.result ?? stderr.trim() ?? "cursor-agent returned no answer"));
|
|
163
|
+
}
|
|
164
|
+
finish({ status: "answered", answer: parsed.result, error: null });
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
105
169
|
/** @param {{question:string, model:string|null, cwd:string, spawn:Function, timeoutMs:number, env:object}} args */
|
|
106
170
|
async function askCodex({ question, model, cwd, spawn, timeoutMs, env }) {
|
|
107
171
|
let outDir;
|
|
@@ -158,10 +222,15 @@ async function askCodex({ question, model, cwd, spawn, timeoutMs, env }) {
|
|
|
158
222
|
|
|
159
223
|
/**
|
|
160
224
|
* Asks the given provider a real, read-only question and returns its real
|
|
161
|
-
* answer text. Supports Codex and
|
|
162
|
-
* an honest "unsupported" result rather than a guess.
|
|
225
|
+
* answer text. Supports Codex, Claude, and Cursor today; any other
|
|
226
|
+
* provider yields an honest "unsupported" result rather than a guess.
|
|
227
|
+
* OpenCode (Go/Zen) is deliberately excluded — its real CLI has no
|
|
228
|
+
* portable, CLI-flag-driven read-only mode (verified via `opencode run
|
|
229
|
+
* --help`: `--auto` only ever loosens permissions further, never
|
|
230
|
+
* restricts them), so a real read-only call can't be guaranteed safe
|
|
231
|
+
* across different users' local opencode.json permission configs.
|
|
163
232
|
* @param {object} args
|
|
164
|
-
* @param {"codex"|"claude"} args.provider
|
|
233
|
+
* @param {"codex"|"claude"|"cursor"} args.provider
|
|
165
234
|
* @param {string} args.question
|
|
166
235
|
* @param {string|null} [args.model]
|
|
167
236
|
* @param {string} args.cwd
|
|
@@ -171,5 +240,6 @@ export async function askProvider({
|
|
|
171
240
|
}) {
|
|
172
241
|
if (provider === "claude") return askClaude({ question, model, cwd, spawn, timeoutMs, env: buildClaudeExecutionEnv(sourceEnv) });
|
|
173
242
|
if (provider === "codex") return askCodex({ question, model, cwd, spawn, timeoutMs, env: buildCodexExecutionEnv(sourceEnv) });
|
|
243
|
+
if (provider === "cursor") return askCursor({ question, model, cwd, spawn, timeoutMs, env: buildCursorExecutionEnv(sourceEnv) });
|
|
174
244
|
return { status: "unsupported", answer: null, error: `ASK is not supported for provider "${provider}" yet.` };
|
|
175
245
|
}
|
|
@@ -89,7 +89,7 @@ export async function readCodexModels({
|
|
|
89
89
|
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before model list")); });
|
|
90
90
|
|
|
91
91
|
writeRequest(child, 1, "initialize", {
|
|
92
|
-
clientInfo: { name: "kairo", title: "Kairo", version: "0.
|
|
92
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.25.0" },
|
|
93
93
|
capabilities: {}
|
|
94
94
|
});
|
|
95
95
|
});
|
|
@@ -151,7 +151,7 @@ export async function readCodexUsage({
|
|
|
151
151
|
child.once?.("close", () => { if (!finished) finish(unknown("codex app-server closed before rate limits")); });
|
|
152
152
|
|
|
153
153
|
writeRequest(child, 1, "initialize", {
|
|
154
|
-
clientInfo: { name: "kairo", title: "Kairo", version: "0.
|
|
154
|
+
clientInfo: { name: "kairo", title: "Kairo", version: "0.25.0" },
|
|
155
155
|
capabilities: {}
|
|
156
156
|
});
|
|
157
157
|
});
|