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.
Files changed (91) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +5 -5
  5. package/agents/kimi-challenge.md +3 -3
  6. package/agents/kimi-pursue.md +1 -1
  7. package/agents/kimi-rescue.md +2 -2
  8. package/agents/kimi-review.md +3 -3
  9. package/commands/README.md +2 -2
  10. package/commands/challenge.md +1 -1
  11. package/commands/pursue.md +1 -1
  12. package/commands/review.md +1 -1
  13. package/dist/background-spawn.js +9 -17
  14. package/dist/cli-client.js +22 -62
  15. package/dist/commands/ask.js +24 -29
  16. package/dist/commands/cancel.js +7 -19
  17. package/dist/commands/cli-helpers.js +9 -17
  18. package/dist/commands/pursue.js +21 -31
  19. package/dist/commands/registry.js +6 -19
  20. package/dist/commands/rescue.js +20 -20
  21. package/dist/commands/review-gate.js +16 -24
  22. package/dist/commands/review.js +37 -43
  23. package/dist/commands/setup.js +85 -171
  24. package/dist/commands/swarm.js +103 -88
  25. package/dist/commands/worker.js +0 -6
  26. package/dist/git.js +14 -7
  27. package/dist/hooks/approval-hook.js +9 -20
  28. package/dist/hooks/approval-policy.js +36 -69
  29. package/dist/hooks/config-safety.js +7 -4
  30. package/dist/hooks/install-paths.js +62 -138
  31. package/dist/hooks/install.js +37 -108
  32. package/dist/hooks/managed-block.js +88 -214
  33. package/dist/job-store.js +6 -28
  34. package/dist/kimi-command.js +1 -3
  35. package/dist/kimi-engine.js +2 -1
  36. package/dist/kimi-timeouts.js +9 -21
  37. package/dist/kimi-version-probe.js +16 -1210
  38. package/dist/native-v2-preflight.js +1 -1
  39. package/dist/parsing.js +4 -18
  40. package/dist/rescue-approval.js +78 -164
  41. package/dist/session-recovery.js +7 -10
  42. package/dist/stream-json.js +22 -174
  43. package/dist/version.js +1 -1
  44. package/docs/migration.md +2 -6
  45. package/docs/registry-releases.md +25 -5
  46. package/docs/safety.md +16 -12
  47. package/package.json +2 -2
  48. package/plugins/kimi-codex/.codex-plugin/plugin.json +4 -4
  49. package/plugins/kimi-codex/dist/background-spawn.js +9 -17
  50. package/plugins/kimi-codex/dist/cli-client.js +22 -62
  51. package/plugins/kimi-codex/dist/commands/ask.js +24 -29
  52. package/plugins/kimi-codex/dist/commands/cancel.js +7 -19
  53. package/plugins/kimi-codex/dist/commands/cli-helpers.js +9 -17
  54. package/plugins/kimi-codex/dist/commands/pursue.js +21 -31
  55. package/plugins/kimi-codex/dist/commands/registry.js +6 -19
  56. package/plugins/kimi-codex/dist/commands/rescue.js +20 -20
  57. package/plugins/kimi-codex/dist/commands/review-gate.js +16 -24
  58. package/plugins/kimi-codex/dist/commands/review.js +37 -43
  59. package/plugins/kimi-codex/dist/commands/setup.js +85 -171
  60. package/plugins/kimi-codex/dist/commands/swarm.js +103 -88
  61. package/plugins/kimi-codex/dist/commands/worker.js +0 -6
  62. package/plugins/kimi-codex/dist/git.js +14 -7
  63. package/plugins/kimi-codex/dist/hooks/approval-hook.js +9 -20
  64. package/plugins/kimi-codex/dist/hooks/approval-policy.js +36 -69
  65. package/plugins/kimi-codex/dist/hooks/config-safety.js +7 -4
  66. package/plugins/kimi-codex/dist/hooks/install-paths.js +62 -138
  67. package/plugins/kimi-codex/dist/hooks/install.js +37 -108
  68. package/plugins/kimi-codex/dist/hooks/managed-block.js +88 -214
  69. package/plugins/kimi-codex/dist/job-store.js +6 -28
  70. package/plugins/kimi-codex/dist/kimi-command.js +1 -3
  71. package/plugins/kimi-codex/dist/kimi-engine.js +2 -1
  72. package/plugins/kimi-codex/dist/kimi-timeouts.js +9 -21
  73. package/plugins/kimi-codex/dist/kimi-version-probe.js +16 -1210
  74. package/plugins/kimi-codex/dist/native-v2-preflight.js +1 -1
  75. package/plugins/kimi-codex/dist/parsing.js +4 -18
  76. package/plugins/kimi-codex/dist/rescue-approval.js +78 -164
  77. package/plugins/kimi-codex/dist/session-recovery.js +7 -10
  78. package/plugins/kimi-codex/dist/stream-json.js +22 -174
  79. package/plugins/kimi-codex/dist/version.js +1 -1
  80. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +2 -2
  81. package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +1 -1
  82. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +1 -1
  83. package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +1 -1
  84. package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +1 -1
  85. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +1 -1
  86. package/plugins/kimi-codex/skills/kimi-result/SKILL.md +1 -1
  87. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +3 -3
  88. package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +1 -1
  89. package/plugins/kimi-codex/skills/kimi-status/SKILL.md +1 -1
  90. package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +1 -1
  91. 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 — READ-ONLY parallel fan-out (kimi-code 0.12.0 AgentSwarm tool).
21
- //
22
- // PROTOTYPE SCOPE (v1.2). The lowest-risk swarm shape: parallel review/analysis
23
- // over N targets, with NO write surface. Deliberately narrow:
24
- // - FOREGROUND ONLY. No --background/--detach. A hard wall-clock budget
25
- // (--budget) bounds the whole run. Two distinct subagent bounds: --cap is a
26
- // SOFT total-count hint injected into the prompt, and --max-concurrency is a
27
- // HARD ceiling on how many subagents run AT ONCE via
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; an orphan only survives
53
- * a hard kill (SIGKILL of the plugin process).
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 HARD concurrency ceiling applied when the user passes no
68
- * `--max-concurrency`. Since v1.3 `/kimi:swarm` is ALSO reachable via the
69
- * model-invocable `kimi-swarm` agent (Claude can auto-dispatch a fan-out), so an
70
- * unbounded peak-parallelism default is no longer acceptable: we always export
71
- * `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` (effective on kimi-code 0.18.0+; older
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 ? `Use at most ${cap} parallel subagents.` : 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
- "Only Read, Grep, and Glob are available in this mode — shell/Bash and every write/edit tool",
125
- "are denied. Launch the parallel fan-out as your FIRST action; do NOT run preliminary shell",
126
- "commands to size or list files first (the subagents inspect the workspace directly with",
127
- "Read/Grep/Glob).",
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 everything into a single markdown report: a short verdict line, a brief summary,",
133
- "then findings as `file:line — issue — severity`. Return plain markdown — no JSON wrapper, no",
134
- "outer code fences.",
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 distinct, DISJOINT target via prompt_template",
159
- "+ items: a single file or module per subagent, with NO overlap between subagents (two",
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
- "Hard rules for every subagent (the safety hook enforces these — violations are denied):",
164
- "- Edit ONLY your assigned target. Writes are confined to this worktree; an out-of-worktree",
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
- "- Do NOT call AgentSwarm yourself (no nested fan-out). One flat generation only.",
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
- /** READ-ONLY swarm (default). Unchanged from v1.2/v1.3. */
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 created here and removed on EVERY
256
- * terminal path (the patch is captured inside executeSwarmJob, before this
257
- * finally removes the tree). Reuses the rescue (write-capable) job lineage.
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, { spawnCwd: worktreePath, workspaceRoot: worktreePath });
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
- // Patch was captured inside executeSwarmJob (before this runs) on every
352
- // terminal path; --force discards the now-disposable worktree.
353
- await removeWorktree(repoIdentity.repoRoot, worktreePath);
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; an orphan only survives a hard kill. Prunes
369
- * stale admin entries (current repo) then force-removes on-disk worktree dirs
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
- // Swarm is read-only BY POLICY, but it fans out N subagents — without the
447
- // PreToolUse hook there is no enforcement on ANY of them. Fail closed before
448
- // spawning, matching the single-turn read-only commands.
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
- // The HARD concurrency ceiling on kimi-code 0.18.0+ (exported as
488
- // KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY; ignored by older binaries).
489
- // ALWAYS set: defaults to SWARM_DEFAULT_MAX_CONCURRENCY (read) /
490
- // SWARM_WRITE_DEFAULT_MAX_CONCURRENCY (write) when the user passes no
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
- // WRITE mode: capture the worktree change set as a .patch artifact (before
524
- // runWriteSwarm's finally removes the worktree) and prepend the patch-handoff
525
- // header so the consolidated report tells the main thread where the patch is.
526
- const finalText = writeMode
527
- ? `${renderWriteSwarmHeader(await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr))}\n\n---\n\n${prose}`.trimEnd()
528
- : prose;
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
- // WRITE mode: best-effort capture of any PARTIAL work before we mark the job
554
- // and before runWriteSwarm's finally removes the worktree, so a cancel /
555
- // budget-expiry doesn't silently discard edits the subagents already made.
556
- if (writeMode) {
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", "Swarm cancelled by user request.", "swarm.runtime", error instanceof Error ? { cause: error } : undefined);
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");
package/dist/git.js CHANGED
@@ -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 (v1.4).
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 .claude/docs/write-swarm-spec.md.
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
- // Best-effort intent-to-add so untracked files surface in `git diff HEAD`
164
- // (without -N, git diff ignores untracked files entirely). A failure here just
165
- // means new files are omitted from the diff, surfaced as a smaller patch.
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 (kimi-code agent-core/src/session/hooks/runner.ts —
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
- // We use exit-code signaling rather than structured JSON because:
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
- // 1. Exit 2 + stderr is the simpler contract — fewer surfaces for a
25
- // version skew between us and kimi-code to break safety.
26
- // 2. The runner short-circuits exit-2 before structuredOutput() is
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;