@kal-elsam/kairo-runtime 0.20.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,20 @@ 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
+
8
22
  ## 0.20.0 — 2026-09-18 (Kairo Runtime)
9
23
 
10
24
  Minor release. Promotes Cursor to a real automatic execution provider.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.20.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) => {
@@ -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
 
@@ -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,
@@ -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.20.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.20.0" },
154
+ clientInfo: { name: "kairo", title: "Kairo", version: "0.21.0" },
155
155
  capabilities: {}
156
156
  });
157
157
  });