kimi-plugin-cc 2.0.8 → 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 (88) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +27 -0
  4. package/README.md +5 -5
  5. package/agents/kimi-ask.md +5 -1
  6. package/agents/kimi-challenge.md +4 -2
  7. package/agents/kimi-pursue.md +3 -1
  8. package/agents/kimi-rescue.md +5 -1
  9. package/agents/kimi-review.md +4 -2
  10. package/agents/kimi-swarm-write.md +3 -1
  11. package/agents/kimi-swarm.md +3 -1
  12. package/commands/ask.md +5 -3
  13. package/commands/challenge.md +5 -3
  14. package/commands/pursue.md +5 -1
  15. package/commands/rescue.md +5 -3
  16. package/commands/review.md +5 -3
  17. package/commands/swarm.md +5 -1
  18. package/dist/cli-client.js +42 -10
  19. package/dist/commands/ask.js +14 -7
  20. package/dist/commands/cli-helpers.js +66 -4
  21. package/dist/commands/pursue.js +7 -3
  22. package/dist/commands/rescue.js +16 -10
  23. package/dist/commands/result.js +5 -1
  24. package/dist/commands/review-gate.js +21 -14
  25. package/dist/commands/review.js +9 -5
  26. package/dist/commands/status.js +2 -2
  27. package/dist/commands/swarm.js +10 -4
  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 +15 -7
  32. package/dist/job-store.js +52 -7
  33. package/dist/kimi-engine.js +2 -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/session-recovery.js +203 -0
  39. package/dist/vendor/smol-toml/date.js +50 -21
  40. package/dist/vendor/smol-toml/error.js +6 -2
  41. package/dist/vendor/smol-toml/extract.js +259 -66
  42. package/dist/vendor/smol-toml/parse.js +64 -48
  43. package/dist/vendor/smol-toml/primitive.js +107 -135
  44. package/dist/vendor/smol-toml/struct.js +106 -134
  45. package/dist/vendor/smol-toml/util.js +22 -78
  46. package/dist/version.js +1 -1
  47. package/docs/models.md +10 -0
  48. package/docs/registry-releases.md +24 -1
  49. package/docs/safety.md +59 -2
  50. package/package.json +1 -1
  51. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  52. package/plugins/kimi-codex/dist/cli-client.js +42 -10
  53. package/plugins/kimi-codex/dist/commands/ask.js +14 -7
  54. package/plugins/kimi-codex/dist/commands/cli-helpers.js +66 -4
  55. package/plugins/kimi-codex/dist/commands/pursue.js +7 -3
  56. package/plugins/kimi-codex/dist/commands/rescue.js +16 -10
  57. package/plugins/kimi-codex/dist/commands/result.js +5 -1
  58. package/plugins/kimi-codex/dist/commands/review-gate.js +21 -14
  59. package/plugins/kimi-codex/dist/commands/review.js +9 -5
  60. package/plugins/kimi-codex/dist/commands/status.js +2 -2
  61. package/plugins/kimi-codex/dist/commands/swarm.js +10 -4
  62. package/plugins/kimi-codex/dist/help.js +1 -1
  63. package/plugins/kimi-codex/dist/hook-escape.js +71 -0
  64. package/plugins/kimi-codex/dist/hooks/approval-policy.js +17 -27
  65. package/plugins/kimi-codex/dist/hooks/config-safety.js +15 -7
  66. package/plugins/kimi-codex/dist/job-store.js +52 -7
  67. package/plugins/kimi-codex/dist/kimi-engine.js +2 -0
  68. package/plugins/kimi-codex/dist/kimi-errors.js +1 -1
  69. package/plugins/kimi-codex/dist/kimi-timeouts.js +6 -10
  70. package/plugins/kimi-codex/dist/kimi-version-probe.js +2 -2
  71. package/plugins/kimi-codex/dist/parsing.js +59 -53
  72. package/plugins/kimi-codex/dist/session-recovery.js +203 -0
  73. package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +50 -21
  74. package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +6 -2
  75. package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +259 -66
  76. package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +64 -48
  77. package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +107 -135
  78. package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +106 -134
  79. package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +22 -78
  80. package/plugins/kimi-codex/dist/version.js +1 -1
  81. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +4 -2
  82. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +4 -2
  83. package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +3 -1
  84. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +4 -2
  85. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +4 -2
  86. package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +2 -0
  87. package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +3 -1
  88. package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +3 -1
@@ -0,0 +1,71 @@
1
+ import { decideHookOutcomePure } from "./hooks/approval-policy.js";
2
+ // Upstream decorates results it still delivers verbatim otherwise (2.2.0,
3
+ // `agent/toolDedupe/toolDedupeService.ts`): the repeat breaker appends one
4
+ // `\n\n<system-reminder>…</system-reminder>` block from the 3rd identical call
5
+ // (`appendReminder`, also for vetoed results — `finalizeToolResult` runs the
6
+ // `onDidExecuteTool` hooks on every settled result), and at the 12th call it
7
+ // replaces the result with a fixed "not executed" text (`HANDOFF_VETO_TEXT`).
8
+ // Both still mean the hook's veto stood. Re-verify each certification.
9
+ const REMINDER_SUFFIX = /^(?:\n\n<system-reminder>[\s\S]*?<\/system-reminder>)+$/;
10
+ const HANDOFF_VETO_PREFIX = "This turn was ended by the repeat breaker after the same tool call was issued ";
11
+ /** True when `content` is the hook's denial, possibly carrying upstream's repeat-breaker decoration. */
12
+ export function carriesDenial(content, reason) {
13
+ if (content === reason)
14
+ return true;
15
+ if (content.startsWith(reason) && REMINDER_SUFFIX.test(content.slice(reason.length)))
16
+ return true;
17
+ return content.startsWith(HANDOFF_VETO_PREFIX);
18
+ }
19
+ /** Detection evidence, never enforcement. Results arrive after a tool may have
20
+ * executed, and the stream carries no outcome field: a result other than the
21
+ * managed denial is also what upstream emits for calls it rejected BEFORE the
22
+ * hook ran (unknown tool, disabled tool, invalid args, sensitive path, per-tool
23
+ * resolve errors) and what a coexisting hook or permission policy emits when it
24
+ * blocks first. So an escape is reported — logged, persisted, shown — and the
25
+ * run continues; failing the job needs positive execution evidence this stream
26
+ * does not provide. Filesystem policy is never replayed here. */
27
+ export function createHookEscapeMonitor(ctx) {
28
+ const pending = new Map();
29
+ return {
30
+ observe(record) {
31
+ if (!ctx.commandLabel)
32
+ return;
33
+ if (record.role === "assistant") {
34
+ for (const call of record.tool_calls ?? []) {
35
+ let args;
36
+ try {
37
+ args = JSON.parse(call.function.arguments);
38
+ }
39
+ catch {
40
+ args = {};
41
+ }
42
+ if (typeof args !== "object" || args === null || Array.isArray(args))
43
+ args = {};
44
+ const result = decideHookOutcomePure({
45
+ hook_event_name: "PreToolUse", tool_name: call.function.name,
46
+ tool_input: args, tool_call_id: call.id, cwd: ctx.cwd,
47
+ }, ctx);
48
+ if (!("evaluator" in result) && result.decision === "deny") {
49
+ pending.set(call.id, { reason: result.reason, tool_name: call.function.name });
50
+ }
51
+ }
52
+ }
53
+ else if (record.role === "tool") {
54
+ const denied = pending.get(record.tool_call_id);
55
+ if (denied) {
56
+ pending.delete(record.tool_call_id);
57
+ if (!carriesDenial(record.content, denied.reason)) {
58
+ return { tool_call_id: record.tool_call_id, tool_name: denied.tool_name,
59
+ kind: "mismatch", content_excerpt: record.content.slice(0, 200) };
60
+ }
61
+ }
62
+ }
63
+ },
64
+ /** Every denied call still without a result; call once at settlement. */
65
+ settle() {
66
+ const unresolved = [...pending.entries()].map(([tool_call_id, { tool_name }]) => ({ tool_call_id, tool_name, kind: "unresolved" }));
67
+ pending.clear();
68
+ return unresolved;
69
+ },
70
+ };
71
+ }
@@ -81,6 +81,21 @@ const KNOWN_LABELS = new Set([
81
81
  "swarm-write",
82
82
  ]);
83
83
  export async function decideHookOutcome(input, ctx) {
84
+ const result = decideHookOutcomePure(input, ctx);
85
+ if (!("evaluator" in result))
86
+ return result;
87
+ const toolName = typeof input.tool_name === "string" ? input.tool_name : "";
88
+ if (ctx.rescueEvaluator !== undefined) {
89
+ return await ctx.rescueEvaluator(result.workspaceRoot, toolName, input.tool_input);
90
+ }
91
+ if (READ_ONLY_TOOLS.has(toolName))
92
+ return { decision: "allow" };
93
+ return {
94
+ decision: "deny",
95
+ reason: `kimi-plugin-cc safety hook: ${ctx.commandLabel} evaluator not configured; tool "${toolName}" denied as a safety default.`,
96
+ };
97
+ }
98
+ export function decideHookOutcomePure(input, ctx) {
84
99
  const label = typeof ctx.commandLabel === "string" ? ctx.commandLabel : undefined;
85
100
  // Out-of-plugin context: any kimi process spawned without our env var
86
101
  // is direct user usage. The hook MUST NOT restrict it.
@@ -176,18 +191,7 @@ export async function decideHookOutcome(input, ctx) {
176
191
  "This is a plugin misconfiguration — writes are refused rather than risk the user's working tree.",
177
192
  };
178
193
  }
179
- if (ctx.rescueEvaluator !== undefined) {
180
- return await ctx.rescueEvaluator(workspaceRoot, toolName, input.tool_input);
181
- }
182
- // Stub for callers that didn't inject the evaluator (tests of the policy
183
- // function alone). The production entry script always injects it.
184
- if (READ_ONLY_TOOLS.has(toolName)) {
185
- return { decision: "allow" };
186
- }
187
- return {
188
- decision: "deny",
189
- reason: `kimi-plugin-cc safety hook: swarm-write evaluator not configured; tool "${toolName}" denied as a safety default.`,
190
- };
194
+ return { evaluator: "rescue", workspaceRoot };
191
195
  }
192
196
  case "rescue": {
193
197
  const workspaceRoot = typeof ctx.trustedWorkspaceRoot === "string" ? ctx.trustedWorkspaceRoot : "";
@@ -200,21 +204,7 @@ export async function decideHookOutcome(input, ctx) {
200
204
  "This is a plugin misconfiguration — writes are refused rather than trusting hook payload cwd.",
201
205
  };
202
206
  }
203
- if (ctx.rescueEvaluator !== undefined) {
204
- return await ctx.rescueEvaluator(workspaceRoot, toolName, input.tool_input);
205
- }
206
- // Stub for callers that didn't inject the evaluator (e.g.,
207
- // tests that exercise only the policy function). Allow
208
- // Read/Grep/Glob and friends; deny everything else. The
209
- // production entry script wires the real evaluator from
210
- // runtime/rescue-approval.ts.
211
- if (READ_ONLY_TOOLS.has(toolName)) {
212
- return { decision: "allow" };
213
- }
214
- return {
215
- decision: "deny",
216
- reason: `kimi-plugin-cc safety hook: rescue evaluator not configured; tool "${toolName}" denied as a safety default.`,
217
- };
207
+ return { evaluator: "rescue", workspaceRoot };
218
208
  }
219
209
  default:
220
210
  if (KNOWN_LABELS.has(label)) {
@@ -417,15 +417,23 @@ export function normalizeTopLevelInlineHooks(contents, lineEnding) {
417
417
  entryCount: hooks.length,
418
418
  };
419
419
  }
420
+ // Same defaults as smol-toml 1.9.1 parse(); this walker only locates assignments.
421
+ // tests/runtime/toml-walker-context.test.ts asserts these against the vendored parse().
422
+ export function assignmentParseContext(s) {
423
+ return { s, p: s.charCodeAt(0) === 0xfeff ? 1 : 0, d: 1_000, bi: false, ld: true, uk: 0 };
424
+ }
420
425
  function findTopLevelHooksAssignment(contents) {
421
- let ptr = skipVoid(contents, 0);
422
- while (ptr < contents.length) {
426
+ const ctx = assignmentParseContext(contents);
427
+ skipVoid(ctx);
428
+ while (ctx.p < contents.length) {
423
429
  // A root-level direct assignment cannot resume after entering a table.
424
- if (contents[ptr] === "[")
430
+ if (contents[ctx.p] === "[")
425
431
  return null;
426
- const declarationStart = ptr;
427
- const [key, valueStart] = parseKey(contents, ptr);
428
- const [, valueEnd] = extractValue(contents, valueStart, undefined, 1_000, undefined);
432
+ const declarationStart = ctx.p;
433
+ const key = parseKey(ctx);
434
+ skipVoid(ctx, true, true);
435
+ extractValue(ctx, undefined);
436
+ const valueEnd = ctx.p;
429
437
  if (key.length === 1 && key[0] === "hooks") {
430
438
  const lineStart = contents.lastIndexOf("\n", declarationStart - 1) + 1;
431
439
  const start = /^[\t ]*$/.test(contents.slice(lineStart, declarationStart))
@@ -445,7 +453,7 @@ function findTopLevelHooksAssignment(contents) {
445
453
  }
446
454
  return { start, end, hadLineEnding };
447
455
  }
448
- ptr = skipVoid(contents, valueEnd);
456
+ skipVoid(ctx);
449
457
  }
450
458
  return null;
451
459
  }
package/dist/job-store.js CHANGED
@@ -11,8 +11,11 @@ export class JobStore {
11
11
  let db;
12
12
  try {
13
13
  db = createSqliteAdapter(paths.stateDbPath);
14
- db.pragma("journal_mode = WAL");
14
+ // busy_timeout FIRST: switching the journal mode takes a brief write lock,
15
+ // and a second opener (status/result racing a worker) otherwise fails
16
+ // immediately with SQLITE_BUSY instead of waiting (seen 2026-10-14).
15
17
  db.pragma("busy_timeout = 5000");
18
+ db.pragma("journal_mode = WAL");
16
19
  }
17
20
  catch (error) {
18
21
  try {
@@ -70,6 +73,11 @@ export class JobStore {
70
73
  if (!tableHasColumn(this.db, "budget_ms")) {
71
74
  this.db.exec(`ALTER TABLE jobs ADD COLUMN budget_ms INTEGER;`);
72
75
  }
76
+ for (const column of ["kimi_session_id_source", "budget_source", "thinking_effort", "hook_escapes"]) {
77
+ if (!tableHasColumn(this.db, column)) {
78
+ this.db.exec("ALTER TABLE jobs ADD COLUMN " + column + " TEXT;");
79
+ }
80
+ }
73
81
  // Additive provenance migration. Existing rows deliberately remain NULL:
74
82
  // v1.9.5 plus kimi-code 0.33/0.34 could have routed to native v2 before the
75
83
  // legacy pin landed, so a blanket historical v1 backfill would invent fact.
@@ -98,6 +106,8 @@ export class JobStore {
98
106
  CREATE INDEX IF NOT EXISTS jobs_repo_type_updated_idx
99
107
  ON jobs (repo_id, command_type, updated_at DESC);
100
108
 
109
+ CREATE INDEX IF NOT EXISTS jobs_session_id_idx ON jobs (kimi_session_id);
110
+
101
111
  CREATE INDEX IF NOT EXISTS jobs_session_idx
102
112
  ON jobs (repo_id, kimi_session_id, updated_at DESC);
103
113
 
@@ -162,15 +172,15 @@ export class JobStore {
162
172
  try {
163
173
  this.db.run(`
164
174
  INSERT INTO jobs (
165
- job_id, repo_id, command_type, created_at, updated_at, cwd, model, thinking,
166
- background, pid, kimi_pid, status, kimi_session_id,
175
+ job_id, repo_id, command_type, created_at, updated_at, cwd, model, thinking, thinking_effort,
176
+ background, pid, kimi_pid, status, kimi_session_id, kimi_session_id_source, budget_source,
167
177
  operation_kind, intended_engine, observed_engine, kimi_version, system_version,
168
178
  kimi_command, kimi_prefix_args, plan_certification, resumed_from_job_id, safety_profile,
169
179
  agent_profile, prompt_digest, summary, phase, budget_ms, final_output_path, stream_log_path, error
170
180
  )
171
181
  VALUES (
172
- @job_id, @repo_id, @command_type, @created_at, @updated_at, @cwd, @model, @thinking,
173
- @background, @pid, @kimi_pid, @status, @kimi_session_id,
182
+ @job_id, @repo_id, @command_type, @created_at, @updated_at, @cwd, @model, @thinking, @thinking_effort,
183
+ @background, @pid, @kimi_pid, @status, @kimi_session_id, @kimi_session_id_source, @budget_source,
174
184
  @operation_kind, @intended_engine, @observed_engine, @kimi_version, @system_version,
175
185
  @kimi_command, @kimi_prefix_args, @plan_certification, @resumed_from_job_id, @safety_profile,
176
186
  @agent_profile, @prompt_digest, @summary, @phase, @budget_ms, @final_output_path, @stream_log_path, @error
@@ -198,6 +208,10 @@ export class JobStore {
198
208
  const row = this.db.get("SELECT * FROM jobs WHERE job_id = ?", jobId);
199
209
  return row ? hydrateRow(row) : null;
200
210
  }
211
+ /** Global across repos and terminal states; optionally exclude the current row. */
212
+ hasSessionId(sessionId, exceptJobId = "") {
213
+ return Boolean(this.db.get("SELECT 1 FROM jobs WHERE kimi_session_id = ? AND job_id != ? LIMIT 1", sessionId, exceptJobId));
214
+ }
201
215
  findLatestJob(options) {
202
216
  const conditions = ["repo_id = @repo_id"];
203
217
  const params = {
@@ -247,6 +261,18 @@ export class JobStore {
247
261
  updateRunningJob(jobId, patch) {
248
262
  return this.updateWhere(jobId, patch, "status = 'running'");
249
263
  }
264
+ /** Recheck uniqueness atomically: another worker can claim an ID during the
265
+ * asynchronous directory scan. Ordinary announced/resumed IDs may be shared. */
266
+ claimRecoveredSessionId(jobId, sessionId) {
267
+ this.db.run(`UPDATE jobs
268
+ SET kimi_session_id = @session_id, kimi_session_id_source = 'recovered_session_dir',
269
+ updated_at = @updated_at
270
+ WHERE job_id = @job_id AND status = 'running' AND kimi_session_id IS NULL
271
+ AND NOT EXISTS (SELECT 1 FROM jobs WHERE kimi_session_id = @session_id AND job_id != @job_id)`, { job_id: jobId, session_id: sessionId, updated_at: new Date().toISOString() });
272
+ const job = this.getJob(jobId);
273
+ return job?.status === "running" && job.kimi_session_id === sessionId &&
274
+ job.kimi_session_id_source === "recovered_session_dir" ? job : null;
275
+ }
250
276
  /**
251
277
  * Evidence-only backfill for pre-provenance rows. COALESCE prevents forensic
252
278
  * inspection from overwriting a field already recorded at execution time.
@@ -280,10 +306,10 @@ export class JobStore {
280
306
  return this.updateWhere(jobId, { ...patch, status: "cancelled", pid: null, kimi_pid: null }, "status = 'running'");
281
307
  }
282
308
  static JOBS_COLUMNS = new Set([
283
- "status", "summary", "phase", "budget_ms", "pid", "kimi_pid",
309
+ "status", "summary", "phase", "budget_ms", "budget_source", "kimi_session_id_source", "pid", "kimi_pid",
284
310
  "final_output_path", "stream_log_path", "kimi_session_id",
285
311
  "error", "command_type", "prompt_digest", "repo_id",
286
- "cwd", "model", "thinking", "background", "agent_profile",
312
+ "cwd", "model", "thinking", "thinking_effort", "hook_escapes", "background", "agent_profile",
287
313
  "operation_kind", "intended_engine", "observed_engine", "kimi_version",
288
314
  "system_version", "kimi_command", "kimi_prefix_args", "plan_certification",
289
315
  "resumed_from_job_id", "safety_profile",
@@ -323,8 +349,11 @@ function hydrateRow(row) {
323
349
  function serializeRecord(record) {
324
350
  return {
325
351
  ...record,
352
+ thinking_effort: record.thinking_effort ?? null,
326
353
  phase: record.phase ?? null,
327
354
  budget_ms: record.budget_ms ?? null,
355
+ budget_source: record.budget_source ?? null,
356
+ kimi_session_id_source: record.kimi_session_id_source ?? null,
328
357
  operation_kind: record.operation_kind ?? null,
329
358
  intended_engine: record.intended_engine ?? null,
330
359
  observed_engine: record.observed_engine ?? null,
@@ -562,3 +591,19 @@ export function withJobStoreSync(paths, fn) {
562
591
  store?.close();
563
592
  }
564
593
  }
594
+ /** Old non-null budgets have unknown provenance; never invent explicit intent. */
595
+ /** Parsed `hook_escapes` column for JSON envelopes: [] when NULL or unreadable. */
596
+ export function parseHookEscapes(raw) {
597
+ if (!raw)
598
+ return [];
599
+ try {
600
+ const parsed = JSON.parse(raw);
601
+ return Array.isArray(parsed) ? parsed : [];
602
+ }
603
+ catch {
604
+ return [];
605
+ }
606
+ }
607
+ export function renderBudgetSource(job) {
608
+ return job.budget_source ?? (job.budget_ms === null ? "default" : "unknown");
609
+ }
@@ -206,6 +206,8 @@ export function observedExecutionFields(result) {
206
206
  return {
207
207
  observed_engine: result.observedEngine,
208
208
  system_version: result.systemVersion ?? null,
209
+ // Detection evidence from runtime/hook-escape.ts, JSON array or NULL.
210
+ hook_escapes: result.hookEscapes && result.hookEscapes.length > 0 ? JSON.stringify(result.hookEscapes) : null,
209
211
  };
210
212
  }
211
213
  export function executionPlanFromPersisted(fields) {
@@ -45,7 +45,7 @@ export function classifyManagedCommandFailure(error, commandType, jobId, options
45
45
  if (error instanceof RuntimeError) {
46
46
  details.cause_code = error.code;
47
47
  if (error.code === "RESPONSE_TIMEOUT") {
48
- for (const key of ["budget_ms", "record_count", "partial_output_chars", "aborted", "settled_post_abort"]) {
48
+ for (const key of ["budget_ms", "record_count", "partial_output_chars", "aborted", "settled_post_abort", "hook_escapes"]) {
49
49
  if (error.details[key] !== undefined)
50
50
  details[key] = error.details[key];
51
51
  }
@@ -8,7 +8,7 @@
8
8
  // startup failures surface as CLI_SPAWN_FAILED / CLI_PROCESS_ERROR —
9
9
  // not as separate timeouts. So only the per-command response budgets
10
10
  // remain.
11
- // Budgets are sized for thinking-on workflow (the default since v1.0).
11
+ // Budgets accommodate thinking-capable workflows; per-run effort is only a request.
12
12
  // kimi-code spends real wallclock on extended reasoning before emitting
13
13
  // the first assistant record; pre-alpha.4 budgets (300s/600s) were sized
14
14
  // against the non-thinking path and timed out in production smoke
@@ -49,15 +49,11 @@ export const KIMI_SWARM_DEFAULT_BUDGET_MS = 1_800_000; // 30 minutes
49
49
  * Review-gate budget. Fires inside Claude Code's Stop hook so any value
50
50
  * above the user's perceptible wait makes the gate feel broken.
51
51
  *
52
- * kimi-code 0.1.1 has no per-spawn CLI flag for thinking control
53
- * (Round 2 multi-agent review caught this — emitting `--no-thinking`
54
- * triggers an unknown-option crash). The 8s budget assumes the user
55
- * has either `default_thinking = false` or `[thinking].mode = "off"`
56
- * in `~/.kimi-code/config.toml`, OR a non-thinking-capable model
57
- * selected for review-gate (via KIMI_PLUGIN_CC_REVIEW_GATE_MODEL).
58
- * Under thinking-on the gate will time out — the gate is fail-open on
59
- * timeout by design, so the worst case is the gate becomes
60
- * always-allow. See docs/safety.md.
52
+ * The gate requests off through KIMI_MODEL_THINKING_EFFORT by default;
53
+ * KIMI_PLUGIN_CC_REVIEW_GATE_THINKING can inherit or request another effort.
54
+ * BYO/non-trait-driven providers and always-thinking models can ignore it.
55
+ * The 8s timeout remains fail-open; a request never guarantees completion.
56
+ * Hook-enforcement escape is a separate blocking failure. See docs/safety.md.
61
57
  */
62
58
  export const KIMI_REVIEW_GATE_TIMEOUT_MS = 8_000;
63
59
  export function isTimeoutCode(code) {
@@ -198,8 +198,8 @@ export const KIMI_TESTED_MINORS = [
198
198
  // import it; `-p` stdout stays the direct PromptJsonWriter), session-lifecycle
199
199
  // changes that only HELP our cancellation story (active-turn cancel on close +
200
200
  // BACKGROUND_KEEP_ALIVE_ON_EXIT default flipped true→false), an `alwaysThinking`
201
- // model-capability flag (read-only detection, the H5 thinking-control knob is
202
- // still upstream-blocked), a SIGHUP cleanup handler (exit 129; SIGTERM still
201
+ // model-capability flag (the H5 env request ships in plugin 2.2.1,
202
+ // but always-thinking models need not honour it), a SIGHUP cleanup handler (exit 129; SIGTERM still
203
203
  // exits 143 and runs cleanup — our SIGTERM→SIGKILL reaping is unaffected), and
204
204
  // a new builtin `import-from-cc-codex` skill (not a plugin surface).
205
205
  // See .claude/kimi-code-research/reports/72-76 for the audit reports.
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
  }