Skip to content

Commit c0e7da2

Browse files
committed
Fix Pi Captain intake delivery
1 parent 194e5e8 commit c0e7da2

19 files changed

Lines changed: 170 additions & 55 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
{
1212
"name": "kimiflow",
1313
"description": "Active-session feature, bug-fix & release loop for Claude Code with full/grill/plan/build/quick/review/audit/fix/release modes, compact intent clarification, adaptive discovery, autonomous verified local commits, audited project release profiles, code-review ensembles, project intelligence, repo docs, local findings, memory, Obsidian, and Vault support. Replies in your language.",
14-
"version": "0.2.39",
14+
"version": "0.2.40",
1515
"author": {
1616
"name": "kimikon"
1717
},

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "kimiflow",
33
"description": "Active-session feature, bug-fix & release loop for Claude Code with full/grill/plan/build/quick/review/audit/fix/release modes, compact intent clarification, adaptive discovery, autonomous verified local commits, audited project release profiles, code-review ensembles, project intelligence, repo docs, local findings, memory, Obsidian, and Vault support. Replies in your language.",
4-
"version": "0.2.39",
4+
"version": "0.2.40",
55
"license": "MIT",
66
"author": {
77
"name": "kimikon"

.codex-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "kimiflow",
3-
"version": "0.2.39",
3+
"version": "0.2.40",
44
"description": "Calm context-aware Kimiflow launcher and active-session feature/fix/release loop for Codex with natural mode aliases, safe workspace preflight, red/green, autonomous recovery, audited project release profiles, code-review, memory, Obsidian, and Vault gates.",
55
"author": {
66
"name": "kimikon",

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ Notable changes to **kimiflow**. Versions track `.claude-plugin/plugin.json`.
66

77
_No unreleased changes._
88

9+
## 0.2.40
10+
11+
The Pi Captain intake-delivery reliability patch.
12+
13+
### Fixed
14+
15+
- Pi workers now remove the exact internal Kimiflow transport envelope before forwarding a Captain reply to
16+
Active Run, so an accepted final product contract writes its durable intake receipt instead of asking again.
17+
- Calm mode hides every reserved Kimiflow transport-envelope version while the worker rejects unsupported or
18+
malformed versions explicitly, preventing internal transport text from leaking into the visible worker UI.
19+
- The Pi transport tests now exercise the real Python envelope generator against the real JavaScript worker
20+
parser and verify the chat-to-final-contract receipt path, closing the test-double gap that missed this bug.
21+
922
## 0.2.39
1023

1124
The single-authority Pi lifecycle simplification release.

COMPATIBILITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ subagent contracts. If a host moves one of these primitives, parts of kimiflow c
66
kimiflow concretely uses, what breaks if it changes, and a smoke checklist to run at each version bump.
77

88
**Last verified against:** Claude Code **2.1.202** · Codex CLI **0.142.5** · Pi **0.83.0** · kimiflow
9-
**0.2.39** · 2026-08-01.
9+
**0.2.40** · 2026-08-01.
1010

1111
> **0.x expectation.** These primitives are NOT a stable public contract. Treat breakage as *expected*
1212
> across Claude Code or Codex minor versions until a version is explicitly pinned — keep the README's

hooks/kimiflow_core/tests/test_active_run.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,51 @@ def test_contract4_schema2_final_correction_does_not_confirm(self):
219219
self.assertEqual(receipt["action"], "confirmed")
220220
self.assertEqual(receipt["stage"], "final")
221221

222+
def test_pi_chat_prompt_records_the_final_contract_receipt(self):
223+
run_rel = ".kimiflow/schema2-pi-chat"
224+
run_dir = os.path.join(self.root, run_rel)
225+
os.makedirs(run_dir)
226+
scope = self.schema2_scope()
227+
final = self.schema2_final()
228+
with open(os.path.join(run_dir, "INTAKE.md"), "w", encoding="utf-8") as handle:
229+
handle.write(scope)
230+
scope_intake = active_run.parse_intake_document(scope, 4, 1, "de")
231+
with open(os.path.join(run_dir, "INTAKE-RECEIPT-1.json"), "w", encoding="utf-8") as handle:
232+
json.dump({
233+
"schema_version": 2,
234+
"contract": 4,
235+
"round": 1,
236+
"stage": "scope",
237+
"action": "scope_ready",
238+
"request": "INTAKE.md",
239+
"request_digest": "sha256:" + hashlib.sha256(scope.encode("utf-8")).hexdigest(),
240+
"contract_digest": active_run.structured_intake_digest(scope_intake),
241+
"user_language": "de",
242+
"channel": "chat",
243+
"responded_at": "2026-07-31T12:00:00Z",
244+
}, handle)
245+
with open(os.path.join(run_dir, "INTAKE-2.md"), "w", encoding="utf-8") as handle:
246+
handle.write(final)
247+
active = self.schema2_active(run_rel, 2, final)
248+
active["host"] = "pi"
249+
active["owner"] = {"host": "pi", "session_id": "pi-worker-00000001"}
250+
active_run.write_active(self.root, active)
251+
252+
payload = json.dumps({
253+
"cwd": self.root,
254+
"session_id": "pi-worker-00000001",
255+
"prompt": "Vertrag bestätigen",
256+
})
257+
with mock.patch.dict(os.environ, {"KIMIFLOW_HOST": "pi"}):
258+
rc, _ = run_main(["prompt-context"], stdin_text=payload)
259+
260+
self.assertEqual(rc, 0)
261+
self.assertFalse(active_run.load_active(self.root).get("awaiting_user", False))
262+
with open(os.path.join(run_dir, "INTAKE-RECEIPT-2.json"), encoding="utf-8") as handle:
263+
receipt = json.load(handle)
264+
self.assertEqual(receipt["action"], "confirmed")
265+
self.assertEqual(receipt["channel"], "chat")
266+
222267
def test_contract4_schema2_rejects_language_drift(self):
223268
run_rel = ".kimiflow/schema2-language"
224269
run_dir = os.path.join(self.root, run_rel)

hooks/test-pi-kimiflow-e2e.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,12 @@ env.update({
125125
"KIMIFLOW_SESSION_HOST": "pi",
126126
"KIMIFLOW_SESSION_ID": session,
127127
})
128+
prefix = "\u2063kimiflow:transport-v1\n"
129+
header = "Authoritative Kimiflow workflow_context:"
128130
delimiter = "\n\nTransport request:\n"
129-
if delimiter not in args[-1]:
131+
if not args[-1].startswith(prefix + header) or delimiter not in args[-1]:
130132
raise RuntimeError("Kimiflow transport wrapper is missing")
131-
transport_prompt = args[-1].split(delimiter, 1)[1]
133+
transport_prompt = args[-1][len(prefix):].split(delimiter, 1)[1]
132134
133135
try:
134136
if "--prompt" in args or "--extension" not in args or "--no-extensions" not in args:

hosts/pi/extensions/calm.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
getKeybindings,
99
} from "@earendil-works/pi-tui";
1010

11-
const TRANSPORT_PREFIX = "\u2063kimiflow:transport-v1\n";
11+
const TRANSPORT_PREFIX_FAMILY = "\u2063kimiflow:transport-";
1212
const ASSISTANT_PATCH = Symbol.for("kimiflow:calm-assistant-layout:pi-0.83");
1313
const USER_PATCH = Symbol.for("kimiflow:calm-user-layout:pi-0.83");
1414
const TOOL_PATCH = Symbol.for("kimiflow:calm-operational-tools:pi-0.83");
@@ -60,7 +60,7 @@ function installOperationalToolLayout() {
6060
}
6161

6262
export function operationalInput(text) {
63-
return typeof text === "string" && text.startsWith(TRANSPORT_PREFIX);
63+
return typeof text === "string" && text.startsWith(TRANSPORT_PREFIX_FAMILY);
6464
}
6565

6666
function textOnly(content) {

hosts/pi/extensions/worker.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import { TextDecoder } from "node:util";
2929
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
3030
const WORKER = /^[A-Za-z0-9][A-Za-z0-9._-]{7,63}$/;
3131
const RUN = /^\.kimiflow\/(?!session(?:\/|$))(?!.*\/\.\.(?:\/|$))[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/;
32+
const TRANSPORT_PREFIX = "\u2063kimiflow:transport-v1\n";
33+
const TRANSPORT_PREFIX_FAMILY = "\u2063kimiflow:transport-";
34+
const WORKFLOW_CONTEXT_HEADER = "Authoritative Kimiflow workflow_context:";
35+
const TRANSPORT_REQUEST_DELIMITER = "\n\nTransport request:\n";
3236
const INTENT_DIGEST = /^sha256:[0-9a-f]{64}$/;
3337
const BRIDGE_ENV = "KIMIFLOW_PI_BRIDGE_BINDING";
3438
const ACTIVE_RUN_ENV = "KIMIFLOW_PI_ACTIVE_RUN";
@@ -1173,14 +1177,18 @@ export function forwardPromptContext(
11731177
?? context?.sessionId,
11741178
"worker_session",
11751179
);
1176-
const wrapper = "Authoritative Kimiflow workflow_context:";
1177-
const delimiter = "\n\nTransport request:\n";
11781180
let prompt = event?.prompt;
1179-
if (typeof prompt === "string" && prompt.startsWith(wrapper)) {
1180-
const boundary = prompt.indexOf(delimiter);
1181-
if (boundary >= 0) {
1182-
prompt = prompt.slice(boundary + delimiter.length);
1181+
if (typeof prompt === "string" && prompt.startsWith(TRANSPORT_PREFIX_FAMILY)) {
1182+
if (!prompt.startsWith(TRANSPORT_PREFIX)) {
1183+
throw new Error("prompt_context_transport_unsupported");
11831184
}
1185+
prompt = prompt.slice(TRANSPORT_PREFIX.length);
1186+
if (!prompt.startsWith(WORKFLOW_CONTEXT_HEADER)) {
1187+
throw new Error("prompt_context_transport_invalid");
1188+
}
1189+
const boundary = prompt.indexOf(TRANSPORT_REQUEST_DELIMITER);
1190+
if (boundary < 0) throw new Error("prompt_context_transport_invalid");
1191+
prompt = prompt.slice(boundary + TRANSPORT_REQUEST_DELIMITER.length);
11841192
}
11851193
if (typeof prompt !== "string" || Buffer.byteLength(prompt, "utf8") > MAX_TASK_BYTES) {
11861194
throw new Error("prompt_context_invalid");

hosts/pi/tests/calm.test.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ assert.equal(
9191
operationalInput("\\u2063kimiflow:transport-v1\\nrequest"),
9292
true,
9393
);
94+
assert.equal(
95+
operationalInput("\\u2063kimiflow:transport-v2\\nrequest"),
96+
true,
97+
);
9498
assert.equal(operationalInput("kimiflow:transport-v1\\nrequest"), false);
9599
96100
const tools = [];

0 commit comments

Comments
 (0)