kimi-plugin-cc 2.4.4 → 2.5.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 (119) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +21 -0
  4. package/README.md +50 -34
  5. package/dist/commands/swarm.js +1 -1
  6. package/dist/dsh/companion.js +174 -0
  7. package/dist/dsh/index.js +156 -0
  8. package/dist/hooks/config-safety.js +3 -2
  9. package/dist/hooks/install-paths.js +2 -2
  10. package/dist/kimi-engine.js +1 -0
  11. package/dist/swarm-activation.js +1 -1
  12. package/dist/version.js +1 -1
  13. package/docs/migration.md +4 -4
  14. package/docs/models.md +5 -1
  15. package/docs/registry-releases.md +20 -17
  16. package/docs/safety.md +28 -15
  17. package/docs/swarm-recovery.md +1 -1
  18. package/docs/upstream-2.4.1-certification.md +54 -0
  19. package/package.json +41 -6
  20. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  21. package/plugins/kimi-codex/dist/commands/swarm.js +1 -1
  22. package/plugins/kimi-codex/dist/dsh/companion.js +174 -0
  23. package/plugins/kimi-codex/dist/dsh/index.js +156 -0
  24. package/plugins/kimi-codex/dist/hooks/config-safety.js +3 -2
  25. package/plugins/kimi-codex/dist/hooks/install-paths.js +2 -2
  26. package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
  27. package/plugins/kimi-codex/dist/swarm-activation.js +1 -1
  28. package/plugins/kimi-codex/dist/version.js +1 -1
  29. package/plugins/kimi-dsh/LICENSE +201 -0
  30. package/plugins/kimi-dsh/README.md +80 -0
  31. package/plugins/kimi-dsh/cordis.patch.yml +45 -0
  32. package/plugins/kimi-dsh/dist/background-spawn.js +216 -0
  33. package/plugins/kimi-dsh/dist/cli-cancellation.js +43 -0
  34. package/plugins/kimi-dsh/dist/cli-client.js +876 -0
  35. package/plugins/kimi-dsh/dist/commands/ask.js +336 -0
  36. package/plugins/kimi-dsh/dist/commands/cancel.js +216 -0
  37. package/plugins/kimi-dsh/dist/commands/cli-helpers.js +201 -0
  38. package/plugins/kimi-dsh/dist/commands/models.js +140 -0
  39. package/plugins/kimi-dsh/dist/commands/not-implemented.js +4 -0
  40. package/plugins/kimi-dsh/dist/commands/pursue.js +307 -0
  41. package/plugins/kimi-dsh/dist/commands/recover-swarm.js +135 -0
  42. package/plugins/kimi-dsh/dist/commands/registry.js +74 -0
  43. package/plugins/kimi-dsh/dist/commands/repair-sessions.js +160 -0
  44. package/plugins/kimi-dsh/dist/commands/replay.js +124 -0
  45. package/plugins/kimi-dsh/dist/commands/rescue.js +347 -0
  46. package/plugins/kimi-dsh/dist/commands/result.js +66 -0
  47. package/plugins/kimi-dsh/dist/commands/review-gate.js +390 -0
  48. package/plugins/kimi-dsh/dist/commands/review.js +220 -0
  49. package/plugins/kimi-dsh/dist/commands/setup.js +1047 -0
  50. package/plugins/kimi-dsh/dist/commands/status.js +27 -0
  51. package/plugins/kimi-dsh/dist/commands/swarm.js +754 -0
  52. package/plugins/kimi-dsh/dist/commands/worker.js +25 -0
  53. package/plugins/kimi-dsh/dist/companion.js +97 -0
  54. package/plugins/kimi-dsh/dist/config.js +22 -0
  55. package/plugins/kimi-dsh/dist/dsh/companion.js +174 -0
  56. package/plugins/kimi-dsh/dist/dsh/index.js +156 -0
  57. package/plugins/kimi-dsh/dist/errors.js +40 -0
  58. package/plugins/kimi-dsh/dist/git.js +224 -0
  59. package/plugins/kimi-dsh/dist/help.js +43 -0
  60. package/plugins/kimi-dsh/dist/hook-escape.js +71 -0
  61. package/plugins/kimi-dsh/dist/hooks/approval-hook.js +101 -0
  62. package/plugins/kimi-dsh/dist/hooks/approval-policy.js +211 -0
  63. package/plugins/kimi-dsh/dist/hooks/config-safety.js +653 -0
  64. package/plugins/kimi-dsh/dist/hooks/install-paths.js +428 -0
  65. package/plugins/kimi-dsh/dist/hooks/install.js +210 -0
  66. package/plugins/kimi-dsh/dist/hooks/managed-block.js +619 -0
  67. package/plugins/kimi-dsh/dist/hooks/review-gate-stop.js +19 -0
  68. package/plugins/kimi-dsh/dist/job-id.js +12 -0
  69. package/plugins/kimi-dsh/dist/job-store.js +659 -0
  70. package/plugins/kimi-dsh/dist/jobs.js +217 -0
  71. package/plugins/kimi-dsh/dist/kimi-command.js +70 -0
  72. package/plugins/kimi-dsh/dist/kimi-engine.js +632 -0
  73. package/plugins/kimi-dsh/dist/kimi-errors.js +260 -0
  74. package/plugins/kimi-dsh/dist/kimi-home.js +9 -0
  75. package/plugins/kimi-dsh/dist/kimi-timeouts.js +52 -0
  76. package/plugins/kimi-dsh/dist/kimi-version-probe.js +216 -0
  77. package/plugins/kimi-dsh/dist/logging.js +22 -0
  78. package/plugins/kimi-dsh/dist/mcp/binding.js +58 -0
  79. package/plugins/kimi-dsh/dist/mcp/job-control.js +345 -0
  80. package/plugins/kimi-dsh/dist/mcp/launcher.js +257 -0
  81. package/plugins/kimi-dsh/dist/mcp/operations.js +301 -0
  82. package/plugins/kimi-dsh/dist/mcp/protocol.js +275 -0
  83. package/plugins/kimi-dsh/dist/mcp/server.js +8 -0
  84. package/plugins/kimi-dsh/dist/mcp/stdio.js +78 -0
  85. package/plugins/kimi-dsh/dist/mcp/supervised-child.js +15 -0
  86. package/plugins/kimi-dsh/dist/mcp/tool-result.js +7 -0
  87. package/plugins/kimi-dsh/dist/mcp/workspace.js +34 -0
  88. package/plugins/kimi-dsh/dist/model-command.js +32 -0
  89. package/plugins/kimi-dsh/dist/native-v2-preflight.js +417 -0
  90. package/plugins/kimi-dsh/dist/package.json +1 -0
  91. package/plugins/kimi-dsh/dist/parsing.js +620 -0
  92. package/plugins/kimi-dsh/dist/paths.js +27 -0
  93. package/plugins/kimi-dsh/dist/plugin-data.js +98 -0
  94. package/plugins/kimi-dsh/dist/process-tree.js +479 -0
  95. package/plugins/kimi-dsh/dist/prompt-size.js +18 -0
  96. package/plugins/kimi-dsh/dist/render.js +177 -0
  97. package/plugins/kimi-dsh/dist/rescue-approval.js +865 -0
  98. package/plugins/kimi-dsh/dist/schemas/review-gate-output.js +43 -0
  99. package/plugins/kimi-dsh/dist/session-recovery.js +200 -0
  100. package/plugins/kimi-dsh/dist/session-title.js +293 -0
  101. package/plugins/kimi-dsh/dist/stream-json.js +346 -0
  102. package/plugins/kimi-dsh/dist/swarm-activation.js +834 -0
  103. package/plugins/kimi-dsh/dist/swarm-recovery.js +134 -0
  104. package/plugins/kimi-dsh/dist/types.js +1 -0
  105. package/plugins/kimi-dsh/dist/vendor/shell-quote/LICENSE +24 -0
  106. package/plugins/kimi-dsh/dist/vendor/shell-quote/index.js +1 -0
  107. package/plugins/kimi-dsh/dist/vendor/shell-quote/parse.js +229 -0
  108. package/plugins/kimi-dsh/dist/vendor/smol-toml/LICENSE +24 -0
  109. package/plugins/kimi-dsh/dist/vendor/smol-toml/date.js +156 -0
  110. package/plugins/kimi-dsh/dist/vendor/smol-toml/error.js +67 -0
  111. package/plugins/kimi-dsh/dist/vendor/smol-toml/extract.js +293 -0
  112. package/plugins/kimi-dsh/dist/vendor/smol-toml/parse.js +164 -0
  113. package/plugins/kimi-dsh/dist/vendor/smol-toml/primitive.js +151 -0
  114. package/plugins/kimi-dsh/dist/vendor/smol-toml/struct.js +156 -0
  115. package/plugins/kimi-dsh/dist/vendor/smol-toml/util.js +57 -0
  116. package/plugins/kimi-dsh/dist/version.js +7 -0
  117. package/plugins/kimi-dsh/package.json +48 -0
  118. package/plugins/kimi-dsh/scripts/companion.sh +37 -0
  119. package/plugins/kimi-dsh/scripts/review-gate-hook.sh +32 -0
@@ -0,0 +1,754 @@
1
+ import { resolveKimiHome } from "../kimi-home.js";
2
+ import { probeProcessIdentity } from "../process-tree.js";
3
+ import { recordRecoveryEvent, finishRecoveryRecord } from "../swarm-recovery.js";
4
+ import { allocateJobId } from "../job-id.js";
5
+ import { mkdir, writeFile } from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { createCliCancellationHandlers } from "../cli-cancellation.js";
8
+ import { ResponseTimeoutError, runCliPromptWithBudget } from "../cli-client.js";
9
+ import { deactivateSwarmSession, preactivateSwarmSession, requiresSwarmPreactivation, } from "../swarm-activation.js";
10
+ import { executionPlanFromPersisted, observedExecutionFields, persistedExecutionPlanFields, prepareKimiExecutionPlan, } from "../kimi-engine.js";
11
+ import { RuntimeError } from "../errors.js";
12
+ import { captureWorktreePatch, createEphemeralWorktree, hasBornHead, isWorkingTreeDirty, removeWorktree, resolveRepoIdentity, } from "../git.js";
13
+ import { digestPrompt, markJobCancelled, markJobFailed, sweepStaleJobs } from "../jobs.js";
14
+ import { JobStore } from "../job-store.js";
15
+ import { classifyManagedCommandFailure } from "../kimi-errors.js";
16
+ import { KIMI_SWARM_DEFAULT_BUDGET_MS } from "../kimi-timeouts.js";
17
+ import { writeInvocationLogHeader } from "../logging.js";
18
+ import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
19
+ import { parseSwarmArgs } from "../parsing.js";
20
+ import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
21
+ import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
22
+ import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
23
+ import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
24
+ // /kimi:swarm — parallel fan-out, read-only by default and runtime-foreground-only.
25
+ // A finite wall-clock budget bounds the whole run. --cap is a SOFT total-count
26
+ // prompt hint; --max-concurrency is a HARD peak ceiling enforced on kimi-code
27
+ // 0.18.0+. The plugin always exports that setting; older read-only swarms retain
28
+ // only the wall-clock hard bound. Read/write modes reuse review/rescue lineage,
29
+ // and the mandatory PreToolUse hook enforces read-only policy or trusted-worktree
30
+ // write confinement on the coordinator and every subagent. Write mode requires
31
+ // kimi-code 0.18.0+. Exact 2.3.2, 2.3.3, 2.4.0 and 2.4.1 additionally activate a same-job
32
+ // session through a bounded loopback web lifecycle, then resume it through the normal print path.
33
+ const SWARM_SUMMARY_MAX = 120;
34
+ const SWARM_AGENT_PROFILE = "<swarm>";
35
+ const SWARM_WRITE_AGENT_PROFILE = "<swarm-write>";
36
+ /**
37
+ * Default concurrency setting when the user passes no `--max-concurrency`.
38
+ * The plugin always exports KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY; kimi-code
39
+ * 0.18.0+ enforces it as a hard peak ceiling, while older read-only swarms
40
+ * ignore it and retain only the wall-clock hard bound. Conservative by design;
41
+ * override with an explicit `--max-concurrency N`.
42
+ */
43
+ export const SWARM_DEFAULT_MAX_CONCURRENCY = 4;
44
+ /**
45
+ * Resolve the effective hard concurrency ceiling: an explicit `--max-concurrency`
46
+ * wins; otherwise fall back to SWARM_DEFAULT_MAX_CONCURRENCY so the ceiling is
47
+ * never unset. Pure + exported for unit testing (the parser leaves the value
48
+ * undefined; swarm.ts owns the default, mirroring `budgetMs`).
49
+ */
50
+ export function resolveSwarmMaxConcurrency(requested) {
51
+ return requested ?? SWARM_DEFAULT_MAX_CONCURRENCY;
52
+ }
53
+ /**
54
+ * Default HARD concurrency ceiling for WRITE mode — 1 (serialized) vs read's 4.
55
+ * Writes are riskier than reads: disjoint-target partitioning is prompt-only and
56
+ * unenforceable (the hook is stateless), so two concurrent `coder` subagents
57
+ * racing the same file in the shared worktree would corrupt the patch. Serialize
58
+ * by default until a real-binary smoke proves clean concurrent disjoint writes;
59
+ * override with an explicit `--max-concurrency N`. Effective only on kimi-code
60
+ * 0.18.0+ (the write-mode version gate), so the ceiling is always enforced.
61
+ */
62
+ export const SWARM_WRITE_DEFAULT_MAX_CONCURRENCY = 1;
63
+ /** Resolve the write-mode concurrency ceiling: explicit wins, else default 1. */
64
+ export function resolveSwarmWriteMaxConcurrency(requested) {
65
+ return requested ?? SWARM_WRITE_DEFAULT_MAX_CONCURRENCY;
66
+ }
67
+ /**
68
+ * Build the swarm coordination prompt. States the READ-ONLY review GOAL and the
69
+ * desired report shape, then trusts the model to fan the work out with its own
70
+ * subagent capability — we deliberately do NOT prescribe the AgentSwarm tool call,
71
+ * subagent_type, or prompt_template/items mechanics. Over-prescribing those made
72
+ * the kimi-code 0.26.0 (k3) coordinator emit a non-sole-tool-call response that
73
+ * failed to launch (0 records, hang to budget); a goal-only prompt lets it launch.
74
+ * Read-only is NOT enforced by this prose — the PreToolUse hook (swarm label) at
75
+ * policy index 0 denies every write/edit/shell for the coordinator AND every
76
+ * subagent, so slimming this text does not move the safety boundary. The optional
77
+ * `cap` is a SOFT subagent-count hint (the hook cannot enforce a count). The HARD
78
+ * concurrency ceiling is separate: --max-concurrency →
79
+ * KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY (see executeSwarmJob / cli-client buildKimiCliEnvironment).
80
+ * The --budget wall-clock ceiling is the always-on hard bound on the whole run.
81
+ */
82
+ export function buildSwarmPrompt(objective, cap) {
83
+ const trimmed = objective.trim();
84
+ const capClause = cap !== undefined
85
+ ? `Use at most ${cap} subagents in total across the whole run (soft cap); split or group targets to stay within it.`
86
+ : undefined;
87
+ return [
88
+ "Review the following read-only: inspect and report; do not modify anything.",
89
+ "",
90
+ `Objective: ${trimmed}`,
91
+ "",
92
+ "Use the allowed read-only tools, primarily Read/Grep/Glob and ReadMediaFile where relevant;",
93
+ "shell/Bash and every write/edit tool are denied. Launch the parallel fan-out as your FIRST action;",
94
+ "do NOT run preliminary shell commands to size or list files first (the subagents inspect the",
95
+ "workspace directly with the allowed read-only tools).",
96
+ "Fan the work out across the relevant targets in parallel using your own subagent capability,",
97
+ "as you judge best — split or group the work however fits the objective.",
98
+ ...(capClause ? [capClause] : []),
99
+ "",
100
+ "Consolidate into a single markdown report: deduplicate findings, preserve coverage across",
101
+ "every target, and explicitly mention targets with no findings rather than dropping them.",
102
+ "Include a short verdict line, a brief summary, then findings as `file:line — issue — severity`.",
103
+ "Return plain markdown — no JSON wrapper, no outer code fences.",
104
+ ].join("\n");
105
+ }
106
+ /**
107
+ * Build the WRITE-mode coordination prompt. Unlike the read-only prompt, this
108
+ * sets subagent_type="coder" (write-capable) and instructs DISJOINT one-target-
109
+ * per-subagent editing with NO git and NO nested swarm. The coordinator and all
110
+ * subagents run in an ephemeral throwaway worktree; the PreToolUse hook
111
+ * (swarm-write label) confines writes there and the plugin captures a patch the
112
+ * user reviews. The `cap` is the same SOFT total-count hint as read mode.
113
+ */
114
+ export function buildSwarmWritePrompt(objective, cap) {
115
+ const trimmed = objective.trim();
116
+ const capClause = cap !== undefined
117
+ ? `Launch at most ${cap} subagents in total across the whole run (soft cap — split or group targets to stay within it).`
118
+ : "Launch one subagent per distinct target; group related targets if there are many.";
119
+ return [
120
+ "Coordinate a parallel CODE-EDITING task using the AgentSwarm tool.",
121
+ "",
122
+ `Objective: ${trimmed}`,
123
+ "",
124
+ "You are running inside a throwaway git worktree dedicated to this task — edit freely;",
125
+ "your changes will be captured as a patch for the human to review and apply. Use the",
126
+ 'AgentSwarm tool to fan the work out across subagents. Set subagent_type to "coder" (the',
127
+ "write-capable profile). Give each subagent ONE target via prompt_template + items: a single",
128
+ "file or module per subagent.",
129
+ capClause,
130
+ "",
131
+ "Safety rules enforced by the PreToolUse hook (violations are denied):",
132
+ "- Writes are confined to this worktree; an out-of-worktree write is denied.",
133
+ "- Do NOT run any git command that mutates state (no add/commit/checkout/etc.) — git mutation",
134
+ " is denied. The human owns all git operations.",
135
+ "",
136
+ "Coordination rules (prompt-only; the hook does NOT enforce them):",
137
+ "- Keep every assigned target DISJOINT: no overlap between subagents (two subagents editing",
138
+ " the same file will corrupt the result).",
139
+ "- Subagents must NOT call AgentSwarm (no nested fan-out). One flat generation only.",
140
+ "- Report exactly what you changed and why.",
141
+ "",
142
+ "After the subagents return, consolidate into a single markdown report: a short verdict line,",
143
+ "a summary of what changed across all targets, then one section per target listing the edited",
144
+ "files. Do NOT paste full diffs (the patch is captured separately). Return plain markdown —",
145
+ "no JSON wrapper, no outer code fences.",
146
+ ].join("\n");
147
+ }
148
+ export async function runSwarm(argv, context) {
149
+ const parsed = parseSwarmArgs(argv);
150
+ const objective = parsed.objective?.trim();
151
+ if (!objective) {
152
+ throw new RuntimeError("INVALID_ARGS", "/kimi:swarm requires an objective. Usage: /kimi:swarm [--write] [--budget 30m] [--cap N] [--max-concurrency N] [-m model] <objective>", "swarm.parse");
153
+ }
154
+ return parsed.write
155
+ ? runWriteSwarm(parsed, objective, context)
156
+ : runReadSwarm(parsed, objective, context);
157
+ }
158
+ /** Read-only swarm (default). */
159
+ async function runReadSwarm(parsed, objective, context) {
160
+ const paths = resolvePluginPaths(context.env);
161
+ await ensurePluginPaths(paths);
162
+ const repoIdentity = await resolveRepoIdentity(context.cwd);
163
+ const store = new JobStore(paths);
164
+ try {
165
+ await sweepStaleJobs(store, paths);
166
+ const executionPlan = await prepareKimiExecutionPlan({
167
+ operationKind: "swarm",
168
+ cwd: context.cwd,
169
+ env: context.env,
170
+ });
171
+ const prompt = buildSwarmPrompt(objective, parsed.cap);
172
+ const jobId = allocateJobId(context);
173
+ const logPath = path.join(paths.logsDir, `swarm-${jobId}.jsonl`);
174
+ const job = store.createJob({
175
+ job_id: jobId,
176
+ repo_id: repoIdentity.repoId,
177
+ // Reuse the read-only review lineage; the hook label below ("swarm") is
178
+ // what actually drives the allowlist.
179
+ command_type: "review",
180
+ cwd: context.cwd,
181
+ model: parsed.model ?? null,
182
+ thinking: null,
183
+ thinking_effort: parsed.thinkingEffort ?? null,
184
+ background: false,
185
+ budget_ms: parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS,
186
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
187
+ pid: process.pid,
188
+ kimi_pid: null,
189
+ status: "running",
190
+ kimi_session_id: null,
191
+ ...persistedExecutionPlanFields(executionPlan),
192
+ agent_profile: SWARM_AGENT_PROFILE,
193
+ prompt_digest: digestPrompt(prompt),
194
+ summary: `[swarm] ${shorten(objective, SWARM_SUMMARY_MAX)}`,
195
+ phase: "starting",
196
+ final_output_path: null,
197
+ stream_log_path: logPath,
198
+ error: null,
199
+ });
200
+ try {
201
+ await writeInvocationLogHeader(logPath, {
202
+ commandType: "swarm",
203
+ kimiSessionId: "(pending)",
204
+ cwd: context.cwd,
205
+ });
206
+ }
207
+ catch (error) {
208
+ const classified = new RuntimeError("SWARM_LOG_HEADER_FAILED", `Failed to write swarm invocation log header: ${error.message ?? String(error)}`, "swarm.log-header", error instanceof Error ? { cause: error } : undefined);
209
+ await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
210
+ throw classified;
211
+ }
212
+ const completed = await executeSwarmJob(job.job_id, prompt, objective, job.budget_ms, resolveSwarmMaxConcurrency(parsed.maxConcurrency), context);
213
+ if (!completed.final_output_path) {
214
+ throw new RuntimeError("SWARM_RESULT_MISSING", "Swarm finished without a rendered result.", "swarm.result");
215
+ }
216
+ return readArtifact(completed.final_output_path);
217
+ }
218
+ finally {
219
+ store.close();
220
+ }
221
+ }
222
+ /**
223
+ * WRITE-capable swarm (--write, v1.4). Spawns the coordinator in an ephemeral
224
+ * throwaway worktree off HEAD; `coder` subagents edit there; writes are confined
225
+ * by the swarm-write hook label; the result is captured as a patch the main
226
+ * thread reviews + applies. The worktree is removed after successful patch
227
+ * capture; capture failure retains it and reports its path for recovery.
228
+ * Reuses the rescue (write-capable) job lineage.
229
+ */
230
+ async function runWriteSwarm(parsed, objective, context) {
231
+ const paths = resolvePluginPaths(context.env);
232
+ await ensurePluginPaths(paths);
233
+ const repoIdentity = await resolveRepoIdentity(context.cwd);
234
+ const store = new JobStore(paths);
235
+ try {
236
+ await sweepStaleJobs(store, paths);
237
+ // Write mode needs a real git repo and a born HEAD before any job state is
238
+ // created. Its execution plan is certified below from the actual throwaway
239
+ // worktree cwd, not from the user's tree.
240
+ assertWriteSwarmPreconditions(repoIdentity);
241
+ if (!(await hasBornHead(context.cwd))) {
242
+ throw new RuntimeError("WRITE_SWARM_NO_HEAD", "/kimi:swarm --write needs a git repository with at least one commit (HEAD). This repo has an unborn HEAD; make an initial commit first.", "swarm.precondition");
243
+ }
244
+ // Abandoned trees are recovery evidence, never age-based garbage.
245
+ if (await isWorkingTreeDirty(context.cwd)) {
246
+ context.stderr.write("[kimi-plugin-cc] /kimi:swarm --write bases its worktree on HEAD — your uncommitted changes are NOT visible to the swarm. Commit or stash them first if the swarm needs them.\n");
247
+ }
248
+ const prompt = buildSwarmWritePrompt(objective, parsed.cap);
249
+ const jobId = allocateJobId(context);
250
+ const logPath = path.join(paths.logsDir, `swarm-write-${jobId}.jsonl`);
251
+ // Namespace worktrees per repo so the cross-repo sweep below only ever
252
+ // touches THIS repo's subtree (the worktreesDir is a single global dir shared
253
+ // across every repo the user runs --write in).
254
+ const worktreePath = path.join(paths.worktreesDir, repoIdentity.repoId, `swarm-write-${jobId}`);
255
+ await mkdir(path.dirname(worktreePath), { recursive: true });
256
+ try {
257
+ await createEphemeralWorktree(repoIdentity.repoRoot, "HEAD", worktreePath);
258
+ }
259
+ catch (error) {
260
+ throw (error instanceof RuntimeError
261
+ ? error
262
+ : new RuntimeError("GIT_WORKTREE_ADD_FAILED", String(error), "swarm.worktree"));
263
+ }
264
+ let preserveWorktree = false;
265
+ try {
266
+ // Prefix args can contain relative wrapper/script paths. Probe from the
267
+ // exact spawn cwd so the certified command tuple and its effective target
268
+ // cannot diverge merely because --write runs in a separate worktree.
269
+ const executionPlan = await prepareKimiExecutionPlan({
270
+ operationKind: "swarm-write",
271
+ cwd: worktreePath,
272
+ env: context.env,
273
+ });
274
+ const job = store.createJob({
275
+ job_id: jobId,
276
+ repo_id: repoIdentity.repoId,
277
+ // Reuse the rescue (write-capable) lineage, like pursue; the hook label
278
+ // below ("swarm-write") drives the allowlist. cwd stays the USER's real
279
+ // cwd for lineage — the kimi spawn cwd is the worktree (threaded via the
280
+ // writeMode arg to executeSwarmJob).
281
+ command_type: "rescue",
282
+ cwd: context.cwd,
283
+ model: parsed.model ?? null,
284
+ thinking: null,
285
+ thinking_effort: parsed.thinkingEffort ?? null,
286
+ background: false,
287
+ budget_ms: parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS,
288
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
289
+ pid: process.pid,
290
+ kimi_pid: null,
291
+ status: "running",
292
+ kimi_session_id: null,
293
+ ...persistedExecutionPlanFields(executionPlan),
294
+ agent_profile: SWARM_WRITE_AGENT_PROFILE,
295
+ prompt_digest: digestPrompt(prompt),
296
+ summary: `[swarm-write] ${shorten(objective, SWARM_SUMMARY_MAX)}`,
297
+ phase: "starting",
298
+ final_output_path: null,
299
+ stream_log_path: logPath,
300
+ error: null,
301
+ });
302
+ try {
303
+ await writeInvocationLogHeader(logPath, {
304
+ commandType: "swarm-write",
305
+ kimiSessionId: "(pending)",
306
+ cwd: context.cwd,
307
+ });
308
+ }
309
+ catch (error) {
310
+ const classified = new RuntimeError("SWARM_LOG_HEADER_FAILED", `Failed to write swarm-write invocation log header: ${error.message ?? String(error)}`, "swarm.log-header", error instanceof Error ? { cause: error } : undefined);
311
+ await markJobFailed(store, paths, job, classified, "Swarm (write) failed.", {
312
+ phase: "failed",
313
+ });
314
+ throw classified;
315
+ }
316
+ const completed = await executeSwarmJob(job.job_id, prompt, objective, job.budget_ms, resolveSwarmWriteMaxConcurrency(parsed.maxConcurrency), context, {
317
+ spawnCwd: worktreePath,
318
+ workspaceRoot: worktreePath,
319
+ onPatchCaptureFailure: () => {
320
+ preserveWorktree = true;
321
+ },
322
+ });
323
+ if (preserveWorktree && completed.error) {
324
+ throw new RuntimeError(completed.error.code, completed.error.message, completed.error.stage, {
325
+ details: completed.error.details,
326
+ });
327
+ }
328
+ if (!completed.final_output_path) {
329
+ throw new RuntimeError("SWARM_RESULT_MISSING", "Swarm (write) finished without a rendered result.", "swarm.result");
330
+ }
331
+ return readArtifact(completed.final_output_path);
332
+ }
333
+ finally {
334
+ if (preserveWorktree) {
335
+ context.stderr.write(`[kimi-plugin-cc] swarm-write patch capture or session cleanup failed; worktree retained at ${worktreePath}.\n`);
336
+ }
337
+ else {
338
+ await removeWorktree(repoIdentity.repoRoot, worktreePath);
339
+ }
340
+ }
341
+ }
342
+ finally {
343
+ store.close();
344
+ }
345
+ }
346
+ /** Refuse --write outside a git repo (git worktree add has nothing to fork). */
347
+ function assertWriteSwarmPreconditions(repoIdentity) {
348
+ if (!repoIdentity.isGitRepo) {
349
+ throw new RuntimeError("WRITE_SWARM_NOT_A_REPO", "/kimi:swarm --write requires a git repository (it works in a throwaway worktree off HEAD). The current directory is not inside a git repo.", "swarm.precondition");
350
+ }
351
+ }
352
+ /**
353
+ * Capture the worktree change set as a `.patch` artifact (write mode). Returns
354
+ * the artifact path + size, or null on capture failure. Called on EVERY terminal
355
+ * path so partial work survives a cancel/budget-expiry.
356
+ */
357
+ async function capturePatchArtifact(paths, jobId, worktreePath, stderr, onFailure) {
358
+ try {
359
+ const patch = await captureWorktreePatch(worktreePath);
360
+ const patchPath = path.join(paths.artifactsDir, `swarm-write-${jobId}.patch`);
361
+ await writeFile(patchPath, patch, "utf8");
362
+ return { path: patchPath, bytes: Buffer.byteLength(patch, "utf8"), empty: patch.trim().length === 0 };
363
+ }
364
+ catch (error) {
365
+ onFailure?.();
366
+ stderr.write(`[kimi-plugin-cc] swarm-write patch capture failed for job ${jobId}: ${error.message ?? String(error)}\n`);
367
+ return null;
368
+ }
369
+ }
370
+ /** Render the patch-handoff header prepended to a write-swarm report. */
371
+ function renderWriteSwarmHeader(captured) {
372
+ if (captured === null) {
373
+ return "**Patch:** capture FAILED — no patch was written; inspect the run logs.";
374
+ }
375
+ if (captured.empty) {
376
+ return "**Patch:** the swarm made NO changes (empty patch).";
377
+ }
378
+ return [
379
+ `**Patch written to:** \`${captured.path}\` (${captured.bytes} bytes).`,
380
+ `Review it, then apply from the repo root with: \`git apply --3way ${captured.path}\` ` +
381
+ "(the worktree was based on HEAD, so `--3way` reconciles against your current tree; " +
382
+ "plain `git apply` works only if your tree hasn't moved since).",
383
+ "The plugin did NOT apply or commit anything — you own the merge.",
384
+ ].join("\n");
385
+ }
386
+ async function executeSwarmJob(jobId, prompt, objective, budgetMs, maxConcurrency, context,
387
+ /**
388
+ * Write mode (v1.4): when set, the kimi spawn cwd is the throwaway worktree
389
+ * (`spawnCwd`), the hook label is "swarm-write", and `workspaceRoot` (the same
390
+ * worktree) is exported as the trusted allowlist root + used to capture the
391
+ * patch. Undefined ⇒ read-only swarm (label "swarm", spawn cwd = job.cwd).
392
+ */
393
+ writeMode) {
394
+ const paths = resolvePluginPaths(context.env);
395
+ await ensurePluginPaths(paths);
396
+ const store = new JobStore(paths);
397
+ const job = store.getJob(jobId);
398
+ if (!job) {
399
+ store.close();
400
+ throw new RuntimeError("JOB_NOT_FOUND", `Swarm job ${jobId} was not found.`, "swarm.worker");
401
+ }
402
+ // Both modes fan out subagents and require the PreToolUse hook: read mode
403
+ // enforces read-only policy, while write mode confines writes to the trusted
404
+ // worktree. Fail closed before spawning.
405
+ if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK !== "1") {
406
+ const installStatus = await verifyHookInstalled(context.env);
407
+ if (!installStatus.installed) {
408
+ maybeWarnHookMissing(installStatus, "swarm", context.stderr);
409
+ const classified = new RuntimeError("SWARM_HOOK_NOT_INSTALLED", [
410
+ "/kimi:swarm refuses to run without the kimi-plugin-cc PreToolUse hook.",
411
+ "Swarm fans out multiple subagents; the hook is the ONLY thing keeping every",
412
+ "one of them read-only, so a missing hook means no enforcement across the fan-out.",
413
+ `Hook check failed: ${installStatus.reason ?? "unknown"}.`,
414
+ "Repair by running Claude Code /kimi:setup or Codex $kimi-setup, then retry.",
415
+ hookRefusalRetryProtocol(context.env),
416
+ ].join(" "), "swarm.hook-check", { details: hookRefusalDetails(installStatus) });
417
+ try {
418
+ return await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
419
+ }
420
+ finally {
421
+ store.close();
422
+ }
423
+ }
424
+ }
425
+ const handlers = createCliCancellationHandlers();
426
+ const budgetDeadline = Date.now() + budgetMs;
427
+ let activationOptions;
428
+ let preactivated;
429
+ let deactivationRequired = false;
430
+ let cleanupAttempted = false;
431
+ let cleanupFailure;
432
+ let finalText;
433
+ let recovery;
434
+ const commandLabel = writeMode ? "swarm-write" : "swarm";
435
+ const recoveryCleanupTimeoutMs = 2_000;
436
+ const cleanupSignal = (timeoutMs) => AbortSignal.any([
437
+ handlers.signal,
438
+ AbortSignal.timeout(Math.max(1, timeoutMs)),
439
+ ]);
440
+ const attemptDeactivation = async (identity, fallbackStage, timeoutMs = Math.max(1, budgetDeadline - Date.now())) => {
441
+ cleanupAttempted = true;
442
+ try {
443
+ await deactivateSwarmSession({
444
+ ...activationOptions,
445
+ ...identity,
446
+ signal: cleanupSignal(timeoutMs),
447
+ });
448
+ deactivationRequired = false;
449
+ return undefined;
450
+ }
451
+ catch (error) {
452
+ return error instanceof RuntimeError
453
+ ? error
454
+ : new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup failed without a safe diagnostic payload.", fallbackStage);
455
+ }
456
+ };
457
+ try {
458
+ const executionPlan = executionPlanFromPersisted(job);
459
+ if (requiresSwarmPreactivation(executionPlan.kimiVersion)) {
460
+ const owner = await probeProcessIdentity(process.pid);
461
+ recovery = { schema: 1, jobId: job.job_id, ownerPid: process.pid, owner: owner.identity ?? null,
462
+ kimiHome: resolveKimiHome(context.env, writeMode?.spawnCwd ?? job.cwd),
463
+ cwd: writeMode?.spawnCwd ?? job.cwd, workspaceRoot: writeMode?.workspaceRoot ?? null,
464
+ launches: [], session: null, sessionDirty: false, complete: false, patchPath: null };
465
+ store.saveSwarmRecovery(job.job_id, recovery);
466
+ }
467
+ activationOptions = {
468
+ cwd: writeMode?.spawnCwd ?? job.cwd,
469
+ env: context.env,
470
+ command: executionPlan.command,
471
+ prefixArgs: executionPlan.prefixArgs,
472
+ executionPlan,
473
+ commandLabel,
474
+ model: job.model ?? undefined,
475
+ thinkingEffort: job.thinking_effort ?? undefined,
476
+ swarmMaxConcurrency: maxConcurrency,
477
+ trustedWorkspaceRoot: writeMode?.workspaceRoot,
478
+ expectedVersion: executionPlan.kimiVersion ?? undefined,
479
+ onRecoveryEvent: recovery ? (event) => recordRecoveryEvent(store, recovery, event) : undefined,
480
+ };
481
+ if (requiresSwarmPreactivation(executionPlan.kimiVersion)) {
482
+ store.updateRunningJob(job.job_id, { phase: "turn-running" });
483
+ const activationController = new AbortController();
484
+ const onParentAbort = () => activationController.abort();
485
+ let activationTimedOut = false;
486
+ handlers.signal.addEventListener("abort", onParentAbort, { once: true });
487
+ if (handlers.signal.aborted)
488
+ onParentAbort();
489
+ const activationTimer = setTimeout(() => {
490
+ activationTimedOut = true;
491
+ activationController.abort();
492
+ }, Math.max(1, budgetDeadline - Date.now()));
493
+ activationTimer.unref();
494
+ try {
495
+ preactivated = await preactivateSwarmSession({
496
+ ...activationOptions,
497
+ signal: activationController.signal,
498
+ });
499
+ deactivationRequired = true;
500
+ store.updateRunningJob(job.job_id, {
501
+ kimi_session_id: preactivated.sessionId,
502
+ kimi_session_id_source: "preactivated",
503
+ });
504
+ }
505
+ catch (error) {
506
+ const activationDidTimeOut = activationTimedOut;
507
+ clearTimeout(activationTimer);
508
+ handlers.signal.removeEventListener("abort", onParentAbort);
509
+ const details = error instanceof RuntimeError ? error.details : {};
510
+ const sessionId = typeof details.session_id === "string" ? details.session_id : undefined;
511
+ const workspaceId = typeof details.workspace_id === "string" ? details.workspace_id : undefined;
512
+ const hasSessionIdentity = sessionId !== undefined && workspaceId !== undefined;
513
+ if (error instanceof RuntimeError &&
514
+ error.code === "SWARM_SESSION_CLEANUP_FAILED" &&
515
+ details.process_cleanup_unverified === true) {
516
+ cleanupFailure = error;
517
+ }
518
+ if (hasSessionIdentity) {
519
+ store.updateRunningJob(job.job_id, {
520
+ kimi_session_id: sessionId,
521
+ kimi_session_id_source: "preactivated",
522
+ });
523
+ }
524
+ if (hasSessionIdentity && !handlers.cancelling) {
525
+ const sessionCleanupFailure = await attemptDeactivation({ sessionId, workspaceId }, "swarm.activation-cleanup", recoveryCleanupTimeoutMs);
526
+ if (sessionCleanupFailure)
527
+ cleanupFailure = sessionCleanupFailure;
528
+ }
529
+ else if (hasSessionIdentity && cleanupFailure === undefined) {
530
+ cleanupFailure = new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup was not completed after activation was cancelled.", "swarm.activation-cleanup", { details: { cancelled: true, cleanup_unverified: true } });
531
+ }
532
+ if (activationDidTimeOut && !handlers.cancelling) {
533
+ throw new ResponseTimeoutError(budgetMs, "swarm.activation", commandLabel, undefined, error);
534
+ }
535
+ throw error;
536
+ }
537
+ finally {
538
+ clearTimeout(activationTimer);
539
+ handlers.signal.removeEventListener("abort", onParentAbort);
540
+ }
541
+ }
542
+ const promptBudgetMs = budgetDeadline - Date.now();
543
+ if (promptBudgetMs <= 0)
544
+ throw new ResponseTimeoutError(budgetMs, "swarm.prompt", commandLabel);
545
+ store.updateRunningJob(job.job_id, { phase: "turn-running" });
546
+ if (recovery)
547
+ recordRecoveryEvent(store, recovery, { kind: "print-spawn" });
548
+ const result = await runCliPromptWithBudget({
549
+ cwd: activationOptions.cwd,
550
+ env: context.env,
551
+ command: executionPlan.command,
552
+ prefixArgs: [...executionPlan.prefixArgs],
553
+ executionPlan,
554
+ prompt,
555
+ commandLabel,
556
+ swarmMaxConcurrency: maxConcurrency,
557
+ trustedWorkspaceRoot: writeMode?.workspaceRoot,
558
+ model: job.model ?? undefined,
559
+ thinkingEffort: job.thinking_effort ?? undefined,
560
+ resumeSessionId: preactivated?.sessionId,
561
+ logPath: job.stream_log_path,
562
+ onSpawn: async (pid) => {
563
+ store.updateRunningJob(job.job_id, { kimi_pid: pid });
564
+ if (recovery) {
565
+ const probe = await probeProcessIdentity(pid);
566
+ recordRecoveryEvent(store, recovery, { kind: "print-processes", identities: probe.identity ? [probe.identity] : [], complete: probe.complete });
567
+ }
568
+ },
569
+ signal: handlers.signal,
570
+ }, promptBudgetMs, "swarm.prompt");
571
+ if (recovery)
572
+ recordRecoveryEvent(store, recovery, { kind: "print-exit" });
573
+ store.updateRunningJob(job.job_id, observedExecutionFields(result));
574
+ finalText = reassembleProseFromRecords(result.records);
575
+ let resultError;
576
+ if (handlers.cancelling) {
577
+ resultError = new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
578
+ }
579
+ else {
580
+ try {
581
+ assertCliResultSuccess(result, "swarm.runtime");
582
+ if (preactivated && result.sessionId !== preactivated.sessionId) {
583
+ throw new RuntimeError("SWARM_SESSION_MISMATCH", "The resumed print session differs from the preactivated swarm session.", "swarm.session-provenance", { details: { expected_session_id: preactivated.sessionId, actual_session_id: result.sessionId ?? null } });
584
+ }
585
+ }
586
+ catch (error) {
587
+ resultError = error;
588
+ }
589
+ }
590
+ warnHookEscapes(result, "swarm", context.stderr);
591
+ if (preactivated) {
592
+ cleanupFailure = await attemptDeactivation(preactivated, "swarm.cleanup");
593
+ }
594
+ if (resultError !== undefined)
595
+ throw resultError;
596
+ if (handlers.cancelling) {
597
+ throw new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
598
+ }
599
+ if (cleanupFailure)
600
+ throw cleanupFailure;
601
+ if (!preactivated &&
602
+ result.sessionId !== undefined &&
603
+ result.sessionId.length > 0 &&
604
+ result.sessionId !== job.kimi_session_id) {
605
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
606
+ }
607
+ await syncKimiSessionTitle({
608
+ promptText: objective,
609
+ env: context.env,
610
+ cwd: job.cwd,
611
+ sessionId: result.sessionId ?? job.kimi_session_id,
612
+ title: buildKimiSessionTitle(writeMode ? "swarm-write" : "swarm", objective),
613
+ stderr: context.stderr,
614
+ });
615
+ warnIfSessionIdMissing(result, "swarm", job.job_id, context.stderr);
616
+ const prose = reassembleProseFromRecords(result.records);
617
+ finalText = prose;
618
+ if (writeMode) {
619
+ const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr, writeMode.onPatchCaptureFailure);
620
+ if (captured === null) {
621
+ throw new RuntimeError("SWARM_PATCH_CAPTURE_FAILED", `Swarm patch capture failed; worktree retained at ${writeMode.workspaceRoot}.`, "swarm.patch-capture", { details: { worktreePath: writeMode.workspaceRoot } });
622
+ }
623
+ if (recovery)
624
+ recovery.patchPath = captured.path;
625
+ finalText = `${renderWriteSwarmHeader(captured)}\n\n---\n\n${prose}`.trimEnd();
626
+ }
627
+ const rendered = renderManagedJobOutput(job, finalText);
628
+ let artifactPath;
629
+ try {
630
+ artifactPath = await writeArtifact(paths, job, rendered.rendered);
631
+ }
632
+ catch (writeError) {
633
+ context.stderr.write(`[kimi-plugin-cc] swarm artifact write failed for job ${job.job_id}; raw output preserved in error details.\n`);
634
+ const classified = new RuntimeError("SWARM_ARTIFACT_WRITE_FAILED", `Failed to write swarm artifact: ${writeError.message ?? String(writeError)}`, "swarm.artifact", {
635
+ ...(writeError instanceof Error ? { cause: writeError } : {}),
636
+ details: { rawOutput: finalText },
637
+ });
638
+ return await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
639
+ }
640
+ if (handlers.cancelling) {
641
+ throw new RuntimeError("SWARM_CANCELLED", "Swarm cancelled after artifact write.", "swarm.runtime");
642
+ }
643
+ if (recovery)
644
+ finishRecoveryRecord(store, recovery);
645
+ return (store.markCompleted(job.job_id, {
646
+ summary: `[${writeMode ? "swarm-write" : "swarm"}] ${shorten(objective, SWARM_SUMMARY_MAX)}`,
647
+ phase: "done",
648
+ final_output_path: artifactPath,
649
+ error: null,
650
+ }) ?? job);
651
+ }
652
+ catch (error) {
653
+ if (preactivated && deactivationRequired && activationOptions) {
654
+ if (handlers.cancelling) {
655
+ cleanupFailure = new RuntimeError("SWARM_SESSION_CLEANUP_FAILED", "Swarm session cleanup was not completed after cancellation.", "swarm.cleanup", { details: { cancelled: true, cleanup_unverified: true } });
656
+ }
657
+ else if (!cleanupAttempted) {
658
+ cleanupFailure = await attemptDeactivation(preactivated, "swarm.cleanup", recoveryCleanupTimeoutMs);
659
+ }
660
+ else if (error instanceof RuntimeError && error.code === "SWARM_SESSION_CLEANUP_FAILED") {
661
+ cleanupFailure ??= error;
662
+ }
663
+ }
664
+ if (cleanupFailure) {
665
+ writeMode?.onPatchCaptureFailure?.();
666
+ context.stderr.write(`[kimi-plugin-cc] swarm session cleanup also failed: ${cleanupFailure.message}\n`);
667
+ }
668
+ let captureFailed = writeMode !== undefined && error instanceof RuntimeError && error.code === "SWARM_PATCH_CAPTURE_FAILED";
669
+ if (writeMode && !(error instanceof RuntimeError && error.code === "SWARM_PATCH_CAPTURE_FAILED")) {
670
+ const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr, writeMode.onPatchCaptureFailure);
671
+ captureFailed = captured === null;
672
+ if (recovery && captured)
673
+ recovery.patchPath = captured.path;
674
+ if (captured && !captured.empty) {
675
+ context.stderr.write(`[kimi-plugin-cc] swarm-write run ended early; partial patch captured at ${captured.path}.\n`);
676
+ }
677
+ }
678
+ const recoveryDetails = {
679
+ ...(error instanceof RuntimeError && typeof error.details.exit_code === "number"
680
+ ? { exit_code: error.details.exit_code }
681
+ : {}),
682
+ ...((captureFailed || cleanupFailure) && writeMode ? { worktreePath: writeMode.workspaceRoot } : {}),
683
+ ...(cleanupFailure
684
+ ? {
685
+ cleanup_error: {
686
+ code: cleanupFailure.code,
687
+ stage: cleanupFailure.stage,
688
+ cancelled: handlers.cancelling,
689
+ unverified: true,
690
+ },
691
+ ...(finalText !== undefined ? { rawOutput: finalText } : {}),
692
+ }
693
+ : {}),
694
+ };
695
+ if (handlers.cancelling) {
696
+ const cancellationCause = classifyManagedCommandFailure(error, writeMode ? "rescue" : "review", job.job_id, {
697
+ preserveStage: true,
698
+ });
699
+ const cancelledError = new RuntimeError("SWARM_CANCELLED", captureFailed && writeMode
700
+ ? `Swarm cancelled; patch capture failed and worktree retained at ${writeMode.workspaceRoot}.`
701
+ : "Swarm cancelled by user request.", "swarm.runtime", {
702
+ ...(error instanceof Error ? { cause: error } : {}),
703
+ details: {
704
+ ...(cancellationCause instanceof RuntimeError ? cancellationCause.details : {}),
705
+ ...recoveryDetails,
706
+ },
707
+ });
708
+ return await markJobCancelled(store, paths, job, "Swarm cancelled by user request.", cancelledError, {
709
+ phase: "cancelled",
710
+ });
711
+ }
712
+ if (error === cleanupFailure && budgetDeadline <= Date.now() && !(error instanceof ResponseTimeoutError)) {
713
+ error = new ResponseTimeoutError(budgetMs, "swarm.cleanup", commandLabel, undefined, cleanupFailure);
714
+ }
715
+ const classified = classifyManagedCommandFailure(error, writeMode ? "rescue" : "review", job.job_id, {
716
+ preserveStage: true,
717
+ });
718
+ const withRecovery = captureFailed || cleanupFailure
719
+ ? classified instanceof RuntimeError
720
+ ? new RuntimeError(classified.code, classified.message, classified.stage, {
721
+ cause: classified,
722
+ details: { ...classified.details, ...recoveryDetails },
723
+ })
724
+ : new RuntimeError(cleanupFailure?.code ?? "SWARM_PATCH_CAPTURE_FAILED", classified.message, cleanupFailure?.stage ?? "swarm.patch-capture", { cause: classified, details: recoveryDetails })
725
+ : classified;
726
+ return await markJobFailed(store, paths, job, withRecovery, "Swarm failed.", { phase: "failed" });
727
+ }
728
+ finally {
729
+ handlers.dispose();
730
+ try {
731
+ if (recovery)
732
+ finishRecoveryRecord(store, recovery);
733
+ if (recovery && !recovery.complete) {
734
+ writeMode?.onPatchCaptureFailure?.();
735
+ context.stderr.write(`[kimi-plugin-cc] recovery record retained: companion.sh recover-swarm ${job.job_id} (preview).\n`);
736
+ }
737
+ }
738
+ catch (error) {
739
+ // A failed final journal commit cannot authorize removal of recovery evidence.
740
+ writeMode?.onPatchCaptureFailure?.();
741
+ throw error;
742
+ }
743
+ finally {
744
+ store.close();
745
+ }
746
+ }
747
+ }
748
+ function shorten(text, max) {
749
+ const normalized = text.replace(/\s+/g, " ").trim();
750
+ if (normalized.length <= max) {
751
+ return normalized;
752
+ }
753
+ return `${normalized.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
754
+ }