Support Kimi Code CLI as a builder (PIR #1201) - #1203
Conversation
…ilder and architect Seed-session bootstrap (kimi -p role seed -> capture session id from stream-json -> TUI resume via -S) validated end-to-end; solves role injection, initial prompt delivery, and the stored-ID session contract. Includes reproducible POC script and full impact map / test matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cluesmith#1149 parity requirement Two new observations: Kimi TUI never enters the alternate screen (no escape-based readiness signal), and PTY input during the seed window has no defined consumer (silently lost). Barrier design: sentinel + grace + store-verified delivery with retry; seed carries role+task, kick is a single BEGIN line. Architect parity correction: stored-ID resume without an async-buildable CrashLoopFallback is cap-exhaustion outage, not cluesmith#1149 safety — ship Codex-like (stage 1) or stored-ID + async fallback (stage 2), no middle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ipt, builder resume
- KIMI_HARNESS in harness.ts + detectHarnessFromCommand('kimi') — kills the
cluesmith#1062 false-Claude fallthrough; buildRoleInjection throws (builder-only)
- New optional HarnessProvider.buildBuilderLaunchScript capability; Kimi
generates the seed-session bootstrap script (idempotent seed via kimi -p
stream-json, session.resume_hint capture, sentinel, pinned -S --yolo loop)
- kimi-session-discovery.ts: store scan / ownership verify / state reader
(undocumented store layout, observed on kimi 0.27.0; fail-soft)
- buildResume: .builder-kimi-session precedence (ownership-verified) → store
scan → null → fresh-with-role fallback
- spawn-worktree branches on the capability; writes .builder-seed.txt and
passes the seedKick request through createPtySession
- Tests incl. the cluesmith#929-class regression: kimi + stale Claude jsonl never
yields --resume <claude-uuid> or --append-system-prompt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ss Enter pacing - seed-kick.ts: readiness barrier — waits for the launch script's __CODEV_KIMI_SEED_DONE__ sentinel (writes during the seed window are silently lost), grace, then a store-verified BEGIN kick with an Enter-resend → kick-resend → loud-warn retry ladder - createTerminal grows an optional seedKick field (core SeedKickRequest); handleTerminalCreate validates and arms it (malformed → ignored) - message-write.ts: optional pacing.enterDelayMs overriding both default Enter delays (Kimi swallows an 80ms Enter; defaults unchanged otherwise) - message-pacing.ts: resolves pacing per target — worktree marker probe first (override-proof for --builder-cmd spawns, survives Tower restarts), then config-resolved harness by terminal role - Wired at all delivery paths: send direct + buffered, cron Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…store smoke probe - Kimi in AI_DEPENDENCIES: kimi --version presence with minVersion 0.27.0 (pins the version the undocumented surfaces were observed against) - verifyKimi(): credential-artifact heuristic (no billed probe — Kimi documents no auth status command), kimi login guidance; supplementary 'kimi doctor' config check (documented exit codes, not an auth check) - Session-store layout smoke probe warns loudly on drift - Architect-shell branch: kimi configured as architect → builder-only warning Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mples, skeleton mirror) - arch.md: dedicated Kimi subsection (builder-only, seed-session bootstrap, sentinel-gated store-verified BEGIN, per-harness pacing, explicit-ID resume, undocumented-surface caveats + 0.27.0 pin, NO write-guard parity) - agent-farm.md (instance + skeleton): builder-harness config examples Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on session type - resolvePacingForSession wraps its whole body in try/catch: pacing is advisory and must never break message delivery (a missing DB in the tower-routes test env surfaced this as 500s on /api/send) - CronDeps session shape carries id/cwd (the real PtySession provides both) - tower-routes test mock gains getTerminalSessionById Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bisected on kimi 0.27.0: 80ms and 100ms swallowed; 120/250/500/1000ms submit. Threshold ~100-120ms; shipped value stays 1000ms (~9x margin). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s pass) codev/spikes/pir-1201-kimi-builder-demo.mjs runs the real dist modules (script generator, armSeedKick, writeMessageToSession, buildResume) against a real kimi PTY: seed bootstrap, sentinel-gated store-verified BEGIN, multiline delivery at the pinned Enter delay, inner-restart context retention, and -S resume. Executed against kimi 0.27.0 — 5/5 PASS; the ack-and-wait-with-task seed discipline held (no fallback needed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ier) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… consultation finding) The delivery check used lastPrompt.includes(kickMessage) — but on a fresh spawn lastPrompt initially holds the SEED prompt, whose ack-and-wait wrapper itself mentions BEGIN, so the verifier reported success before the kick ever submitted (silently defeating the swallowed-Enter recovery; the live demo's happy path masked it). Confirmation now requires whitespace-normalized EQUALITY (submitted messages land in lastPrompt with newlines flattened to spaces — observed on kimi 0.27.0). Two pinning regression tests added; live demo re-run post-fix: 5/5 PASS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Architect Integration ReviewContributor-side review summary for maintainers (this PR was developed under the PIR protocol on our fork; we do not merge — that call is yours). Process: Plan and dev-approval were human-gated pre-PR. The dev-approval gate included a full-path live demo through a locally installed build — a real Consultation (CMAP, single advisory pass): gemini APPROVE, claude APPROVE, codex REQUEST_CHANGES. The codex finding was real and was accepted + fixed in 732f04b: seed-kick delivery confirmation was a substring match on Architect verification of the post-CMAP fix (since PIR's single-pass consultation does not re-review fixes): I reviewed Scope: verified against issue #1201's builder-MVI checklist — no architect-parity changes ( Architect integration review |
… for maintainers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cepted+fixed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
waleedkadous
left a comment
There was a problem hiding this comment.
Outstanding work — this is one of the most disciplined external PRs we've received; the observed-vs-documented honesty around Kimi's store and the store-verified BEGIN ladder are exactly right. I'd like to see one change before merge: the bare launch path (no role, no prompt) doesn't persist .builder-kimi-session, so pacing resolution falls back to workspace config and an override-spawned bare Kimi builder gets Claude's Enter timing — the swallowed-Enter bug this PR fixes. Persisting a marker on every Kimi launch shape (plus a regression test for the override-spawn case, and softening the arch.md "exists iff Kimi-shaped" claim) closes it. I've added the area/tower label to #1201 for you.
…launch shape Maintainer review (PR cluesmith#1203): the bare no-role/no-prompt shape never wrote .builder-kimi-session, so pacing resolution fell back to the config-resolved harness and an override-spawned bare Kimi builder got claude's 80ms Enter — the swallowed-Enter bug this PR fixes. The bare branch now touches the marker (empty; preserves a previously seeded id; seed `! -s` guard and resume empty-id fallthrough unaffected), so every Kimi launch shape persists it. Regression tests: bare-script marker pins at the harness and spawn-worktree layers (both fail pre-fix; the spawn-worktree one previously asserted the marker's ABSENCE), plus a real-fs pacing test pinning the probe as existence-based (empty marker beats claude config). arch.md and the message-pacing.ts header now carry the softened, accurate claim: every launch shape persists the marker; presence is a breadcrumb, not an iff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Architect Response — maintainer review addressed (2abd362)Thank you for the review — the finding is real and is fixed in 2abd362. The gap, confirmed: Fix: the bare branch now Regression tests (all fail pre-fix where noted):
Docs: arch.md's pacing paragraph and the Suites: harness 75/75, spawn-worktree 86/86, message-pacing 8/8, seed-kick 14/14; package build clean. Architect integration review CMAP results (iterated to convergence)The maintainer-response change set was reviewed by a 3-way CMAP loop (gemini, codex, claude), re-run after every change until a full pass returned zero findings: iteration 1 reviewed the fix (2abd362), iteration 2 the resulting test-hardening commit (642b172), and iteration 3 the entire cumulative response diff (47d12ba..1de55e1) as a holistic convergence check.
Loop terminated at iteration 3: unanimous APPROVE, zero findings, on the full change set. Suites green throughout (harness 75, spawn-worktree 86, message-pacing 8, seed-kick 14); package build clean. |
…pe marker tests Post-fix CMAP on 2abd362: gemini APPROVE, claude APPROVE, codex APPROVE with one MINOR — the regression tests asserted `touch .builder-kimi-session` appears in the generated script but not that it precedes the restart loop, so a refactor moving it after/inside the loop would keep them green while racing the first send. Both script-shape pins now assert the ordering, matching the suite's existing exit-1-before-loop precedent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rtions (CMAP iter 2) CMAP iteration 2 on 642b172: 3x APPROVE with two complementary NITs, both accepted: the pre-existing exit-1-before-loop assertion lacked a toContain('exit 1') guard (removing exit 1 would vacuous-pass — indexOf returns -1, and -1 < anything), and the new touch-before-loop assertions lacked toContain('while true') (sound — a positive index is never < -1 — but the failure diagnostic would be opaque). Every ordering assertion in the kimi script-shape tests now guards both of its substrings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, zero findings) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve the tower client re-export conflict by retaining the Husk types from main alongside SeedKickRequest from PIR cluesmith#1201. Co-Authored-By: Claude <noreply@anthropic.com>
|
Heads-up for your next iteration: PR #1244 (in review) changes the builder launch-loop contract — a deliberate agent exit (exit 0, no signal; see |
# Conflicts: # packages/codev/src/agent-farm/commands/spawn-worktree.ts
…n Kimi provider-owned loops Deliberate exit 0 now gates relaunch on a keypress instead of blind auto-respawn, matching the post-cluesmith#1244 builder launch-loop contract. LAUNCH_LOOP_TAIL moves to utils/harness.ts (exported) so provider-owned scripts share it without a circular import; tests pin the tail across all Kimi launch shapes (fresh/resume/bare, seeded/bare interactive). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…il adoption Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#1244 launch-loop contract adopted (bfc8d62)Per the heads-up above: merged Change: Tests: new Live verification (kimi 0.29.1, tmux PTY, the actual bare launch script generated from built
CMAP results (gemini, codex, claude — parallel review of the merge-resolution + adoption change set; clean in one iteration):
Ready for merge whenever you are. |
# Conflicts: # codev/resources/commands/agent-farm.md
|
Heads-up on the exit-handling contract I asked this PR to adopt — it just evolved, and I want you building against the current state rather than the deprecated one.
Practical upshot for this PR: mirror the current builder bash-loop behavior (Enter-gated relaunch on clean exit, auto-restart on crash) rather than the architect-side contract, and keep an eye on #1267 — when it lands, Kimi loops should move with it. Happy to review again whenever you've iterated! |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
… top [PIR cluesmith#1201] The branch had been parked on two upstream blockers and went 900+ commits stale. Merging up brings in three changes that the Kimi integration was built against the absence of, so the feature is reworked rather than replayed: - Spec 1313 (cluesmith#1330) made `afx send` mailbox-first: messages persist first and deliver only onto a render-gate-verified empty prompt. Message writers never write a PTY directly. This retires the whole seed-kick mechanism. - cluesmith#1317/cluesmith#1267 made a builder's clean-exit relaunch run the harness fresh. - cluesmith#1356 (PIR cluesmith#1233) gave crash restarts a session-aware resume contract. Conflict resolution: took main's rewritten spawn-worktree.ts, tower-routes.ts, tower-cron.ts, tower-client.ts and discover-resume-session.test.ts wholesale — our versions were the retired SendBuffer / direct-PTY-write paths and a launch loop that cluesmith#1233/cluesmith#1317 superseded — then re-applied the Kimi seams by hand. doctor.ts and the three docs were hand-merged. The design pivot itself (validated live against real kimi 0.34.0 before any of it was committed): - Role now rides `--agent-file` (kimi 0.31.0+), composed around `${base_prompt}` so it EXTENDS kimi's own system prompt instead of replacing it. Previously the role rode a user turn via a `kimi -p` seed session. - Task now rides the Spec 1313 mailbox and is delivered by the render gate onto a verified-empty composer, never a direct PTY write. - Crash restarts resume with the documented cwd-scoped `kimi -c`, gated on an inlined store probe. This guard is load-bearing: `kimi -c` does NOT fail when there is nothing to continue — it starts a fresh session that never saw `--agent-file`, i.e. a silently ROLELESS builder (cluesmith#929 hazard class). The probe fails closed to a role-carrying fresh launch, and tests EXECUTE it against fixture stores and cross-check it against findLatestKimiSessionId so the hand-written snippet cannot drift from the TypeScript it mirrors. - Deleted seed-kick.ts, the sentinel, the seed bootstrap, .builder-seed.txt, the ack-and-wait BEGIN discipline, and the now-dead SeedKickRequest SDK surface. Pacing was left wired to nothing by the merge (Spec 1313 replaced the routes message-pacing.ts hooked into), which would have meant every `afx send` to a Kimi builder was typed but never submitted. It is re-homed onto the mailbox delivery path (resolvePacingForSession in mailbox-wiring.ts), and both message-pacing.ts and the `.builder-kimi` marker are deleted: the harness now comes out of the generated .builder-start.sh, which is generated FROM the resolved harness and so cannot be forgotten — the marker's coverage obligation is exactly what the maintainer's earlier review finding was about. Shared-code edit, flagged for review: render-gate.ts's marker exemption now follows the profile's matched span instead of column 0, because kimi's marker sits at column 3 inside a rounded box. Carries before/after pinning tests for claude/codex/agy plus three real 0.34.0 gate captures as fixtures. Also: store-drift fix (workDir->cwd, ISO->epoch-ms, lastPrompt gone, with v1 back-compat), a trust-record drift probe that validates our undocumented derivation against kimi's own records, version floor 0.27.0 -> 0.33.0, and the correction that kimi DOES have a blocking PreToolUse hook seam (so cluesmith#1018 write-guard parity is achievable follow-up, not impossible). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The render gate delivers a message only onto a composer it can prove empty. For kimi it could not: KIMI_MARKER matches `│ >`, findMarkerRow takes the LAST match, and the scan started at that row — so a draft whose final line begins with `>` (a pasted quote, a markdown blockquote) put the marker on the CONTINUATION row and left the real text above the scanned region. The composer classified clean while holding unsent input, and a queued message would have been typed on top of it: the exact corruption Spec 1313 removes by construction. Found by the 3-way review, which reproduced it on a constructed screen and flagged that it could not confirm kimi's real multi-row geometry. Measured against real kimi 0.34.0 — it renders exactly that shape, so the defect is reachable, not theoretical. GateProfile gains an optional regionStartPatterns: an upper bound for the composer region, which kimi sets to the box top. The bound is EXCLUSIVE, mirroring the region end — the box-top row's right corner `╮` is not an ignorable glyph, so including that row counted it as user text and held every idle composer forever (caught by the fixture suite on the first attempt). Profiles that declare no region start keep scanning from the marker row exactly as before, and since no row below a last match can match, claude/codex/agy cannot reach any of the new behavior. The other input the review proposed — a marker-matching row below the composer in a second box — is NOT reachable in the shipped UI: measured, kimi's `/` menu renders as unclosed `│` rows with no `╰` beneath them, so a marker inside it yields no-region-end and holds. Captured as a fixture rather than argued. Four new fixtures, all from live capture: multiline-bare (the false CLEAN itself), multiline, menu, picker — the last two also answering the review's point that kimi shipped 3 fixtures where claude/codex ship menu and picker.
…inues The inlined `node -e` store probe and findLatestKimiSessionId are the same question in two languages, and the cross-check test compared them against each other — agreement between duplicated omissions, not validation against kimi's continuation semantics. Two reviewers broke it from opposite directions: - DANGEROUS: an `archived: true` session matched on cwd alone, so the probe authorized `-c`. Kimi excludes archived sessions from the listing `-c` continues from, so it starts a FRESH session instead — one that never saw --agent-file, i.e. a silently roleless builder. That is the cluesmith#929 hazard class this guard exists to prevent, reached through the guard itself. - SAFE BUT HARMFUL: readdirSync on a stray non-directory threw ENOTDIR into the single OUTER try, aborting the whole scan. One .DS_Store in the store disabled resume machine-wide, permanently and silently — a builder that crashed four hours in would restart with no context and its task re-queued. Same for a symlinked worktree and a trailing slash on the recorded cwd. Both implementations now share one resumability predicate — archived !== true and a `session_`-prefixed id, the filters kimi's own listing applies — plus sameDir's realpath tolerance, and each directory level gets its own try. Both filters err toward "not resumable", whose fallback is the role-carrying fresh launch. The predicate is deliberately NOT folded into iterateSessionDirs: inspectKimiStoreLayout must keep seeing unrecognized ids, because reporting that drift is its whole job. Every listed case is now a test asserting BOTH implementations. Also in the generated launch script: - Shell metacharacters. All three reviewers flagged the recovery hints, which interpolated builderId/taskFile into double-quoted bash echoes where bash re-scans them, so `$(…)` in a builder id executed when the hint printed. Every value now enters the script once as a single-quoted escaped assignment and is used through the shell variable; hints print via printf on the expansion, which bash does not re-scan. Pinned by running the generated function with a metacharacter id and asserting nothing executed. - Unbounded re-queueing. codev_launch_fresh queues the task, so a kimi dying before it minted a session re-queued the same mission every ~2s. The mailbox PERSISTS a held row, so one enqueue is enough; the guard resets only on the human-gated clean-exit relaunch, which is a deliberate new conversation and does want its task again. And in the doctor probes: both reported ok if ANY record matched, so after a store migration the pre-migration records hide every new one and the probe reports healthy through exactly the rename it exists to catch. Drift is now reported only when the newest non-conforming record is strictly newer than every conforming one; ties stay ok so the verdict never depends on directory iteration order. verifyKimi no longer reports "config issues" when spawnSync returns status null (spawn failure or timeout), which accused a healthy install on a slow machine, and two user-facing strings that still described the retired seed-session bootstrap now describe --agent-file.
arch.md's Kimi section gains the facts this round established: the composer region's upper bound and why it is exclusive, the resumability filters and the probe divergences they close, the recency rule in both drift probes, and the single-quoted-assignment discipline in the generated script. The demo's two failing steps were the DEMO's fault, not the product's. Its role told the model to prefix every reply with a token and asserted on the prefix — which measures whether K3 honors a persistent output-format constraint, not whether the role was injected. Measured: it answered the task correctly while dropping the prefix, and when asked about its prefix it discussed the idea rather than emitting the token. The live --agent-file probe, run against a production-identical agent file, passed 7/7 including role survival across `kimi -c`. The demo now asks for a codeword — the same oracle the probe uses — with a comment saying why, so the weaker one does not come back. The measurement harness gains the screens the review said a happy-path run never produces: a multi-line draft, the same draft ending in a bare `>`, the `/` menu, and the `@` file picker. Those captures are what settled which of the two proposed false-CLEAN inputs was real. Commits the three probe scripts that arch.md and the profiles cite as evidence; they were untracked, so the "measured, see harness X" chain would have dangled after merge.
This comment was marked as outdated.
This comment was marked as outdated.
Architect review finding 1. Enter a newline then `>` and kimi renders `│ > ` / `│ >`: row one empty, row two matching the marker so its `>` is span-exempted. Every cell is whitespace, box chrome, or an exempted marker, so userCells is 0 and the composer reads CLEAN while holding unsent input — a held message would be typed on top of the user's draft. Bounding the region correctly does not help; the draft is real but literally uncountable. So hold on the region's SHAPE instead: a boxed composer taller than one interior row is a multi-line draft. That is only sound if box growth is exclusive to multi-line drafts, which is a claim about kimi, not about our code — so it was measured on real 0.34.0 first (pir-1201-kimi-box-growth.mjs). Idle, single-line draft, `/` menu, `@` picker and the post-reply steady state all hold at one interior row; only the newline drafts grow. The steady state is load-bearing: growth there would hold every later message forever, a liveness bug rather than a fail-safe one. The working states were measured too (pir-1201-kimi-working-states.mjs) — mid- generation, mode chrome, and a draft typed while the agent works are all one row, so "deliver while busy" does not silently become "hold until idle". Placed AFTER the cell scan, not before it, so the count keeps its ground-truth role: a text-bearing multi-row draft still reports `user-text` and every pre-existing fixture verdict is unchanged. Armed by a dedicated `growsWithDraft` profile field rather than by `regionStartPatterns` (CMAP: codex #1, claude Q5). The two are unrelated properties that merely coincide for kimi, and the hazard is concrete: the shipped codex-idle.clean.txt capture — a real, genuinely EMPTY composer — already spans two interior rows, so arming on the scan bound would have killed codex delivery the day anyone declared a region start for it. The rule now needs both opt-ins, and the inertness tests run on that real capture under four profile variants. Also: `isClassifierStuck` enumerated details as a closed || chain, so widening the union never forced a decision — now a Record keyed by the union, making the next new detail a compile error rather than a silent false (claude F2). Full suite 4906 passed / 48 skipped / 0 failed.
Architect review findings 2 and 3, both message/comment only — no behavior change. The fast-fail branch echoed "Starting a fresh conversation with the original task", but it does not reset codev_task_queued, so codev_queue_task early-returns and nothing is re-queued. That behavior is correct — an undelivered row persists on the mailbox and re-queueing would duplicate it — the message was simply wrong, and in the delivered-then-crash-looping case it would tell an operator the fresh session has its mission when it does not. The first rewording then overcorrected: it asserted unconditionally that a task was still queued, which is false when `afx send` never succeeded (afx off PATH, Tower down), since the flag is only set on success and the fresh launch really does retry in that case (CMAP: codex cluesmith#2, claude F4). The hint now branches on the flag and is accurate in both cases. Finding 3 records the accepted tradeoff in the other direction: the clean-exit branch does reset the flag, so a row that was never delivered gets queued twice and the mission arrives stated twice. Documented rather than fixed — de-duplicating needs either a delivery receipt the script cannot see or a mailbox-side identity check, and a duplicated instruction to an agent that has not started yet is recoverable by reading, unlike the crash-loop direction. Softened "delivered whenever the operator saw a composer": seeing one is necessary, not sufficient — the gate also has to have polled it empty, so a quit-before-delivery race leaves the row held too (CMAP: codex cluesmith#3, claude F5).
…ositions Builder thread plus the full disposition record for this round: gemini APPROVE, codex REQUEST_CHANGES, claude APPROVE-with-changes, every finding from both non-approving reviews accepted and none rejected. Notes the two deviations worth knowing about — the geometry rule moved after the cell scan rather than short-circuiting before it, and arming decoupled from regionStartPatterns onto its own field after codex-idle.clean.txt turned out to already satisfy the geometric predicate while being genuinely empty.
Architect finding 4. cluesmith#1267's contract is "clean exit -> fresh rerun, no recovery", and main's claude loop enforces it BY IDENTITY: a clean exit mints a new session id and the superseded one is never named again. kimi cannot mint on demand and `-c` is cwd-scoped, so identity was never pinned — the guard only asked whether ANY session existed for this cwd. 0.33+ mints no session until the first message lands, so a crash between a clean-exit relaunch and the first delivery found the just-ended conversation still the newest, continued it, and delivered the re-queued task into the conversation the human walked away from — cluesmith#1267's own motivating defect class. The probe now answers WHICH session rather than WHETHER one exists: it prints the newest resumable id for the cwd. The clean-exit branch records that id, and the crash branch takes `-c` only once the newest id differs. Boolean uses derive from "printed something", so there is still one probe and one mirror. Measured before building, since the design assumes `-c` targets the newest session and the existing probe only covered the zero-session case: two live sessions in one cwd on 0.34.0, two oracles — content (codewords ALPHA/BRAVO -> BRAVO) and store identity (only the newest session's dir was touched, nothing new minted, exit 0, no prompt). pir-1201-kimi-continue-newest-probe.mjs. CMAP found a defect this change INTRODUCED (claude F1, codex cluesmith#2, blocking): moving the decision from $? onto stdout meant anything else writing to stdout counted as a session. Measured with NODE_OPTIONS=--require preloading a module that prints — the probe exits 1, the script read RESUME, and `kimi -c` with nothing to continue starts a session that never saw --agent-file. Silently roleless, the cluesmith#929 class, produced by the guard's own upgrade. Now consumes both signals. The sketch's "empty on any error is fail-closed" was also wrong (claude F2, codex #1): a TRANSIENT probe failure records '' and the next crash sees the ended session as different-from-empty. Failure and empty store are now told apart by status, and an unknown baseline blocks resume until the next clean exit re-establishes one. Two probe/discovery divergences fixed rather than documented: `j.cwd ?? j.workDir` short-circuited where readStateJson falls through per-field, and the probe stripped a trailing slash before realpathSync while sameDir does not (the unsafe direction — realpathSync already normalizes one for any directory that exists, so the strip bought nothing). Tests: the composition is now driven for real — the actual `while` loop with stubbed launches, asserting resume,fresh,fresh — because injecting the superseded id from the test left the generated assignment pinned only by a string match. Non-vacuity is demonstrated by running the pre-fix predicate against the same store. Full suite 4915 passed / 48 skipped / 0 failed.
…spositions arch.md's kimi crash-resume section described an existence guard; it now describes the identity one, the measured fact it rests on (`kimi -c` continues the newest session for a cwd), and the two accepted residuals — the in-memory superseded id (contract parity with claude's per-process minted id) and a store GC that evicted newest-first. Plus the builder thread and the full CMAP disposition record: gemini APPROVE, codex and claude REQUEST_CHANGES, every finding accepted, including the blocking one that this round's own change introduced.
PIR Review: Support Kimi Code CLI as a builder
Fixes #1201
Summary
Adds the Kimi Code CLI (
kimi, ≥ 0.33.0) as a supported builder harness —shell.builder: "kimi"/builderHarness: "kimi"/--builder-cmd kiminow produce a working builder instead of the #1062 false-Claude fallthrough (which appended--append-system-promptand a positional prompt, both rejected by kimi, and could route a stale Claude--resume <uuid>into it).Kimi has no system-prompt flag and takes no positional prompt, so role and task travel on two different channels:
--agent-file(kimi 0.31.0+).getWorktreeFileswrites an agent-definition file into the worktree whose body wraps the role around${base_prompt}— the token that interpolates kimi's own default system prompt, so the role extends rather than replaces it. This is theclaude --append-system-promptanalogue.afx send, and the render gate delivers it onto a verified-empty composer. Never a direct PTY write, so a boot screen, a busy line, or kimi's folder-trust dialog holds the message rather than corrupting or losing it.Crash restarts resume with the documented, cwd-scoped
kimi -c— no session id is ever baked into generated bash. Kimi as an architect remains out of scope (stage 2);buildRoleInjectionthrows anddoctorwarns, so misconfiguration fails loudly rather than falling through to claude flags.The finding that shaped the design
kimi -cdoes not fail when there is nothing to continue. It printsNo sessions to continue under "<cwd>"; starting a fresh session.and starts one anyway — and that session never saw--agent-file, so it runs silently roleless. That is the #929 hazard class, arriving through a documented flag.So the launch loop only takes
-cafter an inlinednode -estore probe proves a conversation exists for this cwd, and the probe fails closed: any error (no store, unreadable dir, malformed JSON) exits non-zero and the loop relaunches fresh with the role, which is always safe. Tests execute that probe against fixture stores and cross-check its verdict against the TypeScript discovery it mirrors, so the hand-written snippet cannot silently drift fromfindLatestKimiSessionId.The probe asks "would
kimi -ccontinue this?", not "does a directory exist" — a distinction review had to teach me (see below). Kimi lists a cwd's sessions before continuing one, and that listing drops archived sessions and ids it does not recognize, so a session we call resumable but kimi skips lands right back on the roleless path. Both filters now apply on both sides of the mirror.The same probe makes the script's entry self-configuring, so
afx spawn --resumeand a Tower-side terminal re-create need no second script shape — and a re-run never re-queues the task into a live conversation.servers/render-gate.tsis Spec 1313 code that landed recently. This PR touches its classifier in two places. Both are opt-in per profile, and neither can change behaviour for claude, codex or agy — argued below and pinned by tests.1. The marker exemption follows the matched span instead of column 0
Why: kimi draws its composer inside a rounded box, so its prompt marker sits at column 3 (
│ >), not the row start. Under the column-0 rule the>glyph counts as user text, so a genuinely empty Kimi composer classifiesuser-textforever — i.e. holds all of its mail, permanently.2. A profile may declare an upper bound for the composer region
GateProfilegains an optionalregionStartPatterns. Kimi sets it to the box top; every other profile leaves it unset and keeps scanning from the marker row exactly as before.This one fixes a false CLEAN, and it was found by review, not by me.
KIMI_MARKERmatches│ >,findMarkerRowtakes the last match, and the scan started at that row. So a draft whose final line begins with>— a pasted quote, a markdown blockquote — puts the marker on the continuation row, leaving the real draft text above the scanned region. Measured on real kimi 0.34.0:→ classified
{clean: true, detail: "empty"}. A queued message would then have been typed on top of unsent user input — the exact corruption Spec 1313 removes by construction. Committed as thekimi-multiline-barefixture, captured rather than constructed.The bound is exclusive, mirroring the region end, and that is load-bearing rather than stylistic: the box-top row's right corner
╮is not in the classifier's ignorable-glyph set, so an inclusive bound counted it as user text and held every idle kimi composer forever. The fixture suite caught that on the first attempt.Why other profiles cannot reach it: they declare no
regionStartPatterns, so the region starts atmarkerRow; and sincefindMarkerRowreturns the last match, no row below it can match either. The set of marker-matching rows in their region is exactly{markerRow}— the previous behaviour, by construction. A test asserts all three declare no region start, and that text above the composer still classifies clean for claude and codex.Why it is a no-op for every other app, argued and then pinned by tests:
^[❯›]col < 1≡col === 0— literally the old rule^[❯›]^>^\s*│\s*>Guardrail tests (
render-gate.test.ts, "marker-span exemption is a no-op…"): the exact span per shipped profile; a tightest-possible 1-char draft in the first cell the exemption could wrongly reach, per profile, all stillbusy(over-skipping is the only direction that could cause harm — a false CLEAN); proof that agy's span can never over-reach a typed character (>xdoesn't match its marker at all); and a direct before/after demonstration that a span-2 kimi profile classifies the real 0.34.0 idle captureuser-textwhile the shipped span-4 one classifies it clean. Every pre-existing claude/codex/agy fixture still passes unchanged.Undocumented-surface reliance (audited against kimi 0.34.0, 2026-08-09)
Two surfaces, both dated so the reliance can be re-checked on each Kimi major:
Session store
~/.kimi-code/sessions/wd_*/session_*/state.json. This has already drifted once: 0.33.0 renamedworkDir→cwd, moved timestamps from ISO strings to epoch ms, and droppedlastPrompt. Readers accept both shapes, andcodev doctorasserts the load-bearing facts explicitly and names the one that broke rather than reporting "something changed".Workspace-trust record
~/.kimi-code/workspace-trust/wd_<basename>_<sha256(root)[:12]>→{root, trustedAt}. This is the maintainer veto point, so here is the full argument.kimi 0.33.0 added a startup "Trust this folder?" dialog. A builder worktree is always a brand-new directory; the dialog renders before any composer, and its only non-trusting option exits kimi — so an unattended builder would sit on it forever. The spawn path therefore pre-writes the trust record.
kimi --helphas no flag; a full strings sweep of the 0.34.0 binary forKIMI_*env vars and for trust config keys found nothing (every "trust" hit was KaTeX/V8/OpenSSL noise). I looked for a supported knob first, as requested, and there isn't one..mcp.json,.kimi-code/mcp.json) load from the folder. It does not gate tool execution or writes.--yolo. It grants strictly less than launching the builder already authorized, and never touches a directory the user did not hand us.busy), and mailbox escalation surfaces it. Never a silent misdelivery.codev doctorvalidates our derivation against kimi's own records (each record carries therootit was written for, so the expected filename is recomputable). A scheme change surfaces as a named warning instead of silently stranding every new builder on the dialog.My reviewers split on this one, so you should see both sides rather than just my case. Codex argued it is a real security boundary:
--yologoverns approval of the agent's tool calls, whereas workspace trust governs whether repository-controlled MCP configuration is loaded and its processes started at all — so a builder spawned on a fork PR or other untrusted branch could have attacker-controlled MCP config loaded without a human ever seeing the decision. Claude reviewed the same code and reached the opposite conclusion: a--yolobuilder in a Codev-created worktree already holds strictly more authority than MCP loading confers. I find Claude's reading more persuasive for the worktrees Codev creates, but Codex's fork-PR scenario is the case where the two arguments genuinely diverge, and that is a policy call I do not think is mine to settle.If you'd rather not ship the hash write at all, the fallback is that Kimi builders require one human keypress at first launch; say the word and I'll cut it. A narrower option, if you want the automation but not the blanket: refuse the pre-write when the worktree carries project-level MCP configuration.
Corrections to claims this PR previously made
PreToolUsehooks ([[hooks]]inconfig.toml, exit code 2 blocks, 18 events as of 0.32.0). Parity is achievable. I have scoped it as follow-up rather than growing this PR further, but that is your call — say so and I'll add it here. Until it lands, a Kimi builder can write outside its worktree, and the docs now say exactly that.Two decisions that are yours, not mine
A. The version floor moves 0.27.0 → 0.33.0, which drops working installs
This is a real compatibility reduction and I want it visible rather than buried in a diff. The reasoning chain:
--agent-filerequires ≥ 0.31.0. Below that the role does not inject at all and the builder runs silently roleless — the worst available failure mode, and not one a user would notice quickly.agent-core-v2the default. Every live measurement backing this PR — store shape, trust behaviour, the render-gate composer profile — was taken on 0.34.0, i.e. on that engine. 0.31–0.32 run the old engine and are unmeasured.So the floor sits at the oldest version the evidence actually covers, not the oldest that would nominally function. If you would rather accept 0.31.0 (functional minimum, unmeasured) or hold at 0.27.0 (maximum compat, definitely broken for role injection), say which and I'll change the one constant in
doctor.tsplus the docs.B. The
.builder-kimimarker is deleted — this is not a revert of your July fixYour July REQUEST_CHANGES found a real bug: the bare launch shape (no role, no prompt) never persisted
.builder-kimi-session, so an override-spawned bare Kimi builder (--builder-cmd kimiin a claude-configured workspace) fell through to claude's 80ms Enter and its mail was swallowed. That fix —touchthe marker in the bare branch — was correct and shipped.This PR removes the marker entirely, so I want to be explicit that the property your finding protected is now stronger, not weaker.
The marker was a separate artifact that every launch shape had to remember to write. That is a standing obligation, and the bare shape is precisely the one that forgot it — which is why your review caught a bug rather than a typo. Adding the missing
touchfixed that instance; it did not remove the class. A future fifth launch shape could forget it again.Pacing now reads the harness name out of the generated
.builder-start.sh, which is generated from the resolved harness. There is nothing to remember: any shape that launches kimi necessarily nameskimiin command position, because that is the launch. The obligation is discharged by construction rather than by discipline, and it is still override-proof for exactly your scenario — a--builder-cmd kimispawn against a claude-configured workspace resolveskimi, because the script was generated from the override.Tests keep your scenario pinned directly (
mailbox-pacing.test.ts: "resolves kimi for the BARE launch shape too — the shape the old marker probe missed", plus an explicit override-proofness test), andspawn-worktree.test.tspins that both generated shapes putkimiin command position, so a refactor that hid it would fail rather than silently degrade pacing.What changed since your last review
kimi -pseed → capturesession.resume_hint→ pinnedkimi -S <id>loop--agent-file; task via the mailboxseed-kick.ts: sentinel watcher + grace +BEGINwritten straight to the PTY, verified viastate.json.lastPrompt-S <discovered-id>kimi -cbehind a fail-closed store probe.builder-kimimarker file.builder-start.shmessage-pacing.ts+seedKickoncreateTerminalresolvePacingForSessioninmailbox-wiring.ts;SeedKickRequestremoved from the SDKWhat the 3-way review round found (and what it changed)
Before opening this for re-review I ran gemini, codex and claude over the post-merge delta, asking them to attack the shared gate edit hardest. gemini APPROVE; codex and claude both REQUEST_CHANGES — and they were right. Full dispositions are committed at
codev/projects/1201-*/1201-cmap-postpivot-dispositions.md; the two blocking ones are worth stating here because they say something about where the risk in this PR actually lives:/menu renders as unclosed│rows with no╰beneath them, so anything inside it yieldsno-region-end→ held. Both are now fixtures.archivedsession authorized-c, which kimi then refuses to continue, producing the silently-roleless session the guard exists to prevent. Claude found the safe-but-harmful direction:readdirSyncon a stray non-directory threwENOTDIRinto the single outer try, so one.DS_Storein~/.kimi-code/sessions/disabled resume machine-wide, permanently and silently.Also fixed from that round: shell metacharacters in a builder id or task path could execute when the launch script printed a recovery hint (all three reviewers, from different angles); a crash loop re-queued the same task every ~2s even though the mailbox persists held rows; and both drift probes reported healthy forever after a store migration, because "any record still matches" is satisfied by the pre-migration records.
The common thread: every one of these lives in a state a happy-path run does not produce — an empty composer and a clean store both behave correctly, which is precisely why three passing live demos missed them.
One more disclosure, since it affects how you should read the demo. Two demo steps were failing when I picked this back up, and the cause was the demo, not the product: its role told the model to prefix every reply with a token, which measures whether K3 honours a persistent output-format constraint rather than whether the role was injected. The
--agent-fileprobe, run against a production-identical agent file, passed 7/7 including role survival acrosskimi -c. The demo now asks for a codeword instead — the same oracle the probe uses — and carries a comment explaining why, so the weaker check does not come back.Verification
pnpm buildclean; full suite 4900 passed / 48 skipped / 0 failed.codev/spikes/pir-1201-kimi-builder-demo.mjs, runs the REAL dist modules): render gate classifies the live composer;--agent-filerole honored in the interactive TUI; paced multi-line delivery submits; crash → store probe →kimi -c→ role survives; probe fails closed on an empty store; trust pre-write idempotent.pir-1201-kimi-agentfile-probe.mjs):--agent-fileinjects in both-pand the interactive TUI; TUI start mints no session, the first message mints exactly one;kimi -cresumes with the role binding intact and mints no second session.pir-1201-kimi-gate-measure.mjs), committed raw — they carry only throwaway/tmppaths. Idle → clean; draft, multi-line draft, the bare->multi-line draft,/menu,@picker and the folder-trust dialog → busy (so a blind Enter can never confirm filesystem trust).Out of scope
Kimi as architect (stage 2); ACP /
kimi serveradapter; #1018 write-guard parity (now achievable — see above).