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
|
@@ -17,40 +17,21 @@ import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.j
|
|
|
17
17
|
import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
|
|
18
18
|
import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
|
|
19
19
|
import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
|
|
20
|
-
// /kimi:swarm —
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY on kimi-code 0.18.0+ (PR #888; older
|
|
29
|
-
// binaries ignore the unknown env var).
|
|
30
|
-
// - READ-ONLY. The PreToolUse hook runs under the "swarm" label, which
|
|
31
|
-
// allowlists the read-only tool set PLUS the AgentSwarm tool. Every
|
|
32
|
-
// subagent inherits that label and fires the SAME hook at policy index 0
|
|
33
|
-
// (kimi-code createPermissionDecisionPolicies puts the hook at index 0 for
|
|
34
|
-
// ALL agents — verified against 0.12.0), so a subagent's Write/Edit/Bash is
|
|
35
|
-
// denied exactly like a single-turn review's.
|
|
36
|
-
// - Reuses the REVIEW job lineage (command_type "review") so /kimi:status /
|
|
37
|
-
// /kimi:result / /kimi:cancel work unchanged. The hook label ("swarm") is
|
|
38
|
-
// independent of the job lineage — mirrors how /kimi:pursue reuses the
|
|
39
|
-
// rescue lineage with its own label. Promoting swarm to a first-class
|
|
40
|
-
// command_type is a follow-up.
|
|
41
|
-
//
|
|
42
|
-
// SAFETY — swarm REFUSES without the hook, like every model-spawning command.
|
|
43
|
-
// A swarm fans out up to `cap` subagents, each capable of attempting writes;
|
|
44
|
-
// without the index-0 hook there is ZERO enforcement on ALL of them — an
|
|
45
|
-
// N-fold blast radius. KIMI_PLUGIN_CC_SKIP_HOOK_CHECK remains an explicit,
|
|
46
|
-
// tests/diagnostics-only bypass of that refusal.
|
|
20
|
+
// /kimi:swarm — parallel fan-out, read-only by default and runtime-foreground-only.
|
|
21
|
+
// A finite wall-clock budget bounds the whole run. --cap is a SOFT total-count
|
|
22
|
+
// prompt hint; --max-concurrency is a HARD peak ceiling enforced on kimi-code
|
|
23
|
+
// 0.18.0+. The plugin always exports that setting; older read-only swarms retain
|
|
24
|
+
// only the wall-clock hard bound. Read/write modes reuse review/rescue lineage,
|
|
25
|
+
// and the mandatory PreToolUse hook enforces read-only policy or trusted-worktree
|
|
26
|
+
// write confinement on the coordinator and every subagent. Write mode requires
|
|
27
|
+
// kimi-code 0.18.0+.
|
|
47
28
|
const SWARM_SUMMARY_MAX = 120;
|
|
48
29
|
const SWARM_AGENT_PROFILE = "<swarm>";
|
|
49
30
|
const SWARM_WRITE_AGENT_PROFILE = "<swarm-write>";
|
|
50
31
|
/**
|
|
51
32
|
* Orphaned write-swarm worktrees older than this are reaped by the startup sweep.
|
|
52
|
-
* Every normal run removes its own worktree in a finally;
|
|
53
|
-
*
|
|
33
|
+
* Every normal run removes its own worktree in a finally; orphans can follow a
|
|
34
|
+
* hard kill or failed cleanup.
|
|
54
35
|
*
|
|
55
36
|
* The sweep's only liveness signal is the worktree dir's mtime, which is frozen
|
|
56
37
|
* at `git worktree add` time when a run edits only EXISTING files (in-place edits
|
|
@@ -64,15 +45,11 @@ const SWARM_WRITE_AGENT_PROFILE = "<swarm-write>";
|
|
|
64
45
|
*/
|
|
65
46
|
export const WORKTREE_ORPHAN_TTL_MS = MAX_DURATION_MS + 2 * 60 * 60 * 1000;
|
|
66
47
|
/**
|
|
67
|
-
* Default
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* binaries ignore it) so simultaneous model spend is capped BY CONSTRUCTION for
|
|
73
|
-
* every swarm run — agent-dispatched or human-typed. Conservative by design;
|
|
74
|
-
* override with an explicit `--max-concurrency N`. The `--budget` wall-clock
|
|
75
|
-
* ceiling remains the always-on hard bound on TOTAL cost; this bounds the peak.
|
|
48
|
+
* Default concurrency setting when the user passes no `--max-concurrency`.
|
|
49
|
+
* The plugin always exports KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY; kimi-code
|
|
50
|
+
* 0.18.0+ enforces it as a hard peak ceiling, while older read-only swarms
|
|
51
|
+
* ignore it and retain only the wall-clock hard bound. Conservative by design;
|
|
52
|
+
* override with an explicit `--max-concurrency N`.
|
|
76
53
|
*/
|
|
77
54
|
export const SWARM_DEFAULT_MAX_CONCURRENCY = 4;
|
|
78
55
|
/**
|
|
@@ -115,23 +92,26 @@ export function resolveSwarmWriteMaxConcurrency(requested) {
|
|
|
115
92
|
*/
|
|
116
93
|
export function buildSwarmPrompt(objective, cap) {
|
|
117
94
|
const trimmed = objective.trim();
|
|
118
|
-
const capClause = cap !== undefined
|
|
95
|
+
const capClause = cap !== undefined
|
|
96
|
+
? `Use at most ${cap} subagents in total across the whole run (soft cap); split or group targets to stay within it.`
|
|
97
|
+
: undefined;
|
|
119
98
|
return [
|
|
120
99
|
"Review the following read-only: inspect and report; do not modify anything.",
|
|
121
100
|
"",
|
|
122
101
|
`Objective: ${trimmed}`,
|
|
123
102
|
"",
|
|
124
|
-
"
|
|
125
|
-
"are denied. Launch the parallel fan-out as your FIRST action;
|
|
126
|
-
"commands to size or list files first (the subagents inspect the
|
|
127
|
-
"
|
|
103
|
+
"Use the allowed read-only tools, primarily Read/Grep/Glob and ReadMediaFile where relevant;",
|
|
104
|
+
"shell/Bash and every write/edit tool are denied. Launch the parallel fan-out as your FIRST action;",
|
|
105
|
+
"do NOT run preliminary shell commands to size or list files first (the subagents inspect the",
|
|
106
|
+
"workspace directly with the allowed read-only tools).",
|
|
128
107
|
"Fan the work out across the relevant targets in parallel using your own subagent capability,",
|
|
129
108
|
"as you judge best — split or group the work however fits the objective.",
|
|
130
109
|
...(capClause ? [capClause] : []),
|
|
131
110
|
"",
|
|
132
|
-
"Consolidate
|
|
133
|
-
"
|
|
134
|
-
"
|
|
111
|
+
"Consolidate into a single markdown report: deduplicate findings, preserve coverage across",
|
|
112
|
+
"every target, and explicitly mention targets with no findings rather than dropping them.",
|
|
113
|
+
"Include a short verdict line, a brief summary, then findings as `file:line — issue — severity`.",
|
|
114
|
+
"Return plain markdown — no JSON wrapper, no outer code fences.",
|
|
135
115
|
].join("\n");
|
|
136
116
|
}
|
|
137
117
|
/**
|
|
@@ -145,7 +125,7 @@ export function buildSwarmPrompt(objective, cap) {
|
|
|
145
125
|
export function buildSwarmWritePrompt(objective, cap) {
|
|
146
126
|
const trimmed = objective.trim();
|
|
147
127
|
const capClause = cap !== undefined
|
|
148
|
-
? `Launch at most ${cap} subagents (soft cap — split or group targets to stay within it).`
|
|
128
|
+
? `Launch at most ${cap} subagents in total across the whole run (soft cap — split or group targets to stay within it).`
|
|
149
129
|
: "Launch one subagent per distinct target; group related targets if there are many.";
|
|
150
130
|
return [
|
|
151
131
|
"Coordinate a parallel CODE-EDITING task using the AgentSwarm tool.",
|
|
@@ -155,17 +135,19 @@ export function buildSwarmWritePrompt(objective, cap) {
|
|
|
155
135
|
"You are running inside a throwaway git worktree dedicated to this task — edit freely;",
|
|
156
136
|
"your changes will be captured as a patch for the human to review and apply. Use the",
|
|
157
137
|
'AgentSwarm tool to fan the work out across subagents. Set subagent_type to "coder" (the',
|
|
158
|
-
"write-capable profile). Give each subagent ONE
|
|
159
|
-
"
|
|
160
|
-
"subagents editing the same file will corrupt the result).",
|
|
138
|
+
"write-capable profile). Give each subagent ONE target via prompt_template + items: a single",
|
|
139
|
+
"file or module per subagent.",
|
|
161
140
|
capClause,
|
|
162
141
|
"",
|
|
163
|
-
"
|
|
164
|
-
"-
|
|
165
|
-
" write is denied.",
|
|
142
|
+
"Safety rules enforced by the PreToolUse hook (violations are denied):",
|
|
143
|
+
"- Writes are confined to this worktree; an out-of-worktree write is denied.",
|
|
166
144
|
"- Do NOT run any git command that mutates state (no add/commit/checkout/etc.) — git mutation",
|
|
167
145
|
" is denied. The human owns all git operations.",
|
|
168
|
-
"
|
|
146
|
+
"",
|
|
147
|
+
"Coordination rules (prompt-only; the hook does NOT enforce them):",
|
|
148
|
+
"- Keep every assigned target DISJOINT: no overlap between subagents (two subagents editing",
|
|
149
|
+
" the same file will corrupt the result).",
|
|
150
|
+
"- Subagents must NOT call AgentSwarm (no nested fan-out). One flat generation only.",
|
|
169
151
|
"- Report exactly what you changed and why.",
|
|
170
152
|
"",
|
|
171
153
|
"After the subagents return, consolidate into a single markdown report: a short verdict line,",
|
|
@@ -184,7 +166,7 @@ export async function runSwarm(argv, context) {
|
|
|
184
166
|
? runWriteSwarm(parsed, objective, context)
|
|
185
167
|
: runReadSwarm(parsed, objective, context);
|
|
186
168
|
}
|
|
187
|
-
/**
|
|
169
|
+
/** Read-only swarm (default). */
|
|
188
170
|
async function runReadSwarm(parsed, objective, context) {
|
|
189
171
|
const paths = resolvePluginPaths(context.env);
|
|
190
172
|
await ensurePluginPaths(paths);
|
|
@@ -252,9 +234,9 @@ async function runReadSwarm(parsed, objective, context) {
|
|
|
252
234
|
* WRITE-capable swarm (--write, v1.4). Spawns the coordinator in an ephemeral
|
|
253
235
|
* throwaway worktree off HEAD; `coder` subagents edit there; writes are confined
|
|
254
236
|
* by the swarm-write hook label; the result is captured as a patch the main
|
|
255
|
-
* thread reviews + applies. The worktree is
|
|
256
|
-
*
|
|
257
|
-
*
|
|
237
|
+
* thread reviews + applies. The worktree is removed after successful patch
|
|
238
|
+
* capture; capture failure retains it and reports its path for recovery.
|
|
239
|
+
* Reuses the rescue (write-capable) job lineage.
|
|
258
240
|
*/
|
|
259
241
|
async function runWriteSwarm(parsed, objective, context) {
|
|
260
242
|
const paths = resolvePluginPaths(context.env);
|
|
@@ -290,6 +272,7 @@ async function runWriteSwarm(parsed, objective, context) {
|
|
|
290
272
|
? error
|
|
291
273
|
: new RuntimeError("GIT_WORKTREE_ADD_FAILED", String(error), "swarm.worktree"));
|
|
292
274
|
}
|
|
275
|
+
let preserveWorktree = false;
|
|
293
276
|
try {
|
|
294
277
|
// Prefix args can contain relative wrapper/script paths. Probe from the
|
|
295
278
|
// exact spawn cwd so the certified command tuple and its effective target
|
|
@@ -341,16 +324,30 @@ async function runWriteSwarm(parsed, objective, context) {
|
|
|
341
324
|
});
|
|
342
325
|
throw classified;
|
|
343
326
|
}
|
|
344
|
-
const completed = await executeSwarmJob(job.job_id, prompt, objective, job.budget_ms, resolveSwarmWriteMaxConcurrency(parsed.maxConcurrency), context, {
|
|
327
|
+
const completed = await executeSwarmJob(job.job_id, prompt, objective, job.budget_ms, resolveSwarmWriteMaxConcurrency(parsed.maxConcurrency), context, {
|
|
328
|
+
spawnCwd: worktreePath,
|
|
329
|
+
workspaceRoot: worktreePath,
|
|
330
|
+
onPatchCaptureFailure: () => {
|
|
331
|
+
preserveWorktree = true;
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
if (preserveWorktree && completed.error) {
|
|
335
|
+
throw new RuntimeError(completed.error.code, completed.error.message, completed.error.stage, {
|
|
336
|
+
details: completed.error.details,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
345
339
|
if (!completed.final_output_path) {
|
|
346
340
|
throw new RuntimeError("SWARM_RESULT_MISSING", "Swarm (write) finished without a rendered result.", "swarm.result");
|
|
347
341
|
}
|
|
348
342
|
return readArtifact(completed.final_output_path);
|
|
349
343
|
}
|
|
350
344
|
finally {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
345
|
+
if (preserveWorktree) {
|
|
346
|
+
context.stderr.write(`[kimi-plugin-cc] swarm-write patch capture failed; worktree retained at ${worktreePath}.\n`);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
await removeWorktree(repoIdentity.repoRoot, worktreePath);
|
|
350
|
+
}
|
|
354
351
|
}
|
|
355
352
|
}
|
|
356
353
|
finally {
|
|
@@ -365,9 +362,9 @@ function assertWriteSwarmPreconditions(repoIdentity) {
|
|
|
365
362
|
}
|
|
366
363
|
/**
|
|
367
364
|
* Reap orphaned write-swarm worktrees from prior crashed runs. Normal runs remove
|
|
368
|
-
* their own worktree in a finally;
|
|
369
|
-
* stale admin entries (current repo) then force-
|
|
370
|
-
* older than the TTL. Best-effort — never blocks the run.
|
|
365
|
+
* their own worktree in a finally; orphans can follow hard kills or failed cleanup.
|
|
366
|
+
* Prune stale admin entries (current repo), then force-remove on-disk worktree
|
|
367
|
+
* dirs older than the TTL. Best-effort — never blocks the run.
|
|
371
368
|
*/
|
|
372
369
|
async function sweepStaleWorktrees(paths, repoIdentity) {
|
|
373
370
|
await pruneWorktrees(repoIdentity.repoRoot);
|
|
@@ -399,7 +396,7 @@ async function sweepStaleWorktrees(paths, repoIdentity) {
|
|
|
399
396
|
* the artifact path + size, or null on capture failure. Called on EVERY terminal
|
|
400
397
|
* path so partial work survives a cancel/budget-expiry.
|
|
401
398
|
*/
|
|
402
|
-
async function capturePatchArtifact(paths, jobId, worktreePath, stderr) {
|
|
399
|
+
async function capturePatchArtifact(paths, jobId, worktreePath, stderr, onFailure) {
|
|
403
400
|
try {
|
|
404
401
|
const patch = await captureWorktreePatch(worktreePath);
|
|
405
402
|
const patchPath = path.join(paths.artifactsDir, `swarm-write-${jobId}.patch`);
|
|
@@ -407,6 +404,7 @@ async function capturePatchArtifact(paths, jobId, worktreePath, stderr) {
|
|
|
407
404
|
return { path: patchPath, bytes: Buffer.byteLength(patch, "utf8"), empty: patch.trim().length === 0 };
|
|
408
405
|
}
|
|
409
406
|
catch (error) {
|
|
407
|
+
onFailure?.();
|
|
410
408
|
stderr.write(`[kimi-plugin-cc] swarm-write patch capture failed for job ${jobId}: ${error.message ?? String(error)}\n`);
|
|
411
409
|
return null;
|
|
412
410
|
}
|
|
@@ -443,9 +441,9 @@ writeMode) {
|
|
|
443
441
|
store.close();
|
|
444
442
|
throw new RuntimeError("JOB_NOT_FOUND", `Swarm job ${jobId} was not found.`, "swarm.worker");
|
|
445
443
|
}
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
444
|
+
// Both modes fan out subagents and require the PreToolUse hook: read mode
|
|
445
|
+
// enforces read-only policy, while write mode confines writes to the trusted
|
|
446
|
+
// worktree. Fail closed before spawning.
|
|
449
447
|
if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK !== "1") {
|
|
450
448
|
const installStatus = await verifyHookInstalled(context.env);
|
|
451
449
|
if (!installStatus.installed) {
|
|
@@ -484,12 +482,10 @@ writeMode) {
|
|
|
484
482
|
// PLUS rescue-grade write confinement to the trusted worktree root. The
|
|
485
483
|
// label drives the hook for the coordinator AND every spawned subagent.
|
|
486
484
|
commandLabel: writeMode ? "swarm-write" : "swarm",
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
// --max-concurrency, so a fan-out is never unbounded. Distinct from --cap
|
|
492
|
-
// (the soft total-count prompt hint).
|
|
485
|
+
// Always exported as KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY. kimi-code
|
|
486
|
+
// 0.18.0+ enforces this hard peak ceiling; older read-only binaries ignore
|
|
487
|
+
// it and retain only the wall-clock hard bound. Write mode requires
|
|
488
|
+
// 0.18.0+. Distinct from --cap, the soft total-count prompt hint.
|
|
493
489
|
swarmMaxConcurrency: maxConcurrency,
|
|
494
490
|
// Trusted allowlist root for the swarm-write hook (forge-proof; ignored
|
|
495
491
|
// by other labels). Only set in write mode.
|
|
@@ -520,12 +516,14 @@ writeMode) {
|
|
|
520
516
|
});
|
|
521
517
|
warnIfSessionIdMissing(result, "swarm", job.job_id, context.stderr);
|
|
522
518
|
const prose = reassembleProseFromRecords(result.records);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
519
|
+
let finalText = prose;
|
|
520
|
+
if (writeMode) {
|
|
521
|
+
const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr, writeMode.onPatchCaptureFailure);
|
|
522
|
+
if (captured === null) {
|
|
523
|
+
throw new RuntimeError("SWARM_PATCH_CAPTURE_FAILED", `Swarm patch capture failed; worktree retained at ${writeMode.workspaceRoot}.`, "swarm.patch-capture", { details: { worktreePath: writeMode.workspaceRoot } });
|
|
524
|
+
}
|
|
525
|
+
finalText = `${renderWriteSwarmHeader(captured)}\n\n---\n\n${prose}`.trimEnd();
|
|
526
|
+
}
|
|
529
527
|
const rendered = renderManagedJobOutput(job, finalText);
|
|
530
528
|
let artifactPath;
|
|
531
529
|
try {
|
|
@@ -543,24 +541,29 @@ writeMode) {
|
|
|
543
541
|
throw new RuntimeError("SWARM_CANCELLED", "Swarm cancelled after artifact write.", "swarm.runtime");
|
|
544
542
|
}
|
|
545
543
|
return (store.markCompleted(job.job_id, {
|
|
546
|
-
summary: `[swarm] ${shorten(objective, SWARM_SUMMARY_MAX)}`,
|
|
544
|
+
summary: `[${writeMode ? "swarm-write" : "swarm"}] ${shorten(objective, SWARM_SUMMARY_MAX)}`,
|
|
547
545
|
phase: "done",
|
|
548
546
|
final_output_path: artifactPath,
|
|
549
547
|
error: null,
|
|
550
548
|
}) ?? job);
|
|
551
549
|
}
|
|
552
550
|
catch (error) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr);
|
|
551
|
+
let captureFailed = writeMode !== undefined && error instanceof RuntimeError && error.code === "SWARM_PATCH_CAPTURE_FAILED";
|
|
552
|
+
if (writeMode && !(error instanceof RuntimeError && error.code === "SWARM_PATCH_CAPTURE_FAILED")) {
|
|
553
|
+
const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr, writeMode.onPatchCaptureFailure);
|
|
554
|
+
captureFailed = captured === null;
|
|
558
555
|
if (captured && !captured.empty) {
|
|
559
556
|
context.stderr.write(`[kimi-plugin-cc] swarm-write run ended early; partial patch captured at ${captured.path}.\n`);
|
|
560
557
|
}
|
|
561
558
|
}
|
|
559
|
+
const recoveryDetails = captureFailed && writeMode ? { worktreePath: writeMode.workspaceRoot } : {};
|
|
562
560
|
if (handlers.cancelling) {
|
|
563
|
-
const cancelledError = new RuntimeError("SWARM_CANCELLED",
|
|
561
|
+
const cancelledError = new RuntimeError("SWARM_CANCELLED", captureFailed && writeMode
|
|
562
|
+
? `Swarm cancelled; patch capture failed and worktree retained at ${writeMode.workspaceRoot}.`
|
|
563
|
+
: "Swarm cancelled by user request.", "swarm.runtime", {
|
|
564
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
565
|
+
details: recoveryDetails,
|
|
566
|
+
});
|
|
564
567
|
return await markJobCancelled(store, paths, job, "Swarm cancelled by user request.", cancelledError, {
|
|
565
568
|
phase: "cancelled",
|
|
566
569
|
});
|
|
@@ -568,6 +571,18 @@ writeMode) {
|
|
|
568
571
|
const classified = classifyManagedCommandFailure(error, writeMode ? "rescue" : "review", job.job_id, {
|
|
569
572
|
preserveStage: true,
|
|
570
573
|
});
|
|
574
|
+
if (captureFailed && writeMode) {
|
|
575
|
+
const withRecovery = classified instanceof RuntimeError
|
|
576
|
+
? new RuntimeError(classified.code, classified.message, classified.stage, {
|
|
577
|
+
cause: classified,
|
|
578
|
+
details: { ...classified.details, ...recoveryDetails },
|
|
579
|
+
})
|
|
580
|
+
: new RuntimeError("SWARM_PATCH_CAPTURE_FAILED", classified.message, "swarm.patch-capture", {
|
|
581
|
+
cause: classified,
|
|
582
|
+
details: recoveryDetails,
|
|
583
|
+
});
|
|
584
|
+
return await markJobFailed(store, paths, job, withRecovery, "Swarm failed.", { phase: "failed" });
|
|
585
|
+
}
|
|
571
586
|
return await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
|
|
572
587
|
}
|
|
573
588
|
finally {
|
|
@@ -16,12 +16,6 @@ export async function runWorker(argv, context) {
|
|
|
16
16
|
await executeRescueJob(jobId, prompt, context, { workerPid: process.pid, reusedSession });
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
// kind === "ask"
|
|
20
|
-
//
|
|
21
|
-
// v1.0 note: the ask worker no longer threads `reusedSession` or
|
|
22
|
-
// `rawPrompt` into executeAskJob — both were inputs to v0.4's
|
|
23
|
-
// `announceSessionTitle`, which is gone (kimi-web has no PATCH
|
|
24
|
-
// endpoint). The base64-encoded prompt + job id are sufficient.
|
|
25
19
|
const encodedPrompt = context.env.KIMI_PLUGIN_CC_ASK_PROMPT_B64;
|
|
26
20
|
if (!encodedPrompt) {
|
|
27
21
|
throw new RuntimeError("MISSING_ASK_PROMPT", "Background ask prompt is missing.", "worker");
|
|
@@ -22,6 +22,8 @@ export async function collectReviewContext(cwd, base) {
|
|
|
22
22
|
repoRoot,
|
|
23
23
|
statusSummary,
|
|
24
24
|
diffText: truncateDiff(diffText),
|
|
25
|
+
diffTruncated: diffText.length > MAX_DIFF_CHARS,
|
|
26
|
+
scope: "base",
|
|
25
27
|
targetDescription: `branch diff against ${base}`,
|
|
26
28
|
};
|
|
27
29
|
}
|
|
@@ -35,6 +37,8 @@ export async function collectReviewContext(cwd, base) {
|
|
|
35
37
|
repoRoot,
|
|
36
38
|
statusSummary,
|
|
37
39
|
diffText: truncateDiff(diffText),
|
|
40
|
+
diffTruncated: diffText.length > MAX_DIFF_CHARS,
|
|
41
|
+
scope: "working-tree",
|
|
38
42
|
targetDescription: "current working tree changes",
|
|
39
43
|
};
|
|
40
44
|
}
|
|
@@ -72,7 +76,7 @@ async function getBaseDiff(cwd, base) {
|
|
|
72
76
|
if (threeDot.ok) {
|
|
73
77
|
return threeDot.stdout;
|
|
74
78
|
}
|
|
75
|
-
return runGit(["diff", "--no-ext-diff", "--unified=3", "--end-of-options", base, "--"], cwd);
|
|
79
|
+
return runGit(["diff", "--no-ext-diff", "--unified=3", "--end-of-options", base, "HEAD", "--"], cwd);
|
|
76
80
|
}
|
|
77
81
|
async function runGit(args, cwd) {
|
|
78
82
|
const result = await runGitAllowFailure(args, cwd);
|
|
@@ -109,7 +113,7 @@ function sha256(value) {
|
|
|
109
113
|
return createHash("sha256").update(value).digest("hex");
|
|
110
114
|
}
|
|
111
115
|
// ---------------------------------------------------------------------
|
|
112
|
-
// Ephemeral worktree lifecycle — write-swarm
|
|
116
|
+
// Ephemeral worktree lifecycle — write-swarm.
|
|
113
117
|
//
|
|
114
118
|
// A write-swarm spawns its coordinator `kimi -p` with cwd = a throwaway
|
|
115
119
|
// detached worktree off the user's HEAD, so `coder` subagents edit there and
|
|
@@ -117,7 +121,7 @@ function sha256(value) {
|
|
|
117
121
|
// working tree + index are isolated and disposable, but it SHARES the main
|
|
118
122
|
// repo's object DB and refs. The PreToolUse hook (swarm-write label) confines
|
|
119
123
|
// every subagent write to this worktree; the plugin captures the result as a
|
|
120
|
-
// patch and removes the worktree. See
|
|
124
|
+
// patch and removes the worktree. See docs/safety.md.
|
|
121
125
|
// ---------------------------------------------------------------------
|
|
122
126
|
/**
|
|
123
127
|
* True if the repo at `cwd` has a born HEAD (at least one commit). `git worktree
|
|
@@ -160,10 +164,13 @@ export async function createEphemeralWorktree(repoRoot, base, worktreePath) {
|
|
|
160
164
|
* Returns the patch text (empty string if the swarm changed nothing).
|
|
161
165
|
*/
|
|
162
166
|
export async function captureWorktreePatch(worktreePath) {
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
await runGitAllowFailure(["add", "-N", "--", "."], worktreePath);
|
|
167
|
+
// Intent-to-add is required so untracked files surface in `git diff HEAD`.
|
|
168
|
+
// An add -N failure aborts capture rather than returning a patch that omits
|
|
169
|
+
// new files.
|
|
170
|
+
const intentToAdd = await runGitAllowFailure(["add", "-N", "--", "."], worktreePath);
|
|
171
|
+
if (!intentToAdd.ok) {
|
|
172
|
+
throw new RuntimeError("GIT_WORKTREE_INTENT_TO_ADD_FAILED", `git add -N (worktree capture) failed: ${intentToAdd.stderr || "unknown error"}`, "swarm.worktree", { details: { worktreePath } });
|
|
173
|
+
}
|
|
167
174
|
try {
|
|
168
175
|
const { stdout } = await execFileAsync("git", ["diff", "--binary", "HEAD", "--"], {
|
|
169
176
|
cwd: worktreePath,
|
|
@@ -5,12 +5,10 @@
|
|
|
5
5
|
//
|
|
6
6
|
// [[hooks]]
|
|
7
7
|
// event = "PreToolUse"
|
|
8
|
-
// command = "node /abs/path/to/dist/hooks/approval-hook.js"
|
|
8
|
+
// command = "'/abs/path/to/node' '/abs/path/to/dist/hooks/approval-hook.js'"
|
|
9
9
|
// timeout = 15
|
|
10
10
|
//
|
|
11
|
-
// Hook protocol
|
|
12
|
-
// path was agent/hooks/ in 0.4.0 and earlier; relocated to session/hooks/
|
|
13
|
-
// in 0.5.0 with byte-identical file contents):
|
|
11
|
+
// Hook protocol:
|
|
14
12
|
//
|
|
15
13
|
// stdin: full hook payload as JSON
|
|
16
14
|
// stdout: optional structured JSON (only consulted when exit code is 0)
|
|
@@ -19,23 +17,14 @@
|
|
|
19
17
|
// exit 2: deny — `stderr.trim()` becomes the reason surfaced to the model
|
|
20
18
|
// any other exit code: fall through to allow (fail-open)
|
|
21
19
|
//
|
|
22
|
-
//
|
|
20
|
+
// Exit 2 + stderr avoids structured-output version-skew hazards, and the
|
|
21
|
+
// runner short-circuits it before reading structured output. Because any other
|
|
22
|
+
// exit fails open upstream, every code path here is wrapped so an unexpected
|
|
23
|
+
// error exits 2 with a "hook misconfigured" reason instead.
|
|
23
24
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// called, so emitting both would be redundant.
|
|
28
|
-
// 3. A crash in our hook would normally exit non-zero and silently
|
|
29
|
-
// allow (fail-open). We compensate by wrapping every code path in
|
|
30
|
-
// try/catch and exiting 2 with a "hook misconfigured" reason on
|
|
31
|
-
// any unexpected error.
|
|
32
|
-
//
|
|
33
|
-
// Process boundary:
|
|
34
|
-
//
|
|
35
|
-
// kimi-code spawns this hook with `shell: true` (see runner.ts:46-53,
|
|
36
|
-
// where `spawn(command, { shell: true, ... })` is invoked).
|
|
37
|
-
// The TOML command is parsed by `/bin/sh -c "..."` so the resolved
|
|
38
|
-
// `node /abs/path/...` works regardless of where it's invoked from.
|
|
25
|
+
// kimi-code spawns the command with `shell: true`. The canonical command is two
|
|
26
|
+
// shell-quoted absolute tokens — the resolved Node binary and hook script — so
|
|
27
|
+
// `/bin/sh -c` does not depend on its working directory or PATH.
|
|
39
28
|
import { decideHookOutcome } from "./approval-policy.js";
|
|
40
29
|
import { evaluateRescueHookRequest } from "../rescue-approval.js";
|
|
41
30
|
const STDIN_TIMEOUT_MS = 5_000;
|