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.
- package/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +32 -0
- package/CHANGELOG.md +1096 -0
- package/LICENSE +201 -0
- package/README.md +772 -0
- package/agents/kimi-ask.md +82 -0
- package/agents/kimi-challenge.md +82 -0
- package/agents/kimi-pursue.md +91 -0
- package/agents/kimi-rescue.md +85 -0
- package/agents/kimi-review.md +82 -0
- package/agents/kimi-swarm-write.md +92 -0
- package/agents/kimi-swarm.md +83 -0
- package/commands/README.md +16 -0
- package/commands/ask.md +32 -0
- package/commands/cancel.md +17 -0
- package/commands/challenge.md +29 -0
- package/commands/pursue.md +37 -0
- package/commands/replay.md +13 -0
- package/commands/rescue.md +32 -0
- package/commands/result.md +13 -0
- package/commands/review.md +29 -0
- package/commands/setup.md +23 -0
- package/commands/status.md +13 -0
- package/commands/swarm.md +38 -0
- package/dist/background-spawn.js +224 -0
- package/dist/cli-cancellation.js +43 -0
- package/dist/cli-client.js +845 -0
- package/dist/commands/ask.js +327 -0
- package/dist/commands/cancel.js +178 -0
- package/dist/commands/cli-helpers.js +112 -0
- package/dist/commands/models.js +140 -0
- package/dist/commands/not-implemented.js +4 -0
- package/dist/commands/pursue.js +311 -0
- package/dist/commands/registry.js +87 -0
- package/dist/commands/repair-sessions.js +159 -0
- package/dist/commands/replay.js +123 -0
- package/dist/commands/rescue.js +333 -0
- package/dist/commands/result.js +58 -0
- package/dist/commands/review-gate.js +393 -0
- package/dist/commands/review.js +218 -0
- package/dist/commands/setup.js +1132 -0
- package/dist/commands/status.js +27 -0
- package/dist/commands/swarm.js +576 -0
- package/dist/commands/worker.js +31 -0
- package/dist/companion.js +114 -0
- package/dist/config.js +22 -0
- package/dist/errors.js +40 -0
- package/dist/git.js +215 -0
- package/dist/hooks/approval-hook.js +112 -0
- package/dist/hooks/approval-policy.js +254 -0
- package/dist/hooks/config-safety.js +637 -0
- package/dist/hooks/install-paths.js +504 -0
- package/dist/hooks/install.js +281 -0
- package/dist/hooks/managed-block.js +745 -0
- package/dist/hooks/review-gate-stop.js +19 -0
- package/dist/job-store.js +559 -0
- package/dist/jobs.js +202 -0
- package/dist/kimi-command.js +72 -0
- package/dist/kimi-engine.js +624 -0
- package/dist/kimi-errors.js +220 -0
- package/dist/kimi-home.js +9 -0
- package/dist/kimi-timeouts.js +68 -0
- package/dist/kimi-version-probe.js +1410 -0
- package/dist/logging.js +22 -0
- package/dist/native-v2-preflight.js +417 -0
- package/dist/package.json +1 -0
- package/dist/parsing.js +553 -0
- package/dist/paths.js +27 -0
- package/dist/plugin-data.js +98 -0
- package/dist/process-tree.js +448 -0
- package/dist/render.js +159 -0
- package/dist/rescue-approval.js +951 -0
- package/dist/schemas/review-gate-output.js +43 -0
- package/dist/session-title.js +293 -0
- package/dist/stream-json.js +498 -0
- package/dist/types.js +1 -0
- package/dist/vendor/shell-quote/LICENSE +24 -0
- package/dist/vendor/shell-quote/index.js +1 -0
- package/dist/vendor/shell-quote/parse.js +229 -0
- package/dist/vendor/smol-toml/LICENSE +24 -0
- package/dist/vendor/smol-toml/date.js +127 -0
- package/dist/vendor/smol-toml/error.js +63 -0
- package/dist/vendor/smol-toml/extract.js +100 -0
- package/dist/vendor/smol-toml/parse.js +148 -0
- package/dist/vendor/smol-toml/primitive.js +179 -0
- package/dist/vendor/smol-toml/struct.js +184 -0
- package/dist/vendor/smol-toml/util.js +113 -0
- package/dist/version.js +7 -0
- package/docs/migration.md +215 -0
- package/docs/models.md +82 -0
- package/docs/registry-releases.md +70 -0
- package/docs/safety.md +318 -0
- package/docs/session-visibility.md +32 -0
- package/hooks/README.md +9 -0
- package/hooks/hooks.json +16 -0
- package/package.json +67 -0
- package/plugins/kimi-codex/.codex-plugin/plugin.json +39 -0
- package/plugins/kimi-codex/dist/background-spawn.js +224 -0
- package/plugins/kimi-codex/dist/cli-cancellation.js +43 -0
- package/plugins/kimi-codex/dist/cli-client.js +845 -0
- package/plugins/kimi-codex/dist/commands/ask.js +327 -0
- package/plugins/kimi-codex/dist/commands/cancel.js +178 -0
- package/plugins/kimi-codex/dist/commands/cli-helpers.js +112 -0
- package/plugins/kimi-codex/dist/commands/models.js +140 -0
- package/plugins/kimi-codex/dist/commands/not-implemented.js +4 -0
- package/plugins/kimi-codex/dist/commands/pursue.js +311 -0
- package/plugins/kimi-codex/dist/commands/registry.js +87 -0
- package/plugins/kimi-codex/dist/commands/repair-sessions.js +159 -0
- package/plugins/kimi-codex/dist/commands/replay.js +123 -0
- package/plugins/kimi-codex/dist/commands/rescue.js +333 -0
- package/plugins/kimi-codex/dist/commands/result.js +58 -0
- package/plugins/kimi-codex/dist/commands/review-gate.js +393 -0
- package/plugins/kimi-codex/dist/commands/review.js +218 -0
- package/plugins/kimi-codex/dist/commands/setup.js +1132 -0
- package/plugins/kimi-codex/dist/commands/status.js +27 -0
- package/plugins/kimi-codex/dist/commands/swarm.js +576 -0
- package/plugins/kimi-codex/dist/commands/worker.js +31 -0
- package/plugins/kimi-codex/dist/companion.js +114 -0
- package/plugins/kimi-codex/dist/config.js +22 -0
- package/plugins/kimi-codex/dist/errors.js +40 -0
- package/plugins/kimi-codex/dist/git.js +215 -0
- package/plugins/kimi-codex/dist/hooks/approval-hook.js +112 -0
- package/plugins/kimi-codex/dist/hooks/approval-policy.js +254 -0
- package/plugins/kimi-codex/dist/hooks/config-safety.js +637 -0
- package/plugins/kimi-codex/dist/hooks/install-paths.js +504 -0
- package/plugins/kimi-codex/dist/hooks/install.js +281 -0
- package/plugins/kimi-codex/dist/hooks/managed-block.js +745 -0
- package/plugins/kimi-codex/dist/hooks/review-gate-stop.js +19 -0
- package/plugins/kimi-codex/dist/job-store.js +559 -0
- package/plugins/kimi-codex/dist/jobs.js +202 -0
- package/plugins/kimi-codex/dist/kimi-command.js +72 -0
- package/plugins/kimi-codex/dist/kimi-engine.js +624 -0
- package/plugins/kimi-codex/dist/kimi-errors.js +220 -0
- package/plugins/kimi-codex/dist/kimi-home.js +9 -0
- package/plugins/kimi-codex/dist/kimi-timeouts.js +68 -0
- package/plugins/kimi-codex/dist/kimi-version-probe.js +1410 -0
- package/plugins/kimi-codex/dist/logging.js +22 -0
- package/plugins/kimi-codex/dist/native-v2-preflight.js +417 -0
- package/plugins/kimi-codex/dist/package.json +1 -0
- package/plugins/kimi-codex/dist/parsing.js +553 -0
- package/plugins/kimi-codex/dist/paths.js +27 -0
- package/plugins/kimi-codex/dist/plugin-data.js +98 -0
- package/plugins/kimi-codex/dist/process-tree.js +448 -0
- package/plugins/kimi-codex/dist/render.js +159 -0
- package/plugins/kimi-codex/dist/rescue-approval.js +951 -0
- package/plugins/kimi-codex/dist/schemas/review-gate-output.js +43 -0
- package/plugins/kimi-codex/dist/session-title.js +293 -0
- package/plugins/kimi-codex/dist/stream-json.js +498 -0
- package/plugins/kimi-codex/dist/types.js +1 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/LICENSE +24 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/index.js +1 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/parse.js +229 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/LICENSE +24 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +127 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +63 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +100 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +148 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +179 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +184 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +113 -0
- package/plugins/kimi-codex/dist/version.js +7 -0
- package/plugins/kimi-codex/scripts/companion.sh +37 -0
- package/plugins/kimi-codex/scripts/review-gate-hook.sh +32 -0
- package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-ask/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-cancel/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-challenge/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +30 -0
- package/plugins/kimi-codex/skills/kimi-pursue/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-replay/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +30 -0
- package/plugins/kimi-codex/skills/kimi-rescue/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-result/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-result/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-review/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-review/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +32 -0
- package/plugins/kimi-codex/skills/kimi-setup/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-status/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-status/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-swarm/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +32 -0
- package/plugins/kimi-codex/skills/kimi-swarm-write/agents/openai.yaml +6 -0
- package/scripts/companion.sh +37 -0
- 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
|
+
}
|