kimi-plugin-cc 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +22 -0
- package/README.md +5 -5
- package/agents/kimi-challenge.md +3 -3
- package/agents/kimi-pursue.md +1 -1
- package/agents/kimi-rescue.md +2 -2
- package/agents/kimi-review.md +3 -3
- package/commands/README.md +2 -2
- package/commands/challenge.md +1 -1
- package/commands/pursue.md +1 -1
- package/commands/review.md +1 -1
- package/dist/background-spawn.js +9 -17
- package/dist/cli-client.js +22 -62
- package/dist/commands/ask.js +24 -29
- package/dist/commands/cancel.js +7 -19
- package/dist/commands/cli-helpers.js +9 -17
- package/dist/commands/pursue.js +21 -31
- package/dist/commands/registry.js +6 -19
- package/dist/commands/rescue.js +20 -20
- package/dist/commands/review-gate.js +16 -24
- package/dist/commands/review.js +37 -43
- package/dist/commands/setup.js +85 -171
- package/dist/commands/swarm.js +103 -88
- package/dist/commands/worker.js +0 -6
- package/dist/git.js +14 -7
- package/dist/hooks/approval-hook.js +9 -20
- package/dist/hooks/approval-policy.js +36 -69
- package/dist/hooks/config-safety.js +7 -4
- package/dist/hooks/install-paths.js +62 -138
- package/dist/hooks/install.js +37 -108
- package/dist/hooks/managed-block.js +88 -214
- package/dist/job-store.js +6 -28
- package/dist/kimi-command.js +1 -3
- package/dist/kimi-engine.js +2 -1
- package/dist/kimi-timeouts.js +9 -21
- package/dist/kimi-version-probe.js +16 -1210
- package/dist/native-v2-preflight.js +1 -1
- package/dist/parsing.js +4 -18
- package/dist/rescue-approval.js +78 -164
- package/dist/session-recovery.js +7 -10
- package/dist/stream-json.js +22 -174
- package/dist/version.js +1 -1
- package/docs/migration.md +2 -6
- package/docs/registry-releases.md +25 -5
- package/docs/safety.md +16 -12
- package/package.json +2 -2
- package/plugins/kimi-codex/.codex-plugin/plugin.json +4 -4
- package/plugins/kimi-codex/dist/background-spawn.js +9 -17
- package/plugins/kimi-codex/dist/cli-client.js +22 -62
- package/plugins/kimi-codex/dist/commands/ask.js +24 -29
- package/plugins/kimi-codex/dist/commands/cancel.js +7 -19
- package/plugins/kimi-codex/dist/commands/cli-helpers.js +9 -17
- package/plugins/kimi-codex/dist/commands/pursue.js +21 -31
- package/plugins/kimi-codex/dist/commands/registry.js +6 -19
- package/plugins/kimi-codex/dist/commands/rescue.js +20 -20
- package/plugins/kimi-codex/dist/commands/review-gate.js +16 -24
- package/plugins/kimi-codex/dist/commands/review.js +37 -43
- package/plugins/kimi-codex/dist/commands/setup.js +85 -171
- package/plugins/kimi-codex/dist/commands/swarm.js +103 -88
- package/plugins/kimi-codex/dist/commands/worker.js +0 -6
- package/plugins/kimi-codex/dist/git.js +14 -7
- package/plugins/kimi-codex/dist/hooks/approval-hook.js +9 -20
- package/plugins/kimi-codex/dist/hooks/approval-policy.js +36 -69
- package/plugins/kimi-codex/dist/hooks/config-safety.js +7 -4
- package/plugins/kimi-codex/dist/hooks/install-paths.js +62 -138
- package/plugins/kimi-codex/dist/hooks/install.js +37 -108
- package/plugins/kimi-codex/dist/hooks/managed-block.js +88 -214
- package/plugins/kimi-codex/dist/job-store.js +6 -28
- package/plugins/kimi-codex/dist/kimi-command.js +1 -3
- package/plugins/kimi-codex/dist/kimi-engine.js +2 -1
- package/plugins/kimi-codex/dist/kimi-timeouts.js +9 -21
- package/plugins/kimi-codex/dist/kimi-version-probe.js +16 -1210
- package/plugins/kimi-codex/dist/native-v2-preflight.js +1 -1
- package/plugins/kimi-codex/dist/parsing.js +4 -18
- package/plugins/kimi-codex/dist/rescue-approval.js +78 -164
- package/plugins/kimi-codex/dist/session-recovery.js +7 -10
- package/plugins/kimi-codex/dist/stream-json.js +22 -174
- package/plugins/kimi-codex/dist/version.js +1 -1
- package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +2 -2
- package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-result/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-review/SKILL.md +3 -3
- package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-status/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +1 -1
- package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +1 -1
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
// Subprocess client for `kimi -p --output-format stream-json`.
|
|
2
2
|
//
|
|
3
|
-
// Replaces runtime/wire/client.ts for v1.0. Both modules coexist during the
|
|
4
|
-
// PR 1-3 transition; PR 4 deletes wire/. The contract is intentionally
|
|
5
|
-
// narrower than the v0.4 wire client — one process per prompt, no in-band
|
|
6
|
-
// approvals (PreToolUse hook handles those), no resumable JSON-RPC channel.
|
|
7
|
-
//
|
|
8
|
-
// Where v0.4 streamed turn events to a TurnCapture state machine,
|
|
9
|
-
// stream-json gives us flattened OpenAI-shaped records that callers
|
|
10
|
-
// reassemble at their own seam. The buffered result is the primary return
|
|
11
|
-
// shape; callers needing incremental visibility (e.g., updating SQLite
|
|
12
|
-
// phase as tools run) can pass `onRecord` to observe records as they arrive
|
|
13
|
-
// without paying for a separate event-stream API.
|
|
14
|
-
//
|
|
15
3
|
// Per-job env (`KIMI_PLUGIN_CC_CMD`) is set on this spawn so the
|
|
16
4
|
// PreToolUse hook can branch per command (review vs rescue vs ask). It is
|
|
17
5
|
// passed via the spawn `env` option, not exported to a shell, so concurrent
|
|
@@ -32,12 +20,7 @@ import { collectDescendantIdentities, revalidateProcessIdentities, waitForProces
|
|
|
32
20
|
const STDERR_TAIL_BYTES = 8192;
|
|
33
21
|
/** Hard cap on awaiting diagnostics log drain before resolving the result. */
|
|
34
22
|
const LOG_DRAIN_TIMEOUT_MS = 250;
|
|
35
|
-
/**
|
|
36
|
-
* Default delay between SIGTERM and SIGKILL when an abort fires.
|
|
37
|
-
* Matches v0.4's `cancellation.ts` escalation timing for wire clients,
|
|
38
|
-
* so rescue/ask/review continue to feel identical to a stuck-kimi user
|
|
39
|
-
* after the v1.0 cutover.
|
|
40
|
-
*/
|
|
23
|
+
/** Default 1500 ms delay between SIGTERM and SIGKILL after an abort. */
|
|
41
24
|
const DEFAULT_ESCALATION_MS = 1_500;
|
|
42
25
|
/** Maximum time spent confirming POSIX tree exit after SIGKILL dispatch. */
|
|
43
26
|
const POST_KILL_QUIESCENCE_MS = 500;
|
|
@@ -126,6 +109,7 @@ export async function runCliPrompt(opts) {
|
|
|
126
109
|
let stderrTail = "";
|
|
127
110
|
let announcedSessionId;
|
|
128
111
|
let announcedGoalSummary;
|
|
112
|
+
let announcedCliError;
|
|
129
113
|
let announcedSystemVersion;
|
|
130
114
|
const hookMonitor = createHookEscapeMonitor({ commandLabel: opts.commandLabel,
|
|
131
115
|
operationKind: opts.executionPlan.operationKind, trustedWorkspaceRoot: opts.trustedWorkspaceRoot, cwd: opts.cwd });
|
|
@@ -190,12 +174,16 @@ export async function runCliPrompt(opts) {
|
|
|
190
174
|
for (const outcome of outcomes) {
|
|
191
175
|
if (engineMismatchDetected)
|
|
192
176
|
return;
|
|
177
|
+
if (outcome.cliError !== undefined) {
|
|
178
|
+
if (requireMarkerBeforeOutput("a CLI error record"))
|
|
179
|
+
return;
|
|
180
|
+
announcedCliError ??= outcome.cliError;
|
|
181
|
+
appendLogLine({ event: "cli_error", ...outcome.cliError });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
193
184
|
if (outcome.unknownRecord !== undefined) {
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// records[] and out of onRecord — consumers iterate records[] expecting
|
|
197
|
-
// assistant/tool only. Tolerated, not treated as malformed/error —
|
|
198
|
-
// EXCEPT that on a native-v2 plan nothing may precede the marker.
|
|
185
|
+
// Unknown roles are diagnostic-only and excluded from consumer records;
|
|
186
|
+
// native-v2 still refuses any output before its marker.
|
|
199
187
|
if (requireMarkerBeforeOutput(`an unknown-role (${outcome.unknownRecord.role}) record`)) {
|
|
200
188
|
return;
|
|
201
189
|
}
|
|
@@ -208,9 +196,7 @@ export async function runCliPrompt(opts) {
|
|
|
208
196
|
if (outcome.goalSummary !== undefined) {
|
|
209
197
|
if (requireMarkerBeforeOutput("goal.summary"))
|
|
210
198
|
return;
|
|
211
|
-
//
|
|
212
|
-
// consumer-facing record. First-announce wins (a run emits exactly one,
|
|
213
|
-
// at session end); capture and skip so records[] stays assistant/tool.
|
|
199
|
+
// Out-of-band metadata; first-announce-wins, excluded from consumer records.
|
|
214
200
|
if (announcedGoalSummary === undefined) {
|
|
215
201
|
announcedGoalSummary = outcome.goalSummary;
|
|
216
202
|
appendLogLine({
|
|
@@ -544,21 +530,12 @@ export async function runCliPrompt(opts) {
|
|
|
544
530
|
});
|
|
545
531
|
};
|
|
546
532
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
// an aborted state by the time we reach addEventListener, which does
|
|
551
|
-
// NOT re-fire for already-aborted signals (per the AbortSignal spec).
|
|
552
|
-
// Without this re-check the spawned child is orphaned: SIGTERM/SIGKILL
|
|
553
|
-
// is never sent and the process keeps holding the SQLite row + model
|
|
554
|
-
// tokens. Audit report 28 (Codex H1) tracked this fix.
|
|
533
|
+
// Abort may occur during any pre-spawn await, and addEventListener does not
|
|
534
|
+
// replay an already-aborted signal. Re-check after attaching the listener so
|
|
535
|
+
// the spawned child cannot escape cancellation teardown.
|
|
555
536
|
if (opts.signal?.aborted === true) {
|
|
556
537
|
onAbort();
|
|
557
538
|
}
|
|
558
|
-
// Swap the pre-attached early listener for the real handler. These two
|
|
559
|
-
// statements are synchronous and execute as one unit; no event can
|
|
560
|
-
// interleave between marking earlyErrorHandled and attaching the new
|
|
561
|
-
// listener.
|
|
562
539
|
earlyErrorHandled = true;
|
|
563
540
|
child.on("error", (err) => {
|
|
564
541
|
appendLogLine({ event: "process_error", message: formatError(err) });
|
|
@@ -606,9 +583,10 @@ export async function runCliPrompt(opts) {
|
|
|
606
583
|
await raceWithTimeout(logChain, LOG_DRAIN_TIMEOUT_MS);
|
|
607
584
|
settle("resolve", {
|
|
608
585
|
spawnedAt,
|
|
609
|
-
settledAt: 0,
|
|
586
|
+
settledAt: 0,
|
|
610
587
|
sessionId,
|
|
611
588
|
goalSummary: announcedGoalSummary,
|
|
589
|
+
cliError: announcedCliError,
|
|
612
590
|
systemVersion: announcedSystemVersion,
|
|
613
591
|
observedEngine: announcedSystemVersion !== undefined
|
|
614
592
|
? "native-v2"
|
|
@@ -713,14 +691,6 @@ export class ResponseTimeoutError extends RuntimeError {
|
|
|
713
691
|
*
|
|
714
692
|
* The generic `withTimeout` (runtime/kimi-timeouts.ts) only rejects
|
|
715
693
|
* the outer Promise — it has no handle on the kimi child process.
|
|
716
|
-
* review/ask/review-gate previously wrapped `runCliPrompt` with
|
|
717
|
-
* `withTimeout` and let the orphaned subprocess keep running until
|
|
718
|
-
* it crashed or finished on its own. The reviewers (reports 17 +
|
|
719
|
-
* 18) flagged this as critical for review_gate in particular: the
|
|
720
|
-
* 8 s budget fires inside Claude Code's Stop hook, the parent
|
|
721
|
-
* returns "allow stop", and a runaway kimi keeps holding the
|
|
722
|
-
* SQLite row + model tokens. Subsequent `/kimi:cancel` can't reach
|
|
723
|
-
* it because `kimi_pid` is null.
|
|
724
694
|
*
|
|
725
695
|
* Behavior:
|
|
726
696
|
*
|
|
@@ -802,11 +772,6 @@ function buildArgs(opts) {
|
|
|
802
772
|
args.push("--skills-dir", dir);
|
|
803
773
|
}
|
|
804
774
|
}
|
|
805
|
-
// NOTE: thinking effort is requested through the env binding, never CLI argv.
|
|
806
|
-
// Non-trait-driven providers or a base level already off ignore it silently;
|
|
807
|
-
// an alwaysThinking model receives off unnormalized. stream-json carries no
|
|
808
|
-
// thinking deltas (writeThinkingDelta() {} in the JSON writer), so the plugin
|
|
809
|
-
// cannot observe whether the request was honoured.
|
|
810
775
|
// -p accepts the prompt as the option value. We pass it as a separate argv
|
|
811
776
|
// entry so prompts with leading dashes or whitespace are not interpreted as
|
|
812
777
|
// additional flags.
|
|
@@ -817,15 +782,12 @@ function buildEnv(opts) {
|
|
|
817
782
|
const env = { ...opts.env };
|
|
818
783
|
if (opts.thinkingEffort !== undefined)
|
|
819
784
|
env.KIMI_MODEL_THINKING_EFFORT = opts.thinkingEffort;
|
|
820
|
-
//
|
|
785
|
+
// Ambient workspace trust starts project MCP stdio servers outside PreToolUse.
|
|
821
786
|
delete env.KIMI_CODE_TRUST_WORKSPACE;
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
// plugin-owned spawn invariant rather than ambient operator state. Older
|
|
827
|
-
// kimi-code releases ignore the unknown flag. Always overwrite ambient false
|
|
828
|
-
// values; accepting one would silently reopen v2 on 0.33+.
|
|
787
|
+
// Legacy-v1 plans use this flag on releases whose unflagged -p default can
|
|
788
|
+
// route to native-v2. Always overwrite ambient false values; older releases
|
|
789
|
+
// ignore it. The engine choice stays plugin-owned, while certified native-v2
|
|
790
|
+
// plans use the no-plan construction rather than a legacy pin.
|
|
829
791
|
if (opts.executionPlan.intendedEngine === "legacy-v1") {
|
|
830
792
|
env[KIMI_LEGACY_ENV] = KIMI_LEGACY_FORCED_VALUE;
|
|
831
793
|
}
|
|
@@ -841,8 +803,6 @@ function buildEnv(opts) {
|
|
|
841
803
|
env.KIMI_PLUGIN_CC_CMD = opts.commandLabel;
|
|
842
804
|
}
|
|
843
805
|
if (opts.swarmMaxConcurrency !== undefined) {
|
|
844
|
-
// kimi-code 0.18.0+ caps AgentSwarm's normal-phase concurrency at this
|
|
845
|
-
// value; older binaries ignore it. See the field doc on CliClientOptions.
|
|
846
806
|
env.KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY = String(opts.swarmMaxConcurrency);
|
|
847
807
|
}
|
|
848
808
|
if (opts.trustedWorkspaceRoot !== undefined) {
|
|
@@ -19,32 +19,28 @@ import { startBackgroundJob } from "../background-spawn.js";
|
|
|
19
19
|
import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
|
|
20
20
|
import { warnInheritedBudget, captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
|
|
21
21
|
import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
|
|
22
|
-
// v1.0 cutover note (PR 2):
|
|
23
|
-
//
|
|
24
|
-
// The v0.4 wire client/initialize/prompt sequence is replaced with a
|
|
25
|
-
// single `runCliPrompt` against `kimi -p --output-format stream-json`.
|
|
26
|
-
// The PreToolUse hook reads `KIMI_PLUGIN_CC_CMD=ask` and applies the
|
|
27
|
-
// same read-only tool policy as review/challenge/review_gate.
|
|
28
|
-
//
|
|
29
|
-
// Session id handling differs from v0.4:
|
|
30
|
-
//
|
|
31
|
-
// - v0.4 the client assigned the kimi session id upfront (passed via
|
|
32
|
-
// `--session <uuid>` on the wire command line).
|
|
33
|
-
// - v1.0 kimi-code assigns the session id and announces it via stderr
|
|
34
|
-
// ("To resume this session: kimi -r <uuid>"). We capture that id
|
|
35
|
-
// after the call and update the JobStore row.
|
|
36
|
-
// - For `--resume`, we pass the prior session id via `-r`; kimi-code
|
|
37
|
-
// keeps it across resumes.
|
|
38
22
|
const ASK_SUMMARY_MAX = 120;
|
|
39
23
|
const ASK_AGENT_PROFILE_PLACEHOLDER = "<cli-client>";
|
|
40
|
-
const
|
|
41
|
-
"Answer
|
|
42
|
-
"
|
|
24
|
+
const ASK_PROMPT_CONTRACT = [
|
|
25
|
+
"Answer directly in free-form prose.",
|
|
26
|
+
"For repository-dependent questions, inspect the relevant code with the allowed read tools and cite file:line for concrete code claims where useful.",
|
|
27
|
+
"Distinguish evidence from inference and state unknowns explicitly.",
|
|
28
|
+
"Do not use shell/Bash, writes, background tasks, or delegated operations.",
|
|
43
29
|
"Do not emit JSON unless the user explicitly asks for it.",
|
|
30
|
+
"Do not narrate tool progress; make the final response self-contained.",
|
|
31
|
+
];
|
|
32
|
+
const ASK_PROMPT_PREFIX = [
|
|
33
|
+
...ASK_PROMPT_CONTRACT,
|
|
44
34
|
"",
|
|
45
35
|
"User question:",
|
|
46
36
|
"",
|
|
47
37
|
].join("\n");
|
|
38
|
+
const ASK_CONTINUE_TASK = "Continue the previous ask conversation and answer any outstanding question.";
|
|
39
|
+
const ASK_RESUME_PROMPT = [
|
|
40
|
+
...ASK_PROMPT_CONTRACT,
|
|
41
|
+
"",
|
|
42
|
+
ASK_CONTINUE_TASK,
|
|
43
|
+
].join("\n");
|
|
48
44
|
export async function runAsk(argv, context) {
|
|
49
45
|
const parsed = parseAskArgs(argv);
|
|
50
46
|
if (parsed.help)
|
|
@@ -91,7 +87,7 @@ export async function runAsk(argv, context) {
|
|
|
91
87
|
...persistedExecutionPlanFields(executionPlan),
|
|
92
88
|
agent_profile: ASK_AGENT_PROFILE_PLACEHOLDER,
|
|
93
89
|
prompt_digest: digestPrompt(askPrompt),
|
|
94
|
-
summary: shorten(
|
|
90
|
+
summary: shorten(askPromptSubject(askPrompt), ASK_SUMMARY_MAX),
|
|
95
91
|
phase: parsed.background ? "queued" : "starting",
|
|
96
92
|
final_output_path: null,
|
|
97
93
|
stream_log_path: logPath,
|
|
@@ -192,9 +188,7 @@ export async function executeAskJob(jobId, prompt, context, options) {
|
|
|
192
188
|
// kimi-code mints a new id on each turn.
|
|
193
189
|
if (result.sessionId !== undefined &&
|
|
194
190
|
result.sessionId.length > 0) {
|
|
195
|
-
//
|
|
196
|
-
// warning helper's null-ish check would also fire but only after
|
|
197
|
-
// the bad write. (Kimi alpha.4 challenge finding #3.)
|
|
191
|
+
// Never persist an empty ID because NULL represents a missing session ID.
|
|
198
192
|
store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
|
|
199
193
|
}
|
|
200
194
|
await syncKimiSessionTitle({
|
|
@@ -202,7 +196,7 @@ export async function executeAskJob(jobId, prompt, context, options) {
|
|
|
202
196
|
env: context.env,
|
|
203
197
|
cwd: job.cwd,
|
|
204
198
|
sessionId: result.sessionId ?? job.kimi_session_id,
|
|
205
|
-
title: buildKimiSessionTitle("ask",
|
|
199
|
+
title: buildKimiSessionTitle("ask", askPromptSubject(prompt)),
|
|
206
200
|
stderr: context.stderr,
|
|
207
201
|
});
|
|
208
202
|
// For ask, missing session id breaks both -r (latest-resume) and
|
|
@@ -322,14 +316,15 @@ function buildAskPrompt(question, reusedSession) {
|
|
|
322
316
|
return ASK_PROMPT_PREFIX + question.trim();
|
|
323
317
|
}
|
|
324
318
|
if (reusedSession) {
|
|
325
|
-
return
|
|
326
|
-
"Continue the previous ask conversation in free-form prose.",
|
|
327
|
-
"Stay read-only.",
|
|
328
|
-
"Do not emit JSON unless the user explicitly asks for it.",
|
|
329
|
-
].join("\n");
|
|
319
|
+
return ASK_RESUME_PROMPT;
|
|
330
320
|
}
|
|
331
321
|
throw new RuntimeError("INVALID_ARGS", "ask requires a question after the flags.", "ask.parse");
|
|
332
322
|
}
|
|
323
|
+
function askPromptSubject(prompt) {
|
|
324
|
+
if (prompt.startsWith(ASK_PROMPT_PREFIX))
|
|
325
|
+
return prompt.slice(ASK_PROMPT_PREFIX.length);
|
|
326
|
+
return prompt === ASK_RESUME_PROMPT ? ASK_CONTINUE_TASK : prompt;
|
|
327
|
+
}
|
|
333
328
|
function shorten(text, max) {
|
|
334
329
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
335
330
|
if (normalized.length <= max) {
|
|
@@ -34,27 +34,15 @@ export async function runCancel(argv, context) {
|
|
|
34
34
|
if (job.pid === null && job.kimi_pid === null) {
|
|
35
35
|
throw new RuntimeError("CANCEL_NOT_SUPPORTED", `Job ${job.job_id} does not have a recorded process id to cancel.`, "cancel.runtime");
|
|
36
36
|
}
|
|
37
|
-
// review_gate runs in a Stop hook
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// user's intent is recorded canonically — the subsequent wire exit is a
|
|
41
|
-
// no-op against an already-terminal row (markFailed has a WHERE
|
|
42
|
-
// status='running' guard).
|
|
37
|
+
// review_gate runs in a Stop hook. Pre-mark the row as cancelled before
|
|
38
|
+
// signaling so the subprocess close/result path cannot persist a failure
|
|
39
|
+
// over the user's intent; later updates are no-ops on a terminal row.
|
|
43
40
|
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// not skip cleanup. (v0.2.4)
|
|
41
|
+
// The terminal row also makes waitForCancellation return before its own
|
|
42
|
+
// escalation, so this path always attempts SIGTERM after the pre-mark
|
|
43
|
+
// attempt, waits 1s, and sends SIGKILL only if a recorded process remains
|
|
44
|
+
// live. A pre-mark failure is rethrown only after termination is attempted.
|
|
49
45
|
if (job.command_type === "review_gate") {
|
|
50
|
-
// Pre-mark before signaling so the row persists as `cancelled` rather
|
|
51
|
-
// than the `failed` the hook's catch path would write on WIRE_PROCESS_EXITED.
|
|
52
|
-
//
|
|
53
|
-
// If the pre-mark itself fails (disk I/O, SQLite contention), we must
|
|
54
|
-
// STILL signal the Kimi child — otherwise the user's cancel command
|
|
55
|
-
// produces an alive-but-marked-cancelled stuck state. Capture the
|
|
56
|
-
// pre-mark failure, run the SIGTERM→SIGKILL escalation unconditionally,
|
|
57
|
-
// and rethrow at the end so the user sees the underlying error.
|
|
58
46
|
let preMarkError;
|
|
59
47
|
try {
|
|
60
48
|
await withJobStore(paths, async (preMarkStore) => {
|
|
@@ -44,29 +44,20 @@ export function reassembleProseFromRecords(records) {
|
|
|
44
44
|
return out;
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* case (the row is unresumable), and the user is silently surprised
|
|
50
|
-
* later when `-r` or `/kimi:replay` returns nothing.
|
|
47
|
+
* Warn when kimi finishes without announcing a session ID. The job keeps
|
|
48
|
+
* `kimi_session_id` NULL, so resume is unavailable.
|
|
51
49
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* inline immediately after the prompt completes so they know resume is
|
|
56
|
-
* unavailable for this job.
|
|
57
|
-
*
|
|
58
|
-
* Called at the end of every command that captures sessionId (review,
|
|
59
|
-
* challenge, ask, rescue). Review-gate intentionally skips: it's a Stop
|
|
60
|
-
* hook with no resume semantics and we don't want a runtime warning to
|
|
61
|
-
* leak into Claude Code's transcript on every assistant turn.
|
|
50
|
+
* This warning belongs on human stderr; load-bearing state remains in SQLite.
|
|
51
|
+
* Review-gate intentionally skips it because a Stop hook has no resume
|
|
52
|
+
* semantics and should not warn on every assistant turn.
|
|
62
53
|
*/
|
|
63
54
|
export function warnIfSessionIdMissing(result, commandLabel, jobId, stderr) {
|
|
64
55
|
if (result.sessionId !== undefined && result.sessionId.length > 0) {
|
|
65
56
|
return;
|
|
66
57
|
}
|
|
67
58
|
stderr.write(`[${commandLabel}] kimi did not announce a session id for job ${jobId}. ` +
|
|
68
|
-
`Resume
|
|
69
|
-
`If this happens repeatedly, check kimi-code's
|
|
59
|
+
`Resume will not work for this job. Replay from the stored stream log is unaffected. ` +
|
|
60
|
+
`If this happens repeatedly, check kimi-code's session.resume_hint record (and the legacy stderr resume announcement) and report upstream.\n`);
|
|
70
61
|
}
|
|
71
62
|
/**
|
|
72
63
|
* Translate a cli-client `CliClientResult` into the kind of error
|
|
@@ -99,7 +90,7 @@ export function assertCliResultSuccess(result, stage) {
|
|
|
99
90
|
});
|
|
100
91
|
}
|
|
101
92
|
if (result.exitCode !== 0) {
|
|
102
|
-
const tail = result.stderrTail.trim();
|
|
93
|
+
const tail = result.cliError?.message ?? result.stderrTail.trim();
|
|
103
94
|
throw new RuntimeError("CLI_NONZERO_EXIT", [
|
|
104
95
|
`kimi subprocess exited with code ${result.exitCode}`,
|
|
105
96
|
result.signal ? ` (signal ${result.signal})` : "",
|
|
@@ -110,6 +101,7 @@ export function assertCliResultSuccess(result, stage) {
|
|
|
110
101
|
signal: result.signal,
|
|
111
102
|
record_count: result.records.length,
|
|
112
103
|
malformed_count: result.malformed.length,
|
|
104
|
+
...(result.cliError !== undefined && { cli_error: result.cliError }),
|
|
113
105
|
stderr_tail_len: result.stderrTail.length,
|
|
114
106
|
},
|
|
115
107
|
});
|
|
@@ -16,36 +16,13 @@ import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.j
|
|
|
16
16
|
import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
|
|
17
17
|
import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
|
|
18
18
|
import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
|
|
19
|
-
// /kimi:pursue — autonomous goal mode (kimi-code 0.8.0+
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// wall-clock budget bounds the run. (kimi's own shape-consult, report 60.)
|
|
25
|
-
// - NO --resume. In goal mode `goal.summary` emits a *goalId* that is
|
|
26
|
-
// DISTINCT from the resume-hint *sessionId*; `kimi -r <sessionId>` re-enters
|
|
27
|
-
// the session but not necessarily the goal continuation. Exposing resume
|
|
28
|
-
// before that split is reconciled upstream would be a silent-failure trap,
|
|
29
|
-
// so we capture+surface the goalId but don't offer resume yet.
|
|
30
|
-
// - Reuses the RESCUE job lineage (command_type "rescue", KIMI_PLUGIN_CC_CMD=
|
|
31
|
-
// "rescue") so the PreToolUse hook applies the workspace write allowlist to
|
|
32
|
-
// EVERY continuation turn, and /kimi:status / /kimi:result / /kimi:cancel
|
|
33
|
-
// work unchanged. Promoting pursue to a first-class command_type is a
|
|
34
|
-
// follow-up (would ripple through ManagedCommandType + the registry).
|
|
35
|
-
//
|
|
36
|
-
// SAFETY: the hook fires on every tool call in every continuation turn (policy
|
|
37
|
-
// index 0, verified against kimi-code 0.9.0 — report 58 — and re-verified through
|
|
38
|
-
// 0.12.0 by the goal-mode real-binary smoke, reports 61-65). So a goal-mode run
|
|
39
|
-
// is exactly as write-gated as a single-turn rescue. The only NEW risk is
|
|
40
|
-
// unboundedness, bounded here by the AbortController wall-clock ceiling.
|
|
19
|
+
// /kimi:pursue — experimental autonomous goal mode (kimi-code 0.8.0+).
|
|
20
|
+
// Runtime-foreground-only, with a mandatory finite wall-clock budget. No resume:
|
|
21
|
+
// goalId is distinct from sessionId. Rescue lineage and the "rescue" hook label
|
|
22
|
+
// apply the workspace write allowlist on every continuation turn, and the hook
|
|
23
|
+
// is mandatory. Status, result, and cancellation use the same rescue lineage.
|
|
41
24
|
const PURSUE_SUMMARY_MAX = 120;
|
|
42
25
|
const PURSUE_AGENT_PROFILE = "<goal-mode>";
|
|
43
|
-
/**
|
|
44
|
-
* Map a headless goal-mode exit code to a terminal status. complete=0 is
|
|
45
|
-
* success; blocked=3 and paused=6 are terminal-but-resumable (NOT failures);
|
|
46
|
-
* anything else is a genuine process failure. Source:
|
|
47
|
-
* apps/kimi-code/src/cli/goal-prompt.ts::GOAL_EXIT_CODES.
|
|
48
|
-
*/
|
|
49
26
|
/**
|
|
50
27
|
* Goal-terminal exits (3 = blocked, 6 = paused) are NOT failures, so
|
|
51
28
|
* `assertCliResultSuccess` never runs for them — which means a native-v2 plan
|
|
@@ -69,6 +46,12 @@ export function assertGoalRunProvenance(plan, result) {
|
|
|
69
46
|
},
|
|
70
47
|
});
|
|
71
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Map a headless goal-mode exit code to a terminal status. complete=0 is
|
|
51
|
+
* success; blocked=3 and paused=6 are terminal non-failures; anything else is
|
|
52
|
+
* a genuine process failure. Source:
|
|
53
|
+
* apps/kimi-code/src/cli/goal-prompt.ts::GOAL_EXIT_CODES.
|
|
54
|
+
*/
|
|
72
55
|
export function classifyGoalExit(exitCode) {
|
|
73
56
|
switch (exitCode) {
|
|
74
57
|
case 0:
|
|
@@ -88,9 +71,16 @@ export function classifyGoalExit(exitCode) {
|
|
|
88
71
|
* wall-clock AbortController; goal budget mutation is not authorized.
|
|
89
72
|
*/
|
|
90
73
|
export function buildGoalPrompt(objective, turns) {
|
|
91
|
-
const trimmed = objective.trim();
|
|
92
|
-
const turnHint = turns !== undefined
|
|
93
|
-
|
|
74
|
+
const trimmed = objective.trim().split(/\s+/).join(" ");
|
|
75
|
+
const turnHint = turns !== undefined
|
|
76
|
+
? ` The plugin owns the outer wall-clock budget; do not call SetGoalBudget. Stop after at most ${turns} turns (soft instruction). Unfinished work at the soft stop is not completion.`
|
|
77
|
+
: "";
|
|
78
|
+
if (trimmed.length + turnHint.length > 4_000) {
|
|
79
|
+
throw new RuntimeError("INVALID_ARGS", turns === undefined
|
|
80
|
+
? "pursue objective exceeds Kimi's 4000-character goal limit; shorten the objective."
|
|
81
|
+
: "pursue objective plus --turns guidance exceeds Kimi's 4000-character goal limit; shorten the objective or omit --turns.", "pursue.parse");
|
|
82
|
+
}
|
|
83
|
+
return `/goal -- ${trimmed}${turnHint}`;
|
|
94
84
|
}
|
|
95
85
|
function renderGoalHeader(summary, exitStatus) {
|
|
96
86
|
const status = summary?.status ?? exitStatus;
|
|
@@ -3,11 +3,8 @@ const passthroughOutput = "passthrough";
|
|
|
3
3
|
const parsedOutput = "parsed";
|
|
4
4
|
const DEFAULT_ESCALATION_MS = 1_500;
|
|
5
5
|
/**
|
|
6
|
-
* Build
|
|
7
|
-
*
|
|
8
|
-
* uppercase error-code stem ("ASK_"/"RESCUE_"/...). Centralizes the
|
|
9
|
-
* pattern that ask/rescue/review used to inline in three near-identical
|
|
10
|
-
* variants.
|
|
6
|
+
* Build cancellation config from the display name used verbatim in messages
|
|
7
|
+
* and the uppercase stem used in error codes.
|
|
11
8
|
*/
|
|
12
9
|
function makeCancellationConfig(prefix) {
|
|
13
10
|
const { displayName, errorCodeStem } = prefix;
|
|
@@ -28,21 +25,11 @@ function makeCancellationConfig(prefix) {
|
|
|
28
25
|
};
|
|
29
26
|
}
|
|
30
27
|
/**
|
|
31
|
-
* Single source of truth for per-command behavior.
|
|
32
|
-
*
|
|
33
|
-
* managed-cancellation flow participate. `setup` and `task` are
|
|
34
|
-
* intentionally NOT here: they don't go through the SIGTERM/cancel
|
|
35
|
-
* path that this registry configures, so adding placeholder entries
|
|
36
|
-
* for them would be type-system noise without behavior.
|
|
28
|
+
* Single source of truth for per-command behavior. Only commands using the
|
|
29
|
+
* managed-cancellation flow participate; `setup` and `task` do not.
|
|
37
30
|
*
|
|
38
|
-
*
|
|
39
|
-
* `
|
|
40
|
-
* ("Ask cancelled...", "Rescue failed.", "review_gate cancelled..."
|
|
41
|
-
* for the lowercased compound noun). `review` and `challenge` carry
|
|
42
|
-
* lowercase displayName values to match their pre-registry pattern
|
|
43
|
-
* (the old code interpolated `${commandType}` directly). Mixed casing
|
|
44
|
-
* is established prior art — the registry preserves it rather than
|
|
45
|
-
* homogenizing.
|
|
31
|
+
* Display-name casing is part of the user-visible contract: `Ask` and `Rescue`
|
|
32
|
+
* are title-cased, while `review`, `challenge`, and `review_gate` are lowercase.
|
|
46
33
|
*/
|
|
47
34
|
export const COMMAND_REGISTRY = {
|
|
48
35
|
ask: {
|
|
@@ -20,18 +20,6 @@ import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, ver
|
|
|
20
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
|
-
// v1.0 cutover note (PR 3):
|
|
24
|
-
//
|
|
25
|
-
// Rescue is the only write-capable command. v0.4 enforced safety
|
|
26
|
-
// in-band via `WireClient.approvalPolicy = createRescueApprovalPolicy(cwd)`,
|
|
27
|
-
// which called back into our runtime on every kimi-side approval
|
|
28
|
-
// request. v1.0 moves that enforcement into the PreToolUse hook
|
|
29
|
-
// (`runtime/hooks/approval-hook.ts`), which spawns under kimi-code
|
|
30
|
-
// and calls `evaluateRescueHookRequest` for `KIMI_PLUGIN_CC_CMD=rescue`
|
|
31
|
-
// tool calls. The runtime side now just sets the env var and lets
|
|
32
|
-
// the hook gate everything.
|
|
33
|
-
//
|
|
34
|
-
// The security helpers in `runtime/rescue-approval.ts` are unchanged.
|
|
35
23
|
// "Continue working on the bug" verbs that auto-attach to the latest
|
|
36
24
|
// rescue session. Documented UX trap: a prompt matching this pattern
|
|
37
25
|
// will resume the most recent rescue session regardless of whether
|
|
@@ -40,6 +28,18 @@ export { describeMissingResult } from "../background-spawn.js";
|
|
|
40
28
|
const AUTO_RESUME_PATTERN = /\b(continue|resume|keep going|keep working|apply the top fix|dig deeper)\b/i;
|
|
41
29
|
const RESCUE_SUMMARY_MAX = 120;
|
|
42
30
|
const RESCUE_AGENT_PROFILE_PLACEHOLDER = "<cli-client>";
|
|
31
|
+
const RESCUE_CONTINUE_TASK = "Continue the previous rescue task using the latest repository state.";
|
|
32
|
+
const RESCUE_PROMPT_PREFIX = [
|
|
33
|
+
"Follow repository instructions and inspect the current state before editing.",
|
|
34
|
+
"Implement the smallest complete change that satisfies the task instead of stopping at a plan.",
|
|
35
|
+
"Do not mutate git state.",
|
|
36
|
+
"Run proportionate relevant checks, and never claim unrun checks passed.",
|
|
37
|
+
"Do not narrate progress.",
|
|
38
|
+
"Finish with a self-contained report of changed files, verification commands and results, and unfinished work or blockers.",
|
|
39
|
+
"",
|
|
40
|
+
"Rescue task:",
|
|
41
|
+
"",
|
|
42
|
+
].join("\n");
|
|
43
43
|
export async function runRescue(argv, context) {
|
|
44
44
|
const parsed = parseRescueArgs(argv);
|
|
45
45
|
if (parsed.help)
|
|
@@ -83,7 +83,7 @@ export async function runRescue(argv, context) {
|
|
|
83
83
|
...persistedExecutionPlanFields(executionPlan),
|
|
84
84
|
agent_profile: RESCUE_AGENT_PROFILE_PLACEHOLDER,
|
|
85
85
|
prompt_digest: digestPrompt(prompt),
|
|
86
|
-
summary: shorten(prompt, RESCUE_SUMMARY_MAX),
|
|
86
|
+
summary: shorten(stripRescuePromptPrefix(prompt), RESCUE_SUMMARY_MAX),
|
|
87
87
|
phase: parsed.background ? "queued" : "starting",
|
|
88
88
|
final_output_path: null,
|
|
89
89
|
stream_log_path: logPath,
|
|
@@ -193,8 +193,6 @@ export async function executeRescueJob(jobId, prompt, context, options) {
|
|
|
193
193
|
resumeSessionId: job.kimi_session_id ?? undefined,
|
|
194
194
|
logPath: job.stream_log_path,
|
|
195
195
|
signal: handlers.signal,
|
|
196
|
-
// SIGKILL escalation defaults to 1500ms inside cli-client —
|
|
197
|
-
// matches v0.4's cancellation.ts behavior for the wire client.
|
|
198
196
|
}, job.budget_ms ?? KIMI_RESCUE_PROMPT_TIMEOUT_MS, "rescue.prompt");
|
|
199
197
|
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
200
198
|
if (handlers.cancelling) {
|
|
@@ -204,8 +202,7 @@ export async function executeRescueJob(jobId, prompt, context, options) {
|
|
|
204
202
|
warnHookEscapes(result, "rescue", context.stderr);
|
|
205
203
|
if (result.sessionId !== undefined &&
|
|
206
204
|
result.sessionId.length > 0) {
|
|
207
|
-
//
|
|
208
|
-
// alpha.4 challenge finding #3.)
|
|
205
|
+
// Never persist an empty ID because NULL represents a missing session ID.
|
|
209
206
|
store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
|
|
210
207
|
}
|
|
211
208
|
await syncKimiSessionTitle({
|
|
@@ -213,7 +210,7 @@ export async function executeRescueJob(jobId, prompt, context, options) {
|
|
|
213
210
|
env: context.env,
|
|
214
211
|
cwd: job.cwd,
|
|
215
212
|
sessionId: result.sessionId ?? job.kimi_session_id,
|
|
216
|
-
title: buildKimiSessionTitle("rescue", prompt),
|
|
213
|
+
title: buildKimiSessionTitle("rescue", stripRescuePromptPrefix(prompt)),
|
|
217
214
|
stderr: context.stderr,
|
|
218
215
|
});
|
|
219
216
|
if (job.kimi_session_id === null) {
|
|
@@ -271,13 +268,16 @@ export async function executeRescueJob(jobId, prompt, context, options) {
|
|
|
271
268
|
}
|
|
272
269
|
function buildRescuePrompt(prompt, reusedSession) {
|
|
273
270
|
if (prompt?.trim()) {
|
|
274
|
-
return prompt.trim();
|
|
271
|
+
return RESCUE_PROMPT_PREFIX + prompt.trim();
|
|
275
272
|
}
|
|
276
273
|
if (reusedSession) {
|
|
277
|
-
return
|
|
274
|
+
return RESCUE_PROMPT_PREFIX + RESCUE_CONTINUE_TASK;
|
|
278
275
|
}
|
|
279
276
|
throw new RuntimeError("INVALID_ARGS", "rescue requires a task description.", "rescue.parse");
|
|
280
277
|
}
|
|
278
|
+
function stripRescuePromptPrefix(prompt) {
|
|
279
|
+
return prompt.startsWith(RESCUE_PROMPT_PREFIX) ? prompt.slice(RESCUE_PROMPT_PREFIX.length) : prompt;
|
|
280
|
+
}
|
|
281
281
|
async function resolveRescueSession(store, repoId, prompt, fresh, resume, resumeTarget) {
|
|
282
282
|
if (fresh) {
|
|
283
283
|
return { kimiSessionId: null, reusedSession: false, resumedFromJobId: null, sourceJob: null };
|