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
@@ -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.
@@ -15,9 +15,10 @@ import { KIMI_REVIEW_GATE_TIMEOUT_MS, isTimeoutCode } from "../kimi-timeouts.js"
15
15
  import { writeInvocationLogHeader } from "../logging.js";
16
16
  import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
17
17
  import { renderManagedJobOutput, writeArtifact, } from "../render.js";
18
+ import { validateThinkingEffort } from "../parsing.js";
18
19
  import { maybeWarnHookMissing, verifyHookInstalled } from "../hooks/install.js";
19
20
  import { resolveKimiHome } from "../kimi-home.js";
20
- import { assertCliResultSuccess, reassembleProseFromRecords } from "./cli-helpers.js";
21
+ import { assertCliResultSuccess, describeHookEscapes, reassembleProseFromRecords } from "./cli-helpers.js";
21
22
  const REVIEW_GATE_AGENT_PROFILE_PLACEHOLDER = "<cli-client>";
22
23
  // v1.0 cutover note (PR 2):
23
24
  //
@@ -82,18 +83,19 @@ export async function runReviewGateStopHook(payload, context) {
82
83
  ...payload,
83
84
  cwd,
84
85
  }, assistantMessage, context);
86
+ const note = output.hookEscapeNote !== undefined ? `Kimi review gate ${output.hookEscapeNote}` : undefined;
85
87
  if (output.decision === "BLOCK" && output.confidence === "high") {
86
88
  return {
87
89
  decision: "block",
88
90
  reason: buildBlockReason(output),
91
+ ...(note !== undefined ? { systemMessage: note } : {}),
89
92
  };
90
93
  }
91
94
  if (output.decision === "BLOCK") {
92
- return {
93
- systemMessage: `Kimi review gate noted concerns but allowed stop: ${output.summary}`,
94
- };
95
+ const concern = `Kimi review gate noted concerns but allowed stop: ${output.summary}`;
96
+ return { systemMessage: note !== undefined ? `${concern}\n${note}` : concern };
95
97
  }
96
- return {};
98
+ return note !== undefined ? { systemMessage: note } : {};
97
99
  }
98
100
  catch (error) {
99
101
  return {
@@ -105,6 +107,7 @@ function reviewGateSkipped(reason) {
105
107
  return { systemMessage: `review-gate skipped: ${reason}` };
106
108
  }
107
109
  async function executeReviewGate(payload, assistantMessage, context) {
110
+ let hookEscapeNote;
108
111
  const paths = resolvePluginPaths(context.env);
109
112
  await ensurePluginPaths(paths);
110
113
  const repoIdentity = await resolveRepoIdentity(payload.cwd);
@@ -121,6 +124,10 @@ async function executeReviewGate(payload, assistantMessage, context) {
121
124
  cwd: payload.cwd,
122
125
  repoRoot: repoIdentity.repoRoot,
123
126
  });
127
+ // Unset or empty → request "off" (the 8 s budget is why); "inherit" → export
128
+ // nothing; anything else is lowercased and syntax-checked like --thinking.
129
+ const gateThinking = context.env.KIMI_PLUGIN_CC_REVIEW_GATE_THINKING?.trim() || "off";
130
+ const thinkingEffort = gateThinking.toLowerCase() === "inherit" ? undefined : validateThinkingEffort(gateThinking, "review_gate.parse");
124
131
  const configuredModel = context.env.KIMI_PLUGIN_CC_REVIEW_GATE_MODEL;
125
132
  const model = configuredModel?.trim() ? configuredModel : undefined;
126
133
  const executionPlan = await prepareKimiExecutionPlan({
@@ -148,6 +155,7 @@ async function executeReviewGate(payload, assistantMessage, context) {
148
155
  cwd: payload.cwd,
149
156
  model: model ?? null,
150
157
  thinking: false,
158
+ thinking_effort: thinkingEffort ?? null,
151
159
  background: false,
152
160
  pid: null,
153
161
  kimi_pid: null,
@@ -178,17 +186,15 @@ async function executeReviewGate(payload, assistantMessage, context) {
178
186
  prompt,
179
187
  commandLabel: "review_gate",
180
188
  model,
181
- // Intent: thinking-off so the 8s Stop-hook budget is achievable.
182
- // Mechanism: currently advisory — kimi-code 0.1.1 has no CLI
183
- // flag for this; control is config-based (default_thinking /
184
- // [thinking].mode). The field carries intent through the
185
- // contract; when upstream adds a per-spawn flag, buildArgs will
186
- // translate it. (Round 2 Codex finding.)
187
- thinking: false,
189
+ // Requested via env; provider/model may ignore it. The 8s budget still applies.
190
+ thinkingEffort,
188
191
  logPath,
189
192
  }, KIMI_REVIEW_GATE_TIMEOUT_MS, "review_gate.runtime");
190
193
  activeStore.updateRunningJob(job.job_id, observedExecutionFields(result));
191
194
  assertCliResultSuccess(result, "review_gate.runtime");
195
+ // Detection evidence only (runtime/hook-escape.ts): the gate stays fail-open
196
+ // and annotates its decision; the finding is also on the job row.
197
+ hookEscapeNote = describeHookEscapes(result);
192
198
  if (result.sessionId !== undefined && result.sessionId.length > 0) {
193
199
  // length>0 guard matches the other commands (Kimi alpha.4
194
200
  // challenge finding #3 — defense-in-depth, since
@@ -217,7 +223,7 @@ async function executeReviewGate(payload, assistantMessage, context) {
217
223
  final_output_path: artifactPath,
218
224
  error: null,
219
225
  });
220
- return rendered.output;
226
+ return { ...rendered.output, hookEscapeNote };
221
227
  }
222
228
  catch (error) {
223
229
  const classified = classifyManagedCommandFailure(error, "review_gate", job.job_id);
@@ -17,7 +17,7 @@ import { renderManagedJobOutput, writeArtifact } from "../render.js";
17
17
  import { RuntimeError } from "../errors.js";
18
18
  import { resolveRepoIdentity } from "../git.js";
19
19
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
20
- import { captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { 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
  //
@@ -79,7 +79,8 @@ export async function runReview(argv, context, commandType) {
79
79
  command_type: commandType,
80
80
  cwd: context.cwd,
81
81
  model: parsed.model ?? null,
82
- thinking: parsed.thinking ?? null,
82
+ thinking: null,
83
+ thinking_effort: parsed.thinkingEffort ?? null,
83
84
  background: false,
84
85
  budget_ms: parsed.budgetMs ?? null,
85
86
  budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
@@ -113,6 +114,7 @@ export async function runReview(argv, context, commandType) {
113
114
  prompt: previewPrompt,
114
115
  commandLabel: commandType,
115
116
  model: parsed.model,
117
+ thinkingEffort: parsed.thinkingEffort,
116
118
  logPath,
117
119
  signal: handlers.signal,
118
120
  }, job.budget_ms ?? KIMI_REVIEW_PROMPT_TIMEOUT_MS, `${commandType}.prompt`);
@@ -121,6 +123,7 @@ export async function runReview(argv, context, commandType) {
121
123
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, `${commandType}.runtime`);
122
124
  }
123
125
  assertCliResultSuccess(result, `${commandType}.runtime`);
126
+ warnHookEscapes(result, commandType, context.stderr);
124
127
  // Persist whichever session id kimi announced. Review never resumes,
125
128
  // but the id is the only handle for post-hoc replay against
126
129
  // ~/.kimi-code/sessions/.
@@ -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 { parseHookEscapes, renderBudgetSource, withJobStore } from "../job-store.js";
6
6
  import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
7
7
  import { parseJobLookupArgs } from "../parsing.js";
8
8
  export async function runStatus(argv, context) {
@@ -22,6 +22,6 @@ export async function runStatus(argv, context) {
22
22
  throw new RuntimeError("JOB_NOT_FOUND", "No matching job was found for status.", "status.lookup");
23
23
  }
24
24
  const job = await reconcileHistoricalJobProvenance(store, found);
25
- return `${JSON.stringify({ ...job, budget_source: renderBudgetSource(job) }, null, 2)}\n`;
25
+ return `${JSON.stringify({ ...job, thinking_effort_requested: job.thinking_effort ?? null, hook_escapes: parseHookEscapes(job.hook_escapes), budget_source: renderBudgetSource(job) }, null, 2)}\n`;
26
26
  });
27
27
  }
@@ -15,7 +15,7 @@ import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
15
15
  import { MAX_DURATION_MS, parseSwarmArgs } from "../parsing.js";
16
16
  import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
17
17
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
18
- import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
18
+ import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
19
19
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
20
20
  // /kimi:swarm — READ-ONLY parallel fan-out (kimi-code 0.12.0 AgentSwarm tool).
21
21
  //
@@ -208,7 +208,8 @@ async function runReadSwarm(parsed, objective, context) {
208
208
  command_type: "review",
209
209
  cwd: context.cwd,
210
210
  model: parsed.model ?? null,
211
- thinking: parsed.thinking ?? null,
211
+ thinking: null,
212
+ thinking_effort: parsed.thinkingEffort ?? null,
212
213
  background: false,
213
214
  budget_ms: parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS,
214
215
  budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
@@ -308,7 +309,8 @@ async function runWriteSwarm(parsed, objective, context) {
308
309
  command_type: "rescue",
309
310
  cwd: context.cwd,
310
311
  model: parsed.model ?? null,
311
- thinking: parsed.thinking ?? null,
312
+ thinking: null,
313
+ thinking_effort: parsed.thinkingEffort ?? null,
312
314
  background: false,
313
315
  budget_ms: parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS,
314
316
  budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
@@ -493,6 +495,7 @@ writeMode) {
493
495
  // by other labels). Only set in write mode.
494
496
  trustedWorkspaceRoot: writeMode?.workspaceRoot,
495
497
  model: job.model ?? undefined,
498
+ thinkingEffort: job.thinking_effort ?? undefined,
496
499
  logPath: job.stream_log_path,
497
500
  signal: handlers.signal,
498
501
  }, budgetMs, "swarm.prompt");
@@ -501,6 +504,7 @@ writeMode) {
501
504
  throw new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
502
505
  }
503
506
  assertCliResultSuccess(result, "swarm.runtime");
507
+ warnHookEscapes(result, "swarm", context.stderr);
504
508
  if (result.sessionId !== undefined &&
505
509
  result.sessionId.length > 0 &&
506
510
  result.sessionId !== job.kimi_session_id) {
@@ -11,7 +11,7 @@ export function companionHelp(argv) {
11
11
  : 1;
12
12
  if (!hasHelpFlag(argv.slice(argumentStart)))
13
13
  return undefined;
14
- const suffix = " Use --help or -h among the leading flags (before any prompt text or --) to show this usage.";
14
+ const suffix = " Thinking effort is a per-run request, not a guarantee. Use --help or -h among the leading flags (before any prompt text or --) to show this usage.";
15
15
  const usage = (name, positionals, flags, description) => `Usage: companion.sh ${name}${positionals ? ` ${positionals}` : ""}. ${description} Supported flags: ${flags}.${suffix}`;
16
16
  switch (command) {
17
17
  case "--help":
@@ -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)) {
@@ -593,7 +593,7 @@ function validateVersionSensitiveEvents(events, version) {
593
593
  return invalidHookSet(`version-sensitive event ${JSON.stringify(first.event)} requires a verified kimi-code version`, first.entry, first.line);
594
594
  }
595
595
  // Exact schemas reviewed at 2.0.0 (1b89e4b0), 2.0.1 (caf7d4e2),
596
- // 2.0.2 (9d07f634), 2.1.0 (52437299), 2.1.1 (f67e6398) and 2.2.0 (472192c3):
596
+ // 2.0.2 (9d07f634), 2.1.0 (52437299), 2.1.1 (f67e6398), 2.2.0 (472192c3) and 2.3.0 (b5e28fd6):
597
597
  // the additive events and HookDefSchema are unchanged. Require the full
598
598
  // version string AND matching components; no inferred 2.x compatibility.
599
599
  if (version.major === 2 && ((version.minor === 0 && ((version.version === "2.0.0" && version.patch === 0) ||
@@ -601,7 +601,8 @@ function validateVersionSensitiveEvents(events, version) {
601
601
  (version.version === "2.0.2" && version.patch === 2))) ||
602
602
  (version.minor === 1 && ((version.version === "2.1.0" && version.patch === 0) ||
603
603
  (version.version === "2.1.1" && version.patch === 1))) ||
604
- (version.minor === 2 && ((version.version === "2.2.0" && version.patch === 0))))) {
604
+ (version.minor === 2 && ((version.version === "2.2.0" && version.patch === 0))) ||
605
+ (version.minor === 3 && version.version === "2.3.0" && version.patch === 0))) {
605
606
  return { valid: true };
606
607
  }
607
608
  if (version.major !== 0) {
@@ -73,7 +73,7 @@ export class JobStore {
73
73
  if (!tableHasColumn(this.db, "budget_ms")) {
74
74
  this.db.exec(`ALTER TABLE jobs ADD COLUMN budget_ms INTEGER;`);
75
75
  }
76
- for (const column of ["kimi_session_id_source", "budget_source"]) {
76
+ for (const column of ["kimi_session_id_source", "budget_source", "thinking_effort", "hook_escapes"]) {
77
77
  if (!tableHasColumn(this.db, column)) {
78
78
  this.db.exec("ALTER TABLE jobs ADD COLUMN " + column + " TEXT;");
79
79
  }
@@ -172,14 +172,14 @@ export class JobStore {
172
172
  try {
173
173
  this.db.run(`
174
174
  INSERT INTO jobs (
175
- job_id, repo_id, command_type, created_at, updated_at, cwd, model, thinking,
175
+ job_id, repo_id, command_type, created_at, updated_at, cwd, model, thinking, thinking_effort,
176
176
  background, pid, kimi_pid, status, kimi_session_id, kimi_session_id_source, budget_source,
177
177
  operation_kind, intended_engine, observed_engine, kimi_version, system_version,
178
178
  kimi_command, kimi_prefix_args, plan_certification, resumed_from_job_id, safety_profile,
179
179
  agent_profile, prompt_digest, summary, phase, budget_ms, final_output_path, stream_log_path, error
180
180
  )
181
181
  VALUES (
182
- @job_id, @repo_id, @command_type, @created_at, @updated_at, @cwd, @model, @thinking,
182
+ @job_id, @repo_id, @command_type, @created_at, @updated_at, @cwd, @model, @thinking, @thinking_effort,
183
183
  @background, @pid, @kimi_pid, @status, @kimi_session_id, @kimi_session_id_source, @budget_source,
184
184
  @operation_kind, @intended_engine, @observed_engine, @kimi_version, @system_version,
185
185
  @kimi_command, @kimi_prefix_args, @plan_certification, @resumed_from_job_id, @safety_profile,
@@ -309,7 +309,7 @@ export class JobStore {
309
309
  "status", "summary", "phase", "budget_ms", "budget_source", "kimi_session_id_source", "pid", "kimi_pid",
310
310
  "final_output_path", "stream_log_path", "kimi_session_id",
311
311
  "error", "command_type", "prompt_digest", "repo_id",
312
- "cwd", "model", "thinking", "background", "agent_profile",
312
+ "cwd", "model", "thinking", "thinking_effort", "hook_escapes", "background", "agent_profile",
313
313
  "operation_kind", "intended_engine", "observed_engine", "kimi_version",
314
314
  "system_version", "kimi_command", "kimi_prefix_args", "plan_certification",
315
315
  "resumed_from_job_id", "safety_profile",
@@ -349,6 +349,7 @@ function hydrateRow(row) {
349
349
  function serializeRecord(record) {
350
350
  return {
351
351
  ...record,
352
+ thinking_effort: record.thinking_effort ?? null,
352
353
  phase: record.phase ?? null,
353
354
  budget_ms: record.budget_ms ?? null,
354
355
  budget_source: record.budget_source ?? null,
@@ -591,6 +592,18 @@ export function withJobStoreSync(paths, fn) {
591
592
  }
592
593
  }
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
+ }
594
607
  export function renderBudgetSource(job) {
595
608
  return job.budget_source ?? (job.budget_ms === null ? "default" : "unknown");
596
609
  }
@@ -39,6 +39,7 @@ export const NATIVE_V2_CERTIFIED_VERSIONS = Object.freeze([
39
39
  "2.1.0",
40
40
  "2.1.1",
41
41
  "2.2.0",
42
+ "2.3.0",
42
43
  ]);
43
44
  /** Per-operation native-v2 certification. One green operation never certifies another. */
44
45
  export const NATIVE_V2_CERTIFIED = new Map([
@@ -206,6 +207,8 @@ export function observedExecutionFields(result) {
206
207
  return {
207
208
  observed_engine: result.observedEngine,
208
209
  system_version: result.systemVersion ?? null,
210
+ // Detection evidence from runtime/hook-escape.ts, JSON array or NULL.
211
+ hook_escapes: result.hookEscapes && result.hookEscapes.length > 0 ? JSON.stringify(result.hookEscapes) : null,
209
212
  };
210
213
  }
211
214
  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.