kimi-plugin-cc 2.2.0 → 2.2.2

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 (74) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +20 -0
  4. package/README.md +5 -5
  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 +42 -7
  19. package/dist/commands/ask.js +5 -2
  20. package/dist/commands/cli-helpers.js +15 -1
  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/hooks/config-safety.js +3 -2
  32. package/dist/job-store.js +17 -4
  33. package/dist/kimi-engine.js +3 -0
  34. package/dist/kimi-errors.js +1 -1
  35. package/dist/kimi-timeouts.js +6 -10
  36. package/dist/kimi-version-probe.js +2 -2
  37. package/dist/parsing.js +59 -53
  38. package/dist/stream-json.js +16 -0
  39. package/dist/version.js +1 -1
  40. package/docs/models.md +10 -0
  41. package/docs/registry-releases.md +8 -4
  42. package/docs/safety.md +14 -2
  43. package/package.json +1 -1
  44. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  45. package/plugins/kimi-codex/dist/cli-client.js +42 -7
  46. package/plugins/kimi-codex/dist/commands/ask.js +5 -2
  47. package/plugins/kimi-codex/dist/commands/cli-helpers.js +15 -1
  48. package/plugins/kimi-codex/dist/commands/pursue.js +5 -2
  49. package/plugins/kimi-codex/dist/commands/rescue.js +5 -2
  50. package/plugins/kimi-codex/dist/commands/result.js +3 -1
  51. package/plugins/kimi-codex/dist/commands/review-gate.js +19 -13
  52. package/plugins/kimi-codex/dist/commands/review.js +5 -2
  53. package/plugins/kimi-codex/dist/commands/status.js +2 -2
  54. package/plugins/kimi-codex/dist/commands/swarm.js +7 -3
  55. package/plugins/kimi-codex/dist/help.js +1 -1
  56. package/plugins/kimi-codex/dist/hook-escape.js +71 -0
  57. package/plugins/kimi-codex/dist/hooks/approval-policy.js +17 -27
  58. package/plugins/kimi-codex/dist/hooks/config-safety.js +3 -2
  59. package/plugins/kimi-codex/dist/job-store.js +17 -4
  60. package/plugins/kimi-codex/dist/kimi-engine.js +3 -0
  61. package/plugins/kimi-codex/dist/kimi-errors.js +1 -1
  62. package/plugins/kimi-codex/dist/kimi-timeouts.js +6 -10
  63. package/plugins/kimi-codex/dist/kimi-version-probe.js +2 -2
  64. package/plugins/kimi-codex/dist/parsing.js +59 -53
  65. package/plugins/kimi-codex/dist/stream-json.js +16 -0
  66. package/plugins/kimi-codex/dist/version.js +1 -1
  67. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +3 -1
  68. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +3 -1
  69. package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +3 -1
  70. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +3 -1
  71. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +3 -1
  72. package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +2 -0
  73. package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +3 -1
  74. 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
  }
@@ -12,6 +12,8 @@
12
12
  // {"role":"meta","type":"turn.step.retrying","failed_attempt":1,"next_attempt":2,"max_attempts":3,"delay_ms":300,"error_name":"APIProviderRateLimitError","error_message":"...","status_code":429}
13
13
  // {"role":"meta","type":"system.version","version":"0.24.2"} // experimental v2 only; modeled since v1.8.5 (SystemVersionRecord), filtered like other meta
14
14
  //
15
+ // {"type":"error","code":"session.occupied","message":"...","details":{...}} // 2.3.0; stdout-only refusal, exit 4
16
+ //
15
17
  // Notes on what does and does not appear here:
16
18
  // - The session.resume_hint meta record is NEW in kimi-code 0.2.0
17
19
  // (introduced at run-prompt.ts:477-505 in 0.2.0; the writer region —
@@ -263,6 +265,20 @@ function parseLine(line) {
263
265
  if (role === undefined && parsed["type"] === "goal.summary") {
264
266
  return validateGoalSummary(parsed, line);
265
267
  }
268
+ if (role === undefined && parsed["type"] === "error") {
269
+ if (typeof parsed["code"] !== "string" || parsed["code"].length === 0 ||
270
+ typeof parsed["message"] !== "string" || parsed["message"].length === 0 ||
271
+ (parsed["details"] !== undefined && !isRecord(parsed["details"]))) {
272
+ return { malformedLine: line, malformedReason: "error record field has unexpected type" };
273
+ }
274
+ // Keep bounded diagnostic fields only; holder metadata is not needed and
275
+ // must not become a session-id source or be copied into persisted errors.
276
+ return { cliError: {
277
+ type: "error",
278
+ code: parsed["code"].slice(0, 200),
279
+ message: parsed["message"].slice(0, 4000),
280
+ } };
281
+ }
266
282
  // H3 forward-compat: a non-empty string role we don't model (a role a future
267
283
  // kimi-code adds). Surface it out-of-band on the unknownRecord channel —
268
284
  // tolerated, not malformed — so the parser keeps working and the diagnostic
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.2";
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,13 @@ 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.1 is live on npm](https://www.npmjs.com/package/kimi-plugin-cc/v/2.2.1), published by
10
+ the release-event run of `publish.yml` (run 38720071707) through GitHub Actions OIDC with a signed
11
+ provenance statement (sigstore log index 3263398504; tarball shasum
12
+ `03907c66d440155763a860ab52b5107c0233938b`, registry propagation took a few minutes this time).
13
+ 2.2.0 (same day, run 38658918855, log index 3256863983) was the first release where the tag-triggered
14
+ path worked end to end without a manual dispatch; 2.0.8 (same day, run 38650044033) was the first
15
+ OIDC publication; 2.0.7 (September 26) was the hand-published bootstrap.
12
16
  The first upload used the validated GitHub Actions artifact and interactive npm
13
17
  authentication. Its public tarball matched that artifact byte-for-byte, and a
14
18
  fresh registry install passed isolated setup/check for both hosts.
@@ -17,7 +21,7 @@ The trusted publisher is configured for `linxule/kimi-plugin-cc`, workflow
17
21
  `publish.yml`, environment `npm`, with direct publishing allowed.
18
22
  `NPM_TRUSTED_PUBLISHING=true` is enabled in GitHub Actions. Future GitHub releases
19
23
  use short-lived OIDC credentials, without a stored npm token or interactive 2FA.
20
- The first npm OIDC publication will be the next release; do not republish 2.0.7.
24
+ OIDC publication is established; never republish an existing version.
21
25
 
22
26
  **2026-10-15 status.** The `v2.0.8` release triggered the first OIDC attempt. It
23
27
  failed twice on the publish step for reasons unrelated to the package: current
package/docs/safety.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Safety
2
2
 
3
- This guide describes the shipped 2.2.0 runtime. Read-only tasks deny file writes and shell commands. Rescue and pursue can edit your workspace. Write-swarm returns a patch from a temporary Git worktree.
3
+ This guide describes the current checkout runtime; 2.3.0 certification is unreleased. Read-only tasks deny file writes and shell commands. Rescue and pursue can edit your workspace. Write-swarm returns a patch from a temporary Git worktree.
4
4
 
5
- Native agent-core-v2 is certified at exact CLI versions `0.42.0`, `0.43.0`, `0.43.1`, `2.0.0`, `2.0.1`, `2.0.2`, `2.1.0`, and `2.1.1`, with plan mode disabled. The [runtime certification table](../runtime/kimi-engine.ts) controls routing. The hook is not an operating-system sandbox.
5
+ Native agent-core-v2 is certified at exact CLI versions `0.42.0`, `0.43.0`, `0.43.1`, `2.0.0`, `2.0.1`, `2.0.2`, `2.1.0`, `2.1.1`, `2.2.0`, and `2.3.0`, with plan mode disabled. The [runtime certification table](../runtime/kimi-engine.ts) controls routing. The hook is not an operating-system sandbox.
6
6
 
7
7
  ## Why the safety story is hook-based
8
8
 
@@ -156,6 +156,8 @@ Provenance: a native-v2 plan requires the `system.version` marker as the first s
156
156
 
157
157
  Residuals, unchanged from v1 and stated plainly: `config.toml` and the session journals are trusted operator state (an actor who can write them could already delete the `[[hooks]]` entry); the upstream hook runner fails open on its own internal errors; the plan-path comparison does not realpath (a symlink there would need a prior hook-allowed write). Native plan mode, tower, subagent fork, Remote Control and `--add-dir` are out of scope.
158
158
 
159
+ In CLI 2.3.0, stale-session retry can reread those same journals. Do not concurrently operate on or edit a plugin-managed session from another client. The upstream session lock is not a security boundary; no parent-side rescan can make that reload atomic.
160
+
159
161
  ## The rescue allowlist
160
162
 
161
163
  For `/kimi:rescue`, the hook delegates to [`evaluateRescueHookRequest`](../runtime/rescue-approval.ts). The allowlist has been extended since the original Wire-transport implementation. Highlights:
@@ -363,3 +365,13 @@ Agents use `retryable_after_setup`, not `drift_axis` alone. They run setup for t
363
365
  ## Reporting safety issues
364
366
 
365
367
  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.
368
+
369
+ ## Hook-escape monitor: detection evidence
370
+
371
+ **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).
372
+
373
+ 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 upstream ask, filed as MoonshotAI/kimi-code#4241, 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.
374
+
375
+ ## Review-gate thinking request
376
+
377
+ 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.2",
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.2",
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";
@@ -125,7 +126,16 @@ export async function runCliPrompt(opts) {
125
126
  let stderrTail = "";
126
127
  let announcedSessionId;
127
128
  let announcedGoalSummary;
129
+ let announcedCliError;
128
130
  let announcedSystemVersion;
131
+ const hookMonitor = createHookEscapeMonitor({ commandLabel: opts.commandLabel,
132
+ operationKind: opts.executionPlan.operationKind, trustedWorkspaceRoot: opts.trustedWorkspaceRoot, cwd: opts.cwd });
133
+ const hookEscapes = [];
134
+ const noteHookEscape = (escape) => {
135
+ hookEscapes.push(escape);
136
+ appendLogLine({ event: "hook_escape", ...escape, command_label: opts.commandLabel,
137
+ operation_kind: opts.executionPlan.operationKind });
138
+ };
129
139
  let engineMismatchDetected = false;
130
140
  let handleEngineMismatch;
131
141
  const plannedEngine = opts.executionPlan.intendedEngine;
@@ -181,6 +191,13 @@ export async function runCliPrompt(opts) {
181
191
  for (const outcome of outcomes) {
182
192
  if (engineMismatchDetected)
183
193
  return;
194
+ if (outcome.cliError !== undefined) {
195
+ if (requireMarkerBeforeOutput("a CLI error record"))
196
+ return;
197
+ announcedCliError ??= outcome.cliError;
198
+ appendLogLine({ event: "cli_error", ...outcome.cliError });
199
+ continue;
200
+ }
184
201
  if (outcome.unknownRecord !== undefined) {
185
202
  // H3 forward-compat: a stream-json line with a role we don't model
186
203
  // (a future kimi-code role). Log it for diagnostics but keep it OUT of
@@ -283,6 +300,9 @@ export async function runCliPrompt(opts) {
283
300
  }
284
301
  if (requireMarkerBeforeOutput(`a ${outcome.record.role} record`))
285
302
  return;
303
+ const escape = hookMonitor.observe(outcome.record);
304
+ if (escape)
305
+ noteHookEscape(escape);
286
306
  records.push(outcome.record);
287
307
  appendLogLine({ event: "record", record: outcome.record });
288
308
  invokeOnRecord(outcome.record);
@@ -314,6 +334,13 @@ export async function runCliPrompt(opts) {
314
334
  const settle = (kind, payload) => {
315
335
  if (settlementStarted)
316
336
  return;
337
+ // Denied calls still without a result (a cancel/budget race, an abnormal
338
+ // exit, or a turn that ended without the denial the hook owes) are reported
339
+ // alongside the real outcome, never instead of it.
340
+ for (const escape of hookMonitor.settle())
341
+ noteHookEscape(escape);
342
+ if (kind === "resolve" && hookEscapes.length > 0)
343
+ payload = { ...payload, hookEscapes: [...hookEscapes] };
317
344
  settlementStarted = true;
318
345
  opts.signal?.removeEventListener("abort", onAbort);
319
346
  // Abort teardown can outlive the direct child's close event while a
@@ -472,6 +499,11 @@ export async function runCliPrompt(opts) {
472
499
  }
473
500
  }
474
501
  };
502
+ const stopForPolicyFailure = (error) => {
503
+ engineMismatchDetected = true; // Stops all further consumer output.
504
+ cancellationTeardown ??= teardownChildTree().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
505
+ settle("reject", error);
506
+ };
475
507
  let engineMismatchError;
476
508
  handleEngineMismatch = (mismatch) => {
477
509
  if (engineMismatchError !== undefined)
@@ -493,8 +525,7 @@ export async function runCliPrompt(opts) {
493
525
  system_version: mismatch.systemVersion ?? null,
494
526
  reason: mismatch.reason,
495
527
  });
496
- cancellationTeardown = teardownChildTree().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
497
- settle("reject", engineMismatchError);
528
+ stopForPolicyFailure(engineMismatchError);
498
529
  };
499
530
  const onAbort = () => {
500
531
  if (aborted)
@@ -586,6 +617,7 @@ export async function runCliPrompt(opts) {
586
617
  settledAt: 0, // Filled at settlement, after the teardown barrier.
587
618
  sessionId,
588
619
  goalSummary: announcedGoalSummary,
620
+ cliError: announcedCliError,
589
621
  systemVersion: announcedSystemVersion,
590
622
  observedEngine: announcedSystemVersion !== undefined
591
623
  ? "native-v2"
@@ -674,6 +706,7 @@ export class ResponseTimeoutError extends RuntimeError {
674
706
  partial_output_chars: partialResult?.records.reduce((count, record) => count + (record.role === "assistant" && typeof record.content === "string" ? record.content.length : 0), 0) ?? 0,
675
707
  aborted: partialResult?.aborted ?? true,
676
708
  ...(partialResult ? { settled_post_abort: true } : {}),
709
+ ...(partialResult?.hookEscapes?.length ? { hook_escapes: partialResult.hookEscapes } : {}),
677
710
  },
678
711
  });
679
712
  this.name = "ResponseTimeoutError";
@@ -778,11 +811,11 @@ function buildArgs(opts) {
778
811
  args.push("--skills-dir", dir);
779
812
  }
780
813
  }
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).
814
+ // NOTE: thinking effort is requested through the env binding, never CLI argv.
815
+ // Non-trait-driven providers or a base level already off ignore it silently;
816
+ // an alwaysThinking model receives off unnormalized. stream-json carries no
817
+ // thinking deltas (writeThinkingDelta() {} in the JSON writer), so the plugin
818
+ // cannot observe whether the request was honoured.
786
819
  // -p accepts the prompt as the option value. We pass it as a separate argv
787
820
  // entry so prompts with leading dashes or whitespace are not interpreted as
788
821
  // additional flags.
@@ -791,6 +824,8 @@ function buildArgs(opts) {
791
824
  }
792
825
  function buildEnv(opts) {
793
826
  const env = { ...opts.env };
827
+ if (opts.thinkingEffort !== undefined)
828
+ env.KIMI_MODEL_THINKING_EFFORT = opts.thinkingEffort;
794
829
  // 2.2.0 audit: ambient trust starts project MCP stdio servers outside PreToolUse.
795
830
  delete env.KIMI_CODE_TRUST_WORKSPACE;
796
831
  // 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
@@ -99,7 +99,7 @@ export function assertCliResultSuccess(result, stage) {
99
99
  });
100
100
  }
101
101
  if (result.exitCode !== 0) {
102
- const tail = result.stderrTail.trim();
102
+ const tail = result.cliError?.message ?? result.stderrTail.trim();
103
103
  throw new RuntimeError("CLI_NONZERO_EXIT", [
104
104
  `kimi subprocess exited with code ${result.exitCode}`,
105
105
  result.signal ? ` (signal ${result.signal})` : "",
@@ -110,6 +110,7 @@ export function assertCliResultSuccess(result, stage) {
110
110
  signal: result.signal,
111
111
  record_count: result.records.length,
112
112
  malformed_count: result.malformed.length,
113
+ ...(result.cliError !== undefined && { cli_error: result.cliError }),
113
114
  stderr_tail_len: result.stderrTail.length,
114
115
  },
115
116
  });
@@ -185,6 +186,19 @@ export async function captureTimeoutPartialOutput(classified, store, job, env, c
185
186
  budgetMs: timeout.details.budget_ms,
186
187
  };
187
188
  }
189
+ /** One-line human summary of hook-escape findings, or undefined when none. */
190
+ export function describeHookEscapes(result) {
191
+ const escapes = result.hookEscapes ?? [];
192
+ if (escapes.length === 0)
193
+ return undefined;
194
+ const first = escapes[0];
195
+ 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).`;
196
+ }
197
+ export function warnHookEscapes(result, commandType, stderr) {
198
+ const note = describeHookEscapes(result);
199
+ if (note !== undefined)
200
+ stderr.write(`[${commandType}] ${note}\n`);
201
+ }
188
202
  export function warnInheritedBudget(job, stderr) {
189
203
  if (job.budget_source === "inherited" && job.budget_ms !== null) {
190
204
  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
  }