kimi-plugin-cc 2.2.1 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +5 -5
  5. package/agents/kimi-challenge.md +3 -3
  6. package/agents/kimi-pursue.md +1 -1
  7. package/agents/kimi-rescue.md +2 -2
  8. package/agents/kimi-review.md +3 -3
  9. package/commands/README.md +2 -2
  10. package/commands/challenge.md +1 -1
  11. package/commands/pursue.md +1 -1
  12. package/commands/review.md +1 -1
  13. package/dist/background-spawn.js +9 -17
  14. package/dist/cli-client.js +22 -62
  15. package/dist/commands/ask.js +24 -29
  16. package/dist/commands/cancel.js +7 -19
  17. package/dist/commands/cli-helpers.js +9 -17
  18. package/dist/commands/pursue.js +21 -31
  19. package/dist/commands/registry.js +6 -19
  20. package/dist/commands/rescue.js +20 -20
  21. package/dist/commands/review-gate.js +16 -24
  22. package/dist/commands/review.js +37 -43
  23. package/dist/commands/setup.js +85 -171
  24. package/dist/commands/swarm.js +103 -88
  25. package/dist/commands/worker.js +0 -6
  26. package/dist/git.js +14 -7
  27. package/dist/hooks/approval-hook.js +9 -20
  28. package/dist/hooks/approval-policy.js +36 -69
  29. package/dist/hooks/config-safety.js +7 -4
  30. package/dist/hooks/install-paths.js +62 -138
  31. package/dist/hooks/install.js +37 -108
  32. package/dist/hooks/managed-block.js +88 -214
  33. package/dist/job-store.js +6 -28
  34. package/dist/kimi-command.js +1 -3
  35. package/dist/kimi-engine.js +2 -1
  36. package/dist/kimi-timeouts.js +9 -21
  37. package/dist/kimi-version-probe.js +16 -1210
  38. package/dist/native-v2-preflight.js +1 -1
  39. package/dist/parsing.js +4 -18
  40. package/dist/rescue-approval.js +78 -164
  41. package/dist/session-recovery.js +7 -10
  42. package/dist/stream-json.js +22 -174
  43. package/dist/version.js +1 -1
  44. package/docs/migration.md +2 -6
  45. package/docs/registry-releases.md +25 -5
  46. package/docs/safety.md +16 -12
  47. package/package.json +2 -2
  48. package/plugins/kimi-codex/.codex-plugin/plugin.json +4 -4
  49. package/plugins/kimi-codex/dist/background-spawn.js +9 -17
  50. package/plugins/kimi-codex/dist/cli-client.js +22 -62
  51. package/plugins/kimi-codex/dist/commands/ask.js +24 -29
  52. package/plugins/kimi-codex/dist/commands/cancel.js +7 -19
  53. package/plugins/kimi-codex/dist/commands/cli-helpers.js +9 -17
  54. package/plugins/kimi-codex/dist/commands/pursue.js +21 -31
  55. package/plugins/kimi-codex/dist/commands/registry.js +6 -19
  56. package/plugins/kimi-codex/dist/commands/rescue.js +20 -20
  57. package/plugins/kimi-codex/dist/commands/review-gate.js +16 -24
  58. package/plugins/kimi-codex/dist/commands/review.js +37 -43
  59. package/plugins/kimi-codex/dist/commands/setup.js +85 -171
  60. package/plugins/kimi-codex/dist/commands/swarm.js +103 -88
  61. package/plugins/kimi-codex/dist/commands/worker.js +0 -6
  62. package/plugins/kimi-codex/dist/git.js +14 -7
  63. package/plugins/kimi-codex/dist/hooks/approval-hook.js +9 -20
  64. package/plugins/kimi-codex/dist/hooks/approval-policy.js +36 -69
  65. package/plugins/kimi-codex/dist/hooks/config-safety.js +7 -4
  66. package/plugins/kimi-codex/dist/hooks/install-paths.js +62 -138
  67. package/plugins/kimi-codex/dist/hooks/install.js +37 -108
  68. package/plugins/kimi-codex/dist/hooks/managed-block.js +88 -214
  69. package/plugins/kimi-codex/dist/job-store.js +6 -28
  70. package/plugins/kimi-codex/dist/kimi-command.js +1 -3
  71. package/plugins/kimi-codex/dist/kimi-engine.js +2 -1
  72. package/plugins/kimi-codex/dist/kimi-timeouts.js +9 -21
  73. package/plugins/kimi-codex/dist/kimi-version-probe.js +16 -1210
  74. package/plugins/kimi-codex/dist/native-v2-preflight.js +1 -1
  75. package/plugins/kimi-codex/dist/parsing.js +4 -18
  76. package/plugins/kimi-codex/dist/rescue-approval.js +78 -164
  77. package/plugins/kimi-codex/dist/session-recovery.js +7 -10
  78. package/plugins/kimi-codex/dist/stream-json.js +22 -174
  79. package/plugins/kimi-codex/dist/version.js +1 -1
  80. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +2 -2
  81. package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +1 -1
  82. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +1 -1
  83. package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +1 -1
  84. package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +1 -1
  85. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +1 -1
  86. package/plugins/kimi-codex/skills/kimi-result/SKILL.md +1 -1
  87. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +3 -3
  88. package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +1 -1
  89. package/plugins/kimi-codex/skills/kimi-status/SKILL.md +1 -1
  90. package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +1 -1
  91. package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +1 -1
@@ -1,58 +1,28 @@
1
1
  // Shared parser for the kimi-plugin-cc managed block in
2
- // ~/.kimi-code/config.toml.
2
+ // ~/.kimi-code/config.toml. Setup and the verifier use this one grammar;
3
+ // install-paths.ts separately owns the canonical command bytes and drift
4
+ // classification used here after a mismatch.
3
5
  //
4
- // Why a separate module:
6
+ // Claude Code and Codex share the config but install to different paths. The
7
+ // marker's `:<host-id>` suffix makes each host own and verify only its block.
8
+ // An un-suffixed legacy block can be adopted by the current host on install.
5
9
  //
6
- // PR 2 added `runtime/hooks/install.ts::verifyHookInstalled` as a
7
- // pre-call gate. PR 4 added `runtime/commands/setup.ts` as the
8
- // installer. PR 4 reviewers found that the two were validating
9
- // different shapes — the verifier was a substring check (could be
10
- // bypassed by stray comments containing `kimi-plugin-cc-managed`),
11
- // and `setup --check` only confirmed `blockText.includes(hookPath)`
12
- // without checking for matcher/event/command exactness. A block with
13
- // `matcher = "*"` (which throws inside kimi-code and silently
14
- // disables the hook) would pass both gates.
15
- //
16
- // This module owns the single source of truth for what a valid
17
- // managed block looks like. Both call sites import from here.
18
- //
19
- // (The drift classifier `describeHookCommandDrift` lives in install-paths.ts —
20
- // the single source of truth for the hook command's byte shape — and is used
21
- // here to turn a command MISMATCH into an actionable diagnosis when the caller
22
- // supplies an `nodeExists` fs predicate.)
23
- //
24
- // HOST SCOPING (v1.7.0):
25
- //
26
- // Claude Code and Codex install this plugin to DIFFERENT, version-stamped
27
- // paths but SHARE one `~/.kimi-code/config.toml`. Before v1.7.0 there was a
28
- // single managed block whose `command` was exact-matched to the RUNNING
29
- // host's path, so `/kimi:setup` (Claude) and `$kimi-setup` (Codex) overwrote
30
- // each other. Now the block is HOST-SCOPED — the marker carries a `:<host-id>`
31
- // suffix and each host owns/verifies its OWN block. Un-suffixed markers are
32
- // treated as LEGACY (from pre-v1.7.0 single-host installs); the current host
33
- // adopts a lone legacy block on its next install (see setup.ts).
34
- //
35
- // What a valid managed block looks like (line-by-line, post-trim):
10
+ // A valid block has this line-oriented shape:
36
11
  //
37
12
  // # === BEGIN kimi-plugin-cc-managed:<host-id> (vX.Y.Z) ===
38
- // ... optional comment lines (any number, any content) ...
13
+ // ... optional comments ...
39
14
  // [[hooks]]
40
15
  // event = "PreToolUse"
41
- // command = "node 'absolute/path/to/approval-hook.js'" (or process.execPath form)
16
+ // command = "'/abs/path/to/node' '/abs/path/to/approval-hook.js'"
42
17
  // timeout = <integer>
43
18
  // # === END kimi-plugin-cc-managed:<host-id> ===
44
19
  //
45
20
  // Critical rules:
46
21
  //
47
- // - NO `matcher = ...` line. kimi-code compiles matchers as JS regex;
48
- // `new RegExp("*")` throws and silently disables the hook. Omitting
49
- // matcher means "fire for every tool".
50
- // - `event = "PreToolUse"` is required. Any other event would not
51
- // enforce our safety contract.
52
- // - Exactly ONE `[[hooks]]` declaration inside the block. Two would
53
- // register two hooks, which is fine functionally but breaks
54
- // idempotency checks.
55
- // - The `command` line must contain the hook script path we expect.
22
+ // - No `matcher` in any TOML spelling: it can disable or narrow the hook.
23
+ // - `event = "PreToolUse"` is required.
24
+ // - Exactly one `[[hooks]]` table appears inside the markers.
25
+ // - The decoded command must equal the canonical expected command byte-for-byte.
56
26
  import { parse as parseToml } from "../vendor/smol-toml/parse.js";
57
27
  import { classifyHookCommandDrift, describeHookCommandDrift, hostIdFromHookCommand, isOurApprovalHookCommand, } from "./install-paths.js";
58
28
  const MARKER_TAG = "kimi-plugin-cc-managed";
@@ -82,9 +52,8 @@ function findMarkers(lines) {
82
52
  const trimmed = lines[i].trim();
83
53
  const begin = BEGIN_LINE_RE.exec(trimmed);
84
54
  if (begin !== null) {
85
- // Normalize marker host ids to lowercase so a hand-edited `:Claude-Code`
86
- // isn't treated as a different host from the lowercase `claude-code` the
87
- // installer emits (Kimi review — avoids confusing duplicate blocks).
55
+ // Normalize marker host ids so a hand-edited casing difference cannot
56
+ // create a distinct host and confusing duplicate blocks.
88
57
  hits.push({ index: i, kind: "begin", hostId: begin[1]?.toLowerCase() ?? null });
89
58
  continue;
90
59
  }
@@ -96,9 +65,8 @@ function findMarkers(lines) {
96
65
  return hits;
97
66
  }
98
67
  /**
99
- * Pair BEGIN/END markers into well-formed blocks. Returns the first orphan
100
- * (BEGIN-without-END or END-without-BEGIN) encountered, mirroring the
101
- * pre-v1.7.0 single-block behavior — an orphan is a hard error for install.
68
+ * Pair BEGIN/END markers into well-formed blocks. Return the first orphan
69
+ * (BEGIN-without-END or END-without-BEGIN); either is a hard error for install.
102
70
  */
103
71
  function pairBlocks(lines) {
104
72
  const markers = findMarkers(lines);
@@ -153,7 +121,7 @@ function validateBlockBody(rawBody) {
153
121
  // `event`/`command` lines below could belong to a DIFFERENT table (e.g.
154
122
  // `[[hooks]]` with only an event, then `[not_hooks]` carrying the
155
123
  // canonical command). Binding the block to exactly one [[hooks]] table
156
- // keeps the verifier from blessing a command-less hook. (Codex review.)
124
+ // keeps the verifier from blessing a command-less hook.
157
125
  invalidReason =
158
126
  "block contains an unexpected TOML table — only a single [[hooks]] table is allowed. Reinstall with /kimi:setup to repair.";
159
127
  break;
@@ -170,21 +138,15 @@ function validateBlockBody(rawBody) {
170
138
  const basic = COMMAND_LINE_RE.exec(line);
171
139
  const literal = COMMAND_LITERAL_LINE_RE.exec(line);
172
140
  if (basic !== null || literal !== null) {
173
- // TOML basic strings escape `\`, `"`, and the control chars.
174
- // Setup writes the escaped form via `tomlBasicString`; the
175
- // verifier compares against the JS-string canonical command, so
176
- // we must decode the escapes here before equality. Without this,
177
- // a hook script path containing `'` round-trips through
178
- // `shellSingleQuote` -> backslash -> TOML basic-string escape ->
179
- // captured raw -> mismatch on verify. Audit re-review (reports
180
- // 33/34) flagged this as a UX false-fail. Literal strings ('...')
181
- // have no escapes — pass through as-is.
141
+ // Decode TOML basic-string escapes before comparing with the canonical JS
142
+ // string; literal strings have no escapes. Otherwise shell quoting and
143
+ // TOML escaping can make an equivalent command compare unequal.
182
144
  commandPath = basic !== null ? decodeTomlBasicString(basic[1]) : literal[1];
183
145
  continue;
184
146
  }
185
- // Other TOML lines (timeout = N, etc.) are accepted as opaque
186
- // pass-through. We don't validate them; if kimi-code rejects them
187
- // the probe will catch it.
147
+ // Other TOML lines are ignored only by this local grammar. Whole-file schema
148
+ // validation and foundHookEntryIsClean reject foreign keys before an
149
+ // installed verdict; the shell probe is not the validation backstop.
188
150
  }
189
151
  if (invalidReason === undefined) {
190
152
  if (!foundHooksTable) {
@@ -197,17 +159,11 @@ function validateBlockBody(rawBody) {
197
159
  invalidReason = "block is missing a `command = \"...\"` line";
198
160
  }
199
161
  }
200
- // Parser-based matcher rejection — defense in depth over the line-based
201
- // MATCHER_LINE_RE above. smol-toml normalizes a quoted `"matcher"`, a literal
202
- // `'matcher'`, or a dotted key to the `matcher` key that the bare-line regex
203
- // (`/^matcher\s*=/`) MISSES, while kimi-code's real parser honors it and
204
- // DISABLES the hook (`new RegExp("*")` throws → fire-for-nothing → auto-
205
- // approve). The absent-state fallback (`hasCleanEnforcingHookEntry`) already
206
- // parses and is immune; the primary marked-block path must be too, or a
207
- // quoted matcher slips a disabled hook past the verifier as `installed:true`.
208
- // Fail closed on any parse error — a body that won't parse in isolation is
209
- // anomalous and kimi-code would reject the whole config anyway. (kimi
210
- // whole-repo audit 2026-07-17.)
162
+ // Parser-based matcher rejection backs up the bare-line regex: smol-toml
163
+ // normalizes quoted, literal, and dotted matcher keys that the regex misses,
164
+ // while kimi-code honors them and can disable or narrow the hook. Both this
165
+ // marked-block path and the absent-state fallback must parse the real TOML.
166
+ // Fail closed when the body cannot be parsed in isolation.
211
167
  if (invalidReason === undefined) {
212
168
  try {
213
169
  const parsed = parseToml(rawBody.join("\n"));
@@ -240,9 +196,8 @@ function validateBlockBody(rawBody) {
240
196
  * Other hosts' suffixed blocks are ignored for this host's state (that is the
241
197
  * whole point of host scoping — they coexist).
242
198
  *
243
- * Stripping line endings before the per-line matchers means this is
244
- * CRLF-safe — the trailing `\r` on each line is removed before regex
245
- * comparison.
199
+ * Only a trailing `\r` is removed from each line for CRLF safety; other line
200
+ * whitespace is preserved here and trimmed only by the relevant matchers.
246
201
  */
247
202
  export function parseManagedBlock(contents, hostId) {
248
203
  const rawLines = contents.split("\n");
@@ -252,13 +207,10 @@ export function parseManagedBlock(contents, hostId) {
252
207
  return { state: { kind: "orphan", detail: paired.orphan }, blocks: [], lines };
253
208
  }
254
209
  const blocks = paired.blocks;
255
- // A block "belongs to" the current host when its marker suffix is this host,
256
- // OR (for a legacy un-suffixed block) its command path derives to this host —
257
- // an un-parseable/stale legacy command is claimable by whoever installs next.
258
- // This is what stops one host from adopting/clobbering another host's legacy
259
- // block during migration (Kimi review): a `~/.codex/…` legacy block is NOT
260
- // relevant to the `claude-code` host, so Claude appends its own block and
261
- // leaves Codex's intact.
210
+ // A block belongs to this host when its marker suffix matches or, for a
211
+ // legacy un-suffixed block, its command derives to this host. An unattributable
212
+ // legacy command is claimable by the next installer; a command attributed to
213
+ // another host is ignored so its block remains untouched.
262
214
  const relevant = blocks.filter((b) => effectiveHost(b, hostId) === hostId);
263
215
  if (relevant.length === 0) {
264
216
  return { state: { kind: "absent" }, blocks, lines };
@@ -311,33 +263,6 @@ export function decodeManagedCommandLine(trimmedLine) {
311
263
  return literal[1];
312
264
  return null;
313
265
  }
314
- /**
315
- * Robust, PARSER-based "is our clean enforcing hook present?" check for the
316
- * marker-less fallback. Parses the whole config with the same vendored
317
- * `smol-toml` kimi-code uses, then looks for a `hooks[]` entry that is EXACTLY
318
- * our clean hook:
319
- *
320
- * - `event === "PreToolUse"`,
321
- * - `command === expectedCommand` (byte-exact — the security boundary),
322
- * - NO `matcher` key in ANY TOML spelling (bare `matcher`, quoted
323
- * `"matcher"`, after a blank line, past a multiline array) — a matcher can
324
- * disable the hook (`new RegExp("*")` throws → hook never fires) or narrow
325
- * it to a subset of tools, so its presence means "not a clean install",
326
- * - no keys beyond `{event, command, timeout}` — an unknown key trips
327
- * kimi-code's `.strict()` hook schema and disables the whole hooks array.
328
- *
329
- * Using the real parser (not the line-oriented `findBareApprovalHookTables`,
330
- * which stays the conservative PRUNE scanner) closes the entire class of
331
- * line-scanner blind spots a three-model review panel demonstrated (Opus +
332
- * Codex + kimi all independently: parse the table, don't lex it). Returns
333
- * `false` on any parse error — fail-closed. The real callers
334
- * (`verifyHookInstalled`, `runCheck`) run whole-file schema validation first,
335
- * so a malformed or foreign-invalid file never reaches an installed verdict.
336
- *
337
- * NOTE: this deliberately does NOT range-check `timeout` — that is the config
338
- * schema's job (run upstream by both callers). A clean entry with an
339
- * out-of-range timeout is refused there, not here.
340
- */
341
266
  /**
342
267
  * Return `contents` with every well-formed managed BEGIN/END block removed
343
268
  * (inclusive), so what remains is only the bare/root TOML. Used by
@@ -387,14 +312,15 @@ function hooksArrayHasCleanEntry(hooks, expectedCommand) {
387
312
  }
388
313
  return false;
389
314
  }
315
+ /**
316
+ * Parser-based fallback used only in the absent state. Strip every managed
317
+ * block first so another host's live hook cannot count for this host, then parse
318
+ * the remaining bare/root TOML with the real parser rather than the prune line
319
+ * scanner. `hooksArrayHasCleanEntry` requires the exact event and command, no
320
+ * matcher in any spelling, and no foreign keys. Parse errors fail closed.
321
+ * Callers run whole-file schema validation first, including timeout ranges.
322
+ */
390
323
  export function hasCleanEnforcingHookEntry(contents, expectedCommand) {
391
- // Consider only hooks that live OUTSIDE every managed BEGIN/END block. In the
392
- // `absent` state this host has no block of its own, so any surviving marked
393
- // block belongs to ANOTHER host — its entries are that host's responsibility,
394
- // not evidence that THIS host is installed (host isolation). The fallback's
395
- // whole purpose is to recognize a MARKER-STRIPPED (bare) copy of our own hook,
396
- // so we strip all marked-block lines, then parse the remainder with the real
397
- // TOML parser.
398
324
  let parsed;
399
325
  try {
400
326
  parsed = parseToml(stripManagedBlockLines(contents));
@@ -414,7 +340,7 @@ export function hasCleanEnforcingHookEntry(contents, expectedCommand) {
414
340
  * table), so kimi-code loads it and DISABLES the hook (`new RegExp("*")` throws)
415
341
  * while the marked body looks clean → a fail-open `installed:true`. Parsing the
416
342
  * whole file exactly as kimi-code does attributes that trailing key to our entry
417
- * and rejects it. Fail-closed on any parse error. (Fable/kimi audit 2026-07-17.)
343
+ * and rejects it. Fail-closed on any parse error.
418
344
  */
419
345
  function foundHookEntryIsClean(contents, expectedCommand) {
420
346
  let parsed;
@@ -431,20 +357,11 @@ function foundHookEntryIsClean(contents, expectedCommand) {
431
357
  export function evaluateInstalled(contents, expectedCommand, opts) {
432
358
  const { state } = parseManagedBlock(contents, opts.hostId);
433
359
  if (state.kind === "absent") {
434
- // kimi-code re-serializes config.toml on login/settings writes via a
435
- // comment-dropping TOML stringifier (`stringifyToml(configToTomlData(...))`
436
- // in packages/agent-core/src/config/toml.ts — smol-toml has no comment
437
- // support), which deletes our BEGIN/END markers while leaving the
438
- // `[[hooks]]` TABLE (data) intact and enforcing. Markers were never the
439
- // security boundary — the byte-exact canonical command is — so a
440
- // marker-less table that is EXACTLY our clean enforcing hook still counts
441
- // as installed. This decision is made against the REAL TOML parser (not a
442
- // line scanner): a line scanner has TOML-semantic blind spots that a
443
- // reviewer panel demonstrated let a hook-DISABLING `matcher` hide from it —
444
- // after a blank line, behind a quoted `"matcher"` key, or past a multiline
445
- // array value — while kimi-code still honors it (`new RegExp("*")` throws →
446
- // hook disabled → auto-approve). The parser sees the whole table regardless
447
- // of layout, so `hasCleanEnforcingHookEntry` is immune to all three.
360
+ // kimi-code config rewrites drop comments but leave hook tables intact. In
361
+ // the absent state only, accept a marker-less table that is exactly this
362
+ // host's clean enforcing hook: markers are not the security boundary, the
363
+ // byte-exact command is. The real TOML parser sees matchers and foreign keys
364
+ // across blank lines, quoted spellings, and multiline values.
448
365
  if (hasCleanEnforcingHookEntry(contents, expectedCommand)) {
449
366
  return {
450
367
  installed: true,
@@ -453,12 +370,8 @@ export function evaluateInstalled(contents, expectedCommand, opts) {
453
370
  state,
454
371
  };
455
372
  }
456
- // Markers absent AND no byte-exact bare table. The common real-world cause
457
- // is BOTH failures at once: kimi-code strips markers on every login/settings
458
- // write, and a plugin update then moves the version-stamped hook path. The
459
- // 2026-07-25 Codex incident was exactly this pair. Classify the drift from
460
- // this host's own stale bare table so callers still get an actionable axis
461
- // instead of a bare "not present".
373
+ // If no exact bare table remains, classify drift only from this host's own
374
+ // stale table so the caller gets an actionable axis.
462
375
  const staleOwn = findBareApprovalHookTables(contents).find((table) => table.command !== expectedCommand &&
463
376
  isOurApprovalHookCommand(table.command) &&
464
377
  hostIdFromHookCommand(table.command) === opts.hostId);
@@ -506,11 +419,6 @@ export function evaluateInstalled(contents, expectedCommand, opts) {
506
419
  drift: classifyHookCommandDrift(state.commandPath, expectedCommand),
507
420
  };
508
421
  }
509
- // The marked body (lines between BEGIN/END) passed the line grammar and the
510
- // command matches — but confirm with the REAL whole-file parse that no matcher
511
- // or stray key rides our `[[hooks]]` table from OUTSIDE the markers (e.g. after
512
- // the END comment, which does not terminate the TOML table). Such a key would
513
- // load in kimi-code and disable/reject the hook while the body looks clean.
514
422
  if (!foundHookEntryIsClean(contents, expectedCommand)) {
515
423
  return {
516
424
  installed: false,
@@ -521,23 +429,17 @@ export function evaluateInstalled(contents, expectedCommand, opts) {
521
429
  return { installed: true, via: "managed-block", state };
522
430
  }
523
431
  /**
524
- * The line-oriented PRUNE scanner: find bare (marker-less) `[[hooks]]` tables
525
- * matching our grammar so `findUnmanagedApprovalHookBlocks` can remove them.
526
- *
527
- * NOTE: the INSTALLED decision does NOT use this — it uses the real TOML parser
528
- * (`hasCleanEnforcingHookEntry`), because a line scanner has TOML-semantic
529
- * blind spots (a `matcher` hidden after a blank line / behind a quoted key /
530
- * past a multiline array). This scanner is only for computing physical line
531
- * RANGES to delete, where a conservative, layout-aware view is what we want.
432
+ * Line-oriented PRUNE scanner, never the installed decision; that decision uses
433
+ * the real TOML parser through `hasCleanEnforcingHookEntry` and
434
+ * `foundHookEntryIsClean`.
532
435
  *
533
- * `[start, end)` (the range to delete) stays CONSERVATIVE: it covers only the
534
- * contiguous non-blank body after the `[[hooks]]` header (stopping at the first
535
- * blank line, next table header, or managed marker), so a prune never swallows
536
- * adjacent user content past a blank separator. Separately, to avoid leaving a
537
- * DANGLING `matcher` when a table has one after a blank line, the GRAMMAR check
538
- * scans the table's full TOML span (to the next header/marker/EOF) for a
539
- * `matcher` line and, if found, does not report the table at all (so it is
540
- * never half-cut). `command` is the decoded shell command.
436
+ * The deletion range `[start, end)` is conservative: only the contiguous
437
+ * non-blank body after `[[hooks]]`, stopping at a blank line, table header, or
438
+ * managed marker so adjacent user content is preserved. A separate full-span
439
+ * scan rejects matcher lines recognized by MATCHER_LINE_RE to avoid half-cutting
440
+ * a table. That scan is deliberately heuristic and does not claim to recognize
441
+ * every quoted or dotted TOML matcher spelling. `command` is decoded for the
442
+ * caller's ownership checks.
541
443
  */
542
444
  export function findBareApprovalHookTables(contents) {
543
445
  const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
@@ -560,12 +462,9 @@ export function findBareApprovalHookTables(contents) {
560
462
  let commandDecoded = "";
561
463
  let hasPreToolUseEvent = false;
562
464
  let hasMatcher = false;
563
- // A table we wrote contains ONLY event/command/timeout (+ comments). If any
564
- // OTHER key appears — e.g. a multi-line `metadata = [ … ]` array, whose
565
- // continuation lines would otherwise fool the `[`-boundary check and leave
566
- // dangling TOML after a partial prune — treat the table as NOT ours and
567
- // skip it. Conservative by design (Codex review): never partially cut a
568
- // structure we don't fully recognize.
465
+ // Our tables contain only event/command/timeout and comments. Any other key,
466
+ // including a multiline array, makes the shape unrecognized so pruning never
467
+ // partially cuts a structure and leaves dangling TOML.
569
468
  let simpleGrammar = true;
570
469
  while (j < lines.length) {
571
470
  const trimmed = lines[j].trim();
@@ -574,7 +473,7 @@ export function findBareApprovalHookTables(contents) {
574
473
  // Any TOML table header bounds the table — including one with a trailing
575
474
  // comment (`[[permission.rules]] # note`). A bare leading `[` is a table
576
475
  // header in TOML (value arrays are `key = [...]`), so stop there and
577
- // never swallow the next table. (Codex review.)
476
+ // never swallow the next table.
578
477
  if (trimmed.startsWith("["))
579
478
  break;
580
479
  if (BEGIN_LINE_RE.test(trimmed) || END_LINE_RE.test(trimmed))
@@ -601,22 +500,13 @@ export function findBareApprovalHookTables(contents) {
601
500
  }
602
501
  j += 1;
603
502
  }
604
- // CRITICAL (Opus review, HIGH): a blank line does NOT end a TOML table —
605
- // table membership runs from `[[hooks]]` to the NEXT table header (`[…`) or
606
- // EOF, across any blank lines. The contiguous loop above stops at the first
607
- // blank, so a `matcher = "*"` placed AFTER a blank line is still part of
608
- // this hook table for kimi-code's real parser: it loads (matcher is a
609
- // schema-valid string, so whole-file validation passes), then throws at
610
- // `new RegExp("*")` at hook-execution time and SILENTLY DISABLES the hook.
611
- // If we only checked the contiguous body we'd report `installed: true` for
612
- // a hook that is actually off — the exact auto-approve bypass this plugin
613
- // exists to prevent. So scan the REST of the table's TOML span for a
614
- // matcher and reject the table if one appears anywhere. (The prune line
615
- // range stays `[start, j)` — conservative, never eating past the blank —
616
- // because a rejected table is never pruned anyway.) Foreign keys after a
617
- // blank are NOT re-checked here: an unknown key fails kimi-code's `.strict()`
618
- // hook schema, so `validateKimiHookSetForEnvironment` rejects the whole file
619
- // upstream; only `matcher` is schema-valid-but-hook-disabling.
503
+ // A blank line does NOT end a TOML table; membership continues to the next
504
+ // header or EOF. The deletion range nevertheless stops at the blank to
505
+ // preserve adjacent user content. Scan the remaining span for matcher lines
506
+ // this scanner recognizes and reject the table rather than half-cutting it:
507
+ // matcher is schema-valid but can disable the hook at runtime. Foreign keys
508
+ // and other TOML spellings are handled by whole-file schema validation and
509
+ // the parser-based installed checks, not by this prune heuristic.
620
510
  let matcherBeyondBody = false;
621
511
  for (let k = j; k < lines.length; k += 1) {
622
512
  const t = lines[k].trim();
@@ -629,12 +519,10 @@ export function findBareApprovalHookTables(contents) {
629
519
  break;
630
520
  }
631
521
  }
632
- // Only report a table that matches our managed-block grammar EXACTLY:
633
- // `event = "PreToolUse"`, no matcher anywhere in its TOML span, and no keys
634
- // beyond event/command/timeout in the contiguous body. (Codex + Kimi review
635
- // — reduces false positives and avoids corrupting a multi-line-array table.)
636
- // Command-identity filtering (ours? whose host?) is the caller's concern —
637
- // see `findUnmanagedApprovalHookBlocks` and `evaluateInstalled`.
522
+ // Report only tables matching this conservative line grammar: PreToolUse,
523
+ // a decoded command, no recognized matcher in the TOML span, and no foreign
524
+ // keys in the contiguous body. This is not a full TOML-semantic acceptance
525
+ // check; command and host ownership are the caller's concern.
638
526
  if (simpleGrammar &&
639
527
  commandDecoded.length > 0 &&
640
528
  hasPreToolUseEvent &&
@@ -646,32 +534,19 @@ export function findBareApprovalHookTables(contents) {
646
534
  return ranges;
647
535
  }
648
536
  /**
649
- * Find orphaned, marker-less `[[hooks]]` tables that are unambiguously THIS
650
- * plugin's approval hook (canonical `'<node>' '<...>/approval-hook.js'` command
651
- * under a kimi-marketplace / kimi-plugin-cc tree) and live OUTSIDE any managed
652
- * BEGIN/END block. These arise when a kimi-code config rewrite strips the
653
- * marker comments off a live block (kimi-code's TOML stringifier drops all
654
- * comments), or from pre-v1.7.0 installs / host-path churn.
655
- *
656
- * HOST SCOPING (v1.8.2): pass `ownedBy` (a host id from `resolveHostId`) to
657
- * restrict the result to tables whose command path derives to THAT host via
658
- * `hostIdFromHookCommand`. Pruning MUST pass it — after a kimi-code comment
659
- * strip, another host's marker-less table is that host's LIVE hook, and a
660
- * host-blind prune silently disarms it (the pre-v1.8.2 seesaw: each host's
661
- * setup deleted the other's enforcement). Omitting `ownedBy` is the explicit
662
- * every-host sweep reserved for `/kimi:setup --uninstall --all`.
537
+ * Find marker-less canonical approval-hook tables outside managed blocks.
538
+ * `ownedBy` restricts results to the host derived from each command and is
539
+ * required for scoped pruning: another host's marker-less table may be its live
540
+ * hook after a config rewrite. Omit `ownedBy` only for the explicit every-host
541
+ * `/kimi:setup --uninstall --all` sweep. Non-canonical and hand-rolled hooks are
542
+ * not claimed.
663
543
  */
664
544
  export function findUnmanagedApprovalHookBlocks(contents, ownedBy, alsoMatchCommand) {
665
545
  return findBareApprovalHookTables(contents).filter((table) => {
666
- // A table whose command byte-equals what THIS host would write is
667
- // unambiguously this host's own (marker-stripped) hook — the tightest
668
- // possible ownership signal, stronger than any path derivation. Match it
669
- // regardless of `ownedBy`, so the install re-adorn works even when a
670
- // `KIMI_PLUGIN_CC_HOST_ID` override disagrees with the path-derived host
671
- // (`hostIdFromHookCommand`) — otherwise setup would append a duplicate
672
- // block beside the identical bare table (Kimi review F4). Never widens the
673
- // prune: another host's table and a hand-rolled hook carry different
674
- // commands, so they never equal this host's canonical command.
546
+ // Exact current command bytes are the strongest ownership signal and win
547
+ // even when a host-id override disagrees with path derivation. This does
548
+ // not widen the prune: another host's or a hand-rolled hook has different
549
+ // command bytes.
675
550
  if (alsoMatchCommand !== undefined && table.command === alsoMatchCommand)
676
551
  return true;
677
552
  if (!isOurApprovalHookCommand(table.command))
@@ -694,9 +569,8 @@ export const MARKERS = {
694
569
  * we compare against never contains them, so a mismatch surfaces as
695
570
  * "installed: false" rather than a parse error.
696
571
  *
697
- * The inverse of `tomlBasicString` in `runtime/hooks/install-paths.ts`'s
698
- * sibling helper in setup.ts. Audit re-review (reports 33/34) flagged
699
- * the missing decode as a verifier false-fail for apostrophe paths.
572
+ * Inverse of `tomlBasicString` in `runtime/commands/setup.ts`; decoding before
573
+ * comparison prevents shell/TOML escaping from causing a false mismatch.
700
574
  */
701
575
  function decodeTomlBasicString(raw) {
702
576
  let out = "";
@@ -542,35 +542,13 @@ function translateSqliteError(error) {
542
542
  return new RuntimeError("JOB_STORE_ERROR", `Job store operation failed: ${formatError(error)}`, "job-store", error instanceof Error ? { cause: error } : undefined);
543
543
  }
544
544
  /**
545
- * Run a function with a temporary `JobStore` whose handle is closed when
546
- * the function returns OR when the function throws after construction
547
- * succeeded.
548
- *
549
- * Construction-failure note: if `new JobStore(paths)` itself throws,
550
- * this helper cannot close anything — the constructor's own try/catch
551
- * owns adapter-handle cleanup on mid-migration failures. The helper
552
- * handles "store opened cleanly, function threw"; the constructor
553
- * handles "open or migrate failed mid-construction." Together they
554
- * make the leak class unreachable.
555
- *
556
- * Consolidates the repeated
557
- *
558
- * const store = new JobStore(paths);
559
- * try { ... } finally { store.close(); }
560
- *
561
- * dance that every command file used to copy. For the rare case where
562
- * the caller needs to keep a reference to the store past the function
563
- * body (e.g., async cancel handlers that close elsewhere), continue
564
- * using the explicit `new JobStore` + try/finally idiom — `withJobStore`
565
- * is for the common single-scope case.
545
+ * Run a function with a temporary `JobStore`, closing its handle on return or
546
+ * throw after construction succeeds. The constructor owns cleanup when opening
547
+ * or migrating the store fails.
566
548
  */
567
549
  export async function withJobStore(paths, fn) {
568
- // Construct INSIDE the try. `new JobStore` must not live above this
569
- // line — the constructor can throw after opening the SQLite adapter
570
- // (pragma failure, schema migration), and the helper's `finally`
571
- // is the second line of defense behind the constructor's own
572
- // cleanup. Moving `new JobStore` above the try would silently
573
- // reintroduce a handle leak the JSDoc above promises against.
550
+ // Keep construction inside try: the constructor cleans up its own failures,
551
+ // while this finally closes every successfully constructed store.
574
552
  let store;
575
553
  try {
576
554
  store = new JobStore(paths);
@@ -591,7 +569,6 @@ export function withJobStoreSync(paths, fn) {
591
569
  store?.close();
592
570
  }
593
571
  }
594
- /** Old non-null budgets have unknown provenance; never invent explicit intent. */
595
572
  /** Parsed `hook_escapes` column for JSON envelopes: [] when NULL or unreadable. */
596
573
  export function parseHookEscapes(raw) {
597
574
  if (!raw)
@@ -604,6 +581,7 @@ export function parseHookEscapes(raw) {
604
581
  return [];
605
582
  }
606
583
  }
584
+ /** Old non-null budgets have unknown provenance; never invent explicit intent. */
607
585
  export function renderBudgetSource(job) {
608
586
  return job.budget_source ?? (job.budget_ms === null ? "default" : "unknown");
609
587
  }
@@ -10,9 +10,7 @@ export function resolveKimiCliCommand(env) {
10
10
  parsed = JSON.parse(raw);
11
11
  }
12
12
  catch {
13
- // Plain-text fallback. Mirrors v0.4's permissive shape so users with
14
- // an existing `KIMI_PLUGIN_CC_KIMI_PREFIX_ARGS="--import tsx ..."`
15
- // export don't have to migrate to JSON for v1.0.
13
+ // Plain-text values are space-split for launcher-style overrides.
16
14
  const plainArgs = raw.split(" ").filter(Boolean);
17
15
  assertPrefixArgsSafe(plainArgs, raw);
18
16
  return { command, prefixArgs: plainArgs };
@@ -39,6 +39,8 @@ export const NATIVE_V2_CERTIFIED_VERSIONS = Object.freeze([
39
39
  "2.1.0",
40
40
  "2.1.1",
41
41
  "2.2.0",
42
+ "2.3.0",
43
+ "2.3.1",
42
44
  ]);
43
45
  /** Per-operation native-v2 certification. One green operation never certifies another. */
44
46
  export const NATIVE_V2_CERTIFIED = new Map([
@@ -577,7 +579,6 @@ async function resolveExactExecutable(command, cwd, env) {
577
579
  return await realpath(candidate);
578
580
  }
579
581
  catch {
580
- // Keep searching; failure is reported once with the original command.
581
582
  }
582
583
  }
583
584
  throw new RuntimeError("KIMI_EXECUTION_PLAN_UNRESOLVED", `Refusing to spawn kimi because executable ${JSON.stringify(command)} could not be resolved to an exact runnable path. Run Claude Code \`/kimi:setup\` or Codex \`$kimi-setup\` to verify the active binary, or correct KIMI_PLUGIN_CC_KIMI_BIN and retry.`, "kimi-engine.plan", { details: { command, cwd } });
@@ -1,21 +1,8 @@
1
- // Per-command response-budget constants for the v1.0 cli-client path.
2
- //
3
- // v0.4 also exposed startup / initialize timeouts plus a generic
4
- // `withTimeout` helper. Those existed because the Wire transport had a
5
- // three-phase lifecycle (spawn → initialize → prompt). The v1.0
6
- // subprocess transport collapses spawn + prompt into a single
7
- // `runCliPromptWithBudget` call (see `runtime/cli-client.ts`), and
8
- // startup failures surface as CLI_SPAWN_FAILED / CLI_PROCESS_ERROR —
9
- // not as separate timeouts. So only the per-command response budgets
10
- // remain.
11
- // Budgets accommodate thinking-capable workflows; per-run effort is only a request.
12
- // kimi-code spends real wallclock on extended reasoning before emitting
13
- // the first assistant record; pre-alpha.4 budgets (300s/600s) were sized
14
- // against the non-thinking path and timed out in production smoke
15
- // testing under default flags. Defaults are now 1800/1800/2700 seconds
16
- // for ask/review/rescue to allow repo-wide work; review-gate stays at
17
- // 8s because it fires inside Claude Code's Stop hook and is forced to
18
- // thinking-off in its own caller.
1
+ // Per-command response budgets leave room for thinking-capable workflows;
2
+ // per-run effort is only a request. Ask/review default to 30 minutes and rescue
3
+ // to 45 minutes for repo-wide work. Review-gate stays at 8 seconds because it
4
+ // runs inside Claude Code's Stop hook and requests thinking off by default;
5
+ // KIMI_PLUGIN_CC_REVIEW_GATE_THINKING can override that request.
19
6
  /** Ask budget. Conversational; thinking-on free-form reasoning. */
20
7
  export const KIMI_ASK_PROMPT_TIMEOUT_MS = 1_800_000; // 30 minutes
21
8
  /** Review/challenge budget. Thinking-on single-turn analysis over the workspace. */
@@ -25,8 +12,8 @@ export const KIMI_RESCUE_PROMPT_TIMEOUT_MS = 2_700_000; // 45 minutes
25
12
  /**
26
13
  * Pursue (autonomous goal mode) DEFAULT wall-clock ceiling when the user
27
14
  * passes no `--budget`. This is the ONLY hard bound on an autonomous goal —
28
- * headless goal create sets no token/turn budget (only the model, via
29
- * SetGoalBudget, or this AbortController stops the loop). The same duration as
15
+ * headless goal create sets no token/turn budget, and the managed hook denies
16
+ * SetGoalBudget. The same duration as
30
17
  * the single-turn rescue default, but a safety ceiling rather than a patience
31
18
  * knob. Deliberately finite. Overridable per-job via `--budget` (parsed to ms in
32
19
  * parsePursueArgs). See runtime/commands/pursue.ts and docs/safety.md.
@@ -53,7 +40,8 @@ export const KIMI_SWARM_DEFAULT_BUDGET_MS = 1_800_000; // 30 minutes
53
40
  * KIMI_PLUGIN_CC_REVIEW_GATE_THINKING can inherit or request another effort.
54
41
  * BYO/non-trait-driven providers and always-thinking models can ignore it.
55
42
  * The 8s timeout remains fail-open; a request never guarantees completion.
56
- * Hook-enforcement escape is a separate blocking failure. See docs/safety.md.
43
+ * Hook-escape findings annotate the result and never block or change job outcome.
44
+ * See docs/safety.md.
57
45
  */
58
46
  export const KIMI_REVIEW_GATE_TIMEOUT_MS = 8_000;
59
47
  export function isTimeoutCode(code) {