@kal-elsam/kairo-runtime 0.22.0 → 0.23.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,43 @@ Historical entries below may reference the legacy `@kal-elsam/harness` package n
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.23.0 — 2026-09-18 (Kairo Runtime)
9
+
10
+ Minor release. First slice of PROJECT TEAM's automatic model-fallback
11
+ cascade.
12
+
13
+ ### Changed
14
+
15
+ - When a role's assigned model is unavailable but a real, currently-
16
+ eligible fallback exists, the cockpit no longer stops for an extra
17
+ y/n confirm on the substitution. The human already asked for this
18
+ task to execute now (picked the role, pressed execute) — Kairo runs
19
+ the real fallback automatically and narrates the substitution into
20
+ the transcript, so it stays visible without a confirmation prompt.
21
+ ROUTED, MANUAL_HANDOFF, and WAIT_FOR_PROJECT_TEAM with no real
22
+ alternative are all unchanged — still explicit confirm steps.
23
+
24
+ Not yet in this release: showing which model is currently running for
25
+ a role directly in the PROJECT TEAM view, auto-reverting to the
26
+ originally-assigned model once it becomes eligible again, and
27
+ retrying a run that starts on the assigned model and then fails or
28
+ hangs mid-execution.
29
+
30
+ ## 0.22.1 — 2026-09-18 (Kairo Runtime)
31
+
32
+ Patch release.
33
+
34
+ ### Fixed
35
+
36
+ - Execution links no longer hardcode `provider: "claude"` on every
37
+ write regardless of which real adapter (Codex, Cursor, OpenCode Go,
38
+ Claude) actually executed the task. `provider` now mirrors the
39
+ link's own real `agentId`, matching what `service.js`'s
40
+ `executionFor` already treated as ground truth at read time. The
41
+ cockpit reads this field directly for its provider display, so every
42
+ task's shown execution provider had always silently said "claude"
43
+ regardless of what actually ran it.
44
+
8
45
  ## 0.22.0 — 2026-09-18 (Kairo Runtime)
9
46
 
10
47
  Minor release. Promotes OpenCode Go to a real automatic execution
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.22.0",
3
+ "version": "0.23.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",
@@ -307,23 +307,41 @@ export async function readExecutionLink(projectRoot, taskId) {
307
307
  const stat = await statOrNull(paths.executionPath);
308
308
  if (!stat) return null;
309
309
  const link = JSON.parse(await readFile(paths.executionPath, "utf8"));
310
+ // `provider` is real, structural validation only (a non-empty real
311
+ // string) — never a single hardcoded value. PROJECT TEAM can route a
312
+ // real task to any real adapter (Codex, Claude, Cursor, OpenCode Go),
313
+ // and a legacy artifact from before that existed still carries "claude"
314
+ // for real, so this must accept every real one honestly, never lie
315
+ // by forcing them all to read back as "claude".
310
316
  if (link?.schema !== EXECUTION_SCHEMA || link?.taskId !== taskId
311
- || !/^run_[a-z0-9_]+$/.test(link?.runId ?? "") || link?.provider !== "claude") {
317
+ || !/^run_[a-z0-9_]+$/.test(link?.runId ?? "") || typeof link?.provider !== "string" || link.provider.length === 0) {
312
318
  throw new Error(`Invalid execution artifact: ${taskId}`);
313
319
  }
314
320
  return link;
315
321
  }
316
322
 
323
+ /**
324
+ * `provider` always mirrors the link's own real `agentId` — the field a
325
+ * real caller (executePlan) actually sets, and the one PROJECT TEAM's own
326
+ * routing decision already named. Never a fabricated or hardcoded value;
327
+ * a legacy caller that never set agentId falls back to "claude" only
328
+ * because that's genuinely what every execution WAS before PROJECT TEAM's
329
+ * multi-provider routing existed — never a guess for real, current data.
330
+ */
331
+ function resolveExecutionProvider(link) {
332
+ return link?.agentId ?? link?.provider ?? "claude";
333
+ }
334
+
317
335
  export async function writeExecutionLink(projectRoot, taskId, link) {
318
336
  const paths = await prepareTaskDirectory(projectRoot, taskId);
319
- const value = { ...link, schema: EXECUTION_SCHEMA, taskId, provider: "claude" };
337
+ const value = { ...link, schema: EXECUTION_SCHEMA, taskId, provider: resolveExecutionProvider(link) };
320
338
  await writeAtomicJson(paths.executionPath, value, { createExclusive: true });
321
339
  return value;
322
340
  }
323
341
 
324
342
  export async function updateExecutionLink(projectRoot, taskId, link) {
325
343
  const paths = await prepareTaskDirectory(projectRoot, taskId);
326
- const value = { ...link, schema: EXECUTION_SCHEMA, taskId, provider: "claude" };
344
+ const value = { ...link, schema: EXECUTION_SCHEMA, taskId, provider: resolveExecutionProvider(link) };
327
345
  await writeAtomicJson(paths.executionPath, value);
328
346
  return value;
329
347
  }
@@ -189,6 +189,24 @@ export async function runCockpitApp({
189
189
  // handleListInput — 'x' never calls this without one).
190
190
  return runAction("Asking PROJECT TEAM who should execute this", async () => {
191
191
  const decision = await service.planExecution({ cwd, taskId, role });
192
+ // WAIT_FOR_PROJECT_TEAM with a real suggested alternative: the
193
+ // human already explicitly asked for this task to execute now
194
+ // (picked the role, pressed execute) — the assigned model just
195
+ // isn't the one that got them there. Kairo runs the real,
196
+ // currently-eligible fallback automatically, with zero extra
197
+ // y/n gate, and narrates the substitution into the transcript
198
+ // so it stays visible even without a confirm prompt. Every
199
+ // other decision (ROUTED, MANUAL_HANDOFF, or WAIT_FOR_PROJECT_TEAM
200
+ // with no real alternative) is unchanged — still an explicit
201
+ // confirm step.
202
+ if (decision.decision === "WAIT_FOR_PROJECT_TEAM" && decision.confirmationTarget?.selection === "suggested-alternative") {
203
+ const blockedLabel = decision.blockedAssignment?.model?.displayName ?? decision.blockedAssignment?.model?.modelId ?? decision.blockedAssignment?.provider ?? "the assigned model";
204
+ const alt = decision.suggestedAlternative;
205
+ const altLabel = alt?.model?.displayName ?? alt?.model?.modelId ?? "unknown model";
206
+ pushTranscript("kairo", `${blockedLabel} is unavailable for ${decision.role} — automatically falling back to ${alt?.provider} · ${altLabel}.`);
207
+ await service.executePlan({ cwd, taskId, confirmationTarget: decision.confirmationTarget });
208
+ return;
209
+ }
192
210
  view.showExecuteConfirm(taskId, decision);
193
211
  // MANUAL_HANDOFF: Kairo can't launch this itself, so the real
194
212
  // task text (the exact same one an automatic run would get —
@@ -86,9 +86,13 @@ function blocked(role, strategyFingerprint, why, { blockedAssignment = null, sug
86
86
  * `suggestedAlternative` set to the strategy's own PERSISTED fallback
87
87
  * for this role (see project-strategy.js) when that fallback is
88
88
  * itself currently real-eligible and automatically executable —
89
- * otherwise honestly null. Never a silent automatic substitution: the
90
- * caller must still explicitly confirm before anything runs on the
91
- * suggested alternative.
89
+ * otherwise honestly null. This module never launches anything itself
90
+ * it only ever decides what WOULD run but by explicit, deliberate
91
+ * product decision the cockpit caller (app.js's onRequestExecute) does
92
+ * treat a real suggestedAlternative as automatically executable once
93
+ * the human has already asked for this task to run, narrating the
94
+ * substitution into the transcript instead of gating it behind an
95
+ * extra y/n prompt.
92
96
  * 5. Otherwise -> ROUTED, with the real provider/model/assignmentSource
93
97
  * this role delegates to.
94
98
  * @param {object} args
@@ -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.22.0" },
92
+ clientInfo: { name: "kairo", title: "Kairo", version: "0.23.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.22.0" },
154
+ clientInfo: { name: "kairo", title: "Kairo", version: "0.23.0" },
155
155
  capabilities: {}
156
156
  });
157
157
  });