kimi-plugin-cc 2.2.0 → 2.2.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 (70) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +6 -0
  4. package/README.md +1 -1
  5. package/agents/kimi-ask.md +4 -0
  6. package/agents/kimi-challenge.md +3 -1
  7. package/agents/kimi-pursue.md +3 -1
  8. package/agents/kimi-rescue.md +4 -0
  9. package/agents/kimi-review.md +3 -1
  10. package/agents/kimi-swarm-write.md +3 -1
  11. package/agents/kimi-swarm.md +3 -1
  12. package/commands/ask.md +4 -2
  13. package/commands/challenge.md +4 -2
  14. package/commands/pursue.md +5 -1
  15. package/commands/rescue.md +4 -2
  16. package/commands/review.md +4 -2
  17. package/commands/swarm.md +5 -1
  18. package/dist/cli-client.js +33 -7
  19. package/dist/commands/ask.js +5 -2
  20. package/dist/commands/cli-helpers.js +13 -0
  21. package/dist/commands/pursue.js +5 -2
  22. package/dist/commands/rescue.js +5 -2
  23. package/dist/commands/result.js +3 -1
  24. package/dist/commands/review-gate.js +19 -13
  25. package/dist/commands/review.js +5 -2
  26. package/dist/commands/status.js +2 -2
  27. package/dist/commands/swarm.js +7 -3
  28. package/dist/help.js +1 -1
  29. package/dist/hook-escape.js +71 -0
  30. package/dist/hooks/approval-policy.js +17 -27
  31. package/dist/job-store.js +17 -4
  32. package/dist/kimi-engine.js +2 -0
  33. package/dist/kimi-errors.js +1 -1
  34. package/dist/kimi-timeouts.js +6 -10
  35. package/dist/kimi-version-probe.js +2 -2
  36. package/dist/parsing.js +59 -53
  37. package/dist/version.js +1 -1
  38. package/docs/models.md +10 -0
  39. package/docs/registry-releases.md +5 -3
  40. package/docs/safety.md +10 -0
  41. package/package.json +1 -1
  42. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  43. package/plugins/kimi-codex/dist/cli-client.js +33 -7
  44. package/plugins/kimi-codex/dist/commands/ask.js +5 -2
  45. package/plugins/kimi-codex/dist/commands/cli-helpers.js +13 -0
  46. package/plugins/kimi-codex/dist/commands/pursue.js +5 -2
  47. package/plugins/kimi-codex/dist/commands/rescue.js +5 -2
  48. package/plugins/kimi-codex/dist/commands/result.js +3 -1
  49. package/plugins/kimi-codex/dist/commands/review-gate.js +19 -13
  50. package/plugins/kimi-codex/dist/commands/review.js +5 -2
  51. package/plugins/kimi-codex/dist/commands/status.js +2 -2
  52. package/plugins/kimi-codex/dist/commands/swarm.js +7 -3
  53. package/plugins/kimi-codex/dist/help.js +1 -1
  54. package/plugins/kimi-codex/dist/hook-escape.js +71 -0
  55. package/plugins/kimi-codex/dist/hooks/approval-policy.js +17 -27
  56. package/plugins/kimi-codex/dist/job-store.js +17 -4
  57. package/plugins/kimi-codex/dist/kimi-engine.js +2 -0
  58. package/plugins/kimi-codex/dist/kimi-errors.js +1 -1
  59. package/plugins/kimi-codex/dist/kimi-timeouts.js +6 -10
  60. package/plugins/kimi-codex/dist/kimi-version-probe.js +2 -2
  61. package/plugins/kimi-codex/dist/parsing.js +59 -53
  62. package/plugins/kimi-codex/dist/version.js +1 -1
  63. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +3 -1
  64. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +3 -1
  65. package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +3 -1
  66. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +3 -1
  67. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +3 -1
  68. package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +2 -0
  69. package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +3 -1
  70. package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +3 -1
package/dist/parsing.js CHANGED
@@ -21,38 +21,30 @@ function looksLikeFlag(token) {
21
21
  // Short flag: a single dash followed by exactly one letter (`-r`, `-m`).
22
22
  return /^-[a-zA-Z]$/.test(token);
23
23
  }
24
- export const ASK_SUPPORTED_FLAGS = "-m/--model <name>, --budget <30m|1h|90s>, --background, --wait, --fresh, -r, --resume <job-id|session-id>";
25
- export const RESCUE_SUPPORTED_FLAGS = "-m/--model <name>, --budget <30m|1h|90s>, --background, --wait, --fresh, -r, --resume <job-id|session-id>";
26
- export const REVIEW_SUPPORTED_FLAGS = "--base <ref>, -m/--model <name>, --budget <30m|1h|90s>";
24
+ export const ASK_SUPPORTED_FLAGS = "-m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>, --background, --wait, --fresh, -r, --resume <job-id|session-id>";
25
+ export const RESCUE_SUPPORTED_FLAGS = "-m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>, --background, --wait, --fresh, -r, --resume <job-id|session-id>";
26
+ export const REVIEW_SUPPORTED_FLAGS = "--base <ref>, -m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>";
27
27
  export const JOB_LOOKUP_SUPPORTED_FLAGS = "--type <review|challenge|rescue|review_gate|ask>, --json";
28
28
  export const SETUP_SUPPORTED_FLAGS = "--check, --uninstall, --all (with --uninstall), --enable-review-gate, --disable-review-gate, --models [--json] (without other setup flags)";
29
29
  export const REPAIR_SESSIONS_SUPPORTED_FLAGS = "--apply, --all";
30
30
  export const REPLAY_SUPPORTED_FLAGS = "none (one job-id required)";
31
- // Thinking-on is always-on for user-facing commands in v1.0. review-gate
32
- // pins thinking=false via the cli-client options bag instead of an argv
33
- // flag (see runtime/cli-client.ts::CliClientOptions). The parser rejects
34
- // --thinking and --no-thinking with a hard error so a documented-removed
35
- // flag doesn't get silently accepted as a no-op. Both bare (`--thinking`)
36
- // and assignment (`--thinking=true`) forms are rejected — otherwise the
37
- // assignment form would fall through to the generic "Unknown flag" path
38
- // and a wrapper agent reading the error would think the cure is to drop
39
- // the `=value` rather than the flag itself.
40
- const THINKING_FLAG_REMOVED_MESSAGE = "--thinking / --no-thinking were removed in v1.0. Thinking is always on for user-facing commands.";
41
- function isRemovedThinkingFlag(token) {
42
- return (token === "--thinking" ||
43
- token === "--no-thinking" ||
44
- token.startsWith("--thinking=") ||
45
- token.startsWith("--no-thinking="));
31
+ /** Validate syntax only; upstream per-model supportEfforts remains authoritative. */
32
+ export function validateThinkingEffort(value, stage = "args.parse", lowercase = true) {
33
+ const effort = lowercase ? value?.toLowerCase() : value;
34
+ if (!effort || !/^[a-z][a-z0-9-]{0,31}$/.test(effort)) {
35
+ throw new RuntimeError("INVALID_ARGS", "--thinking requires an effort matching [a-z][a-z0-9-]{0,31} (e.g. off, low, medium, high).", stage);
36
+ }
37
+ return effort;
46
38
  }
47
- export const PURSUE_SUPPORTED_FLAGS = "-m/--model <name>, --budget <30m|1h|90s>, --turns <N>";
48
- export const SWARM_SUPPORTED_FLAGS = "-m/--model <name>, --budget <30m|1h|90s>, --cap <N>, --max-concurrency <N>, --write";
39
+ export const PURSUE_SUPPORTED_FLAGS = "-m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>, --turns <N>";
40
+ export const SWARM_SUPPORTED_FLAGS = "-m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>, --cap <N>, --max-concurrency <N>, --write";
49
41
  /**
50
42
  * Parser for /kimi:swarm (read-only parallel fan-out). Foreground-only:
51
43
  * no --background/--fresh/--resume. Trailing tokens are the objective.
52
44
  */
53
45
  export function parseSwarmArgs(argv) {
54
46
  let model;
55
- const thinking = true;
47
+ let thinkingEffort;
56
48
  let budgetMs;
57
49
  let cap;
58
50
  let maxConcurrency;
@@ -60,6 +52,13 @@ export function parseSwarmArgs(argv) {
60
52
  let trailingTokens;
61
53
  for (let index = 0; index < argv.length; index += 1) {
62
54
  const token = argv[index];
55
+ if (token === "--thinking" || token.startsWith("--thinking=")) {
56
+ thinkingEffort = validateThinkingEffort(token === "--thinking" ? argv[++index] : token.slice("--thinking=".length));
57
+ continue;
58
+ }
59
+ if (token === "--no-thinking" || token.startsWith("--no-thinking=")) {
60
+ throw new RuntimeError("INVALID_ARGS", "--no-thinking is unsupported; use --thinking off", "args.parse");
61
+ }
63
62
  if (token === "--") {
64
63
  trailingTokens = argv.slice(index + 1);
65
64
  break;
@@ -105,9 +104,6 @@ export function parseSwarmArgs(argv) {
105
104
  break;
106
105
  }
107
106
  default:
108
- if (isRemovedThinkingFlag(token)) {
109
- throw new RuntimeError("INVALID_ARGS", THINKING_FLAG_REMOVED_MESSAGE, "swarm.parse");
110
- }
111
107
  if (looksLikeFlag(token)) {
112
108
  throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token} for swarm. Supported flags: ${SWARM_SUPPORTED_FLAGS}. Use \`--\` before flag-shaped objective text to pass it through.`, "swarm.parse");
113
109
  }
@@ -122,7 +118,7 @@ export function parseSwarmArgs(argv) {
122
118
  maxConcurrency,
123
119
  write,
124
120
  model,
125
- thinking,
121
+ thinkingEffort,
126
122
  objective: trailingTokens?.join(" "),
127
123
  };
128
124
  }
@@ -186,12 +182,19 @@ export function parseDurationMs(token, flag, stage, budgetKind) {
186
182
  */
187
183
  export function parsePursueArgs(argv) {
188
184
  let model;
189
- const thinking = true;
185
+ let thinkingEffort;
190
186
  let budgetMs;
191
187
  let turns;
192
188
  let trailingTokens;
193
189
  for (let index = 0; index < argv.length; index += 1) {
194
190
  const token = argv[index];
191
+ if (token === "--thinking" || token.startsWith("--thinking=")) {
192
+ thinkingEffort = validateThinkingEffort(token === "--thinking" ? argv[++index] : token.slice("--thinking=".length));
193
+ continue;
194
+ }
195
+ if (token === "--no-thinking" || token.startsWith("--no-thinking=")) {
196
+ throw new RuntimeError("INVALID_ARGS", "--no-thinking is unsupported; use --thinking off", "args.parse");
197
+ }
195
198
  if (token === "--") {
196
199
  trailingTokens = argv.slice(index + 1);
197
200
  break;
@@ -237,9 +240,6 @@ export function parsePursueArgs(argv) {
237
240
  break;
238
241
  }
239
242
  default:
240
- if (isRemovedThinkingFlag(token)) {
241
- throw new RuntimeError("INVALID_ARGS", THINKING_FLAG_REMOVED_MESSAGE, "pursue.parse");
242
- }
243
243
  if (looksLikeFlag(token)) {
244
244
  throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token} for pursue. Supported flags: ${PURSUE_SUPPORTED_FLAGS}. Use \`--\` before flag-shaped objective text to pass it through.`, "pursue.parse");
245
245
  }
@@ -252,12 +252,12 @@ export function parsePursueArgs(argv) {
252
252
  budgetMs,
253
253
  turns,
254
254
  model,
255
- thinking,
255
+ thinkingEffort,
256
256
  objective: trailingTokens?.join(" "),
257
257
  };
258
258
  }
259
259
  /** Help before -- wins even over malformed flags or prose trailing-token mode. */
260
- const VALUE_TAKING_FLAGS = new Set(["-m", "--model", "--budget", "--resume", "--base", "--cap", "--max-concurrency", "--turns", "--type"]);
260
+ const VALUE_TAKING_FLAGS = new Set(["--thinking", "-m", "--model", "--budget", "--resume", "--base", "--cap", "--max-concurrency", "--turns", "--type"]);
261
261
  /**
262
262
  * `--help` / `-h` counts only inside the leading flag region: before `--`
263
263
  * and before the first prose token. The value of a value-taking flag is
@@ -289,10 +289,7 @@ export function parseAskArgs(argv) {
289
289
  return { help: true, background: false, wait: false, fresh: false, resume: false };
290
290
  let budgetMs;
291
291
  let model;
292
- // Thinking is on by default across ask/review/challenge/rescue. The
293
- // parser rejects --thinking / --no-thinking; review-gate's runtime
294
- // caller pins thinking=false via CliClientOptions instead.
295
- let thinking = true;
292
+ let thinkingEffort;
296
293
  let background = false;
297
294
  let wait = false;
298
295
  let fresh = false;
@@ -302,6 +299,13 @@ export function parseAskArgs(argv) {
302
299
  let trailingTokens;
303
300
  for (let index = 0; index < argv.length; index += 1) {
304
301
  const token = argv[index];
302
+ if (token === "--thinking" || token.startsWith("--thinking=")) {
303
+ thinkingEffort = validateThinkingEffort(token === "--thinking" ? argv[++index] : token.slice("--thinking=".length));
304
+ continue;
305
+ }
306
+ if (token === "--no-thinking" || token.startsWith("--no-thinking=")) {
307
+ throw new RuntimeError("INVALID_ARGS", "--no-thinking is unsupported; use --thinking off", "args.parse");
308
+ }
305
309
  if (token === "--") {
306
310
  trailingTokens = argv.slice(index + 1);
307
311
  break;
@@ -357,9 +361,6 @@ export function parseAskArgs(argv) {
357
361
  break;
358
362
  }
359
363
  default:
360
- if (isRemovedThinkingFlag(token)) {
361
- throw new RuntimeError("INVALID_ARGS", THINKING_FLAG_REMOVED_MESSAGE, "args.parse");
362
- }
363
364
  if (looksLikeFlag(token)) {
364
365
  throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token} for ask. Supported flags: ${ASK_SUPPORTED_FLAGS}. Use \`--\` before flag-shaped prompt text to pass it through.`, "args.parse");
365
366
  }
@@ -382,7 +383,7 @@ export function parseAskArgs(argv) {
382
383
  resume,
383
384
  resumeTarget,
384
385
  model,
385
- thinking,
386
+ thinkingEffort,
386
387
  prompt: trailingTokens?.join(" "),
387
388
  };
388
389
  }
@@ -394,7 +395,7 @@ export function parseReviewArgs(argv, commandName = "review") {
394
395
  background: parsed.background,
395
396
  wait: parsed.wait,
396
397
  model: parsed.model,
397
- thinking: parsed.thinking,
398
+ thinkingEffort: parsed.thinkingEffort,
398
399
  focus: parsed.trailingText,
399
400
  };
400
401
  }
@@ -403,7 +404,7 @@ export function parseRescueArgs(argv) {
403
404
  return { help: true, background: false, wait: false, fresh: false, resume: false };
404
405
  let budgetMs;
405
406
  let model;
406
- let thinking = true;
407
+ let thinkingEffort;
407
408
  let background = false;
408
409
  let wait = false;
409
410
  let fresh = false;
@@ -413,6 +414,13 @@ export function parseRescueArgs(argv) {
413
414
  let trailingTokens;
414
415
  for (let index = 0; index < argv.length; index += 1) {
415
416
  const token = argv[index];
417
+ if (token === "--thinking" || token.startsWith("--thinking=")) {
418
+ thinkingEffort = validateThinkingEffort(token === "--thinking" ? argv[++index] : token.slice("--thinking=".length));
419
+ continue;
420
+ }
421
+ if (token === "--no-thinking" || token.startsWith("--no-thinking=")) {
422
+ throw new RuntimeError("INVALID_ARGS", "--no-thinking is unsupported; use --thinking off", "args.parse");
423
+ }
416
424
  if (token === "--") {
417
425
  trailingTokens = argv.slice(index + 1);
418
426
  break;
@@ -468,9 +476,6 @@ export function parseRescueArgs(argv) {
468
476
  break;
469
477
  }
470
478
  default:
471
- if (isRemovedThinkingFlag(token)) {
472
- throw new RuntimeError("INVALID_ARGS", THINKING_FLAG_REMOVED_MESSAGE, "args.parse");
473
- }
474
479
  if (looksLikeFlag(token)) {
475
480
  throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token} for rescue. Supported flags: ${RESCUE_SUPPORTED_FLAGS}. Use \`--\` before flag-shaped prompt text to pass it through.`, "args.parse");
476
481
  }
@@ -493,7 +498,7 @@ export function parseRescueArgs(argv) {
493
498
  resume,
494
499
  resumeTarget,
495
500
  model,
496
- thinking,
501
+ thinkingEffort,
497
502
  prompt: trailingTokens?.join(" "),
498
503
  };
499
504
  }
@@ -540,21 +545,25 @@ function isManagedCommandType(value) {
540
545
  function parseKnownFlags(argv, knownFlags, commandName) {
541
546
  let budgetMs;
542
547
  let model;
543
- let thinking = true;
548
+ let thinkingEffort;
544
549
  let base;
545
550
  let background = false;
546
551
  let wait = false;
547
552
  let trailingTokens;
548
553
  for (let index = 0; index < argv.length; index += 1) {
549
554
  const token = argv[index];
555
+ if (token === "--thinking" || token.startsWith("--thinking=")) {
556
+ thinkingEffort = validateThinkingEffort(token === "--thinking" ? argv[++index] : token.slice("--thinking=".length));
557
+ continue;
558
+ }
559
+ if (token === "--no-thinking" || token.startsWith("--no-thinking=")) {
560
+ throw new RuntimeError("INVALID_ARGS", "--no-thinking is unsupported; use --thinking off", "args.parse");
561
+ }
550
562
  if (token === "--") {
551
563
  trailingTokens = argv.slice(index + 1);
552
564
  break;
553
565
  }
554
566
  if (!knownFlags.has(token)) {
555
- if (isRemovedThinkingFlag(token)) {
556
- throw new RuntimeError("INVALID_ARGS", THINKING_FLAG_REMOVED_MESSAGE, "args.parse");
557
- }
558
567
  if (looksLikeFlag(token)) {
559
568
  // Wrapper agents routinely hallucinate flags like `--file` /
560
569
  // `--context` because that shape is common in other CLIs. Silently
@@ -610,9 +619,6 @@ function parseKnownFlags(argv, knownFlags, commandName) {
610
619
  // The early `if (!knownFlags.has(token))` gate above means we only
611
620
  // reach this switch with tokens that ARE in knownFlags, and every
612
621
  // member is handled above. A `default` arm would be unreachable.
613
- // --thinking and --no-thinking are NOT in knownFlags any more (v1.0
614
- // removed them); the early gate above hard-fails on them with
615
- // THINKING_FLAG_REMOVED_MESSAGE.
616
622
  }
617
623
  }
618
624
  return {
@@ -621,7 +627,7 @@ function parseKnownFlags(argv, knownFlags, commandName) {
621
627
  background,
622
628
  wait,
623
629
  model,
624
- thinking,
630
+ thinkingEffort,
625
631
  trailingText: trailingTokens?.join(" "),
626
632
  };
627
633
  }
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  // and AGENTS.md on every release. A future improvement would read this from
5
5
  // package.json at build time, but that adds a build-step dependency we
6
6
  // don't want yet.
7
- export const KIMI_PLUGIN_CC_VERSION = "2.2.0";
7
+ export const KIMI_PLUGIN_CC_VERSION = "2.2.1";
package/docs/models.md CHANGED
@@ -80,3 +80,13 @@ validator. Every task still uses the existing exact-version, hook, workspace,
80
80
  and no-plan checks. Source review is against kimi-code 2.0.0 (`1b89e4b0`),
81
81
  including `kosongConfig/configSection.ts`, `envOverlay.ts`, and
82
82
  `cli/v2/run-v2-print.ts`.
83
+
84
+ ## Requested thinking effort
85
+
86
+ `--thinking <effort>` requests per-run effort through `KIMI_MODEL_THINKING_EFFORT`, never a CLI flag or saved-default change; omitted flags preserve the operator-owned ambient variable, and resume does not inherit a previous job's request. Providers that are not trait-driven silently ignore it, as does a base level already `off`; an `alwaysThinking` model receives `off` unnormalized. Stream-json carries no thinking deltas (`writeThinkingDelta() {}`), so the plugin cannot verify whether the request was honoured.
87
+
88
+ Use `--thinking off`, `--thinking high`, or `--thinking=<effort>` before the prompt. Values are lowercased and must match `/^[a-z][a-z0-9-]{0,31}$/` (a plugin-owned syntax bound; a future upstream token outside it is rejected before upstream sees it); upstream per-model `supportEfforts`, not a plugin enum, determines supported values. `--no-thinking` is an error: use `--thinking off`. In CLI 2.2.0 only the three Moonshot `kimi*` provider definitions are trait-driven. BYO providers ignore the request silently; an always-thinking model may remain thinking.
89
+
90
+ Jobs persist nullable `thinking_effort`; status and `result --json` expose `thinking_effort_requested`, not observed effort. Detached ask/rescue workers replay the request. A resume without `--thinking` exports no new override (any ambient operator value remains). No config.toml write occurs.
91
+
92
+ The review gate requests `off` by default. `KIMI_PLUGIN_CC_REVIEW_GATE_THINKING=inherit` exports no override; another non-empty value is lowercased and must match the same regex. Unset or empty means `off`; an invalid value makes the gate skip visibly. The gate remains fail-open on its 8 s timeout: a BYO provider or always-thinking model can ignore the request, and even an honoured request does not guarantee completion within the budget.
@@ -6,9 +6,11 @@ remain available. The package has no install scripts and no separate CLI.
6
6
 
7
7
  ## Publishing status
8
8
 
9
- As of October 15, 2026, [2.0.8 is live on npm](https://www.npmjs.com/package/kimi-plugin-cc/v/2.0.8), published through
10
- GitHub Actions OIDC with a signed provenance statement (run 38650044033). 2.0.7 (September 26) was the
11
- hand-published bootstrap.
9
+ As of October 15, 2026, [2.2.0 is live on npm](https://www.npmjs.com/package/kimi-plugin-cc/v/2.2.0), published by
10
+ the release-event run of `publish.yml` (run 38658918855) through GitHub Actions OIDC with a signed
11
+ provenance statement (sigstore log index 3256863983) — the first release where the tag-triggered
12
+ path worked end to end without a manual dispatch. 2.0.8 (same day, run 38650044033) was the first
13
+ OIDC publication; 2.0.7 (September 26) was the hand-published bootstrap.
12
14
  The first upload used the validated GitHub Actions artifact and interactive npm
13
15
  authentication. Its public tarball matched that artifact byte-for-byte, and a
14
16
  fresh registry install passed isolated setup/check for both hosts.
package/docs/safety.md CHANGED
@@ -363,3 +363,13 @@ Agents use `retryable_after_setup`, not `drift_axis` alone. They run setup for t
363
363
  ## Reporting safety issues
364
364
 
365
365
  If you find a way to coax /kimi:review, /kimi:challenge, /kimi:review_gate, or /kimi:ask into running a non-read tool — or to make /kimi:rescue mutate `.git/` or escape the workspace — please open a GitHub security advisory rather than a public issue.
366
+
367
+ ## Hook-escape monitor: detection evidence
368
+
369
+ **Hook-escape monitor** (§1): the cli-client replays the pure hook policy on every streamed `tool_calls` entry; a call the policy denies whose tool result is not the hook's denial (byte-exact, or with upstream's repeat-breaker `<system-reminder>` decoration / handoff text) is recorded as `mismatch`, and a denied call still without a result at settlement as `unresolved`. Findings are **detection evidence, never a failure**: logged (`hook_escape`), persisted on the job row (`hook_escapes`, JSON), exposed by `status`/`result --json`, printed as a stderr `safety hook notice`, and carried in timeout details; the run continues, the job's outcome is unchanged, and the review gate stays fail-open and only annotates. The stream has no outcome field, so a mismatch is equally an upstream pre-hook rejection (unknown tool, disabled tool, invalid args, sensitive path, per-tool resolve errors), a coexisting hook or permission policy blocking first, or hook drift — failing the job needs positive execution evidence this stream does not provide. Streamed calls under read-only labels are covered (swarm subagents are in-process, not streamed); write labels only for the pure denials (plan/goal tools).
370
+
371
+ A denied attempt alone is not a finding: the hook's denial text in the corresponding tool result (byte-exact, or carrying upstream's repeat-breaker decoration) proves enforcement and clears it. Anything else is recorded as `mismatch` with a 200-character excerpt; a denied call with no result at settlement as `unresolved`. **What a finding means:** the model received something other than the managed denial. On certified 2.2.0 that is produced just as readily by calls upstream rejects before the hook runs (`Tool "LS" not found`, `Invalid args for tool …`, the sensitive-path `PathSecurityError` for `.env`, per-tool resolve errors, `Tool skipped because a previous tool call stopped the turn.`), by a coexisting PreToolUse hook ahead of the managed block or an upstream permission policy blocking first, or by a cancel/budget abort landing before the result — none of which executed anything — as by a hook that failed open. Because the stream carries no outcome field, the plugin cannot tell these apart, so it reports and does not fail: the job completes or fails on its own merits, `status`/`result --json` carry `hook_escapes`, stderr prints a `safety hook notice`, and the review gate annotates `systemMessage` without changing its decision. Treat a finding as a prompt to run `/kimi:setup --check` and read the excerpt, not as proof of a breach; treat a *recurring* `mismatch` whose excerpt looks like tool output (`Wrote N bytes to …`) as a drifted hook. Coverage is streamed calls under read-only labels and pure denials under write labels — not filesystem-dependent rescue allowlist decisions, not swarm subagent calls (in-process, never streamed). Non-vacuous live control (CLI 2.2.0, 2026-10-15): with the hook command pointed at a nonexistent script upstream failed open, `Write` created the file, and the finding was `mismatch` with excerpt `Wrote 7 bytes to ESCAPE.txt`; with the hook intact the same prompt produced the byte-exact denial and no finding. Enforcement (stopping the run, failing the job) is deliberately not shipped: it needs positive execution evidence. The parked upstream ask is an `outcome: executed|vetoed|rejected` field on print-mode tool records; a plugin-side alternative is a per-job trace the hook writes before it exits.
372
+
373
+ ## Review-gate thinking request
374
+
375
+ The gate requests `off` through `KIMI_MODEL_THINKING_EFFORT` by default. `KIMI_PLUGIN_CC_REVIEW_GATE_THINKING=inherit` leaves the ambient environment alone; any other non-empty value matching `/^[a-z][a-z0-9-]{0,31}$/` is passed verbatim. This is best-effort, never an enforced latency guarantee: non-trait-driven/BYO providers ignore it silently, an already-off base remains off, and an always-thinking model receives `off` unnormalized and may continue reasoning. Stream-json has no thinking deltas, so honouring the request cannot be observed. The 8 s budget and fail-open-on-timeout contract are unchanged; a hook-escape finding only annotates the gate's `systemMessage`. `KIMI_PLUGIN_CC_REVIEW_GATE_THINKING` is lowercased like `--thinking`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimi-plugin-cc",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Kimi for Claude Code and Codex: code questions, reviews, fixes, and parallel tasks through the local Kimi Code CLI.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimi",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Codex plugin that delegates review, challenge, ask, rescue, pursue, and swarm workflows to the local kimi-code CLI through the kimi-plugin-cc companion runtime.",
5
5
  "author": {
6
6
  "name": "linxule"
@@ -19,6 +19,7 @@
19
19
  import { spawn } from "node:child_process";
20
20
  import { appendFile, mkdir } from "node:fs/promises";
21
21
  import path from "node:path";
22
+ import { createHookEscapeMonitor } from "./hook-escape.js";
22
23
  import { RuntimeError, formatError } from "./errors.js";
23
24
  import { assertNoUnsafeExperimentalSelector, assertExecutionPlanMatchesSpawn, } from "./kimi-engine.js";
24
25
  import { assertPrefixArgsSafe } from "./kimi-command.js";
@@ -126,6 +127,14 @@ export async function runCliPrompt(opts) {
126
127
  let announcedSessionId;
127
128
  let announcedGoalSummary;
128
129
  let announcedSystemVersion;
130
+ const hookMonitor = createHookEscapeMonitor({ commandLabel: opts.commandLabel,
131
+ operationKind: opts.executionPlan.operationKind, trustedWorkspaceRoot: opts.trustedWorkspaceRoot, cwd: opts.cwd });
132
+ const hookEscapes = [];
133
+ const noteHookEscape = (escape) => {
134
+ hookEscapes.push(escape);
135
+ appendLogLine({ event: "hook_escape", ...escape, command_label: opts.commandLabel,
136
+ operation_kind: opts.executionPlan.operationKind });
137
+ };
129
138
  let engineMismatchDetected = false;
130
139
  let handleEngineMismatch;
131
140
  const plannedEngine = opts.executionPlan.intendedEngine;
@@ -283,6 +292,9 @@ export async function runCliPrompt(opts) {
283
292
  }
284
293
  if (requireMarkerBeforeOutput(`a ${outcome.record.role} record`))
285
294
  return;
295
+ const escape = hookMonitor.observe(outcome.record);
296
+ if (escape)
297
+ noteHookEscape(escape);
286
298
  records.push(outcome.record);
287
299
  appendLogLine({ event: "record", record: outcome.record });
288
300
  invokeOnRecord(outcome.record);
@@ -314,6 +326,13 @@ export async function runCliPrompt(opts) {
314
326
  const settle = (kind, payload) => {
315
327
  if (settlementStarted)
316
328
  return;
329
+ // Denied calls still without a result (a cancel/budget race, an abnormal
330
+ // exit, or a turn that ended without the denial the hook owes) are reported
331
+ // alongside the real outcome, never instead of it.
332
+ for (const escape of hookMonitor.settle())
333
+ noteHookEscape(escape);
334
+ if (kind === "resolve" && hookEscapes.length > 0)
335
+ payload = { ...payload, hookEscapes: [...hookEscapes] };
317
336
  settlementStarted = true;
318
337
  opts.signal?.removeEventListener("abort", onAbort);
319
338
  // Abort teardown can outlive the direct child's close event while a
@@ -472,6 +491,11 @@ export async function runCliPrompt(opts) {
472
491
  }
473
492
  }
474
493
  };
494
+ const stopForPolicyFailure = (error) => {
495
+ engineMismatchDetected = true; // Stops all further consumer output.
496
+ cancellationTeardown ??= teardownChildTree().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
497
+ settle("reject", error);
498
+ };
475
499
  let engineMismatchError;
476
500
  handleEngineMismatch = (mismatch) => {
477
501
  if (engineMismatchError !== undefined)
@@ -493,8 +517,7 @@ export async function runCliPrompt(opts) {
493
517
  system_version: mismatch.systemVersion ?? null,
494
518
  reason: mismatch.reason,
495
519
  });
496
- cancellationTeardown = teardownChildTree().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
497
- settle("reject", engineMismatchError);
520
+ stopForPolicyFailure(engineMismatchError);
498
521
  };
499
522
  const onAbort = () => {
500
523
  if (aborted)
@@ -674,6 +697,7 @@ export class ResponseTimeoutError extends RuntimeError {
674
697
  partial_output_chars: partialResult?.records.reduce((count, record) => count + (record.role === "assistant" && typeof record.content === "string" ? record.content.length : 0), 0) ?? 0,
675
698
  aborted: partialResult?.aborted ?? true,
676
699
  ...(partialResult ? { settled_post_abort: true } : {}),
700
+ ...(partialResult?.hookEscapes?.length ? { hook_escapes: partialResult.hookEscapes } : {}),
677
701
  },
678
702
  });
679
703
  this.name = "ResponseTimeoutError";
@@ -778,11 +802,11 @@ function buildArgs(opts) {
778
802
  args.push("--skills-dir", dir);
779
803
  }
780
804
  }
781
- // NOTE: `opts.thinking === false` is intentionally NOT translated into a
782
- // `--no-thinking` argv flag. kimi-code 0.1.1 rejects unknown options and
783
- // has no per-spawn thinking control — the Round 2 multi-agent review
784
- // caught this. The option stays in CliClientOptions for future wiring
785
- // when upstream adds the flag (see field doc).
805
+ // NOTE: thinking effort is requested through the env binding, never CLI argv.
806
+ // Non-trait-driven providers or a base level already off ignore it silently;
807
+ // an alwaysThinking model receives off unnormalized. stream-json carries no
808
+ // thinking deltas (writeThinkingDelta() {} in the JSON writer), so the plugin
809
+ // cannot observe whether the request was honoured.
786
810
  // -p accepts the prompt as the option value. We pass it as a separate argv
787
811
  // entry so prompts with leading dashes or whitespace are not interpreted as
788
812
  // additional flags.
@@ -791,6 +815,8 @@ function buildArgs(opts) {
791
815
  }
792
816
  function buildEnv(opts) {
793
817
  const env = { ...opts.env };
818
+ if (opts.thinkingEffort !== undefined)
819
+ env.KIMI_MODEL_THINKING_EFFORT = opts.thinkingEffort;
794
820
  // 2.2.0 audit: ambient trust starts project MCP stdio servers outside PreToolUse.
795
821
  delete env.KIMI_CODE_TRUST_WORKSPACE;
796
822
  // kimi-code 0.33.0 inverted its CLI engine default: an unflagged `kimi -p`
@@ -17,7 +17,7 @@ import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.j
17
17
  import { classifyManagedCommandFailure } from "../kimi-errors.js";
18
18
  import { startBackgroundJob } from "../background-spawn.js";
19
19
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
20
- import { warnInheritedBudget, captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { warnInheritedBudget, captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
21
21
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
22
22
  // v1.0 cutover note (PR 2):
23
23
  //
@@ -77,7 +77,8 @@ export async function runAsk(argv, context) {
77
77
  command_type: "ask",
78
78
  cwd: context.cwd,
79
79
  model: parsed.model ?? null,
80
- thinking: parsed.thinking ?? null,
80
+ thinking: null,
81
+ thinking_effort: parsed.thinkingEffort ?? null,
81
82
  background: parsed.background,
82
83
  budget_ms: parsed.budgetMs ?? (sessionResolution.sourceJob?.operation_kind === "ask" ? sessionResolution.sourceJob.budget_ms : null) ?? null,
83
84
  budget_source: parsed.budgetMs !== undefined ? "explicit"
@@ -174,6 +175,7 @@ export async function executeAskJob(jobId, prompt, context, options) {
174
175
  prompt,
175
176
  commandLabel: "ask",
176
177
  model: job.model ?? undefined,
178
+ thinkingEffort: job.thinking_effort ?? undefined,
177
179
  resumeSessionId: job.kimi_session_id ?? undefined,
178
180
  logPath: job.stream_log_path,
179
181
  signal: handlers.signal,
@@ -183,6 +185,7 @@ export async function executeAskJob(jobId, prompt, context, options) {
183
185
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "ask.runtime");
184
186
  }
185
187
  assertCliResultSuccess(result, "ask.runtime");
188
+ warnHookEscapes(result, "ask", context.stderr);
186
189
  // Persist whichever session id kimi actually announced. For fresh
187
190
  // sessions this is the only chance to record it. For resumed
188
191
  // sessions it should match the input, but capture anyway in case
@@ -185,6 +185,19 @@ export async function captureTimeoutPartialOutput(classified, store, job, env, c
185
185
  budgetMs: timeout.details.budget_ms,
186
186
  };
187
187
  }
188
+ /** One-line human summary of hook-escape findings, or undefined when none. */
189
+ export function describeHookEscapes(result) {
190
+ const escapes = result.hookEscapes ?? [];
191
+ if (escapes.length === 0)
192
+ return undefined;
193
+ const first = escapes[0];
194
+ return `safety hook notice: ${escapes.length} policy-denied tool call(s) did not come back as the managed hook's denial (first: ${first.tool_name} ${first.kind}). Detection evidence only — the call may have been rejected upstream before the hook ran, blocked by another hook, or executed because the hook drifted. Run /kimi:setup --check; details in \`result <jobId> --json\` (hook_escapes).`;
195
+ }
196
+ export function warnHookEscapes(result, commandType, stderr) {
197
+ const note = describeHookEscapes(result);
198
+ if (note !== undefined)
199
+ stderr.write(`[${commandType}] ${note}\n`);
200
+ }
188
201
  export function warnInheritedBudget(job, stderr) {
189
202
  if (job.budget_source === "inherited" && job.budget_ms !== null) {
190
203
  stderr.write(`[${job.command_type}] budget ${formatBudgetDuration(job.budget_ms)} inherited from job ${job.resumed_from_job_id}; pass --budget to override\n`);
@@ -14,7 +14,7 @@ import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
14
14
  import { parsePursueArgs } from "../parsing.js";
15
15
  import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
16
16
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
17
- import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
17
+ import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
18
18
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
19
19
  // /kimi:pursue — autonomous goal mode (kimi-code 0.8.0+ headless `/goal`).
20
20
  //
@@ -141,7 +141,8 @@ export async function runPursue(argv, context) {
141
141
  command_type: "rescue",
142
142
  cwd: context.cwd,
143
143
  model: parsed.model ?? null,
144
- thinking: parsed.thinking ?? null,
144
+ thinking: null,
145
+ thinking_effort: parsed.thinkingEffort ?? null,
145
146
  background: false,
146
147
  budget_ms: parsed.budgetMs ?? KIMI_PURSUE_DEFAULT_BUDGET_MS,
147
148
  budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
@@ -233,10 +234,12 @@ async function executePursueJob(jobId, prompt, objective, budgetMs, context) {
233
234
  commandLabel: "rescue",
234
235
  trustedWorkspaceRoot: job.cwd,
235
236
  model: job.model ?? undefined,
237
+ thinkingEffort: job.thinking_effort ?? undefined,
236
238
  logPath: job.stream_log_path,
237
239
  signal: handlers.signal,
238
240
  }, budgetMs, "pursue.prompt");
239
241
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
242
+ warnHookEscapes(result, "pursue", context.stderr);
240
243
  if (handlers.cancelling) {
241
244
  throw new RuntimeError("PURSUE_CANCELLED", "Pursue cancelled by user request.", "pursue.runtime");
242
245
  }
@@ -17,7 +17,7 @@ import { parseRescueArgs } from "../parsing.js";
17
17
  import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
18
18
  import { startBackgroundJob } from "../background-spawn.js";
19
19
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
20
- import { warnInheritedBudget, captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { warnInheritedBudget, captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
21
21
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
22
22
  export { describeMissingResult } from "../background-spawn.js";
23
23
  // v1.0 cutover note (PR 3):
@@ -69,7 +69,8 @@ export async function runRescue(argv, context) {
69
69
  command_type: "rescue",
70
70
  cwd: context.cwd,
71
71
  model: parsed.model ?? null,
72
- thinking: parsed.thinking ?? null,
72
+ thinking: null,
73
+ thinking_effort: parsed.thinkingEffort ?? null,
73
74
  background: parsed.background,
74
75
  budget_ms: parsed.budgetMs ?? (sessionResolution.sourceJob?.operation_kind === "rescue" ? sessionResolution.sourceJob.budget_ms : null) ?? null,
75
76
  budget_source: parsed.budgetMs !== undefined ? "explicit"
@@ -188,6 +189,7 @@ export async function executeRescueJob(jobId, prompt, context, options) {
188
189
  commandLabel: "rescue",
189
190
  trustedWorkspaceRoot: job.cwd,
190
191
  model: job.model ?? undefined,
192
+ thinkingEffort: job.thinking_effort ?? undefined,
191
193
  resumeSessionId: job.kimi_session_id ?? undefined,
192
194
  logPath: job.stream_log_path,
193
195
  signal: handlers.signal,
@@ -199,6 +201,7 @@ export async function executeRescueJob(jobId, prompt, context, options) {
199
201
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "rescue.runtime");
200
202
  }
201
203
  assertCliResultSuccess(result, "rescue.runtime");
204
+ warnHookEscapes(result, "rescue", context.stderr);
202
205
  if (result.sessionId !== undefined &&
203
206
  result.sessionId.length > 0) {
204
207
  // length>0 guard: empty-string capture would poison the row. (Kimi
@@ -2,7 +2,7 @@ import { RuntimeError } from "../errors.js";
2
2
  import { resolveRepoIdentity } from "../git.js";
3
3
  import { sweepStaleJobs } from "../jobs.js";
4
4
  import { reconcileHistoricalJobProvenance } from "../kimi-engine.js";
5
- import { renderBudgetSource, withJobStore } from "../job-store.js";
5
+ import { renderBudgetSource, withJobStore, parseHookEscapes } from "../job-store.js";
6
6
  import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
7
7
  import { parseJobLookupArgs } from "../parsing.js";
8
8
  import { readArtifact, renderTerminalJobArtifact } from "../render.js";
@@ -44,6 +44,8 @@ function renderResultEnvelope(job, artifactPath, body) {
44
44
  status: job.status,
45
45
  budget_ms: job.budget_ms,
46
46
  budget_source: renderBudgetSource(job),
47
+ thinking_effort_requested: job.thinking_effort ?? null,
48
+ hook_escapes: parseHookEscapes(job.hook_escapes),
47
49
  kimi_session_id_source: job.kimi_session_id_source,
48
50
  kimi_session_id: job.kimi_session_id,
49
51
  // True when a budget timeout preserved assistant prose in the artifact.