kimi-plugin-cc 2.0.7

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 (190) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +16 -0
  3. package/.claude-plugin/plugin.json +32 -0
  4. package/CHANGELOG.md +1096 -0
  5. package/LICENSE +201 -0
  6. package/README.md +772 -0
  7. package/agents/kimi-ask.md +82 -0
  8. package/agents/kimi-challenge.md +82 -0
  9. package/agents/kimi-pursue.md +91 -0
  10. package/agents/kimi-rescue.md +85 -0
  11. package/agents/kimi-review.md +82 -0
  12. package/agents/kimi-swarm-write.md +92 -0
  13. package/agents/kimi-swarm.md +83 -0
  14. package/commands/README.md +16 -0
  15. package/commands/ask.md +32 -0
  16. package/commands/cancel.md +17 -0
  17. package/commands/challenge.md +29 -0
  18. package/commands/pursue.md +37 -0
  19. package/commands/replay.md +13 -0
  20. package/commands/rescue.md +32 -0
  21. package/commands/result.md +13 -0
  22. package/commands/review.md +29 -0
  23. package/commands/setup.md +23 -0
  24. package/commands/status.md +13 -0
  25. package/commands/swarm.md +38 -0
  26. package/dist/background-spawn.js +224 -0
  27. package/dist/cli-cancellation.js +43 -0
  28. package/dist/cli-client.js +845 -0
  29. package/dist/commands/ask.js +327 -0
  30. package/dist/commands/cancel.js +178 -0
  31. package/dist/commands/cli-helpers.js +112 -0
  32. package/dist/commands/models.js +140 -0
  33. package/dist/commands/not-implemented.js +4 -0
  34. package/dist/commands/pursue.js +311 -0
  35. package/dist/commands/registry.js +87 -0
  36. package/dist/commands/repair-sessions.js +159 -0
  37. package/dist/commands/replay.js +123 -0
  38. package/dist/commands/rescue.js +333 -0
  39. package/dist/commands/result.js +58 -0
  40. package/dist/commands/review-gate.js +393 -0
  41. package/dist/commands/review.js +218 -0
  42. package/dist/commands/setup.js +1132 -0
  43. package/dist/commands/status.js +27 -0
  44. package/dist/commands/swarm.js +576 -0
  45. package/dist/commands/worker.js +31 -0
  46. package/dist/companion.js +114 -0
  47. package/dist/config.js +22 -0
  48. package/dist/errors.js +40 -0
  49. package/dist/git.js +215 -0
  50. package/dist/hooks/approval-hook.js +112 -0
  51. package/dist/hooks/approval-policy.js +254 -0
  52. package/dist/hooks/config-safety.js +637 -0
  53. package/dist/hooks/install-paths.js +504 -0
  54. package/dist/hooks/install.js +281 -0
  55. package/dist/hooks/managed-block.js +745 -0
  56. package/dist/hooks/review-gate-stop.js +19 -0
  57. package/dist/job-store.js +559 -0
  58. package/dist/jobs.js +202 -0
  59. package/dist/kimi-command.js +72 -0
  60. package/dist/kimi-engine.js +624 -0
  61. package/dist/kimi-errors.js +220 -0
  62. package/dist/kimi-home.js +9 -0
  63. package/dist/kimi-timeouts.js +68 -0
  64. package/dist/kimi-version-probe.js +1410 -0
  65. package/dist/logging.js +22 -0
  66. package/dist/native-v2-preflight.js +417 -0
  67. package/dist/package.json +1 -0
  68. package/dist/parsing.js +553 -0
  69. package/dist/paths.js +27 -0
  70. package/dist/plugin-data.js +98 -0
  71. package/dist/process-tree.js +448 -0
  72. package/dist/render.js +159 -0
  73. package/dist/rescue-approval.js +951 -0
  74. package/dist/schemas/review-gate-output.js +43 -0
  75. package/dist/session-title.js +293 -0
  76. package/dist/stream-json.js +498 -0
  77. package/dist/types.js +1 -0
  78. package/dist/vendor/shell-quote/LICENSE +24 -0
  79. package/dist/vendor/shell-quote/index.js +1 -0
  80. package/dist/vendor/shell-quote/parse.js +229 -0
  81. package/dist/vendor/smol-toml/LICENSE +24 -0
  82. package/dist/vendor/smol-toml/date.js +127 -0
  83. package/dist/vendor/smol-toml/error.js +63 -0
  84. package/dist/vendor/smol-toml/extract.js +100 -0
  85. package/dist/vendor/smol-toml/parse.js +148 -0
  86. package/dist/vendor/smol-toml/primitive.js +179 -0
  87. package/dist/vendor/smol-toml/struct.js +184 -0
  88. package/dist/vendor/smol-toml/util.js +113 -0
  89. package/dist/version.js +7 -0
  90. package/docs/migration.md +215 -0
  91. package/docs/models.md +82 -0
  92. package/docs/registry-releases.md +70 -0
  93. package/docs/safety.md +318 -0
  94. package/docs/session-visibility.md +32 -0
  95. package/hooks/README.md +9 -0
  96. package/hooks/hooks.json +16 -0
  97. package/package.json +67 -0
  98. package/plugins/kimi-codex/.codex-plugin/plugin.json +39 -0
  99. package/plugins/kimi-codex/dist/background-spawn.js +224 -0
  100. package/plugins/kimi-codex/dist/cli-cancellation.js +43 -0
  101. package/plugins/kimi-codex/dist/cli-client.js +845 -0
  102. package/plugins/kimi-codex/dist/commands/ask.js +327 -0
  103. package/plugins/kimi-codex/dist/commands/cancel.js +178 -0
  104. package/plugins/kimi-codex/dist/commands/cli-helpers.js +112 -0
  105. package/plugins/kimi-codex/dist/commands/models.js +140 -0
  106. package/plugins/kimi-codex/dist/commands/not-implemented.js +4 -0
  107. package/plugins/kimi-codex/dist/commands/pursue.js +311 -0
  108. package/plugins/kimi-codex/dist/commands/registry.js +87 -0
  109. package/plugins/kimi-codex/dist/commands/repair-sessions.js +159 -0
  110. package/plugins/kimi-codex/dist/commands/replay.js +123 -0
  111. package/plugins/kimi-codex/dist/commands/rescue.js +333 -0
  112. package/plugins/kimi-codex/dist/commands/result.js +58 -0
  113. package/plugins/kimi-codex/dist/commands/review-gate.js +393 -0
  114. package/plugins/kimi-codex/dist/commands/review.js +218 -0
  115. package/plugins/kimi-codex/dist/commands/setup.js +1132 -0
  116. package/plugins/kimi-codex/dist/commands/status.js +27 -0
  117. package/plugins/kimi-codex/dist/commands/swarm.js +576 -0
  118. package/plugins/kimi-codex/dist/commands/worker.js +31 -0
  119. package/plugins/kimi-codex/dist/companion.js +114 -0
  120. package/plugins/kimi-codex/dist/config.js +22 -0
  121. package/plugins/kimi-codex/dist/errors.js +40 -0
  122. package/plugins/kimi-codex/dist/git.js +215 -0
  123. package/plugins/kimi-codex/dist/hooks/approval-hook.js +112 -0
  124. package/plugins/kimi-codex/dist/hooks/approval-policy.js +254 -0
  125. package/plugins/kimi-codex/dist/hooks/config-safety.js +637 -0
  126. package/plugins/kimi-codex/dist/hooks/install-paths.js +504 -0
  127. package/plugins/kimi-codex/dist/hooks/install.js +281 -0
  128. package/plugins/kimi-codex/dist/hooks/managed-block.js +745 -0
  129. package/plugins/kimi-codex/dist/hooks/review-gate-stop.js +19 -0
  130. package/plugins/kimi-codex/dist/job-store.js +559 -0
  131. package/plugins/kimi-codex/dist/jobs.js +202 -0
  132. package/plugins/kimi-codex/dist/kimi-command.js +72 -0
  133. package/plugins/kimi-codex/dist/kimi-engine.js +624 -0
  134. package/plugins/kimi-codex/dist/kimi-errors.js +220 -0
  135. package/plugins/kimi-codex/dist/kimi-home.js +9 -0
  136. package/plugins/kimi-codex/dist/kimi-timeouts.js +68 -0
  137. package/plugins/kimi-codex/dist/kimi-version-probe.js +1410 -0
  138. package/plugins/kimi-codex/dist/logging.js +22 -0
  139. package/plugins/kimi-codex/dist/native-v2-preflight.js +417 -0
  140. package/plugins/kimi-codex/dist/package.json +1 -0
  141. package/plugins/kimi-codex/dist/parsing.js +553 -0
  142. package/plugins/kimi-codex/dist/paths.js +27 -0
  143. package/plugins/kimi-codex/dist/plugin-data.js +98 -0
  144. package/plugins/kimi-codex/dist/process-tree.js +448 -0
  145. package/plugins/kimi-codex/dist/render.js +159 -0
  146. package/plugins/kimi-codex/dist/rescue-approval.js +951 -0
  147. package/plugins/kimi-codex/dist/schemas/review-gate-output.js +43 -0
  148. package/plugins/kimi-codex/dist/session-title.js +293 -0
  149. package/plugins/kimi-codex/dist/stream-json.js +498 -0
  150. package/plugins/kimi-codex/dist/types.js +1 -0
  151. package/plugins/kimi-codex/dist/vendor/shell-quote/LICENSE +24 -0
  152. package/plugins/kimi-codex/dist/vendor/shell-quote/index.js +1 -0
  153. package/plugins/kimi-codex/dist/vendor/shell-quote/parse.js +229 -0
  154. package/plugins/kimi-codex/dist/vendor/smol-toml/LICENSE +24 -0
  155. package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +127 -0
  156. package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +63 -0
  157. package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +100 -0
  158. package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +148 -0
  159. package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +179 -0
  160. package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +184 -0
  161. package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +113 -0
  162. package/plugins/kimi-codex/dist/version.js +7 -0
  163. package/plugins/kimi-codex/scripts/companion.sh +37 -0
  164. package/plugins/kimi-codex/scripts/review-gate-hook.sh +32 -0
  165. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +31 -0
  166. package/plugins/kimi-codex/skills/kimi-ask/agents/openai.yaml +6 -0
  167. package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +25 -0
  168. package/plugins/kimi-codex/skills/kimi-cancel/agents/openai.yaml +6 -0
  169. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +31 -0
  170. package/plugins/kimi-codex/skills/kimi-challenge/agents/openai.yaml +6 -0
  171. package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +30 -0
  172. package/plugins/kimi-codex/skills/kimi-pursue/agents/openai.yaml +6 -0
  173. package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +25 -0
  174. package/plugins/kimi-codex/skills/kimi-replay/agents/openai.yaml +6 -0
  175. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +30 -0
  176. package/plugins/kimi-codex/skills/kimi-rescue/agents/openai.yaml +6 -0
  177. package/plugins/kimi-codex/skills/kimi-result/SKILL.md +25 -0
  178. package/plugins/kimi-codex/skills/kimi-result/agents/openai.yaml +6 -0
  179. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +31 -0
  180. package/plugins/kimi-codex/skills/kimi-review/agents/openai.yaml +6 -0
  181. package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +32 -0
  182. package/plugins/kimi-codex/skills/kimi-setup/agents/openai.yaml +6 -0
  183. package/plugins/kimi-codex/skills/kimi-status/SKILL.md +25 -0
  184. package/plugins/kimi-codex/skills/kimi-status/agents/openai.yaml +6 -0
  185. package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +31 -0
  186. package/plugins/kimi-codex/skills/kimi-swarm/agents/openai.yaml +6 -0
  187. package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +32 -0
  188. package/plugins/kimi-codex/skills/kimi-swarm-write/agents/openai.yaml +6 -0
  189. package/scripts/companion.sh +37 -0
  190. package/scripts/review-gate-hook.sh +32 -0
@@ -0,0 +1,393 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { runCliPromptWithBudget } from "../cli-client.js";
6
+ import { observedExecutionFields, persistedExecutionPlanFields, prepareKimiExecutionPlan, } from "../kimi-engine.js";
7
+ import { readPluginConfig } from "../config.js";
8
+ import { getManagedCommandConfig } from "./registry.js";
9
+ import { RuntimeError, formatError } from "../errors.js";
10
+ import { resolveRepoIdentity } from "../git.js";
11
+ import { digestPrompt, markJobFailed } from "../jobs.js";
12
+ import { JobStore } from "../job-store.js";
13
+ import { classifyManagedCommandFailure, summarizeKimiAvailabilityWarning } from "../kimi-errors.js";
14
+ import { KIMI_REVIEW_GATE_TIMEOUT_MS, isTimeoutCode } from "../kimi-timeouts.js";
15
+ import { writeInvocationLogHeader } from "../logging.js";
16
+ import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
17
+ import { renderManagedJobOutput, writeArtifact, } from "../render.js";
18
+ import { maybeWarnHookMissing, verifyHookInstalled } from "../hooks/install.js";
19
+ import { resolveKimiHome } from "../kimi-home.js";
20
+ import { assertCliResultSuccess, reassembleProseFromRecords } from "./cli-helpers.js";
21
+ const REVIEW_GATE_AGENT_PROFILE_PLACEHOLDER = "<cli-client>";
22
+ // v1.0 cutover note (PR 2):
23
+ //
24
+ // review_gate is the only command that still parses Kimi's stdout
25
+ // (JSON allow/block decision). The transport switched from
26
+ // `kimi --wire` to `kimi -p --output-format stream-json`; the parser
27
+ // stayed.
28
+ //
29
+ // No cancellation handler — the 8s timeout is the only governor.
30
+ // review_gate is invoked from Claude Code's Stop hook and runs
31
+ // strictly foreground-synchronous.
32
+ export async function runReviewGateStopHook(payload, context) {
33
+ if (payload.hook_event_name !== "Stop") {
34
+ throw new RuntimeError("INVALID_HOOK_EVENT", `review gate hook expected Stop input, received ${payload.hook_event_name}.`, "review_gate.hook");
35
+ }
36
+ let paths;
37
+ try {
38
+ paths = resolvePluginPaths(context.env);
39
+ }
40
+ catch (error) {
41
+ if (error instanceof RuntimeError && ["PLUGIN_DATA_CONFLICT", "INVALID_PLUGIN_DATA", "MISSING_PLUGIN_DATA"].includes(error.code)) {
42
+ return reviewGateSkipped(formatError(error));
43
+ }
44
+ throw error;
45
+ }
46
+ await ensurePluginPaths(paths);
47
+ const config = await readPluginConfig(paths);
48
+ if (!config.reviewGateEnabled) {
49
+ return reviewGateSkipped("disabled");
50
+ }
51
+ if (payload.stop_hook_active) {
52
+ return reviewGateSkipped("stop hook already active");
53
+ }
54
+ const cwd = payload.cwd || context.cwd;
55
+ // Resolve KIMI_CODE_HOME ONCE, against the SAME cwd the child will be
56
+ // spawned with, and use that env for hook verification, the execution plan
57
+ // and the spawn. The Stop-hook entry script runs from the plugin root, so a
58
+ // relative KIMI_CODE_HOME would otherwise be verified under the plugin root
59
+ // while the child (cwd = payload.cwd) read a different home.
60
+ context = {
61
+ ...context,
62
+ env: { ...context.env, KIMI_CODE_HOME: resolveKimiHome(context.env, cwd) },
63
+ };
64
+ const assistantMessage = extractText(payload.last_assistant_message) ??
65
+ (await extractLastAssistantMessage(payload.transcript_path));
66
+ if (!assistantMessage) {
67
+ return reviewGateSkipped("no assistant message");
68
+ }
69
+ if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK !== "1") {
70
+ const installStatus = await verifyHookInstalled(context.env);
71
+ if (!installStatus.installed) {
72
+ maybeWarnHookMissing(installStatus, "review_gate", context.stderr);
73
+ return reviewGateSkipped([
74
+ "canonical PreToolUse hook is missing or invalid",
75
+ `(${installStatus.reason ?? "unknown"})`,
76
+ "allowing stop without invoking Kimi",
77
+ ].join(" "));
78
+ }
79
+ }
80
+ try {
81
+ const output = await executeReviewGate({
82
+ ...payload,
83
+ cwd,
84
+ }, assistantMessage, context);
85
+ if (output.decision === "BLOCK" && output.confidence === "high") {
86
+ return {
87
+ decision: "block",
88
+ reason: buildBlockReason(output),
89
+ };
90
+ }
91
+ if (output.decision === "BLOCK") {
92
+ return {
93
+ systemMessage: `Kimi review gate noted concerns but allowed stop: ${output.summary}`,
94
+ };
95
+ }
96
+ return {};
97
+ }
98
+ catch (error) {
99
+ return {
100
+ systemMessage: buildWarningMessage(error),
101
+ };
102
+ }
103
+ }
104
+ function reviewGateSkipped(reason) {
105
+ return { systemMessage: `review-gate skipped: ${reason}` };
106
+ }
107
+ async function executeReviewGate(payload, assistantMessage, context) {
108
+ const paths = resolvePluginPaths(context.env);
109
+ await ensurePluginPaths(paths);
110
+ const repoIdentity = await resolveRepoIdentity(payload.cwd);
111
+ let store;
112
+ try {
113
+ store = new JobStore(paths);
114
+ const userRequest = extractText(payload.last_user_message ?? payload.user_request ?? payload.prompt) ??
115
+ (await extractLastUserMessage(payload.transcript_path));
116
+ const jobId = randomUUID();
117
+ const logPath = path.join(paths.logsDir, `review-gate-${jobId}.jsonl`);
118
+ const prompt = buildReviewGatePrompt({
119
+ assistantMessage,
120
+ userRequest,
121
+ cwd: payload.cwd,
122
+ repoRoot: repoIdentity.repoRoot,
123
+ });
124
+ const configuredModel = context.env.KIMI_PLUGIN_CC_REVIEW_GATE_MODEL;
125
+ const model = configuredModel?.trim() ? configuredModel : undefined;
126
+ const executionPlan = await prepareKimiExecutionPlan({
127
+ operationKind: "review_gate",
128
+ cwd: payload.cwd,
129
+ env: context.env,
130
+ });
131
+ // Header-before-job-row mirrors v0.4's reordering (the comment
132
+ // chain there explains why). If the disk-bound writeInvocationLogHeader
133
+ // throws (full disk, permission), we'd otherwise leave an orphan
134
+ // running row with pid=null,kimi_pid=null that the sweeper can't
135
+ // see. Keep the header first.
136
+ await writeInvocationLogHeader(logPath, {
137
+ commandType: "review_gate",
138
+ // kimi-code assigns the session id; we don't know it until the
139
+ // call returns. The header carries a "(pending)" placeholder so
140
+ // post-mortem log diffing still finds the right row.
141
+ kimiSessionId: "(pending)",
142
+ cwd: payload.cwd,
143
+ });
144
+ const job = store.createJob({
145
+ job_id: jobId,
146
+ repo_id: repoIdentity.repoId,
147
+ command_type: "review_gate",
148
+ cwd: payload.cwd,
149
+ model: model ?? null,
150
+ thinking: false,
151
+ background: false,
152
+ pid: null,
153
+ kimi_pid: null,
154
+ status: "running",
155
+ kimi_session_id: null,
156
+ ...persistedExecutionPlanFields(executionPlan),
157
+ agent_profile: REVIEW_GATE_AGENT_PROFILE_PLACEHOLDER,
158
+ prompt_digest: digestPrompt(prompt),
159
+ summary: "Running review gate.",
160
+ final_output_path: null,
161
+ stream_log_path: logPath,
162
+ error: null,
163
+ });
164
+ try {
165
+ const activeStore = store;
166
+ // runCliPromptWithBudget ties the 8 s timeout to an AbortController
167
+ // that kills the kimi child on expiry — review_gate runs inside
168
+ // Claude Code's Stop hook, so a runaway kimi after timeout would
169
+ // hold model tokens with no way for /kimi:cancel to reach it
170
+ // (the SQLite row's kimi_pid is null). See reports/17 and 18.
171
+ const result = await runCliPromptWithBudget({
172
+ cwd: payload.cwd,
173
+ env: context.env,
174
+ command: executionPlan.command,
175
+ prefixArgs: [...executionPlan.prefixArgs],
176
+ executionPlan,
177
+ prompt,
178
+ commandLabel: "review_gate",
179
+ model,
180
+ // Intent: thinking-off so the 8s Stop-hook budget is achievable.
181
+ // Mechanism: currently advisory — kimi-code 0.1.1 has no CLI
182
+ // flag for this; control is config-based (default_thinking /
183
+ // [thinking].mode). The field carries intent through the
184
+ // contract; when upstream adds a per-spawn flag, buildArgs will
185
+ // translate it. (Round 2 Codex finding.)
186
+ thinking: false,
187
+ logPath,
188
+ }, KIMI_REVIEW_GATE_TIMEOUT_MS, "review_gate.runtime");
189
+ activeStore.updateRunningJob(job.job_id, observedExecutionFields(result));
190
+ assertCliResultSuccess(result, "review_gate.runtime");
191
+ if (result.sessionId !== undefined && result.sessionId.length > 0) {
192
+ // length>0 guard matches the other commands (Kimi alpha.4
193
+ // challenge finding #3 — defense-in-depth, since
194
+ // extractSessionIdFromStderr returns undefined on no-match and
195
+ // can't synthesize empty strings today, but the guard locks the
196
+ // SQLite contract independent of upstream behavior).
197
+ activeStore.updateRunningJob(job.job_id, {
198
+ kimi_session_id: result.sessionId,
199
+ });
200
+ }
201
+ const finalText = reassembleProseFromRecords(result.records);
202
+ const rendered = renderManagedJobOutput(job, finalText);
203
+ // Cancel-vs-completed race: /kimi:cancel pre-marks the row as
204
+ // `cancelled` and SIGTERMs the worker. If kimi managed to return
205
+ // in the SIGTERM→exit window before our wire-equivalent picked
206
+ // up the close, we'd overwrite the cancellation artifact and
207
+ // confuse the hook's allow/block decision. Check the persisted
208
+ // status before committing the success path.
209
+ const persisted = activeStore.getJob(job.job_id);
210
+ if (persisted && persisted.status !== "running") {
211
+ throw new RuntimeError(getManagedCommandConfig("review_gate").cancellation.errorCodes.cancelled, "review_gate cancelled before completion artifact was written.", "review_gate.runtime");
212
+ }
213
+ const artifactPath = await writeArtifact(paths, job, rendered.rendered);
214
+ activeStore.markCompleted(job.job_id, {
215
+ summary: rendered.summary,
216
+ final_output_path: artifactPath,
217
+ error: null,
218
+ });
219
+ return rendered.output;
220
+ }
221
+ catch (error) {
222
+ const classified = classifyManagedCommandFailure(error, "review_gate", job.job_id);
223
+ const summary = isTimeoutError(classified) ? "Review gate timed out." : "Review gate failed.";
224
+ await markJobFailed(store, paths, job, classified, summary);
225
+ throw classified;
226
+ }
227
+ }
228
+ finally {
229
+ store?.close();
230
+ }
231
+ }
232
+ function buildReviewGatePrompt(input) {
233
+ return [
234
+ "Review the just-finished Claude response and decide whether Claude should be allowed to stop.",
235
+ "Block only for concrete, high-confidence problems that require an immediate corrective follow-up turn.",
236
+ "Do not block for style nits, optional improvements, or low-confidence concerns.",
237
+ "Return exactly one JSON object with no prose wrapper and no code fences.",
238
+ "",
239
+ "Required output schema:",
240
+ '{"decision":"ALLOW|BLOCK","confidence":"low|medium|high","summary":"string","issues":[{"title":"string","body":"string","severity":"low|medium|high"}]}',
241
+ "",
242
+ `Workspace cwd: ${input.cwd}`,
243
+ `Repository root: ${input.repoRoot}`,
244
+ "",
245
+ "User request:",
246
+ input.userRequest ?? "(unavailable from transcript)",
247
+ "",
248
+ "Claude response under review:",
249
+ input.assistantMessage,
250
+ ].join("\n");
251
+ }
252
+ function buildBlockReason(output) {
253
+ const issueLines = output.issues
254
+ .slice(0, 5)
255
+ .map((issue) => `- [${issue.severity}] ${issue.title}: ${issue.body}`);
256
+ return [
257
+ "Kimi review gate blocked stop. Revise the previous response before ending the turn.",
258
+ `Summary: ${output.summary}`,
259
+ ...(issueLines.length > 0 ? ["Issues:", ...issueLines] : []),
260
+ ].join("\n");
261
+ }
262
+ function isTimeoutError(error) {
263
+ if (!(error instanceof RuntimeError)) {
264
+ return false;
265
+ }
266
+ return (isTimeoutCode(error.code) ||
267
+ error.code === "REVIEW_GATE_KIMI_TIMEOUT" ||
268
+ error.code === "REVIEW_GATE_KIMI_STARTUP_TIMEOUT" ||
269
+ error.code === "REVIEW_GATE_KIMI_INITIALIZE_TIMEOUT" ||
270
+ error.code === "REVIEW_GATE_KIMI_RESPONSE_TIMEOUT");
271
+ }
272
+ function buildWarningMessage(error) {
273
+ if (isTimeoutError(error)) {
274
+ return "Kimi review gate timed out after 8s; allowing stop.";
275
+ }
276
+ if (error instanceof RuntimeError) {
277
+ if (error.code === "REVIEW_GATE_PARSE_FAILED" ||
278
+ error.code === "MISSING_TURN_END" ||
279
+ error.code === "TURN_INTERRUPTED") {
280
+ return "Kimi review gate returned malformed output; allowing stop.";
281
+ }
282
+ if (error.code === "MAX_STEPS_REACHED") {
283
+ return "Kimi review gate exhausted its step budget; allowing stop.";
284
+ }
285
+ // v0.4 codes from the deleted wire/* path. Kept here so a stale
286
+ // job row or in-flight test that still produces them maps to the
287
+ // same allowing-stop language. The v1.0 equivalents
288
+ // (CLI_NONZERO_EXIT, CLI_PROCESS_ERROR, CLI_ABORTED) get the same
289
+ // user-visible warning.
290
+ if (error.code === "WIRE_SPAWN_FAILED" ||
291
+ error.code === "WIRE_PROCESS_EXITED" ||
292
+ error.code === "WIRE_REQUEST_FAILED" ||
293
+ error.code === "CLI_SPAWN_FAILED" ||
294
+ error.code === "CLI_PROCESS_ERROR" ||
295
+ error.code === "CLI_NONZERO_EXIT" ||
296
+ error.code === "CLI_ABORTED" ||
297
+ error.code === "CLI_NO_SESSION_ID") {
298
+ return "Kimi review gate is unavailable in this environment; allowing stop.";
299
+ }
300
+ }
301
+ const warning = summarizeKimiAvailabilityWarning(error, "review_gate");
302
+ if (warning) {
303
+ return warning;
304
+ }
305
+ return "Kimi review gate failed unexpectedly; allowing stop.";
306
+ }
307
+ async function extractLastUserMessage(transcriptPath) {
308
+ return scanTranscriptForLastRoleText(transcriptPath, "user");
309
+ }
310
+ async function extractLastAssistantMessage(transcriptPath) {
311
+ return scanTranscriptForLastRoleText(transcriptPath, "assistant");
312
+ }
313
+ async function scanTranscriptForLastRoleText(transcriptPath, role) {
314
+ if (!transcriptPath) {
315
+ return null;
316
+ }
317
+ try {
318
+ const raw = await readFile(expandHomeDir(transcriptPath), "utf8");
319
+ let lastMatch = null;
320
+ for (const line of raw.split("\n")) {
321
+ const trimmed = line.trim();
322
+ if (!trimmed) {
323
+ continue;
324
+ }
325
+ let entry;
326
+ try {
327
+ entry = JSON.parse(trimmed);
328
+ }
329
+ catch {
330
+ continue;
331
+ }
332
+ const extracted = extractRoleText(entry, role);
333
+ if (extracted) {
334
+ lastMatch = extracted;
335
+ }
336
+ }
337
+ return lastMatch;
338
+ }
339
+ catch {
340
+ return null;
341
+ }
342
+ }
343
+ function extractRoleText(entry, role) {
344
+ if (!isObject(entry)) {
345
+ return null;
346
+ }
347
+ if (role === "user" &&
348
+ entry.hook_event_name === "UserPromptSubmit" &&
349
+ typeof entry.prompt === "string") {
350
+ return entry.prompt.trim() || null;
351
+ }
352
+ if (entry.role === role) {
353
+ return extractText(entry.content ?? entry.message ?? entry.text ?? entry.prompt);
354
+ }
355
+ if (entry.type === role) {
356
+ return extractText(entry.content ?? entry.message ?? entry.text ?? entry.prompt);
357
+ }
358
+ if (isObject(entry.message) && entry.message.role === role) {
359
+ return extractText(entry.message.content ?? entry.message.text);
360
+ }
361
+ return null;
362
+ }
363
+ function extractText(value) {
364
+ if (typeof value === "string") {
365
+ const trimmed = value.trim();
366
+ return trimmed.length > 0 ? trimmed : null;
367
+ }
368
+ if (Array.isArray(value)) {
369
+ const parts = value
370
+ .map((part) => extractText(part))
371
+ .filter((part) => Boolean(part));
372
+ return parts.length > 0 ? parts.join("\n") : null;
373
+ }
374
+ if (isObject(value)) {
375
+ if (value.type === "text" && typeof value.text === "string") {
376
+ return value.text.trim() || null;
377
+ }
378
+ return extractText(value.text ?? value.content ?? value.message);
379
+ }
380
+ return null;
381
+ }
382
+ function isObject(value) {
383
+ return typeof value === "object" && value !== null && !Array.isArray(value);
384
+ }
385
+ function expandHomeDir(filePath) {
386
+ if (filePath === "~") {
387
+ return os.homedir();
388
+ }
389
+ if (filePath.startsWith("~/")) {
390
+ return path.join(os.homedir(), filePath.slice(2));
391
+ }
392
+ return filePath;
393
+ }
@@ -0,0 +1,218 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { createCliCancellationHandlers } from "../cli-cancellation.js";
5
+ import { runCliPromptWithBudget } from "../cli-client.js";
6
+ import { observedExecutionFields, persistedExecutionPlanFields, prepareKimiExecutionPlan, } from "../kimi-engine.js";
7
+ import { getManagedCommandConfig } from "./registry.js";
8
+ import { collectReviewContext } from "../git.js";
9
+ import { digestPrompt, markJobCancelled, markJobFailed } from "../jobs.js";
10
+ import { JobStore } from "../job-store.js";
11
+ import { classifyManagedCommandFailure } from "../kimi-errors.js";
12
+ import { KIMI_REVIEW_PROMPT_TIMEOUT_MS } from "../kimi-timeouts.js";
13
+ import { writeInvocationLogHeader } from "../logging.js";
14
+ import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
15
+ import { parseReviewArgs } from "../parsing.js";
16
+ import { renderManagedJobOutput, writeArtifact } from "../render.js";
17
+ import { RuntimeError } from "../errors.js";
18
+ import { resolveRepoIdentity } from "../git.js";
19
+ import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
20
+ import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
21
+ import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
22
+ // v1.0 cutover note (PR 2):
23
+ //
24
+ // Replaced the v0.4 wire client + initialize + prompt sequence with a
25
+ // single `runCliPrompt` call against `kimi -p --output-format
26
+ // stream-json`. The PreToolUse hook (installed via /kimi:setup) reads
27
+ // `KIMI_PLUGIN_CC_CMD=review` (or `=challenge`) and denies anything
28
+ // but Read/Grep/Glob — so the read-only contract that v0.4 enforced
29
+ // via WireClient.approvalPolicy is now enforced out-of-band by the
30
+ // hook. The command no longer needs to wire its own approval policy.
31
+ //
32
+ // What stays:
33
+ //
34
+ // - SQLite job store, prompt digest, artifact rendering, withTimeout
35
+ // for the prompt phase.
36
+ // - Cancellation: still SIGTERM/SIGINT-driven, but via AbortController
37
+ // instead of WireClient.cancel().
38
+ // - Job error classification via classifyManagedCommandFailure.
39
+ //
40
+ // What's gone:
41
+ //
42
+ // - announceSessionTitle — vis-server has no PATCH endpoint; the
43
+ // kimi-web title cannot be written from the plugin in v1.0.
44
+ // - agent_profile — kimi-code doesn't load YAML agent profiles. The
45
+ // SQLite column stays for backward compatibility; we write a
46
+ // placeholder "<cli-client>" until PR 4 makes it nullable.
47
+ // - KIMI_INITIALIZE_TIMEOUT_MS / KIMI_START_TIMEOUT_MS — there's no
48
+ // separate initialize/start phase under -p mode.
49
+ const REVIEW_AGENT_PROFILE_PLACEHOLDER = "<cli-client>";
50
+ export async function runReview(argv, context, commandType) {
51
+ const parsed = parseReviewArgs(argv, commandType);
52
+ if (parsed.background || parsed.wait) {
53
+ throw new RuntimeError("INVALID_FLAGS", `${commandType} does not support --background or --wait in v1; review runs foreground-synchronously.`, `${commandType}.parse`);
54
+ }
55
+ if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK !== "1") {
56
+ await requireReadOnlyHookInstalled(context, commandType);
57
+ }
58
+ const reviewContext = await collectReviewContext(context.cwd, parsed.base);
59
+ const paths = resolvePluginPaths(context.env);
60
+ await ensurePluginPaths(paths);
61
+ const repoIdentity = await resolveRepoIdentity(context.cwd);
62
+ const executionPlan = await prepareKimiExecutionPlan({
63
+ operationKind: commandType,
64
+ cwd: context.cwd,
65
+ env: context.env,
66
+ });
67
+ const store = new JobStore(paths);
68
+ const jobId = randomUUID();
69
+ // kimi-code mints the actual session id and announces it on stderr;
70
+ // we leave the row's kimi_session_id NULL until the call returns to
71
+ // avoid storing a fictional id that no on-disk session matches.
72
+ // (See PR 2 review feedback in reports/17-pr2-claude-review.md.)
73
+ const reviewSessionId = null;
74
+ const logPath = path.join(paths.logsDir, `${commandType}-${jobId}.jsonl`);
75
+ const previewPrompt = buildReviewPrompt(commandType, reviewContext, parsed.focus);
76
+ const job = store.createJob({
77
+ job_id: jobId,
78
+ repo_id: repoIdentity.repoId,
79
+ command_type: commandType,
80
+ cwd: context.cwd,
81
+ model: parsed.model ?? null,
82
+ thinking: parsed.thinking ?? null,
83
+ background: false,
84
+ pid: process.pid,
85
+ kimi_pid: null,
86
+ status: "running",
87
+ kimi_session_id: reviewSessionId,
88
+ ...persistedExecutionPlanFields(executionPlan),
89
+ agent_profile: REVIEW_AGENT_PROFILE_PLACEHOLDER,
90
+ prompt_digest: digestPrompt(previewPrompt),
91
+ summary: `Running ${commandType}.`,
92
+ final_output_path: null,
93
+ stream_log_path: logPath,
94
+ error: null,
95
+ });
96
+ await writeInvocationLogHeader(logPath, {
97
+ commandType,
98
+ kimiSessionId: reviewSessionId ?? "(pending)",
99
+ cwd: context.cwd,
100
+ });
101
+ const reviewConfig = getManagedCommandConfig(commandType);
102
+ const cancel = reviewConfig.cancellation;
103
+ const handlers = createCliCancellationHandlers();
104
+ try {
105
+ const result = await runCliPromptWithBudget({
106
+ cwd: context.cwd,
107
+ env: context.env,
108
+ command: executionPlan.command,
109
+ prefixArgs: [...executionPlan.prefixArgs],
110
+ executionPlan,
111
+ prompt: previewPrompt,
112
+ commandLabel: commandType,
113
+ model: parsed.model,
114
+ logPath,
115
+ signal: handlers.signal,
116
+ }, KIMI_REVIEW_PROMPT_TIMEOUT_MS, `${commandType}.prompt`);
117
+ store.updateRunningJob(job.job_id, observedExecutionFields(result));
118
+ if (handlers.cancelling) {
119
+ throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, `${commandType}.runtime`);
120
+ }
121
+ assertCliResultSuccess(result, `${commandType}.runtime`);
122
+ // Persist whichever session id kimi announced. Review never resumes,
123
+ // but the id is the only handle for post-hoc replay against
124
+ // ~/.kimi-code/sessions/.
125
+ if (result.sessionId !== undefined && result.sessionId.length > 0) {
126
+ // Tighten on length>0 so a degenerate empty-string capture doesn't
127
+ // poison the row — consumers reading SQLite distinguish "missing"
128
+ // (NULL) from "captured" (non-empty UUID) only via NULL-ness.
129
+ // (Kimi alpha.4 challenge finding #3.)
130
+ store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
131
+ }
132
+ await syncKimiSessionTitle({
133
+ promptText: previewPrompt,
134
+ env: context.env,
135
+ cwd: job.cwd,
136
+ sessionId: result.sessionId ?? job.kimi_session_id,
137
+ title: buildKimiSessionTitle(commandType, parsed.focus ?? reviewContext.targetDescription),
138
+ stderr: context.stderr,
139
+ });
140
+ // Match ask/rescue: gate on the JOB row's pre-call session id so that
141
+ // a future --resume code path won't warn on a resumed review whose
142
+ // prior session id remains valid even if this turn didn't re-announce.
143
+ // Today review never resumes, so the gate is always true at call time.
144
+ if (job.kimi_session_id === null) {
145
+ warnIfSessionIdMissing(result, commandType, job.job_id, context.stderr);
146
+ }
147
+ const finalText = reassembleProseFromRecords(result.records);
148
+ const rendered = renderManagedJobOutput(job, finalText);
149
+ const artifactPath = await writeArtifact(paths, job, rendered.rendered);
150
+ if (handlers.cancelling) {
151
+ throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterArtifact, `${commandType}.runtime`);
152
+ }
153
+ store.markCompleted(job.job_id, {
154
+ summary: rendered.summary,
155
+ final_output_path: artifactPath,
156
+ error: null,
157
+ });
158
+ return rendered.output;
159
+ }
160
+ catch (error) {
161
+ if (handlers.cancelling) {
162
+ const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, `${commandType}.runtime`, error instanceof Error ? { cause: error } : undefined);
163
+ await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError);
164
+ throw cancelledError;
165
+ }
166
+ const classified = classifyManagedCommandFailure(error, commandType, job.job_id);
167
+ await markJobFailed(store, paths, job, classified, cancel.failedSummary);
168
+ throw classified;
169
+ }
170
+ finally {
171
+ handlers.dispose();
172
+ store.close();
173
+ }
174
+ }
175
+ async function requireReadOnlyHookInstalled(context, commandType) {
176
+ if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK === "1") {
177
+ return;
178
+ }
179
+ const installStatus = await verifyHookInstalled(context.env);
180
+ if (installStatus.installed) {
181
+ return;
182
+ }
183
+ maybeWarnHookMissing(installStatus, commandType, context.stderr);
184
+ throw new RuntimeError(`${commandType.toUpperCase()}_HOOK_NOT_INSTALLED`, [
185
+ `${commandType} refuses to run without the canonical kimi-plugin-cc PreToolUse hook.`,
186
+ `Hook check failed: ${installStatus.reason ?? "unknown"}.`,
187
+ "Run /kimi:setup or $kimi-setup to install or repair this host's managed block.",
188
+ "Set KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1 only if you intentionally accept un-enforced execution.",
189
+ hookRefusalRetryProtocol(context.env),
190
+ ].join(" "), `${commandType}.hook-check`, { details: hookRefusalDetails(installStatus) });
191
+ }
192
+ function buildReviewPrompt(commandType, reviewContext, focus) {
193
+ const modeInstructions = commandType === "challenge"
194
+ ? [
195
+ "Take a challenging stance.",
196
+ "Challenge assumptions, identify brittle design choices, and surface safer alternatives.",
197
+ ]
198
+ : ["Focus on concrete bugs, regressions, and missing safeguards in the supplied changes."];
199
+ return [
200
+ "Perform a read-only code review of the supplied repository changes.",
201
+ ...modeInstructions,
202
+ "Use repository read tools as needed, but do not attempt any write, shell, background, or delegated operations.",
203
+ "Return your review as plain markdown — a short verdict line (approve / concern / block), then a brief summary, then one section per finding with file:line refs. No JSON wrapper, no code fences around the whole response.",
204
+ "If the supplied diff context is empty or shows no changes to review, return immediately with a one-line note that there were no changes — do not explore the repository.",
205
+ focus ? `Review focus: ${focus}` : undefined,
206
+ "",
207
+ `Target: ${reviewContext.targetDescription}`,
208
+ `Repository root: ${reviewContext.repoRoot}`,
209
+ "",
210
+ "git status --short:",
211
+ reviewContext.statusSummary || "(clean)",
212
+ "",
213
+ "Unified diff context:",
214
+ reviewContext.diffText || "(no diff text available)",
215
+ ]
216
+ .filter((line) => typeof line === "string")
217
+ .join("\n");
218
+ }