kimi-plugin-cc 2.4.5 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +42 -26
- package/dist/dsh/companion.js +174 -0
- package/dist/dsh/index.js +156 -0
- package/dist/hooks/install-paths.js +2 -2
- package/dist/version.js +1 -1
- package/docs/models.md +5 -1
- package/docs/registry-releases.md +20 -17
- package/docs/safety.md +22 -9
- package/docs/upstream-2.4.1-certification.md +6 -0
- package/package.json +40 -6
- package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/kimi-codex/dist/dsh/companion.js +174 -0
- package/plugins/kimi-codex/dist/dsh/index.js +156 -0
- package/plugins/kimi-codex/dist/hooks/install-paths.js +2 -2
- package/plugins/kimi-codex/dist/version.js +1 -1
- package/plugins/kimi-dsh/LICENSE +201 -0
- package/plugins/kimi-dsh/README.md +80 -0
- package/plugins/kimi-dsh/cordis.patch.yml +45 -0
- package/plugins/kimi-dsh/dist/background-spawn.js +216 -0
- package/plugins/kimi-dsh/dist/cli-cancellation.js +43 -0
- package/plugins/kimi-dsh/dist/cli-client.js +876 -0
- package/plugins/kimi-dsh/dist/commands/ask.js +336 -0
- package/plugins/kimi-dsh/dist/commands/cancel.js +216 -0
- package/plugins/kimi-dsh/dist/commands/cli-helpers.js +201 -0
- package/plugins/kimi-dsh/dist/commands/models.js +140 -0
- package/plugins/kimi-dsh/dist/commands/not-implemented.js +4 -0
- package/plugins/kimi-dsh/dist/commands/pursue.js +307 -0
- package/plugins/kimi-dsh/dist/commands/recover-swarm.js +135 -0
- package/plugins/kimi-dsh/dist/commands/registry.js +74 -0
- package/plugins/kimi-dsh/dist/commands/repair-sessions.js +160 -0
- package/plugins/kimi-dsh/dist/commands/replay.js +124 -0
- package/plugins/kimi-dsh/dist/commands/rescue.js +347 -0
- package/plugins/kimi-dsh/dist/commands/result.js +66 -0
- package/plugins/kimi-dsh/dist/commands/review-gate.js +390 -0
- package/plugins/kimi-dsh/dist/commands/review.js +220 -0
- package/plugins/kimi-dsh/dist/commands/setup.js +1047 -0
- package/plugins/kimi-dsh/dist/commands/status.js +27 -0
- package/plugins/kimi-dsh/dist/commands/swarm.js +754 -0
- package/plugins/kimi-dsh/dist/commands/worker.js +25 -0
- package/plugins/kimi-dsh/dist/companion.js +97 -0
- package/plugins/kimi-dsh/dist/config.js +22 -0
- package/plugins/kimi-dsh/dist/dsh/companion.js +174 -0
- package/plugins/kimi-dsh/dist/dsh/index.js +156 -0
- package/plugins/kimi-dsh/dist/errors.js +40 -0
- package/plugins/kimi-dsh/dist/git.js +224 -0
- package/plugins/kimi-dsh/dist/help.js +43 -0
- package/plugins/kimi-dsh/dist/hook-escape.js +71 -0
- package/plugins/kimi-dsh/dist/hooks/approval-hook.js +101 -0
- package/plugins/kimi-dsh/dist/hooks/approval-policy.js +211 -0
- package/plugins/kimi-dsh/dist/hooks/config-safety.js +653 -0
- package/plugins/kimi-dsh/dist/hooks/install-paths.js +428 -0
- package/plugins/kimi-dsh/dist/hooks/install.js +210 -0
- package/plugins/kimi-dsh/dist/hooks/managed-block.js +619 -0
- package/plugins/kimi-dsh/dist/hooks/review-gate-stop.js +19 -0
- package/plugins/kimi-dsh/dist/job-id.js +12 -0
- package/plugins/kimi-dsh/dist/job-store.js +659 -0
- package/plugins/kimi-dsh/dist/jobs.js +217 -0
- package/plugins/kimi-dsh/dist/kimi-command.js +70 -0
- package/plugins/kimi-dsh/dist/kimi-engine.js +632 -0
- package/plugins/kimi-dsh/dist/kimi-errors.js +260 -0
- package/plugins/kimi-dsh/dist/kimi-home.js +9 -0
- package/plugins/kimi-dsh/dist/kimi-timeouts.js +52 -0
- package/plugins/kimi-dsh/dist/kimi-version-probe.js +216 -0
- package/plugins/kimi-dsh/dist/logging.js +22 -0
- package/plugins/kimi-dsh/dist/mcp/binding.js +58 -0
- package/plugins/kimi-dsh/dist/mcp/job-control.js +345 -0
- package/plugins/kimi-dsh/dist/mcp/launcher.js +257 -0
- package/plugins/kimi-dsh/dist/mcp/operations.js +301 -0
- package/plugins/kimi-dsh/dist/mcp/protocol.js +275 -0
- package/plugins/kimi-dsh/dist/mcp/server.js +8 -0
- package/plugins/kimi-dsh/dist/mcp/stdio.js +78 -0
- package/plugins/kimi-dsh/dist/mcp/supervised-child.js +15 -0
- package/plugins/kimi-dsh/dist/mcp/tool-result.js +7 -0
- package/plugins/kimi-dsh/dist/mcp/workspace.js +34 -0
- package/plugins/kimi-dsh/dist/model-command.js +32 -0
- package/plugins/kimi-dsh/dist/native-v2-preflight.js +417 -0
- package/plugins/kimi-dsh/dist/package.json +1 -0
- package/plugins/kimi-dsh/dist/parsing.js +620 -0
- package/plugins/kimi-dsh/dist/paths.js +27 -0
- package/plugins/kimi-dsh/dist/plugin-data.js +98 -0
- package/plugins/kimi-dsh/dist/process-tree.js +479 -0
- package/plugins/kimi-dsh/dist/prompt-size.js +18 -0
- package/plugins/kimi-dsh/dist/render.js +177 -0
- package/plugins/kimi-dsh/dist/rescue-approval.js +865 -0
- package/plugins/kimi-dsh/dist/schemas/review-gate-output.js +43 -0
- package/plugins/kimi-dsh/dist/session-recovery.js +200 -0
- package/plugins/kimi-dsh/dist/session-title.js +293 -0
- package/plugins/kimi-dsh/dist/stream-json.js +346 -0
- package/plugins/kimi-dsh/dist/swarm-activation.js +834 -0
- package/plugins/kimi-dsh/dist/swarm-recovery.js +134 -0
- package/plugins/kimi-dsh/dist/types.js +1 -0
- package/plugins/kimi-dsh/dist/vendor/shell-quote/LICENSE +24 -0
- package/plugins/kimi-dsh/dist/vendor/shell-quote/index.js +1 -0
- package/plugins/kimi-dsh/dist/vendor/shell-quote/parse.js +229 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/LICENSE +24 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/date.js +156 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/error.js +67 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/extract.js +293 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/parse.js +164 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/primitive.js +151 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/struct.js +156 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/util.js +57 -0
- package/plugins/kimi-dsh/dist/version.js +7 -0
- package/plugins/kimi-dsh/package.json +48 -0
- package/plugins/kimi-dsh/scripts/companion.sh +37 -0
- 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
|
+
}
|