AGENTS.md · diff

git:20260906.76bb08f to git:20260909.040d618

15 added, 14 removed. Audit A to A.

# AGENTS.md
Project context for coding agents working in this repository.
## Quick reference
- - **Version**: 1.9.14 (kimi-code, subprocess transport). **Hook-pin durability contract — the current headline, do not regress:** the canonical hook command must never embed a version-stamped Node token. `preferStableNodePath` prefers `process.argv0` over `process.execPath` ONLY when it is absolute AND `realpath`-identical, so it can pick a more stable NAME for the running interpreter, never a DIFFERENT binary; any doubt falls back to `execPath`. `verifyHookInstalled` additionally runs `access(nodeBin, X_OK)`, fail-closed on any errno. `nodeBin` is derived by parsing the built command back (`parseHookShellCommand`), NEVER by re-calling `resolveNodeBinary` — that is non-pure (`preferStableNodePath` does realpath I/O) and could probe a stale realpath while the command pins the symlink. **Scope, adversarially established — do not widen it in docs:** it proves the exec bit only, not a working Node, and it does NOT catch a dangling interpreter symlink (that resolves to a different node, changes the recomputed command, and byte-exact equality already refuses it as RECOVERABLE drift). The bypass is NARROWED, NOT CLOSED: it covers only "config and expected command still byte-equal, yet the embedded node token fails X_OK at verification time" — a dead `KIMI_PLUGIN_CC_NODE_BIN` override, or post-launch `chmod -x`/removal on the default path. That refusal carries `refusal_kind:"node-bin-not-executable"` + `node_bin`; it is the one refusal whose remedy is NOT /kimi:setup, and it can never co-occur with `drift` (drift requires equality to FAIL, this requires it to PASS). Re-pin-recoverable refusals carry `drift`/`hookRefusalDetails`: agents re-pin through `${CLAUDE_PLUGIN_ROOT}/scripts/companion.sh setup` and retry ONCE only when `retryable_after_setup` AND setup exits 0. That protocol also rides every refusal message (`hookRefusalRetryProtocol`, single source in `hooks/install.ts`), so slash-command callers self-heal too — keep the surface files thin, never duplicate it there. Startup-failure wraps thread `availability`/`cause_code`/tail-truncated `cause_message` into `details`, and `auth.login_required` classifies to `*_KIMI_AUTH_UNAVAILABLE` with a `kimi login` remedy (`/kimi:setup` does not repair auth). **In-verifier auto-repin is REJECTED — do not re-propose** (lock self-deadlock; the "target cannot be steered" premise is false; no version-monotonicity bound; write→spawn TOCTOU). Analysis: `.claude/hook-pin-durability-spec-2026-07-25.md`. **Background-worker test discipline:** detached-worker hook-enforcement tests MUST run the companion CLI as a real node subprocess through a symlink with NO `KIMI_PLUGIN_CC_NODE_BIN` override and NO `KIMI_PLUGIN_CC_SKIP_HOOK_CHECK` (`tests/runtime/background-hook-enforcement.test.ts`) — an override makes spawner and verifier agree by construction and hides node-bin drift, which is how a `spawn(process.execPath)` regression once reached pre-release review instead of CI. **Smoke-home representativeness:** a real-binary smoke case MUST assert its own precondition when the behaviour under test depends on host state. Every isolated `KIMI_CODE_HOME` starts EMPTY, and kimi-code's cross-session indexer short-circuits on zero sessions — so the first exact-0.30.0 smoke passed GREEN while never executing the path it was believed to cover. A green test whose precondition never held is worse than a red one: it reads as evidence. The exact-0.30.0 release evidence therefore used a populated-home case that failed loudly when no `wire.jsonl` existed and recorded the KIMI-home-only side effect; v1.9.4 now refuses experimental-v2 before spawn, so its smoke asserts the refusal instead of launching the unsafe engine. **Migration from pre-1.9.0: the canonical command changed, so every install drifts ONCE and needs one /kimi:setup per host** (enforcement is not lost meanwhile). **Full per-version release + compat history → [CHANGELOG.md](./CHANGELOG.md) and [ROADMAP-TO-GA.md § Post-GA audit log](./ROADMAP-TO-GA.md#post-ga-audit-log)** (the version-by-version compat reasoning also lives in `runtime/kimi-version-probe.ts` comments).
- - **Upstream compat**: verified against `@moonshot-ai/kimi-code` from 0.2.0 (GA reference) through **0.41.0**. **Native agent-core-v2 is fail-closed disabled:** truthy `KIMI_CODE_EXPERIMENTAL_FLAG` values (`1|true|yes|on`, trimmed and case-insensitive) still refuse before spawn with `CLI_V2_HOOK_ORDER_UNSAFE`, and every accepted child spawn overwrites `KIMI_CODE_LEGACY_FLAG=1`. That explicit per-spawn pin became load-bearing when 0.33.0 inverted unflagged `kimi -p` to native v2; older releases ignore the unknown flag. Source audit corrected the older claim that v2 external PreToolUse always precedes plan approval: `AgentPlanService` registers first and final-allows exact plan-file writes, reachable on a fresh session through `default_plan_mode=true` or through restored plan state. Those writes are confined to `<KIMI_CODE_HOME>/sessions/.../plans/`, not the user worktree, but skipping the managed hook violates the load-bearing every-tool contract. Fresh and resumed plugin runs remain on v1. 0.30.0–0.39.1 also contain the now-unreachable experimental-v2 KAP eager-index caveat (last re-verified at 0.39.1; not re-checked at 0.40.0 or 0.41.0 since the engine is refused); the ordering defect is now filed upstream as MoonshotAI/kimi-code#3431 (2026-09-01, bug-template framing with a live 0.39.1 reproduction — the hook denies `Glob` in the same `-p` session, then the plan-file `Write` executes with zero hook invocations; v1 blocks it) and re-enabling v2 stays gated on a released external-hook-before-final-allow guarantee; #2376 (KAP eager-index) was closed by us on 2026-09-01 as unlinked per upstream CONTRIBUTING (external PRs need a maintainer-`/approve`d issue) and can be refiled behind one — and the 0.39.0 audit found that guarantee is not merely missing but explicitly undecided upstream: the then-present `packages/agent-core-v2/docs/Permission.md` reserved `event.allow()` for the plan-file guard "which must bypass the entire permission chain" and listed listener ordering as an open design question (0.41.0 deletes that document; it is historical evidence), so treat the v2 refusal as gated on a released ordering contract, not on a pending bugfix. 0.39.0's experimental tower mode (`KIMI_CODE_EXPERIMENTAL_TOWER`, TUI `/tower`) and subagent fork (`KIMI_CODE_EXPERIMENTAL_SUBAGENT_FORK` / `[experimental] subagent_fork`) are both agent-core-v2-only and unreachable on the pinned path (zero v1 flag-registry presence; the v1 `AgentSwarm` schema is `.strict()` so a `fork` arg is rejected; both are subsumed by the already-refused `KIMI_CODE_EXPERIMENTAL_FLAG`); 0.39.0's experimental Remote Control can patch `hooks` through a tunneled config API, which the per-spawn byte-exact hook verification catches fail-closed (an altered entry refuses as drift, a removed one as not-installed — same /kimi:setup remedy). 0.40.0 changed the whole `packages/agent-core` source tree by one additive schema line and left permission/hooks/wire 0 bytes; it adds a v2-ONLY reason to keep refusing — #3444 removed `RuntimeWorkspaceView.resolve()`'s root assertion, so native-v2 Bash can run with an out-of-workspace `cwd` (no shared code with v1 or kaos) — and its new `fork`/`session` subcommands are off the `-p` path. 0.41.0 leaves the entire v1 core, prompt driver, hooks, permissions, and bootstrap unchanged; its additive SDK `suggestFiles` facade returns `undefined` on v1. Native-v2 `staleGuard` is removed, and the dangerous-command policy skips auto mode; neither repairs plan-before-hook ordering. Current evidence and follow-ups: [docs/native-v2-status.md](docs/native-v2-status.md). Engine selection reads only `KIMI_CODE_LEGACY_FLAG` and never config, so no `config.toml` key defeats the pin: `[experimental]` tables CAN enable a registered v1 flag without the env master switch, but the v1 registry holds only `tool-select`/`secondary-model` (neither touches hooks, permissions, or cwd) — re-check that registry each audit. The authoritative tested set is `KIMI_TESTED_MINORS` in `runtime/kimi-version-probe.ts`, whose per-entry comments hold the version-by-version audit detail; the per-release narrative is in [ROADMAP-TO-GA.md § Post-GA audit log](./ROADMAP-TO-GA.md#post-ga-audit-log) + [CHANGELOG.md](./CHANGELOG.md), and the audit routine is [docs/upstream-compat-audit.md](docs/upstream-compat-audit.md). **The current load-bearing contract — what the safety model rests on, re-verified each audit:**
- - **Engine provenance (pre-enable slice):** [docs/native-v2-certification-provenance.md](docs/native-v2-certification-provenance.md) is the approved gate. Every new model job persists its real operation, explicit intended/observed engine, exact command tuple, probed version, certification source, and resume lineage. Historical rows remain unknown unless saved-log evidence proves an engine. Native v2's production capability matrix is empty; a `system.version` marker under a forced-v1 plan tears down the process tree with `CLI_ENGINE_PROVENANCE_MISMATCH`. Rescue/pursue/swarm-write receive their trusted root from plugin-owned `KIMI_PLUGIN_CC_WORKSPACE_ROOT`, never upstream payload `cwd`.
- - **Policy queue order** (`packages/agent-core/src/agent/permission/policies/index.ts`): `PreToolCallHookPermissionPolicy` is **index 0**, `AgentSwarmExclusiveDenyPermissionPolicy` index 1, `AutoModeApprovePermissionPolicy` index 5 (the first approve). Invariant: *every policy between the index-0 hook and the first approve is a DENY*, so nothing auto-approves before our hook can deny. (The `goal-start-review-ask` `ask` at index ~10 sits after auto-approve and is dead on the `-p` auto path — not a gap.)
- - **Hook engine** lives in `packages/agent-core/src/session/hooks/` (the live v1 path; the old `agent/hooks/` tree was removed by a relocation commit, so audit scripts retain both paths only for cross-version coverage). Contract: stdin JSON shape (`{tool_name, tool_input, session_id, cwd, …}`), exit-2-as-deny, empty matcher = all tools, fail-open on internal error. **`tool_input` field names matter** — the rescue allowlist reads them: `Bash` uses `command`; `Write`/`Edit` use **`path`** (`tools/builtin/file/{write,edit}.ts`, `z.object({ path })`), NOT `file_path` — reading `file_path` here denied every real Write/Edit until v1.4.1. Re-verify these keys on each audit — a rename silently fails the path allowlist closed. **We are not the sole occupant of the PreToolUse channel:** `rpc/core-impl.ts` (create + resume) merges hooks from enabled kimi-code *plugins* into the `-p` session hook list (`[...config.hooks, ...plugins.enabledHooks()]`). Compat-benign because engine aggregation is **any-block-wins** (`session/hooks/engine.ts::blockDecision` = first `action:'block'` wins; an allow never pre-empts a block) — a plugin hook can only *add* denials, never override our managed deny. Our config.toml managed block is unaffected: the config `HookDefSchema` is `.strict()`, so the per-hook `cwd`/`env` fields are programmatic/plugin-only (no config-injection vector). Load-bearing invariant to re-verify each audit: `blockDecision` stays find-first-block.
- - **`-p` mode**: through 0.32.x upstream defaulted to the v1 driver; 0.33.0+ defaults to native v2 unless `KIMI_CODE_LEGACY_FLAG` is truthy. The plugin therefore owns engine selection: `cli-client.ts::buildEnv` overwrites that child-only flag to `1` for every accepted fresh/resumed spawn, while upstream-truthy `KIMI_CODE_EXPERIMENTAL_FLAG` values still **refuse before spawn**. Do not silently strip the experimental flag: the explicit refusal tells the operator which unsafe selector was rejected. The pinned v1 driver hard-codes `permission: 'auto'` + `installHeadlessHandlers`; stdout is stream-json via `PromptJsonWriter`, carrying assistant/tool records, the `role:"meta", type:"session.resume_hint"` session id, and the role-less `goal.summary`. V2 external hooks are awaited on ordinary calls and preserve the hook payload/exit-2 contract, but production instrumentation on 0.39.1 established plan before external hooks in fresh and resumed runs. Exact 0.41.0 source retains plan → external hooks ordering (`index.ts:329/341`, `planService.ts:110`); its `staleGuard` feature was removed, so the older complete listener list is not current; plan-file `event.allow()` is final and skips later listeners, and the same short-circuit hides the plan guard's veto branches from the hook too. Both services subscribe from their constructors and activate in the same DI cascade pass, so the order is the `index.ts` feature-import tie-break — `test/wire/` hand-seeds `IAgentExternalHooksService` and is not evidence of production order in either direction. Re-enable v2 only after an exact released tag guarantees external-hook veto before any final allow and both fresh `default_plan_mode=true` plus restored-plan smoke cases pass. The parser still models v2's extra `role:"meta", type:"system.version"` line for historical logs/future compatibility; exact 0.41.0 release smoke asserts it is absent. argv we consume (`options.ts`/`commands.ts`): `-p`/`-r`/`--output-format`/`-m`/`--skills-dir`; `--auto`/`--yolo`/`--plan` are rejected with `-p`. Other `kimi` subcommands (`vis`, `server`/`web`, `doctor`, …) are off the `-p` path — the plugin passes the prompt as the VALUE of `-p`, never a bare positional.
- - **Plugin slash commands** are RPC/host-initiated, not model tools, and absent from the `kimi -p` CLI path; activation only macro-expands prompt text, whose later tool calls still pass through the index-0 hook. Re-verify each audit that this stays host/UI-only and never becomes a model-reachable permission bypass.
- - **0.31 custom-agent trust boundary:** default-v1 print mode now discovers user/project/plugin agent profiles and plugin system-prompt contributions. Project roots include `<git-root>/.kimi-code/agents` and `<git-root>/.agents/agents`; `override: true` can replace builtin `agent` or `coder` instructions/tool availability, and the bound profile is snapshotted with the session. These inputs can redirect intended work, so inspect them before rescue/pursue/swarm-write in an untrusted repository. Agent-profile fields and system-prompt text cannot themselves set hooks, permission mode, or cwd, widen the plugin allowlist, or escape swarm-write's trusted worktree root. Enabled plugins may separately contribute hooks; any-block-wins aggregation means those hooks can add denials but cannot override this plugin's managed block.
- - **Daily upstream monitor reports**: a Codex automation checks upstream daily and saves a dated signal under `.claude/kimi-code-research/daily-monitor/` (gitignored by the repo-wide `.claude/` rule). Before starting an upstream audit or release catch-up, read `daily-monitor/LATEST.md` plus the most recent dated reports. Carry forward unresolved blockers/follow-ups, and explicitly correct any prior judgement that today's evidence contradicts. These monitor reports are for continuity only; they do **not** certify a version or justify extending `KIMI_TESTED_MINORS` without the normal source audit + real-binary smoke gate.
- - **Goal mode** (`kimi -p "/goal …"`) triggers on the `/^\/goal(\s|$)/` prompt prefix ALONE (the old `goal-command` experimental-flag gate is gone). Read-only commands hard-prefix an English instruction line so their trimmed prompt never starts with `/goal`, and the index-0 hook denies writes regardless. `/kimi:pursue` opts into goal mode per-job and is hook-gated on every continuation turn.
- - **AgentSwarm subagents** (`SessionSubagentHost.spawn()` via `SubagentBatch`) use the STANDARD permission stack — hook at index 0, NO `deny-all` (the `deny-all` unshift is `startBtw()`-only, the side-question path, NOT the swarm path). So under the `swarm` label every subagent's write/edit/shell fires our hook at index 0 and is denied — read-only swarm opens zero write surface.
- - **Background auto-upgrade** is default-on and drifts the operator's binary out-of-band; it does not swap the binary for the plugin's own `-p` spawns. Setup warns on an out-of-range version, and every model command separately probes the exact command tuple and refuses an untested minor before spawn. Recovery is a plugin release that certifies the minor or an explicitly selected certified binary; `KIMI_PLUGIN_CC_SKIP_VERSION_PROBE` is tests/smoke only, never a production repair path.
- - **Print-mode background defaults**: upstream steers completed background work by default and allows task waits without a timeout ceiling. The plugin does not rely on those upstream limits: pursue/swarm keep mandatory finite AbortController budgets, swarm keeps hard concurrency caps, and cancellation remains an identity-safe settlement barrier before the caller or worktree cleanup resumes.
+ - **Version**: 1.10.0 (kimi-code, subprocess transport, **native agent-core-v2**). **The native-v2 safety contract — the current headline, do not regress:** every executed tool call in a plugin-managed session passes the managed PreToolUse hook, and the engine's sole chain-breaking final allow (the plan-file guard, `features/plan/planService.ts`, active plan mode only) is NEVER armed. That is a construction, not an upstream ordering promise, and it rests on four plugin-owned closures that must all stay in place: (1) `runtime/native-v2-preflight.ts` refuses `default_plan_mode` unless absent or literal `false` — parsed from the SAME `<KIMI_CODE_HOME>/config.toml` the child reads once inside `sessions.create()`, before spawn and again at the spawn boundary (`CLI_V2_PLAN_MODE_CONFIGURED`, remedy is the config key, NEVER `/kimi:setup`); (2) the hook denies `EnterPlanMode`/`ExitPlanMode` for every label (`runtime/hooks/approval-policy.ts`); (3) resume accepts only proven `native-v2` plugin lineage (`KIMI_SESSION_LINEAGE_UNKNOWN` for unknown rows, `KIMI_SESSION_ENGINE_MISMATCH` for v1) after a raw scan of every agent `wire.jsonl` for `plan_mode.*`/`plan.*` records (`KIMI_SESSION_PLAN_TAINTED`) — `restore()` folds only that journal, nothing else can restore plan state; (4) v2-only experimental features (`tower`, `subagent_fork`, from `[experimental]` config or per-flag env) and the master `KIMI_CODE_EXPERIMENTAL_FLAG` refuse before spawn. Certification is per EXACT version and per operation (`NATIVE_V2_CERTIFIED` in `runtime/kimi-engine.ts`; `KIMI_TESTED_MINORS` is the legacy-v1 table and never gains 0.42); a new kimi-code version is appended only after `tests/audit/v2-tag-scan.test.ts` passes against its source tree, the plan-mode-ON live control still shows the bypass (proving the refusal is load-bearing), and the per-operation smoke is green. A native-v2 plan REQUIRES the `system.version` marker as the first stream-json line and requires it to equal the probed version; a legacy plan still refuses it (`CLI_ENGINE_PROVENANCE_MISMATCH`, teardown before any record is delivered). `Bash.cwd` is validated against the trusted root before the command string (`runtime/rescue-approval.ts`) — the hook payload's top-level `cwd` is the process cwd, not the Bash cwd. Basis and residuals: `docs/native-v2-certification-provenance.md` §2. **Hook-pin durability contract (unchanged):** the canonical hook command must never embed a version-stamped Node token. `preferStableNodePath` prefers `process.argv0` over `process.execPath` ONLY when it is absolute AND `realpath`-identical; `verifyHookInstalled` runs `access(nodeBin, X_OK)` fail-closed; `nodeBin` is parsed back from the built command (`parseHookShellCommand`), never re-resolved. That check covers only "config and expected command still byte-equal, yet the embedded node token fails X_OK" (`refusal_kind:"node-bin-not-executable"`, the one refusal whose remedy is NOT /kimi:setup). Re-pin-recoverable refusals carry `drift`/`hookRefusalDetails`; agents re-pin through `${CLAUDE_PLUGIN_ROOT}/scripts/companion.sh setup` and retry ONCE only when `retryable_after_setup` AND setup exits 0 (`hookRefusalRetryProtocol`, single source in `hooks/install.ts`). Startup-failure wraps thread `availability`/`cause_code`/`cause_message` into `details`; `auth.login_required` classifies to `*_KIMI_AUTH_UNAVAILABLE` with a `kimi login` remedy. **In-verifier auto-repin is REJECTED — do not re-propose.** **Background-worker test discipline:** detached-worker hook-enforcement tests MUST run the companion CLI as a real node subprocess through a symlink with NO `KIMI_PLUGIN_CC_NODE_BIN` override and NO `KIMI_PLUGIN_CC_SKIP_HOOK_CHECK` (`tests/runtime/background-hook-enforcement.test.ts`). **Smoke-home representativeness:** a real-binary smoke case MUST assert its own precondition when the behaviour under test depends on host state; do NOT write a "plan-file write is denied under plan mode" smoke — under this construction that path is never armed and a green result would be exactly the vacuous-precondition failure. **Full per-version release + compat history → [CHANGELOG.md](./CHANGELOG.md) and [ROADMAP-TO-GA.md § Post-GA audit log](./ROADMAP-TO-GA.md#post-ga-audit-log).**
+ - **Upstream compat**: **native agent-core-v2 certified at exact `@moonshot-ai/kimi-code@0.42.0`** (commit `6954d2c8`) for all eight operations; legacy-v1 remains certified from 0.2.0 through **0.41.x** for an explicitly pinned binary only (0.42.0 deleted the v1 engine and `KIMI_CODE_LEGACY_FLAG`, #3542, unmentioned in its changelog). Engine selection is plugin-owned (`selectIntendedEngine`): exact version in `NATIVE_V2_CERTIFIED` → v2; minor in `KIMI_TESTED_MINORS` → v1 with the child-only legacy pin; else refuse. Truthy `KIMI_CODE_EXPERIMENTAL_FLAG` (`1|true|yes|on`) still refuses before spawn (`CLI_V2_HOOK_ORDER_UNSAFE`), now joined by `[experimental] tower`/`subagent_fork` from config or per-flag env (`CLI_V2_EXPERIMENTAL_UNSAFE`). The upstream ordering defect (plan before external hooks; sole final allow at `features/plan/planService.ts:110`) is UNCHANGED at 0.42.0 and confirmed in the shipped bundle; it stays filed as MoonshotAI/kimi-code#3431 (open, unanswered) as the preferred end state that would permit native plan mode. Current evidence: [docs/native-v2-status.md](docs/native-v2-status.md); basis and residuals: [docs/native-v2-certification-provenance.md](docs/native-v2-certification-provenance.md) §2; audit routine: [docs/upstream-compat-audit.md](docs/upstream-compat-audit.md) (+ `tests/audit/v2-tag-scan.test.ts`); per-release narrative: [ROADMAP-TO-GA.md § Post-GA audit log](./ROADMAP-TO-GA.md#post-ga-audit-log) + [CHANGELOG.md](./CHANGELOG.md). **The load-bearing v2 contract — re-verified each audit:**
+ - **Engine provenance:** every model job persists operation kind, intended/observed engine, exact command tuple, probed version, certification, `safety_profile` (`native-v2-no-plan/1` for v2 plans, null for v1) and resume lineage. A v2 plan requires the first stream-json line to be `{"role":"meta","type":"system.version","version":<probed>}`; a v1 plan refuses that marker. Either mismatch tears down the process tree before any record is delivered. Historical rows stay unknown unless saved-log evidence proves an engine. Rescue/pursue/swarm-write receive their trusted root from plugin-owned `KIMI_PLUGIN_CC_WORKSPACE_ROOT`, never the payload `cwd`.
+ - **Before-execute channel** (`packages/agent-core-v2/src/agent/toolExecutor/beforeToolExecuteEvent.ts`): listeners run sequentially in registration order; `finalAllowed` (`event.allow()`) ends iteration; `veto` wins; `pass` is non-terminal. Subscribers at 0.42.0: permissionGate, toolDedupe, btw, externalHooks, goal, plan, swarm, tower — only plan calls `allow()`, gated on active plan mode. The auto-approve policy (`permissionGate` → `AutoModeApprovePermissionPolicyService`, `pass()`) can never override a hook veto. Invariant: the `.allow()` count stays 1 and the subscriber set stays this list (tag scan).
+ - **Hook engine** lives in `packages/agent-core-v2/src/features/externalHooks/` (config `configSection.ts` — strict `{event, matcher?, command, timeout?}`; runner `internal/runHook.ts` — exit 2 = deny with stderr reason, exit 0 + JSON `permissionDecision:"deny"` = deny, anything else allow, fail-OPEN on spawn error/timeout/abort/bad JSON, 30 s default; matcher `internal/matchHooks.ts` — empty = all tools, throwing regex matches nothing, `Promise.all` then first block in match order, allow never overrides block). Payload keys `hook_event_name, session_id, cwd (process cwd), client_type, session_title, tool_name, tool_input, tool_call_id`. **`tool_input` field names:** `Write`/`Edit` use **`path`** (Write also `mode: overwrite|append`); `Bash` uses `command` plus **`cwd`**, `timeout`, `run_in_background`, … — `Bash.cwd` executes without upstream membership assertion (`bashTool.ts:191`), so `rescue-approval.ts` confines it to the trusted root before the command string. Plugin-contributed hooks are appended and deduped on `cwd\0command`; any-block-wins keeps them compat-benign. Re-verify all of this per tag.
+ - **`-p` mode** (`apps/kimi-code/src/cli/v2/run-v2-print.ts`): `auto` forced fresh and resumed, `nonInteractive:true` (dangerous-command policy omitted). argv consumed: `-p` (prompt as VALUE), `-r` (alias of `-S/--session`), `--output-format stream-json`, `-m`, `--skills-dir`; `--auto/--yolo/--plan` rejected with `-p`; `--agent`/`--agent-file`/`--add-dir` exist and are NEVER passed (profiles replace the system prompt and don't reach children; `--add-dir` widens roots). `-r` refuses when the session's recorded cwd ≠ current cwd. Records: assistant/tool, meta `system.version`/`session.resume_hint`/`turn.step.retrying`, role-less `goal.summary`; hook denials also surface as assistant text `"PreToolUse hook blocked…"`. Every spawn exports `KIMI_CODE_NO_AUTO_UPDATE=1` (the update preflight runs on `-p`) and an absolute `KIMI_CODE_HOME`.
+ - **Plan mode arming vectors** (exhaustive at 0.42.0, all closed — see the Version headline): `default_plan_mode` (single user file, no env/argv/project overlay, read once in `sessions.create()`); `EnterPlanMode` tool; journal replay on resume (`restore()` folds only `agents/<id>/wire.jsonl`). NOT reachable from `-p`: kap-server routes, TUI `/plan`, `--plan`, agent profiles (no plan field), subagent/fork/tower inheritance (fresh scope; `snapshotExcluded`), undo.
+ - **Plugin slash commands** are RPC/host-initiated and absent from `-p`; `-p` intercepts exactly one prefix, `/goal`. Read-only commands hard-prefix an instruction line so their prompt never starts with `/goal`. Re-verify this stays host/UI-only.
+ - **Custom-agent trust boundary (0.31+):** print mode discovers user/project/plugin agent profiles and plugin system-prompt contributions (`<git-root>/.kimi-code/agents`, `<git-root>/.agents/agents`); they can steer intended work but cannot set hooks, permission mode, cwd, plan state, or widen the allowlist. Inspect them before rescue/pursue/swarm-write in an untrusted repository.
+ - **Daily upstream monitor reports** live under `.claude/kimi-code-research/daily-monitor/` (gitignored). Read `LATEST.md` plus recent dated reports before any audit; they never certify a version — certification needs the tag scan + live control + smoke.
+ - **Goal mode** triggers on `/^\/goal(\s|$)/` alone (`KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND` no longer exists); exit 0/3/6 = complete/blocked/paused; `/kimi:pursue` is hook-gated on every continuation turn.
+ - **AgentSwarm / subagents** (`features/swarm/tools/agent-swarm/agentSwarmTool.ts`, `'AgentSwarm'`, strict schema; `subagent_type` free string, default `coder`): children run in-process in a child DI scope sharing the session workspace, with their OWN eager external-hooks service — every child tool call fires the hook; no deny-all path on the swarm route. The singular `Agent` tool exists (non-strict) and is denied by every label. `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` is a hard cap but **unset = no cap** — `swarm.ts` always exports it (4 read / 1 write). `fork` is gated by `subagent_fork` (refused).
+ - **Background auto-upgrade** drifts the operator's binary out-of-band; each spawn probes the exact tuple and refuses an uncertified version. Recovery is a plugin release or `KIMI_PLUGIN_CC_KIMI_BIN`; `KIMI_PLUGIN_CC_SKIP_VERSION_PROBE` is tests/smoke only.
+ - **Print-mode defaults:** upstream sets subagent/swarm timeouts unbounded in `-p`; the plugin keeps mandatory finite `--budget` AbortController ceilings, hard concurrency caps, and the identity-safe cancellation barrier.
- **Toolchain**: Node >= 22.5, TypeScript, **bun** (not npm/yarn)
- **Workflow**: edit `runtime/**/*.ts` → `bun run check` (build + typecheck + test + drift gate)
- **Installed-state verification**: For questions about what a host is actually running, resolve that host's active version-stamped install cache and run its bundled `scripts/companion.sh setup --check`. The checkout, package metadata, and another host's cache are not proof of active runtime state.
- **Credential-bearing config**: Treat `~/.kimi-code/config.toml` as credential-bearing (it can hold a BYO-provider `apiKey`) **and `~/.kimi-code/credentials/` (e.g. `kimi-code.json`, mode 0600) as the real OAuth-token store** — the managed login token has lived under `credentials/` since the earliest kimi-code (source-verified), never in config.toml, so it is NOT a per-version migration. For routine status checks, do not read, print, copy, or retain the raw config **or anything under `credentials/`**; use the sanitized setup check. If an explicitly authorized diagnosis requires direct inspection of config.toml, restrict it to the plugin-managed hook block and redact all credential material from output; the `credentials/` store is never read or copied.
## Directory layout
```
.claude-plugin/ Claude Code plugin manifest (plugin.json, marketplace.json) — id stays as kimi/kimi-marketplace (no rename for v1); v0.4 users opt-in to v1 by updating the same install
.claude/ Gitignored local audit workspace. Upstream clones/reports live here, including daily monitor reports under kimi-code-research/daily-monitor/
.agents/ Codex repo marketplace sidecar (plugins/marketplace.json) — generated; source.path points at plugins/kimi-codex
commands/ Slash command markdown — thin wrappers over companion.sh (Claude Code surface)
agents/ Claude Code subagent definitions (kimi-rescue, kimi-review, kimi-challenge, kimi-ask, kimi-swarm, kimi-pursue, kimi-swarm-write)
hooks/ Stop hook for the review gate (kimi-code-side PreToolUse hook lives at runtime/hooks/)
scripts/ Shell entry points (companion.sh, review-gate-hook.sh) + dev-only surface generator (surface-registry.ts, generate-surfaces.ts)
plugins/kimi-codex/ SELF-CONTAINED Codex plugin root — GENERATED, do not hand-edit. Codex copies a plugin root to its install cache and forbids ../ escapes, so this dir bundles everything it needs:
├── .codex-plugin/plugin.json Codex manifest (skills: "./skills/")
├── skills/ 12 Codex skills that shell out to companion.sh (no MCP). Lives HERE, not at repo root, so Claude Code does NOT auto-discover them
├── scripts/ byte-mirror of companion.sh + review-gate-hook.sh (0755)
└── dist/ byte-mirror of the compiled runtime
runtime/ TypeScript source — the real runtime
├── background-spawn.ts Shared detached-worker spawn helper (rescue + ask)
├── cli-client.ts Subprocess wrapper around `kimi -p --output-format stream-json`
├── stream-json.ts Pure parser for kimi-code's NDJSON output
├── cli-cancellation.ts AbortController-based cancellation for long-running commands
├── rescue-approval.ts Workspace-bound allowlist (called by the PreToolUse hook)
├── commands/ One file per companion subcommand
├── hooks/ PreToolUse approval hook (entry script + policy + install verifier)
└── schemas/ Structured output contract for review_gate (review/challenge dropped theirs in v0.2.3)
dist/ Compiled JS — committed for zero-build install
tests/ bun test suite
```
## Commands
- `bun run check` — rebuild `dist/`, verify generated surfaces (`check:surfaces`: Claude hash gate + Codex sidecars + the `plugins/kimi-codex` runtime mirror/orphan checks), typecheck, run full test suite, then drift gate (`git diff --exit-code -- dist plugins/kimi-codex`). If `dist/` or the bundled Codex copy has unstaged changes, check fails — stage them and retry.
- `bun test <path>` — run a single test file
- `bun run build` — compile `runtime/**/*.ts` → `dist/**/*.js`
The companion runs via `scripts/companion.sh <subcommand>`, which resolves `node` and runs `dist/companion.js`. Subcommands: `setup`, `review`, `task` (`task rescue`, `task challenge`, `task pursue`, `task swarm`), `ask`, `status`, `result`, `cancel`, `replay`.
## Architecture
**Thin plugin, rich runtime** — mirrors [codex-plugin-cc](https://github.com/openai/codex-plugin-cc):
- Plugin layer (commands/, agents/, hooks/) handles routing only
- Runtime layer (runtime/, scripts/) owns subprocess lifecycle, SQLite job store, hook-side approval policy, and rendering
- Flow: `slash command → companion.sh → companion.js → kimi -p --output-format stream-json → job store → artifact`
**Key invariants:**
- Subprocess-first. One `kimi -p` process per job. Session id capture is **dual-source for kimi-code compatibility**: kimi-code 0.2.0+ emits a `role:"meta", type:"session.resume_hint"` record on stdout in stream-json mode once per prompt run at session END (after `runPromptTurn` settles), carrying a `session_<uuid>` token; kimi-code 0.1.x emitted a plain `To resume this session: kimi -r <uuid>` line on stderr. The cli-client consumes both channels — first-announce-wins, pinned, idempotent — and filters the meta record out of the consumer-facing records[] surface so commands iterating assistant/tool prose don't see it. The stderr regex anchors to line bounds and requires a full UUID payload (both bare and `session_<uuid>` alternations) so a malformed line can't pin a garbage id. 0.1.x users on text-output mode (or any future kimi version that re-introduces stderr emission with the new shape) keep working. Captured id round-trips verbatim via `kimi -r <token>` — we treat it as an opaque identifier whose shape may evolve again.
- - Every model-spawning command on the supported default-v1 path is enforced by the PreToolUse hook in `~/.kimi-code/config.toml` because `kimi -p`'s built-in auto-approve cannot be overridden via argv. Experimental-v2 is refused before process creation because upstream plan-file final allow can precede that external hook. Ask/review/challenge/rescue/pursue/swarm refuse before spawning Kimi when verification fails; the review gate skips visibly. Claude Code `/kimi:setup` and Codex `$kimi-setup` install their host-scoped managed blocks. `KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1` is the explicit tests/diagnostics bypass for hook-verification refusals only; it does not bypass `CLI_V2_HOOK_ORDER_UNSAFE` and is never a repair path.
+ - Every model-spawning command is enforced by the PreToolUse hook in `~/.kimi-code/config.toml` because `kimi -p`'s built-in auto-approve cannot be overridden via argv. On native v2 the hook is load-bearing under the no-plan construction (Version headline): the preflight refuses `default_plan_mode`, tainted/unknown resume, and unsafe experimental selectors before process creation, and the hook denies plan-mode tools and every non-allowlisted tool. Ask/review/challenge/rescue/pursue/swarm refuse before spawning Kimi when hook verification fails; the review gate skips visibly. Claude Code `/kimi:setup` and Codex `$kimi-setup` install their host-scoped managed blocks. `KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1` is the explicit tests/diagnostics bypass for hook-verification refusals only; it does not bypass the v2 preflight or `CLI_V2_HOOK_ORDER_UNSAFE`, and is never a repair path.
- Hook verification is **strict-by-default and exact**: `verifyHookInstalled` reconstructs the canonical shell command from env via `runtime/hooks/install-paths.ts::tryBuildExpectedHookCommand` and equality-checks it against the current host's installed command. Setup/check first parse the **complete** TOML with vendored upstream `smol-toml@1.6.1`, then strictly validate every configured hook against kimi-code's config schema before applying the plugin's managed-block grammar; malformed unrelated config therefore cannot be hidden by a locally valid managed block. (The strict validation is hooks-scoped by design — unknown top-level tables such as 0.39's `[swarm]`/`[experimental]` parse and are tolerated, so new upstream config sections never lock out setup/check.) Substring and partial-parse paths are forbidden because crafted commands or malformed foreign tables can otherwise bypass setup-time safety checks. The canonical command bytes still have one source in `install-paths.ts`, consumed by install, check, the shell probe, and the per-command verifier. **Markers are NOT durable:** kimi-code persists its config via smol-toml `stringify(parse(...))` on every login/settings write, which deletes ALL comments — including the managed-block BEGIN/END markers — while the `[[hooks]]` table (data) survives and keeps enforcing. `evaluateInstalled` therefore falls back, ONLY from the absent state, to a marker-less table that is our clean enforcing hook. **That decision is made with the REAL `smol-toml` parser** (`hasCleanEnforcingHookEntry`), not a line scanner: it strips managed-block lines (host isolation), parses the remainder, and accepts only a `hooks[]` entry with `event="PreToolUse"`, `command===` the byte-exact canonical command, **NO `matcher` in any TOML spelling** (a matcher can disable/narrow the hook — `new RegExp("*")` throws → hook off → auto-approve), and no keys beyond `{event,command,timeout}`. Never regress this to a line scanner: one has demonstrated blind spots (a matcher hidden after a blank line, behind a quoted `"matcher"` key, or past a multiline array value); the parser is immune. `findBareApprovalHookTables` remains ONLY the conservative line-based PRUNE scanner. Orphan pruning (`findUnmanagedApprovalHookBlocks`) is **host-scoped** via `hostIdFromHookCommand` plus an `alsoMatchCommand` byte-exact clause — one host's install/scoped-uninstall/`--uninstall` never removes another host's marker-less live hook yet always reclaims its own by exact command even under a host-id override (`--uninstall --all` is the explicit every-host sweep; hand-rolled hooks are never touched). Invariants to re-verify each audit: the installed check stays **parser-based, matcher-rejecting, byte-exact, absent-state-only**; the prune stays host-scoped.
- **Serialized config mutation.** Install and uninstall hold a private adjacent `config.toml.kimi-plugin-cc.lock` across the complete read-modify-write transaction. Candidate publication is atomic; lock reads are no-follow/nonblocking/regular-file-only and capped; acquisition is bounded; live owners are never stolen; stale recovery uses an ABA-safe recovery lease; and inode/token ownership is rechecked before mutation or removal. A newly-created kimi-code home is mode `0700`, and both config and lock files are mode `0600`. Valid inline `hooks = [...]` assignments are normalized to canonical `[[hooks]]` tables with an explicit formatting/comment-loss warning before insertion. `/kimi:setup --check` exits nonzero when any whole-file, hook-schema, managed-block, path, or shell-probe check fails.
- **Host-scoped managed blocks.** Claude Code and Codex install the plugin to **different, version-stamped paths** (`~/.claude/plugins/cache/…` vs `~/.codex/plugins/cache/…`) but share one `~/.kimi-code/config.toml`. The managed block is therefore keyed by a **host id** — the marker carries a `:<host-id>` suffix (`# === BEGIN kimi-plugin-cc-managed:claude-code (vX) ===`). **Never collapse back to one shared block:** a single un-suffixed block was exact-matched to the running host, so `/kimi:setup` (Claude) and `$kimi-setup` (Codex) overwrote each other and the loser's write-commands refused with "hook path drift." `resolveHostId` (`install-paths.ts`) derives the id *version-independently* from the hook path (`.claude`→`claude-code`, `.codex`→`codex`, else `host-<sha1[:16]>`; `KIMI_PLUGIN_CC_HOST_ID` overrides), so a plugin upgrade **refreshes** the same host's block instead of accumulating one per version. `parseManagedBlock(contents, hostId)` resolves to the current host's own suffixed block (a lone legacy un-suffixed block is adopted as a fallback); `evaluateInstalled` **requires** `hostId` and keeps the per-host exact-equality contract. `runInstall` upserts only this host's block, converts a matching legacy block in place, and prunes orphaned marker-less approval-hook `[[hooks]]` (via `findUnmanagedApprovalHookBlocks`/`isOurApprovalHookCommand` — strict: canonical two-single-quoted-token command whose script is `approval-hook.js` under a kimi-marketplace/kimi-plugin-cc tree, so a hand-rolled hook is never removed). `runUninstall` is host-scoped by default; `/kimi:setup --uninstall --all` removes every host's block. Enforcement stays correct under coexistence because kimi-code fires every `[[hooks]]` and aggregates **any-block-wins** — the current host's live hook always denies; a stale *other*-host path fails open harmlessly; a drifted *own* block is caught at the per-command verify gate (prompts a re-setup for that host only). Invariant to re-verify: the exact-equality check is per-host, and no install/uninstall path ever mutates another host's block.
- Rescue defines the direct-workspace write allowlist used by both rescue and pursue. Workspace allowlist (shell-quote parser, mutating-flag detector, symlink reject, path-realpath check) lives in `runtime/rescue-approval.ts` and is called by the hook via `evaluateRescueHookRequest`. Mutating-flag list includes `--fix`/`--write`/`--apply`/`--in-place`/`-i`/`-w` **and** the output-to-file class (`--output`/`--output-file`/`--output-directory`/`--output-dir`, exact + `=` prefix) so tools can't write outside the workspace via their own report-output mechanism (e.g. `git diff --output=`, `curl --output`, `eslint --output-file=`). The `-o` short form is rejected per-tool where its semantics are write-shape (`eslint -o`, `go -o`, `ruff check -o`, `sort -o`; `uniq IN OUT` in pipelines). A global `hasExecDelegatingFlag` rejects, for every binary, flags whose *value* runs as a command/tool — `--open-files-in-pager` (a CRITICAL RCE: `git grep --open-files-in-pager=<cmd>` makes git exec `<cmd>`, the same pager-smuggle class as the pre-subcommand `git -c core.pager=` defense but via a subcommand flag; the `-O` short form is rejected git-locally to avoid colliding with `find -O<level>`), `-vettool`/`-toolexec`/`-exec`. `mypy --junit-xml`/`--*-report` and `pytest --junitxml`/`--result-log`/`--report-log` are rejected (report-writers escape the workspace). Test runners (`go test`/`cargo test`/`pytest`) still execute repo code by design — documented as an explicit trust boundary in `docs/safety.md`.
- Rescue cannot mutate git state. The main Claude thread owns branch/commit.
- **`/kimi:pursue` (experimental) = autonomous goal mode.** Exposes kimi-code 0.8.0+ headless goal mode (`kimi -p "/goal ..."`) so Kimi pursues an objective across continuation turns. Goal mode is reached by the `/goal` prompt prefix; on 0.8–0.11 it is *also* gated by the `goal-command` experimental flag, which `pursue.ts` sets per-spawn via `KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1` — redundant since 0.12.0 removed that gate, but keep it for 0.8–0.11 compat. It is **write-capable and reuses the rescue trust boundary verbatim**: `runtime/commands/pursue.ts` spawns with `commandLabel: "rescue"` (so the PreToolUse hook applies the same workspace allowlist on *every* continuation turn — the hook is policy index 0, verified end-to-end by the goal-mode real-binary smoke), refuses without the hook, reuses the `command_type:"rescue"` job lineage (so status/result/cancel work unchanged), and cannot mutate git state. The only new risk is unboundedness, bounded by a **mandatory finite `--budget` wall-clock AbortController ceiling** (default 45m) — the sole hard bound (`--turns` is a soft prompt-injected hint). Runtime-foreground-only (no detached-worker flag), but the *caller* detaches its own shell call — same contract as write-swarm (see that bullet), except pursue's edits land in the real tree and cancel does not roll them back. No `--resume` (goal mode's `goalId` differs from the resume-hint `sessionId`; the goalId is captured/surfaced but resume is deferred until upstream reconciles the split). Exit codes 0/3/6 (complete/blocked/paused) are terminal, not failures. See `docs/safety.md` § "Autonomous goal mode".
- **`/kimi:swarm` = read-only parallel fan-out.** Exposes kimi-code 0.12.0+'s `AgentSwarm` tool so Kimi fans a **read-only** review out across subagents (one per target) and consolidates the findings. `runtime/commands/swarm.ts` spawns with `commandLabel: "swarm"` — a hook label (`runtime/hooks/approval-policy.ts`) that allows the read-only tool set PLUS the exact tool name `AgentSwarm` (verified `readonly name = 'AgentSwarm' as const`), and nothing else (the singular `Agent` tool is denied). The coordinator MUST be allowed to call `AgentSwarm` or the swarm never launches; every spawned subagent inherits the `swarm` label and fires the SAME hook at policy index 0 (swarm subagents use the standard permission stack — no `deny-all`, see the compat note above), so a subagent's write/edit/shell is denied exactly like a single-turn review's → **read-only swarm opens ZERO new write surface.** It reuses the `command_type:"review"` job lineage (status/result/cancel work unchanged; `agent_profile` is `<swarm>`). Like every model-spawning command, swarm **REFUSES without the hook**; its fan-out makes the reason especially acute (`KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1` still explicitly opts out). The only new risk is COST/runaway from N parallel runs (+ possible nested `AgentSwarm`), bounded by a **mandatory finite `--budget` wall-clock AbortController ceiling** (default 30m). Two **distinct** subagent bounds: `--cap N` is a SOFT prompt-injected *total*-count hint (the hook is stateless — it can't count subagents), and `--max-concurrency N` is a HARD ceiling on *concurrent* subagents on **kimi-code 0.18.0+** (`KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY`) — `swarm.ts` passes the latter via `CliClientOptions.swarmMaxConcurrency`, which `cli-client.ts::buildEnv` exports per-spawn (older binaries ignore the unknown env var). **Never re-merge the two flags into one** (the "cap-as-total-count illusion"): a user reading `--cap N` expects a total-cost bound, but the env var caps only *peak* parallelism — a coordinator can still launch many subagents sequentially. `--max-concurrency` defaults to **4** (`resolveSwarmMaxConcurrency` in `swarm.ts`) when the user passes none, so the env var is ALWAYS exported and an auto-dispatched fan-out is never unbounded on 0.18.0+. Foreground-only; `--write` mode lives in the next bullet. **A model-invocable `kimi-swarm` agent wraps this command** so the main Claude thread can dispatch a read-only fan-out autonomously (distinct from the human-only `/kimi:swarm` slash command; agents are the model-invocable path, as for review/challenge/ask/rescue/pursue). Auto-dispatch widens no write surface (swarm is read-only) and removes no cost bound: `--budget` plus the default `--max-concurrency` (4, enforced on 0.18.0+ for every run) hold whoever launches it. See `docs/safety.md` § "Read-only swarm".
- **`/kimi:swarm --write` = write-capable parallel fan-out.** `runtime/commands/swarm.ts::runWriteSwarm` creates an **ephemeral detached git worktree off HEAD** under the plugin's own `worktreesDir` (never inside the user repo), spawns the coordinator `kimi -p` with **cwd = that worktree** (the load-bearing `spawnCwd` seam in `executeSwarmJob`; `job.cwd` stays the user's real cwd for lineage), and prompts `subagent_type:"coder"` (write-capable) to edit **disjoint** targets with no git and no nested `AgentSwarm`. Spawns with `commandLabel: "swarm-write"` — a hook label that allows `AgentSwarm` + the read-only set and routes write/edit/shell through `evaluateRescueHookRequest` **verbatim**, BUT scoped to a **forge-proof trusted worktree root**: `cli-client.buildEnv` exports the worktree path as `KIMI_PLUGIN_CC_WORKSPACE_ROOT`, the entry script passes it as `ctx.trustedWorkspaceRoot`, and the `swarm-write` case confines writes to THAT (not the hook payload `cwd`) — the model inside kimi cannot forge an env var on its own process, so confinement does not depend on upstream payload-cwd derivation. **Fail-closed**: a missing trusted root denies all writes. Confinement works only because kimi-code spawns `AgentSwarm` subagents in-process sharing `parent.config.cwd` (`subagent-host.ts:362`) under the STANDARD permission stack (no deny-all; the `DenyAllPermissionPolicy.unshift` is `startBtw()`-only, NOT the swarm path), so every `coder` subagent's write fires the index-0 hook and is confined to the one shared worktree → **the user's real tree is never touched**; re-verify that permission-stack claim each audit. The plugin captures the change set as an **untruncated, applyable `.patch`** (`git add -N` intent-to-add + `git diff --binary`, writing NO loose objects into the shared object DB) on **every** terminal path (success/cancel/budget/failure) BEFORE removing the worktree; a startup sweep reaps orphans from a hard kill. **The plugin never applies or commits — the main thread owns the merge** (`docs/safety.md`); the result report prepends the patch path. Reuses the `command_type:"rescue"` lineage (`agent_profile "<swarm-write>"`). **Refuses** without the hook (like read swarm). Gated to kimi-code **≥ 0.18.0** (the hard `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` cap — below it a write fan-out has no peak bound), a git repo with a **born HEAD** (`WRITE_SWARM_NOT_A_REPO`/`WRITE_SWARM_NO_HEAD`), with a **loud dirty-tree warning** (worktree is off HEAD, so uncommitted work is not included). Default `--max-concurrency` is **1** (`SWARM_WRITE_DEFAULT_MAX_CONCURRENCY`) — writes serialize since disjoint-target partitioning is prompt-only. **Runtime-foreground-only ≠ caller-foreground-only:** the parser rejects `--background`/`--wait`/`--fresh`/`--resume`, but the *caller's* shell call is separate and detaching it is the documented default — a fan-out outlives a foreground timeout (Claude Code caps foreground Bash at 10m vs the 30m default budget), and the bounds (`--budget`, `--max-concurrency`, hook-required, patch-only) are runtime-enforced, not watcher-enforced. **Cancellation is no-id:** job ids are `randomUUID()` and a detached run emits nothing until its final report, so the stop path is `companion.sh cancel` with NO id (`findLatestJob({runningOnly:true})` for the repo); never document "the agent keeps the job id from the launch output" — that flow cannot execute. With several runs in flight the no-id form takes the most recent, so a multi-run session confirms first. **Prefer that command over Esc/TaskStop:** a harness interrupt is SIGTERM→SIGKILL with ~1.35s of grace, delivered process-group-wide (so no Kimi descendant survives), which is less than `cli-client.ts`'s 1500ms child escalation + quiescence check + `git diff --binary` capture — interrupting can therefore kill the companion mid-teardown and lose the patch, while the cancel command signals from a separate process not racing that deadline. The margin is thin; re-measure on harness changes. **Pursue is NOT swarm-write:** its edits land in the user's real tree and cancel does not roll them back; swarm-write's stay in a throwaway worktree and return as a discardable patch. Keep that asymmetry visible in the agent docs. **A model-invocable `kimi-swarm-write` agent** wraps this command: auto-dispatch widens no write surface and removes no bound — it runs the identical `runSwarm --write` (patch-only; the plugin never applies/commits; worktree confinement + index-0 hook + `--budget` + default `--max-concurrency` 1 + hook-required all hold), making it a *less* dangerous agent than the write-capable `kimi-pursue` (which writes directly to the workspace). Strict triggering: BOTH many disjoint write targets AND explicit fan-out intent, else steer to `kimi-rescue`/`kimi-swarm`/`kimi-pursue`. See `docs/safety.md` § "Write-capable swarm".
- The `goal.summary` stream-json record (kimi-code 0.8.0+, role-less) is recognized by `runtime/stream-json.ts` on a dedicated `StreamJsonOutcome.goalSummary` channel (the `StreamJsonRecord` union stays role-keyed; no `.role`-consumer churn) and captured out-of-band on `CliClientResult.goalSummary`, mirroring the resume-hint meta record. kimi-code 0.23.5's `role:"meta", type:"turn.step.retrying"` record is modeled exactly and normalized, then filtered out of consumer `records[]`/`onRecord` with the other wrapper metadata; malformed retry shapes still enter diagnostics.
- Jobs in SQLite are the source of truth. Terminal states are permanent.
- Review/challenge/ask/rescue are all prose pass-through (review and challenge emit no JSON schema). Review gate is the only command that still parses Kimi output (JSON allow/block).
- Review gate is a Stop hook, disabled by default, fail-open on malformed output. It skips visibly without spawning Kimi when hook enforcement is missing or invalid; other skip cases (missing assistant message, unreadable transcript) also surface their reason through `systemMessage`.
- LLM-caller discipline: stderr is for humans only; anything load-bearing for an agent caller goes in stdout, exit codes, or persisted SQLite state. Parsers hard-fail on unknown flag-shaped tokens (INVALID_ARGS) instead of warn-and-swallowing — wrappers never see stderr warnings. `RuntimeError` carries an optional `details: Record<string, unknown>` for structured failure context.
- Default command stdout: review/challenge/ask/rescue emit raw prose; status emits raw job-row JSON; result emits raw artifact markdown. `result <jobId> --json` opts into a structured envelope (`{job_id, kind, status, summary, error, artifact_path, body, created_at, completed_at}`) for downstream automation.
- Cancellation uses AbortController + SIGTERM → SIGKILL (1500ms default escalation) and treats teardown as a settlement barrier: neither a normal result nor a budget error reaches its caller while the owned teardown is still running. POSIX spawns detached, captures descendant ancestry plus PID/PGID/start-time identity in one bounded process-table snapshot (no PID-only handoff), revalidates identities in batched bounded passes before positive-pid or group signals, starts the grace interval only after SIGTERM dispatch, and waits through a bounded 500ms post-SIGKILL quiescence check. A changed identity is never signaled; unexpected teardown failure rejects explicitly and emergency-kills the controlled root rather than stranding settlement. This closes the normal close-before-escalation race that could let a detached Bash grandchild keep writing while write-swarm captured and removed its worktree. A post-kill probe timeout is logged and bounded rather than hanging the host forever. win32 retains direct-child-only cancellation, so descendant reaping there remains a known gap. The pre-listener abort race is still covered by re-checking `signal.aborted` after listener attachment.
- Config writes preserve mode 0o600. `writeConfigAtomic` chmods the temp file before rename so the final inode never exists at a wider mode, matching the user's existing kimi-code config permissions (the file holds API keys + tokens).
## Post-GA roadmap
See [ROADMAP-TO-GA.md](./ROADMAP-TO-GA.md) for the full pre-GA history and v1.1 backlog. **v1.0.0 GA shipped 2026-05-26**, closing the kimi-code 0.2.0 stream-json session-meta gap (caught by alpha.4's loud-warning surface in production smoke), H6 (kimi-version probe at setup time), H2 (closed by upstream + plugin consumption), and H3 partial (forward-compat for unknown meta types). **2026-05-27 audit**: 4-reviewer pass verified compat through kimi-code 0.4.0 (released same day); no runtime changes required. Remaining: H1 (hook fail-open runtime drift) and H5 (per-spawn thinking control — upstream-blocked). **H3, H4, H8, H9 closed in v1.2.x** (see the Version line above), and **H7 is effectively complete** — the local real-binary smoke (`tests/runtime/real-binary-smoke.test.ts`, `bun run smoke:real`, delivered 2026-05-28) ships and runs on manual CI dispatch (`smoke.yml`); wiring it to *every* push is gated only on adding a `KIMI_MODEL_API_KEY` secret and is a deliberate cost choice, not a blocker.
## When editing
- Read the code before changing it — the runtime has specific invariants that aren't obvious from file names
- Run `bun run check` before considering any change done
- After any `runtime/**` (or `scripts/*.sh`) change, regenerate the Codex package: `bun run build && bun run generate:surfaces`, then stage `dist/` AND `plugins/kimi-codex/`. The `plugins/kimi-codex/` tree is GENERATED — never hand-edit it; edit `runtime/`/`scripts/`/`scripts/surface-registry.ts` and regenerate.
- Codex sidecars are generated by `bun run generate:surfaces` from `scripts/surface-registry.ts`. `bun run check:surfaces` (run after `build` inside `check`) verifies: the Claude plugin bytes match the locked hash gate, the generated Codex text surfaces are current, the bundled runtime mirror (`plugins/kimi-codex/{scripts,dist}`) byte-matches root, and no orphaned generated files remain.
- Editing a Claude command/agent or any `.claude-plugin/*` file changes its bytes, so you MUST re-pin its sha256 in `CLAUDE_SURFACE_HASHES` (`scripts/surface-registry.ts`) — `shasum -a 256 <file>` — or `check:surfaces` fails closed.
- `dist/` and `plugins/kimi-codex/` are committed intentionally (zero-build install for both hosts; Codex copies the subfolder to its cache). The drift gate catches forgotten rebuilds/regenerations.
- Agent files register at session start. Adding or editing `agents/*.md` mid-session doesn't activate them until Claude Code reloads — reach for slash commands or direct `companion.sh` in the same session.
- `.claude/` is gitignored — notes, worktrees, internal docs under it stay local. Don't try to commit them.
## Releasing
`runtime/version.ts` is the SINGLE version source — `scripts/surface-registry.ts` (`PLUGIN_VERSION`) imports it, so the Codex manifest/marketplace version propagates automatically. A version bump touches these and then regenerates:
- `runtime/version.ts` (`KIMI_PLUGIN_CC_VERSION` — written into the managed-block marker comment by /kimi:setup; imported by surface-registry)
- `package.json`
- `.claude-plugin/plugin.json` — **also re-pin its sha256 in `CLAUDE_SURFACE_HASHES`** (its bytes change)
- `.claude-plugin/marketplace.json` — **also re-pin its sha256 in `CLAUDE_SURFACE_HASHES`**
- `AGENTS.md` — bump the version number, and **REPLACE** the Version-line headline if the release changes a standing contract. The headline states the **contract an agent must not regress**, in present tense — NOT what the release did. "v1.9.1 closes the test gap that let X reach review" is a changelog entry; "detached-worker tests MUST run a real node subprocess with no override" is a headline. Keep ONE, never append. Everything a release *did* — dates, PR numbers, what broke, how it was reviewed — goes in `CHANGELOG.md` / ROADMAP's Post-GA audit log, never here. Same rule for the invariant bullets below: state today's contract plus the rationale that stops it being re-broken, and let the archaeology live in the log files.
- `CHANGELOG.md` (add a new version section — see its top banner. Docs-only compat checkups that DON'T bump the version are logged in ROADMAP's Post-GA audit log instead, not here.)
- then run `bun run build && bun run generate:surfaces` to regenerate `plugins/kimi-codex/` (manifest version + bundled runtime), and stage it.
Then `bun run check`, commit, `git tag -a vX.Y.Z -m "..."`, `git push` + `git push origin vX.Y.Z`, then `gh release create`.