@kal-elsam/kairo-runtime 0.19.0 → 0.21.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 CHANGED
@@ -5,6 +5,40 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.21.0 — 2026-09-18 (Kairo Runtime)
9
+
10
+ Patch-level polish. When a role resolves to a manual-only provider
11
+ (OpenCode Go), the `planExecution` preview now carries the exact
12
+ real task text an automatic run would use, and the cockpit pushes it
13
+ into the transcript ready to paste into that provider's own chat —
14
+ instead of only naming the assigned model.
15
+
16
+ ### Added
17
+
18
+ - `buildExecutionTaskPrompt` (service.js) is the one real formula for a
19
+ task's launch text, shared by `executePlan` (real automatic runs)
20
+ and `planExecution`'s `MANUAL_HANDOFF` preview (`taskPrompt`).
21
+
22
+ ## 0.20.0 — 2026-09-18 (Kairo Runtime)
23
+
24
+ Minor release. Promotes Cursor to a real automatic execution provider.
25
+
26
+ ### Changed
27
+
28
+ - Cursor's accessMode flips from "manual" to "automatic"
29
+ (`model-candidate-catalog.js`). Its own execution adapter already
30
+ builds a real, auditable non-interactive launch (`cursor-agent -p
31
+ --output-format stream-json`) and parses its structured event stream
32
+ — the same shape as Codex/Claude, and an officially documented,
33
+ supported use of the Cursor CLI. Real task routing now judges Cursor
34
+ by the exact same launchable/eligibility gate as Codex/Claude, for
35
+ both execution and recommendations — no more special-cased
36
+ manual-only rejection in `execution-router.js`'s `checkCandidate`.
37
+ - Cursor's own opaque "auto" router model still stays manual (its
38
+ identity is non-deterministic, unlike a named model). OpenCode Go is
39
+ unaffected — it remains manual for its own, real, already-confirmed
40
+ Go/Zen billing-attribution ambiguity.
41
+
8
42
  ## 0.19.0 — 2026-09-18 (Kairo Runtime)
9
43
 
10
44
  Patch-level polish on top of 0.18.0's PROJECT TEAM work: fixes the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.19.0",
3
+ "version": "0.21.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",
@@ -190,6 +190,16 @@ export async function runCockpitApp({
190
190
  return runAction("Asking PROJECT TEAM who should execute this", async () => {
191
191
  const decision = await service.planExecution({ cwd, taskId, role });
192
192
  view.showExecuteConfirm(taskId, decision);
193
+ // MANUAL_HANDOFF: Kairo can't launch this itself, so the real
194
+ // task text (the exact same one an automatic run would get —
195
+ // see service.js's buildExecutionTaskPrompt) goes into the
196
+ // transcript, ready to paste into the assigned provider's own
197
+ // chat — never just naming the model and leaving the human to
198
+ // reconstruct the prompt themselves.
199
+ if (decision.decision === "MANUAL_HANDOFF" && decision.taskPrompt) {
200
+ const modelLabel = decision.modelRef?.displayName ?? decision.model ?? "the assigned model";
201
+ pushTranscript("kairo", `${decision.provider} · ${modelLabel} can't be launched automatically — paste this into its chat:\n\n${decision.taskPrompt}`);
202
+ }
193
203
  });
194
204
  },
195
205
  onExecute: (taskId, decision) => {
@@ -1258,7 +1258,7 @@ export class CockpitView {
1258
1258
  `Claude ${claudeText}`,
1259
1259
  `Go ${goText}`,
1260
1260
  `Zen ${zenText}`,
1261
- `Cursor ${entry("Cursor", "MANUAL · usage unknown")}`
1261
+ `Cursor ${entry("Cursor", "READY · usage unknown")}`
1262
1262
  ];
1263
1263
  }
1264
1264
 
@@ -367,6 +367,24 @@ export function createConversationService(deps = {}) {
367
367
 
368
368
  async function root(cwd) { return resolveRoot(cwd); }
369
369
 
370
+ /**
371
+ * The exact real task text an automatic run gets launched with (see
372
+ * executePlan) — the ONE real formula, never a second one invented for
373
+ * display purposes. Reused by toExecutionPreview so a MANUAL_HANDOFF
374
+ * preview can hand the human this same text, ready to paste into the
375
+ * manual-only provider's own chat, instead of just naming the model.
376
+ * @param {string} planMarkdown
377
+ */
378
+ function buildExecutionTaskPrompt(planMarkdown) {
379
+ return [
380
+ "Implement the explicitly approved architecture plan below.",
381
+ "Follow repository AGENTS.md and Gentle governance. Do not treat plan approval as any additional governance receipt.",
382
+ "Use safe, non-bypassed permissions for this session.",
383
+ "",
384
+ planMarkdown
385
+ ].join("\n");
386
+ }
387
+
370
388
  /**
371
389
  * Projects a real project-router decision into the public
372
390
  * ProjectExecutionPreview shape — never recalculates anything the
@@ -380,20 +398,28 @@ export function createConversationService(deps = {}) {
380
398
  * - Everything else (MANUAL_HANDOFF, or WAIT_FOR_PROJECT_TEAM with no
381
399
  * real alternative): null — nothing to confirm into an automatic run.
382
400
  * @param {ReturnType<typeof resolveProjectRoute>} route
401
+ * @param {{planMarkdown: string}|null} [record] - present only from
402
+ * planExecution (which already read the real plan record); used to
403
+ * attach `taskPrompt` for a MANUAL_HANDOFF decision only — a ROUTED/
404
+ * WAIT_FOR_PROJECT_TEAM preview never needs it, since executePlan
405
+ * builds the real task text itself from the SAME buildExecutionTaskPrompt.
383
406
  */
384
- function toExecutionPreview(route) {
407
+ function toExecutionPreview(route, record = null) {
385
408
  let confirmationTarget = null;
386
409
  if (route.decision === "ROUTED" && route.model) {
387
410
  confirmationTarget = { role: route.role, selection: "assigned", strategyFingerprint: route.strategyFingerprint, candidateKey: route.model.candidateKey ?? null };
388
411
  } else if (route.decision === "WAIT_FOR_PROJECT_TEAM" && route.suggestedAlternative?.model) {
389
412
  confirmationTarget = { role: route.role, selection: "suggested-alternative", strategyFingerprint: route.strategyFingerprint, candidateKey: route.suggestedAlternative.model.candidateKey ?? null };
390
413
  }
414
+ const taskPrompt = route.decision === "MANUAL_HANDOFF" && record?.planMarkdown
415
+ ? buildExecutionTaskPrompt(record.planMarkdown)
416
+ : null;
391
417
  return {
392
418
  decision: route.decision, role: route.role,
393
419
  provider: route.provider, model: route.model?.modelId ?? null, modelRef: route.model,
394
420
  assignmentSource: route.assignmentSource, strategyFingerprint: route.strategyFingerprint, why: route.why,
395
421
  blockedAssignment: route.blockedAssignment, suggestedAlternative: route.suggestedAlternative,
396
- confirmationTarget
422
+ confirmationTarget, taskPrompt
397
423
  };
398
424
  }
399
425
 
@@ -496,8 +522,8 @@ export function createConversationService(deps = {}) {
496
522
  // scoredAllRaw directly, so an old generation Cursor still
497
523
  // re-exposes (e.g. Claude Sonnet 4) naturally stops competing
498
524
  // without buildAiTeam/buildEfficientTeam's own ranking logic
499
- // needing to know why. Manual-only real candidates (Cursor,
500
- // OpenCode Go) stay in it — this is "what Kairo can honestly
525
+ // needing to know why. Manual-only real candidates (OpenCode Go,
526
+ // and Cursor's own opaque "auto" router model) stay in it — this is "what Kairo can honestly
501
527
  // recommend", not "what Kairo can launch by itself".
502
528
  const scoredAll = buildRecommendationPool(scoredAllRaw, completeCandidateCatalog);
503
529
  // The Automatic Execution Pool: the real subset of scoredAll
@@ -961,7 +987,7 @@ export function createConversationService(deps = {}) {
961
987
  const record = await readPlan(projectRoot, taskId);
962
988
  if (!record) throw new Error(`Plan "${taskId}" not found.`);
963
989
  const route = await this.routeProjectExecution(role, projectRoot);
964
- return { ...toExecutionPreview(route), projectRoot, taskId };
990
+ return { ...toExecutionPreview(route, record), projectRoot, taskId };
965
991
  },
966
992
  /**
967
993
  * @param {object} args
@@ -1010,13 +1036,7 @@ export function createConversationService(deps = {}) {
1010
1036
  if (!raced) throw error;
1011
1037
  return { ...publicPlan(record, raced), projectRoot, reused: true };
1012
1038
  }
1013
- const task = [
1014
- "Implement the explicitly approved architecture plan below.",
1015
- "Follow repository AGENTS.md and Gentle governance. Do not treat plan approval as any additional governance receipt.",
1016
- "Use safe, non-bypassed permissions for this session.",
1017
- "",
1018
- record.planMarkdown
1019
- ].join("\n");
1039
+ const task = buildExecutionTaskPrompt(record.planMarkdown);
1020
1040
  try {
1021
1041
  const started = await launchRun({
1022
1042
  homeDir, runId, agentId: resolvedAgentId, task, cwd: projectRoot, model: resolvedModel,
@@ -156,40 +156,32 @@ export const LOW_QUOTA_WARN_PERCENT = 20;
156
156
  * have the model via the Go subscription — without claiming Kairo can
157
157
  * safely auto-execute through it yet (see opencode.js's checkAvailability
158
158
  * for why: no per-event way to prove a run didn't silently bill Zen).
159
- * It also lets Cursor be named as a real recommendation (e.g. Builder ->
160
- * Cursor Composer 2.5) even though Kairo never auto-executes through it —
161
- * the user continues that work manually inside the Cursor IDE; this is
162
- * permanent for Cursor, not a temporary safety gap like opencode-go's.
163
- * Real task routing (selectExecutionProvider/selectAskProvider) always
164
- * uses the default `true` — it must never pick something guaranteed to
165
- * fail at launch (run-manager.js's own launchable gate would reject it).
159
+ * Cursor no longer needs this exemption: its own execution adapter
160
+ * (execution-adapters/cursor.js) builds a real, auditable non-interactive
161
+ * launch and Cursor's own docs support headless/CI use, so real task
162
+ * routing now judges it exactly like Codex/Claude same launchable gate,
163
+ * no special case. Real task routing (selectExecutionProvider/
164
+ * selectAskProvider) always uses the default `true` — it must never pick
165
+ * something guaranteed to fail at launch (run-manager.js's own
166
+ * launchable gate would reject it).
166
167
  */
167
168
  export function checkCandidate(adapterId, { adapters, codexUsage, claudeUsage, opencodeGoUsage, cursorManualQuota }, { requireLaunchable = true } = {}) {
168
169
  // Zen carries real PAYG/billing risk (see conversation/service.js's
169
170
  // capabilities.openCodeExecution) — never an automatic pick, regardless
170
171
  // of what its real catalog/benchmarks might otherwise say.
171
172
  if (adapterId === "opencode-zen") return { ok: false, reason: "OpenCode Zen is excluded from automatic routing (PAYG risk)" };
172
- // Cursor is a deliberate manual-only destination for real task
173
- // execution — recommendation and execution are different capabilities
174
- // (see model-intelligence.js's role assignments): Kairo can genuinely
175
- // recommend a real Cursor model for a role (Builder -> Cursor Composer
176
- // 2.5, say), the same requireLaunchable:false exception opencode-go
177
- // already gets below, but real task routing (requireLaunchable: true,
178
- // the default) always refuses it — the user continues that work
179
- // manually inside the Cursor IDE, never an automatic Kairo-launched run.
180
- if (adapterId === "cursor" && requireLaunchable) {
181
- return { ok: false, reason: "Cursor is manual-only — continue the work in the Cursor IDE, never auto-executed by Kairo" };
182
- }
183
173
 
184
174
  const adapter = findAdapter(adapterId, adapters);
185
175
  if (!adapter) return { ok: false, reason: `${adapterId}: no adapter found` };
186
176
  if (!adapter.available) return { ok: false, reason: adapter.reason ?? `${adapterId}: not available` };
187
177
  // "launchable" means safe for Kairo to invoke programmatically — not the
188
- // bar a manual recommendation needs. Cursor gets the same exemption
189
- // opencode-go already has: real availability is enough to recommend it,
190
- // even though (unlike opencode-go) it's exempt from launchable for a
191
- // structurally different reason it will NEVER be launchable, by design.
192
- const launchableRequired = requireLaunchable || (adapterId !== "opencode-go" && adapterId !== "cursor");
178
+ // bar a manual recommendation needs. OpenCode Go alone keeps the
179
+ // exemption: real availability is enough to recommend it, even though
180
+ // Kairo can't safely auto-execute through it yet (unresolved Go/Zen
181
+ // billing-attribution gapsee opencode.js). Cursor no longer needs
182
+ // this exemption its real launchability is judged the same way for
183
+ // both recommendation and execution now.
184
+ const launchableRequired = requireLaunchable || adapterId !== "opencode-go";
193
185
  if (launchableRequired && !adapter.launchable) return { ok: false, reason: adapter.reason ?? `${adapterId}: not launchable yet` };
194
186
 
195
187
  if (adapterId === "codex") {
@@ -209,11 +201,10 @@ export function checkCandidate(adapterId, { adapters, codexUsage, claudeUsage, o
209
201
  if (limited) return { ok: false, reason: `OpenCode Go ${limited.name} window is rate-limited` };
210
202
  }
211
203
  // Cursor: only ever the human's own last word (see this function's own
212
- // doc) — never fabricated from a guess. Checked here, after the
213
- // requireLaunchable-gated manual-only return above, so it only ever
214
- // takes effect on the recommendation path (requireLaunchable: false)
215
- // real task routing already refuses Cursor unconditionally regardless
216
- // of quota.
204
+ // doc) — never fabricated from a guess. Applies to BOTH the
205
+ // recommendation path AND real task routing now that Cursor is a real
206
+ // automatic candidate a human-reported "out of credits" must block
207
+ // an actual launch, not just a suggestion.
217
208
  if (adapterId === "cursor" && cursorManualQuota?.manualExhausted) {
218
209
  return { ok: false, reason: cursorManualQuota.reason ?? "Cursor marked out of credits (manual, via /project cursor exhausted)" };
219
210
  }
@@ -39,7 +39,7 @@ import { matchArtificialAnalysisScore } from "./model-intelligence.js";
39
39
  * @property {string} modelName - human-readable clean name, with real effort/context/privacy variant tokens stripped (see stripDisplayVariant). Never invented — always derived from the provider's own real displayName.
40
40
  * @property {string} rawDisplayName - the provider's own displayName, completely unmodified — the real evidence modelName was derived from. Whatever stripDisplayVariant peeled off (effort/context/privacy tokens) to produce modelName is still visible here, never a separate field: /models --evidence's own "technical detail" is just this string.
41
41
  * @property {string} adapterId - "codex" | "claude" | "cursor" | "opencode-go".
42
- * @property {"automatic"|"manual"} accessMode - whether Kairo can actually launch this candidate itself right now, or whether it's a real, recommendable option the human runs manually (Cursor always; OpenCode Go until its own empirical automatic-execution proof lands — see this module's own doc).
42
+ * @property {"automatic"|"manual"} accessMode - whether Kairo can actually launch this candidate itself right now, or whether it's a real, recommendable option the human runs manually (Cursor's own "auto" router model always; OpenCode Go until its own empirical automatic-execution proof lands — see this module's own doc). Named Cursor models are automatic.
43
43
  * @property {"scored"|"partial"|"unscored"} evidenceStatus - "scored": AA matched this exact model AND reports at least one of intelligenceIndex/codingIndex. "partial": AA matched it but both composite indices are null (real match, thin evidence). "unscored": no confident AA match at all. Never role-specific — see this module's own doc for why.
44
44
  * @property {string|null} lineageKey - real, recognized model family/lineage (see LINEAGE_PARSERS) — null when the modelId doesn't match any recognized, conservative pattern. Never guessed.
45
45
  * @property {number|null} generation - a real, comparable version number within that lineage — null whenever lineageKey is null.
@@ -285,12 +285,22 @@ function applyLifecycle(catalog) {
285
285
  // automatic-execution proof this session's plan calls for
286
286
  // (`opencode run -m opencode-go/<model>`, verifying real provider
287
287
  // attribution and Go-only consumption) actually runs and passes — see
288
- // this module's own doc. Cursor is permanently "manual" by design (see
289
- // execution-router.js's own checkCandidate: real task execution always
290
- // refuses it, recommendation never does).
291
- const ACCESS_MODE_BY_ADAPTER = { codex: "automatic", claude: "automatic", cursor: "manual", "opencode-go": "manual" };
288
+ // this module's own doc. Cursor is "automatic": its own execution
289
+ // adapter (execution-adapters/cursor.js) already builds a real,
290
+ // auditable non-interactive launch (`cursor-agent -p --output-format
291
+ // stream-json`) and parses its structured event stream, the exact same
292
+ // shape as Codex/Claude — Cursor's own docs explicitly support this
293
+ // (headless/CI use is an intended, documented capability, not a hack).
294
+ // The earlier "permanent manual" policy predated this adapter being
295
+ // finished and had no technical or billing reason behind it (unlike
296
+ // OpenCode Go's real, confirmed Go/Zen billing-attribution gap above).
297
+ const ACCESS_MODE_BY_ADAPTER = { codex: "automatic", claude: "automatic", cursor: "automatic", "opencode-go": "manual" };
292
298
 
293
299
  function resolveAccessMode(adapterId, modelId) {
300
+ // Cursor's own "auto" router picks whichever underlying model it wants
301
+ // per request — an opaque, non-deterministic identity Kairo can't
302
+ // attribute to a real scored model, so THIS one candidate stays manual
303
+ // even though named Cursor models are now real automatic candidates.
294
304
  if (adapterId === "cursor" && modelId === "auto") return "manual";
295
305
  return ACCESS_MODE_BY_ADAPTER[adapterId] ?? "manual";
296
306
  }
@@ -437,8 +447,9 @@ export function buildRecommendationPool(scoredAll, completeCatalog) {
437
447
  * The Automatic Execution Pool: the subset of the Recommendation Pool
438
448
  * Kairo can actually launch itself, right now — real routing's own
439
449
  * candidate source, never QUALITY/EFFICIENT TEAM's. Requires BOTH a real
440
- * accessMode of "automatic" (Cursor/OpenCode Go are permanently or
441
- * currently manual see ModelCandidateIdentity's own doc) AND real,
450
+ * accessMode of "automatic" (OpenCode Go is currently manual, and
451
+ * Cursor's own "auto" router model stays manual see
452
+ * ModelCandidateIdentity's own doc) AND real,
442
453
  * current eligibility (adapter availability, quota, launchability — the
443
454
  * exact same `eligibility` object checkCandidate/execution-router.js
444
455
  * already compute, reused here rather than reimplemented). Never
@@ -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.19.0" },
92
+ clientInfo: { name: "kairo", title: "Kairo", version: "0.21.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.19.0" },
154
+ clientInfo: { name: "kairo", title: "Kairo", version: "0.21.0" },
155
155
  capabilities: {}
156
156
  });
157
157
  });