kimi-plugin-cc 2.0.7
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/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +32 -0
- package/CHANGELOG.md +1096 -0
- package/LICENSE +201 -0
- package/README.md +772 -0
- package/agents/kimi-ask.md +82 -0
- package/agents/kimi-challenge.md +82 -0
- package/agents/kimi-pursue.md +91 -0
- package/agents/kimi-rescue.md +85 -0
- package/agents/kimi-review.md +82 -0
- package/agents/kimi-swarm-write.md +92 -0
- package/agents/kimi-swarm.md +83 -0
- package/commands/README.md +16 -0
- package/commands/ask.md +32 -0
- package/commands/cancel.md +17 -0
- package/commands/challenge.md +29 -0
- package/commands/pursue.md +37 -0
- package/commands/replay.md +13 -0
- package/commands/rescue.md +32 -0
- package/commands/result.md +13 -0
- package/commands/review.md +29 -0
- package/commands/setup.md +23 -0
- package/commands/status.md +13 -0
- package/commands/swarm.md +38 -0
- package/dist/background-spawn.js +224 -0
- package/dist/cli-cancellation.js +43 -0
- package/dist/cli-client.js +845 -0
- package/dist/commands/ask.js +327 -0
- package/dist/commands/cancel.js +178 -0
- package/dist/commands/cli-helpers.js +112 -0
- package/dist/commands/models.js +140 -0
- package/dist/commands/not-implemented.js +4 -0
- package/dist/commands/pursue.js +311 -0
- package/dist/commands/registry.js +87 -0
- package/dist/commands/repair-sessions.js +159 -0
- package/dist/commands/replay.js +123 -0
- package/dist/commands/rescue.js +333 -0
- package/dist/commands/result.js +58 -0
- package/dist/commands/review-gate.js +393 -0
- package/dist/commands/review.js +218 -0
- package/dist/commands/setup.js +1132 -0
- package/dist/commands/status.js +27 -0
- package/dist/commands/swarm.js +576 -0
- package/dist/commands/worker.js +31 -0
- package/dist/companion.js +114 -0
- package/dist/config.js +22 -0
- package/dist/errors.js +40 -0
- package/dist/git.js +215 -0
- package/dist/hooks/approval-hook.js +112 -0
- package/dist/hooks/approval-policy.js +254 -0
- package/dist/hooks/config-safety.js +637 -0
- package/dist/hooks/install-paths.js +504 -0
- package/dist/hooks/install.js +281 -0
- package/dist/hooks/managed-block.js +745 -0
- package/dist/hooks/review-gate-stop.js +19 -0
- package/dist/job-store.js +559 -0
- package/dist/jobs.js +202 -0
- package/dist/kimi-command.js +72 -0
- package/dist/kimi-engine.js +624 -0
- package/dist/kimi-errors.js +220 -0
- package/dist/kimi-home.js +9 -0
- package/dist/kimi-timeouts.js +68 -0
- package/dist/kimi-version-probe.js +1410 -0
- package/dist/logging.js +22 -0
- package/dist/native-v2-preflight.js +417 -0
- package/dist/package.json +1 -0
- package/dist/parsing.js +553 -0
- package/dist/paths.js +27 -0
- package/dist/plugin-data.js +98 -0
- package/dist/process-tree.js +448 -0
- package/dist/render.js +159 -0
- package/dist/rescue-approval.js +951 -0
- package/dist/schemas/review-gate-output.js +43 -0
- package/dist/session-title.js +293 -0
- package/dist/stream-json.js +498 -0
- package/dist/types.js +1 -0
- package/dist/vendor/shell-quote/LICENSE +24 -0
- package/dist/vendor/shell-quote/index.js +1 -0
- package/dist/vendor/shell-quote/parse.js +229 -0
- package/dist/vendor/smol-toml/LICENSE +24 -0
- package/dist/vendor/smol-toml/date.js +127 -0
- package/dist/vendor/smol-toml/error.js +63 -0
- package/dist/vendor/smol-toml/extract.js +100 -0
- package/dist/vendor/smol-toml/parse.js +148 -0
- package/dist/vendor/smol-toml/primitive.js +179 -0
- package/dist/vendor/smol-toml/struct.js +184 -0
- package/dist/vendor/smol-toml/util.js +113 -0
- package/dist/version.js +7 -0
- package/docs/migration.md +215 -0
- package/docs/models.md +82 -0
- package/docs/registry-releases.md +70 -0
- package/docs/safety.md +318 -0
- package/docs/session-visibility.md +32 -0
- package/hooks/README.md +9 -0
- package/hooks/hooks.json +16 -0
- package/package.json +67 -0
- package/plugins/kimi-codex/.codex-plugin/plugin.json +39 -0
- package/plugins/kimi-codex/dist/background-spawn.js +224 -0
- package/plugins/kimi-codex/dist/cli-cancellation.js +43 -0
- package/plugins/kimi-codex/dist/cli-client.js +845 -0
- package/plugins/kimi-codex/dist/commands/ask.js +327 -0
- package/plugins/kimi-codex/dist/commands/cancel.js +178 -0
- package/plugins/kimi-codex/dist/commands/cli-helpers.js +112 -0
- package/plugins/kimi-codex/dist/commands/models.js +140 -0
- package/plugins/kimi-codex/dist/commands/not-implemented.js +4 -0
- package/plugins/kimi-codex/dist/commands/pursue.js +311 -0
- package/plugins/kimi-codex/dist/commands/registry.js +87 -0
- package/plugins/kimi-codex/dist/commands/repair-sessions.js +159 -0
- package/plugins/kimi-codex/dist/commands/replay.js +123 -0
- package/plugins/kimi-codex/dist/commands/rescue.js +333 -0
- package/plugins/kimi-codex/dist/commands/result.js +58 -0
- package/plugins/kimi-codex/dist/commands/review-gate.js +393 -0
- package/plugins/kimi-codex/dist/commands/review.js +218 -0
- package/plugins/kimi-codex/dist/commands/setup.js +1132 -0
- package/plugins/kimi-codex/dist/commands/status.js +27 -0
- package/plugins/kimi-codex/dist/commands/swarm.js +576 -0
- package/plugins/kimi-codex/dist/commands/worker.js +31 -0
- package/plugins/kimi-codex/dist/companion.js +114 -0
- package/plugins/kimi-codex/dist/config.js +22 -0
- package/plugins/kimi-codex/dist/errors.js +40 -0
- package/plugins/kimi-codex/dist/git.js +215 -0
- package/plugins/kimi-codex/dist/hooks/approval-hook.js +112 -0
- package/plugins/kimi-codex/dist/hooks/approval-policy.js +254 -0
- package/plugins/kimi-codex/dist/hooks/config-safety.js +637 -0
- package/plugins/kimi-codex/dist/hooks/install-paths.js +504 -0
- package/plugins/kimi-codex/dist/hooks/install.js +281 -0
- package/plugins/kimi-codex/dist/hooks/managed-block.js +745 -0
- package/plugins/kimi-codex/dist/hooks/review-gate-stop.js +19 -0
- package/plugins/kimi-codex/dist/job-store.js +559 -0
- package/plugins/kimi-codex/dist/jobs.js +202 -0
- package/plugins/kimi-codex/dist/kimi-command.js +72 -0
- package/plugins/kimi-codex/dist/kimi-engine.js +624 -0
- package/plugins/kimi-codex/dist/kimi-errors.js +220 -0
- package/plugins/kimi-codex/dist/kimi-home.js +9 -0
- package/plugins/kimi-codex/dist/kimi-timeouts.js +68 -0
- package/plugins/kimi-codex/dist/kimi-version-probe.js +1410 -0
- package/plugins/kimi-codex/dist/logging.js +22 -0
- package/plugins/kimi-codex/dist/native-v2-preflight.js +417 -0
- package/plugins/kimi-codex/dist/package.json +1 -0
- package/plugins/kimi-codex/dist/parsing.js +553 -0
- package/plugins/kimi-codex/dist/paths.js +27 -0
- package/plugins/kimi-codex/dist/plugin-data.js +98 -0
- package/plugins/kimi-codex/dist/process-tree.js +448 -0
- package/plugins/kimi-codex/dist/render.js +159 -0
- package/plugins/kimi-codex/dist/rescue-approval.js +951 -0
- package/plugins/kimi-codex/dist/schemas/review-gate-output.js +43 -0
- package/plugins/kimi-codex/dist/session-title.js +293 -0
- package/plugins/kimi-codex/dist/stream-json.js +498 -0
- package/plugins/kimi-codex/dist/types.js +1 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/LICENSE +24 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/index.js +1 -0
- package/plugins/kimi-codex/dist/vendor/shell-quote/parse.js +229 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/LICENSE +24 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +127 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +63 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +100 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +148 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +179 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +184 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +113 -0
- package/plugins/kimi-codex/dist/version.js +7 -0
- package/plugins/kimi-codex/scripts/companion.sh +37 -0
- package/plugins/kimi-codex/scripts/review-gate-hook.sh +32 -0
- package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-ask/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-cancel/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-challenge/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +30 -0
- package/plugins/kimi-codex/skills/kimi-pursue/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-replay/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +30 -0
- package/plugins/kimi-codex/skills/kimi-rescue/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-result/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-result/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-review/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-review/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +32 -0
- package/plugins/kimi-codex/skills/kimi-setup/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-status/SKILL.md +25 -0
- package/plugins/kimi-codex/skills/kimi-status/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +31 -0
- package/plugins/kimi-codex/skills/kimi-swarm/agents/openai.yaml +6 -0
- package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +32 -0
- package/plugins/kimi-codex/skills/kimi-swarm-write/agents/openai.yaml +6 -0
- package/scripts/companion.sh +37 -0
- package/scripts/review-gate-hook.sh +32 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { RuntimeError } from "../errors.js";
|
|
2
|
+
import { resolveRepoIdentity } from "../git.js";
|
|
3
|
+
import { sweepStaleJobs } from "../jobs.js";
|
|
4
|
+
import { reconcileHistoricalJobProvenance } from "../kimi-engine.js";
|
|
5
|
+
import { withJobStore } from "../job-store.js";
|
|
6
|
+
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
7
|
+
import { parseJobLookupArgs } from "../parsing.js";
|
|
8
|
+
export async function runStatus(argv, context) {
|
|
9
|
+
const parsed = parseJobLookupArgs(argv);
|
|
10
|
+
const paths = resolvePluginPaths(context.env);
|
|
11
|
+
await ensurePluginPaths(paths);
|
|
12
|
+
const repoIdentity = await resolveRepoIdentity(context.cwd);
|
|
13
|
+
return withJobStore(paths, async (store) => {
|
|
14
|
+
await sweepStaleJobs(store, paths);
|
|
15
|
+
const found = parsed.jobId
|
|
16
|
+
? store.getJob(parsed.jobId)
|
|
17
|
+
: store.findLatestJob({
|
|
18
|
+
repoId: repoIdentity.repoId,
|
|
19
|
+
commandType: parsed.type,
|
|
20
|
+
});
|
|
21
|
+
if (!found) {
|
|
22
|
+
throw new RuntimeError("JOB_NOT_FOUND", "No matching job was found for status.", "status.lookup");
|
|
23
|
+
}
|
|
24
|
+
const job = await reconcileHistoricalJobProvenance(store, found);
|
|
25
|
+
return `${JSON.stringify(job, null, 2)}\n`;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createCliCancellationHandlers } from "../cli-cancellation.js";
|
|
5
|
+
import { runCliPromptWithBudget } from "../cli-client.js";
|
|
6
|
+
import { executionPlanFromPersisted, observedExecutionFields, persistedExecutionPlanFields, prepareKimiExecutionPlan, } from "../kimi-engine.js";
|
|
7
|
+
import { RuntimeError } from "../errors.js";
|
|
8
|
+
import { captureWorktreePatch, createEphemeralWorktree, hasBornHead, isWorkingTreeDirty, pruneWorktrees, removeWorktree, resolveRepoIdentity, } from "../git.js";
|
|
9
|
+
import { digestPrompt, markJobCancelled, markJobFailed, sweepStaleJobs } from "../jobs.js";
|
|
10
|
+
import { JobStore } from "../job-store.js";
|
|
11
|
+
import { classifyManagedCommandFailure } from "../kimi-errors.js";
|
|
12
|
+
import { KIMI_SWARM_DEFAULT_BUDGET_MS } from "../kimi-timeouts.js";
|
|
13
|
+
import { writeInvocationLogHeader } from "../logging.js";
|
|
14
|
+
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
15
|
+
import { MAX_DURATION_MS, parseSwarmArgs } from "../parsing.js";
|
|
16
|
+
import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
|
|
17
|
+
import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
|
|
18
|
+
import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
|
|
19
|
+
import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
|
|
20
|
+
// /kimi:swarm — READ-ONLY parallel fan-out (kimi-code 0.12.0 AgentSwarm tool).
|
|
21
|
+
//
|
|
22
|
+
// PROTOTYPE SCOPE (v1.2). The lowest-risk swarm shape: parallel review/analysis
|
|
23
|
+
// over N targets, with NO write surface. Deliberately narrow:
|
|
24
|
+
// - FOREGROUND ONLY. No --background/--detach. A hard wall-clock budget
|
|
25
|
+
// (--budget) bounds the whole run. Two distinct subagent bounds: --cap is a
|
|
26
|
+
// SOFT total-count hint injected into the prompt, and --max-concurrency is a
|
|
27
|
+
// HARD ceiling on how many subagents run AT ONCE via
|
|
28
|
+
// KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY on kimi-code 0.18.0+ (PR #888; older
|
|
29
|
+
// binaries ignore the unknown env var).
|
|
30
|
+
// - READ-ONLY. The PreToolUse hook runs under the "swarm" label, which
|
|
31
|
+
// allowlists the read-only tool set PLUS the AgentSwarm tool. Every
|
|
32
|
+
// subagent inherits that label and fires the SAME hook at policy index 0
|
|
33
|
+
// (kimi-code createPermissionDecisionPolicies puts the hook at index 0 for
|
|
34
|
+
// ALL agents — verified against 0.12.0), so a subagent's Write/Edit/Bash is
|
|
35
|
+
// denied exactly like a single-turn review's.
|
|
36
|
+
// - Reuses the REVIEW job lineage (command_type "review") so /kimi:status /
|
|
37
|
+
// /kimi:result / /kimi:cancel work unchanged. The hook label ("swarm") is
|
|
38
|
+
// independent of the job lineage — mirrors how /kimi:pursue reuses the
|
|
39
|
+
// rescue lineage with its own label. Promoting swarm to a first-class
|
|
40
|
+
// command_type is a follow-up.
|
|
41
|
+
//
|
|
42
|
+
// SAFETY — swarm REFUSES without the hook, like every model-spawning command.
|
|
43
|
+
// A swarm fans out up to `cap` subagents, each capable of attempting writes;
|
|
44
|
+
// without the index-0 hook there is ZERO enforcement on ALL of them — an
|
|
45
|
+
// N-fold blast radius. KIMI_PLUGIN_CC_SKIP_HOOK_CHECK remains an explicit,
|
|
46
|
+
// tests/diagnostics-only bypass of that refusal.
|
|
47
|
+
const SWARM_SUMMARY_MAX = 120;
|
|
48
|
+
const SWARM_AGENT_PROFILE = "<swarm>";
|
|
49
|
+
const SWARM_WRITE_AGENT_PROFILE = "<swarm-write>";
|
|
50
|
+
/**
|
|
51
|
+
* Orphaned write-swarm worktrees older than this are reaped by the startup sweep.
|
|
52
|
+
* Every normal run removes its own worktree in a finally; an orphan only survives
|
|
53
|
+
* a hard kill (SIGKILL of the plugin process).
|
|
54
|
+
*
|
|
55
|
+
* The sweep's only liveness signal is the worktree dir's mtime, which is frozen
|
|
56
|
+
* at `git worktree add` time when a run edits only EXISTING files (in-place edits
|
|
57
|
+
* never bump the containing dir's mtime). A live run can therefore look "idle" for
|
|
58
|
+
* its entire wall-clock life. So the TTL MUST exceed the longest a run can live —
|
|
59
|
+
* the hard `--budget` ceiling, `MAX_DURATION_MS` (24h) — or a concurrent run's
|
|
60
|
+
* sweep could `git worktree remove --force` a still-live worktree out from under
|
|
61
|
+
* its owner (ENOENT writes, lost patch). The +2h margin covers budget-expiry
|
|
62
|
+
* teardown (AbortController fires → the finally removes the worktree well within
|
|
63
|
+
* it). Orphans are still reaped, just after this longer, provably-safe delay.
|
|
64
|
+
*/
|
|
65
|
+
export const WORKTREE_ORPHAN_TTL_MS = MAX_DURATION_MS + 2 * 60 * 60 * 1000;
|
|
66
|
+
/**
|
|
67
|
+
* Default HARD concurrency ceiling applied when the user passes no
|
|
68
|
+
* `--max-concurrency`. Since v1.3 `/kimi:swarm` is ALSO reachable via the
|
|
69
|
+
* model-invocable `kimi-swarm` agent (Claude can auto-dispatch a fan-out), so an
|
|
70
|
+
* unbounded peak-parallelism default is no longer acceptable: we always export
|
|
71
|
+
* `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` (effective on kimi-code 0.18.0+; older
|
|
72
|
+
* binaries ignore it) so simultaneous model spend is capped BY CONSTRUCTION for
|
|
73
|
+
* every swarm run — agent-dispatched or human-typed. Conservative by design;
|
|
74
|
+
* override with an explicit `--max-concurrency N`. The `--budget` wall-clock
|
|
75
|
+
* ceiling remains the always-on hard bound on TOTAL cost; this bounds the peak.
|
|
76
|
+
*/
|
|
77
|
+
export const SWARM_DEFAULT_MAX_CONCURRENCY = 4;
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the effective hard concurrency ceiling: an explicit `--max-concurrency`
|
|
80
|
+
* wins; otherwise fall back to SWARM_DEFAULT_MAX_CONCURRENCY so the ceiling is
|
|
81
|
+
* never unset. Pure + exported for unit testing (the parser leaves the value
|
|
82
|
+
* undefined; swarm.ts owns the default, mirroring `budgetMs`).
|
|
83
|
+
*/
|
|
84
|
+
export function resolveSwarmMaxConcurrency(requested) {
|
|
85
|
+
return requested ?? SWARM_DEFAULT_MAX_CONCURRENCY;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Default HARD concurrency ceiling for WRITE mode — 1 (serialized) vs read's 4.
|
|
89
|
+
* Writes are riskier than reads: disjoint-target partitioning is prompt-only and
|
|
90
|
+
* unenforceable (the hook is stateless), so two concurrent `coder` subagents
|
|
91
|
+
* racing the same file in the shared worktree would corrupt the patch. Serialize
|
|
92
|
+
* by default until a real-binary smoke proves clean concurrent disjoint writes;
|
|
93
|
+
* override with an explicit `--max-concurrency N`. Effective only on kimi-code
|
|
94
|
+
* 0.18.0+ (the write-mode version gate), so the ceiling is always enforced.
|
|
95
|
+
*/
|
|
96
|
+
export const SWARM_WRITE_DEFAULT_MAX_CONCURRENCY = 1;
|
|
97
|
+
/** Resolve the write-mode concurrency ceiling: explicit wins, else default 1. */
|
|
98
|
+
export function resolveSwarmWriteMaxConcurrency(requested) {
|
|
99
|
+
return requested ?? SWARM_WRITE_DEFAULT_MAX_CONCURRENCY;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build the swarm coordination prompt. States the READ-ONLY review GOAL and the
|
|
103
|
+
* desired report shape, then trusts the model to fan the work out with its own
|
|
104
|
+
* subagent capability — we deliberately do NOT prescribe the AgentSwarm tool call,
|
|
105
|
+
* subagent_type, or prompt_template/items mechanics. Over-prescribing those made
|
|
106
|
+
* the kimi-code 0.26.0 (k3) coordinator emit a non-sole-tool-call response that
|
|
107
|
+
* failed to launch (0 records, hang to budget); a goal-only prompt lets it launch.
|
|
108
|
+
* Read-only is NOT enforced by this prose — the PreToolUse hook (swarm label) at
|
|
109
|
+
* policy index 0 denies every write/edit/shell for the coordinator AND every
|
|
110
|
+
* subagent, so slimming this text does not move the safety boundary. The optional
|
|
111
|
+
* `cap` is a SOFT subagent-count hint (the hook cannot enforce a count). The HARD
|
|
112
|
+
* concurrency ceiling is separate: --max-concurrency →
|
|
113
|
+
* KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY (see executeSwarmJob / cli-client buildEnv).
|
|
114
|
+
* The --budget wall-clock ceiling is the always-on hard bound on the whole run.
|
|
115
|
+
*/
|
|
116
|
+
export function buildSwarmPrompt(objective, cap) {
|
|
117
|
+
const trimmed = objective.trim();
|
|
118
|
+
const capClause = cap !== undefined ? `Use at most ${cap} parallel subagents.` : undefined;
|
|
119
|
+
return [
|
|
120
|
+
"Review the following read-only: inspect and report; do not modify anything.",
|
|
121
|
+
"",
|
|
122
|
+
`Objective: ${trimmed}`,
|
|
123
|
+
"",
|
|
124
|
+
"Only Read, Grep, and Glob are available in this mode — shell/Bash and every write/edit tool",
|
|
125
|
+
"are denied. Launch the parallel fan-out as your FIRST action; do NOT run preliminary shell",
|
|
126
|
+
"commands to size or list files first (the subagents inspect the workspace directly with",
|
|
127
|
+
"Read/Grep/Glob).",
|
|
128
|
+
"Fan the work out across the relevant targets in parallel using your own subagent capability,",
|
|
129
|
+
"as you judge best — split or group the work however fits the objective.",
|
|
130
|
+
...(capClause ? [capClause] : []),
|
|
131
|
+
"",
|
|
132
|
+
"Consolidate everything into a single markdown report: a short verdict line, a brief summary,",
|
|
133
|
+
"then findings as `file:line — issue — severity`. Return plain markdown — no JSON wrapper, no",
|
|
134
|
+
"outer code fences.",
|
|
135
|
+
].join("\n");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Build the WRITE-mode coordination prompt. Unlike the read-only prompt, this
|
|
139
|
+
* sets subagent_type="coder" (write-capable) and instructs DISJOINT one-target-
|
|
140
|
+
* per-subagent editing with NO git and NO nested swarm. The coordinator and all
|
|
141
|
+
* subagents run in an ephemeral throwaway worktree; the PreToolUse hook
|
|
142
|
+
* (swarm-write label) confines writes there and the plugin captures a patch the
|
|
143
|
+
* user reviews. The `cap` is the same SOFT total-count hint as read mode.
|
|
144
|
+
*/
|
|
145
|
+
export function buildSwarmWritePrompt(objective, cap) {
|
|
146
|
+
const trimmed = objective.trim();
|
|
147
|
+
const capClause = cap !== undefined
|
|
148
|
+
? `Launch at most ${cap} subagents (soft cap — split or group targets to stay within it).`
|
|
149
|
+
: "Launch one subagent per distinct target; group related targets if there are many.";
|
|
150
|
+
return [
|
|
151
|
+
"Coordinate a parallel CODE-EDITING task using the AgentSwarm tool.",
|
|
152
|
+
"",
|
|
153
|
+
`Objective: ${trimmed}`,
|
|
154
|
+
"",
|
|
155
|
+
"You are running inside a throwaway git worktree dedicated to this task — edit freely;",
|
|
156
|
+
"your changes will be captured as a patch for the human to review and apply. Use the",
|
|
157
|
+
'AgentSwarm tool to fan the work out across subagents. Set subagent_type to "coder" (the',
|
|
158
|
+
"write-capable profile). Give each subagent ONE distinct, DISJOINT target via prompt_template",
|
|
159
|
+
"+ items: a single file or module per subagent, with NO overlap between subagents (two",
|
|
160
|
+
"subagents editing the same file will corrupt the result).",
|
|
161
|
+
capClause,
|
|
162
|
+
"",
|
|
163
|
+
"Hard rules for every subagent (the safety hook enforces these — violations are denied):",
|
|
164
|
+
"- Edit ONLY your assigned target. Writes are confined to this worktree; an out-of-worktree",
|
|
165
|
+
" write is denied.",
|
|
166
|
+
"- Do NOT run any git command that mutates state (no add/commit/checkout/etc.) — git mutation",
|
|
167
|
+
" is denied. The human owns all git operations.",
|
|
168
|
+
"- Do NOT call AgentSwarm yourself (no nested fan-out). One flat generation only.",
|
|
169
|
+
"- Report exactly what you changed and why.",
|
|
170
|
+
"",
|
|
171
|
+
"After the subagents return, consolidate into a single markdown report: a short verdict line,",
|
|
172
|
+
"a summary of what changed across all targets, then one section per target listing the edited",
|
|
173
|
+
"files. Do NOT paste full diffs (the patch is captured separately). Return plain markdown —",
|
|
174
|
+
"no JSON wrapper, no outer code fences.",
|
|
175
|
+
].join("\n");
|
|
176
|
+
}
|
|
177
|
+
export async function runSwarm(argv, context) {
|
|
178
|
+
const parsed = parseSwarmArgs(argv);
|
|
179
|
+
const objective = parsed.objective?.trim();
|
|
180
|
+
if (!objective) {
|
|
181
|
+
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");
|
|
182
|
+
}
|
|
183
|
+
return parsed.write
|
|
184
|
+
? runWriteSwarm(parsed, objective, context)
|
|
185
|
+
: runReadSwarm(parsed, objective, context);
|
|
186
|
+
}
|
|
187
|
+
/** READ-ONLY swarm (default). Unchanged from v1.2/v1.3. */
|
|
188
|
+
async function runReadSwarm(parsed, objective, context) {
|
|
189
|
+
const paths = resolvePluginPaths(context.env);
|
|
190
|
+
await ensurePluginPaths(paths);
|
|
191
|
+
const repoIdentity = await resolveRepoIdentity(context.cwd);
|
|
192
|
+
const store = new JobStore(paths);
|
|
193
|
+
try {
|
|
194
|
+
await sweepStaleJobs(store, paths);
|
|
195
|
+
const executionPlan = await prepareKimiExecutionPlan({
|
|
196
|
+
operationKind: "swarm",
|
|
197
|
+
cwd: context.cwd,
|
|
198
|
+
env: context.env,
|
|
199
|
+
});
|
|
200
|
+
const prompt = buildSwarmPrompt(objective, parsed.cap);
|
|
201
|
+
const jobId = randomUUID();
|
|
202
|
+
const logPath = path.join(paths.logsDir, `swarm-${jobId}.jsonl`);
|
|
203
|
+
const job = store.createJob({
|
|
204
|
+
job_id: jobId,
|
|
205
|
+
repo_id: repoIdentity.repoId,
|
|
206
|
+
// Reuse the read-only review lineage; the hook label below ("swarm") is
|
|
207
|
+
// what actually drives the allowlist.
|
|
208
|
+
command_type: "review",
|
|
209
|
+
cwd: context.cwd,
|
|
210
|
+
model: parsed.model ?? null,
|
|
211
|
+
thinking: parsed.thinking ?? null,
|
|
212
|
+
background: false,
|
|
213
|
+
pid: process.pid,
|
|
214
|
+
kimi_pid: null,
|
|
215
|
+
status: "running",
|
|
216
|
+
kimi_session_id: null,
|
|
217
|
+
...persistedExecutionPlanFields(executionPlan),
|
|
218
|
+
agent_profile: SWARM_AGENT_PROFILE,
|
|
219
|
+
prompt_digest: digestPrompt(prompt),
|
|
220
|
+
summary: `[swarm] ${shorten(objective, SWARM_SUMMARY_MAX)}`,
|
|
221
|
+
phase: "starting",
|
|
222
|
+
final_output_path: null,
|
|
223
|
+
stream_log_path: logPath,
|
|
224
|
+
error: null,
|
|
225
|
+
});
|
|
226
|
+
try {
|
|
227
|
+
await writeInvocationLogHeader(logPath, {
|
|
228
|
+
commandType: "swarm",
|
|
229
|
+
kimiSessionId: "(pending)",
|
|
230
|
+
cwd: context.cwd,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
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);
|
|
235
|
+
await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
|
|
236
|
+
throw classified;
|
|
237
|
+
}
|
|
238
|
+
const completed = await executeSwarmJob(job.job_id, prompt, objective, parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS, resolveSwarmMaxConcurrency(parsed.maxConcurrency), context);
|
|
239
|
+
if (!completed.final_output_path) {
|
|
240
|
+
throw new RuntimeError("SWARM_RESULT_MISSING", "Swarm finished without a rendered result.", "swarm.result");
|
|
241
|
+
}
|
|
242
|
+
return readArtifact(completed.final_output_path);
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
store.close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* WRITE-capable swarm (--write, v1.4). Spawns the coordinator in an ephemeral
|
|
250
|
+
* throwaway worktree off HEAD; `coder` subagents edit there; writes are confined
|
|
251
|
+
* by the swarm-write hook label; the result is captured as a patch the main
|
|
252
|
+
* thread reviews + applies. The worktree is created here and removed on EVERY
|
|
253
|
+
* terminal path (the patch is captured inside executeSwarmJob, before this
|
|
254
|
+
* finally removes the tree). Reuses the rescue (write-capable) job lineage.
|
|
255
|
+
*/
|
|
256
|
+
async function runWriteSwarm(parsed, objective, context) {
|
|
257
|
+
const paths = resolvePluginPaths(context.env);
|
|
258
|
+
await ensurePluginPaths(paths);
|
|
259
|
+
const repoIdentity = await resolveRepoIdentity(context.cwd);
|
|
260
|
+
const store = new JobStore(paths);
|
|
261
|
+
try {
|
|
262
|
+
await sweepStaleJobs(store, paths);
|
|
263
|
+
// Write mode needs a real git repo and a born HEAD before any job state is
|
|
264
|
+
// created. Its execution plan is certified below from the actual throwaway
|
|
265
|
+
// worktree cwd, not from the user's tree.
|
|
266
|
+
assertWriteSwarmPreconditions(repoIdentity);
|
|
267
|
+
if (!(await hasBornHead(context.cwd))) {
|
|
268
|
+
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");
|
|
269
|
+
}
|
|
270
|
+
await sweepStaleWorktrees(paths, repoIdentity);
|
|
271
|
+
if (await isWorkingTreeDirty(context.cwd)) {
|
|
272
|
+
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");
|
|
273
|
+
}
|
|
274
|
+
const prompt = buildSwarmWritePrompt(objective, parsed.cap);
|
|
275
|
+
const jobId = randomUUID();
|
|
276
|
+
const logPath = path.join(paths.logsDir, `swarm-write-${jobId}.jsonl`);
|
|
277
|
+
// Namespace worktrees per repo so the cross-repo sweep below only ever
|
|
278
|
+
// touches THIS repo's subtree (the worktreesDir is a single global dir shared
|
|
279
|
+
// across every repo the user runs --write in).
|
|
280
|
+
const worktreePath = path.join(paths.worktreesDir, repoIdentity.repoId, `swarm-write-${jobId}`);
|
|
281
|
+
await mkdir(path.dirname(worktreePath), { recursive: true });
|
|
282
|
+
try {
|
|
283
|
+
await createEphemeralWorktree(repoIdentity.repoRoot, "HEAD", worktreePath);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
throw (error instanceof RuntimeError
|
|
287
|
+
? error
|
|
288
|
+
: new RuntimeError("GIT_WORKTREE_ADD_FAILED", String(error), "swarm.worktree"));
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
// Prefix args can contain relative wrapper/script paths. Probe from the
|
|
292
|
+
// exact spawn cwd so the certified command tuple and its effective target
|
|
293
|
+
// cannot diverge merely because --write runs in a separate worktree.
|
|
294
|
+
const executionPlan = await prepareKimiExecutionPlan({
|
|
295
|
+
operationKind: "swarm-write",
|
|
296
|
+
cwd: worktreePath,
|
|
297
|
+
env: context.env,
|
|
298
|
+
});
|
|
299
|
+
const job = store.createJob({
|
|
300
|
+
job_id: jobId,
|
|
301
|
+
repo_id: repoIdentity.repoId,
|
|
302
|
+
// Reuse the rescue (write-capable) lineage, like pursue; the hook label
|
|
303
|
+
// below ("swarm-write") drives the allowlist. cwd stays the USER's real
|
|
304
|
+
// cwd for lineage — the kimi spawn cwd is the worktree (threaded via the
|
|
305
|
+
// writeMode arg to executeSwarmJob).
|
|
306
|
+
command_type: "rescue",
|
|
307
|
+
cwd: context.cwd,
|
|
308
|
+
model: parsed.model ?? null,
|
|
309
|
+
thinking: parsed.thinking ?? null,
|
|
310
|
+
background: false,
|
|
311
|
+
pid: process.pid,
|
|
312
|
+
kimi_pid: null,
|
|
313
|
+
status: "running",
|
|
314
|
+
kimi_session_id: null,
|
|
315
|
+
...persistedExecutionPlanFields(executionPlan),
|
|
316
|
+
agent_profile: SWARM_WRITE_AGENT_PROFILE,
|
|
317
|
+
prompt_digest: digestPrompt(prompt),
|
|
318
|
+
summary: `[swarm-write] ${shorten(objective, SWARM_SUMMARY_MAX)}`,
|
|
319
|
+
phase: "starting",
|
|
320
|
+
final_output_path: null,
|
|
321
|
+
stream_log_path: logPath,
|
|
322
|
+
error: null,
|
|
323
|
+
});
|
|
324
|
+
try {
|
|
325
|
+
await writeInvocationLogHeader(logPath, {
|
|
326
|
+
commandType: "swarm-write",
|
|
327
|
+
kimiSessionId: "(pending)",
|
|
328
|
+
cwd: context.cwd,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
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);
|
|
333
|
+
await markJobFailed(store, paths, job, classified, "Swarm (write) failed.", {
|
|
334
|
+
phase: "failed",
|
|
335
|
+
});
|
|
336
|
+
throw classified;
|
|
337
|
+
}
|
|
338
|
+
const completed = await executeSwarmJob(job.job_id, prompt, objective, parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS, resolveSwarmWriteMaxConcurrency(parsed.maxConcurrency), context, { spawnCwd: worktreePath, workspaceRoot: worktreePath });
|
|
339
|
+
if (!completed.final_output_path) {
|
|
340
|
+
throw new RuntimeError("SWARM_RESULT_MISSING", "Swarm (write) finished without a rendered result.", "swarm.result");
|
|
341
|
+
}
|
|
342
|
+
return readArtifact(completed.final_output_path);
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
// Patch was captured inside executeSwarmJob (before this runs) on every
|
|
346
|
+
// terminal path; --force discards the now-disposable worktree.
|
|
347
|
+
await removeWorktree(repoIdentity.repoRoot, worktreePath);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
store.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/** Refuse --write outside a git repo (git worktree add has nothing to fork). */
|
|
355
|
+
function assertWriteSwarmPreconditions(repoIdentity) {
|
|
356
|
+
if (!repoIdentity.isGitRepo) {
|
|
357
|
+
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");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Reap orphaned write-swarm worktrees from prior crashed runs. Normal runs remove
|
|
362
|
+
* their own worktree in a finally; an orphan only survives a hard kill. Prunes
|
|
363
|
+
* stale admin entries (current repo) then force-removes on-disk worktree dirs
|
|
364
|
+
* older than the TTL. Best-effort — never blocks the run.
|
|
365
|
+
*/
|
|
366
|
+
async function sweepStaleWorktrees(paths, repoIdentity) {
|
|
367
|
+
await pruneWorktrees(repoIdentity.repoRoot);
|
|
368
|
+
// Only sweep THIS repo's namespaced subdir, so we never `rm` a worktree that a
|
|
369
|
+
// different repo still considers live (and never leave that repo a dangling
|
|
370
|
+
// admin entry). repoId is the realpath-stable hash from resolveRepoIdentity.
|
|
371
|
+
const repoWorktreesDir = path.join(paths.worktreesDir, repoIdentity.repoId);
|
|
372
|
+
const entries = await readdir(repoWorktreesDir, { withFileTypes: true }).catch(() => null);
|
|
373
|
+
if (entries === null)
|
|
374
|
+
return; // not created yet — nothing to sweep.
|
|
375
|
+
const now = Date.now();
|
|
376
|
+
for (const entry of entries) {
|
|
377
|
+
if (!entry.isDirectory() || !entry.name.startsWith("swarm-write-"))
|
|
378
|
+
continue;
|
|
379
|
+
const full = path.join(repoWorktreesDir, entry.name);
|
|
380
|
+
try {
|
|
381
|
+
const st = await stat(full);
|
|
382
|
+
if (now - st.mtimeMs > WORKTREE_ORPHAN_TTL_MS) {
|
|
383
|
+
await removeWorktree(repoIdentity.repoRoot, full);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// best-effort per entry
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Capture the worktree change set as a `.patch` artifact (write mode). Returns
|
|
393
|
+
* the artifact path + size, or null on capture failure. Called on EVERY terminal
|
|
394
|
+
* path so partial work survives a cancel/budget-expiry.
|
|
395
|
+
*/
|
|
396
|
+
async function capturePatchArtifact(paths, jobId, worktreePath, stderr) {
|
|
397
|
+
try {
|
|
398
|
+
const patch = await captureWorktreePatch(worktreePath);
|
|
399
|
+
const patchPath = path.join(paths.artifactsDir, `swarm-write-${jobId}.patch`);
|
|
400
|
+
await writeFile(patchPath, patch, "utf8");
|
|
401
|
+
return { path: patchPath, bytes: Buffer.byteLength(patch, "utf8"), empty: patch.trim().length === 0 };
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
stderr.write(`[kimi-plugin-cc] swarm-write patch capture failed for job ${jobId}: ${error.message ?? String(error)}\n`);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/** Render the patch-handoff header prepended to a write-swarm report. */
|
|
409
|
+
function renderWriteSwarmHeader(captured) {
|
|
410
|
+
if (captured === null) {
|
|
411
|
+
return "**Patch:** capture FAILED — no patch was written; inspect the run logs.";
|
|
412
|
+
}
|
|
413
|
+
if (captured.empty) {
|
|
414
|
+
return "**Patch:** the swarm made NO changes (empty patch).";
|
|
415
|
+
}
|
|
416
|
+
return [
|
|
417
|
+
`**Patch written to:** \`${captured.path}\` (${captured.bytes} bytes).`,
|
|
418
|
+
`Review it, then apply from the repo root with: \`git apply --3way ${captured.path}\` ` +
|
|
419
|
+
"(the worktree was based on HEAD, so `--3way` reconciles against your current tree; " +
|
|
420
|
+
"plain `git apply` works only if your tree hasn't moved since).",
|
|
421
|
+
"The plugin did NOT apply or commit anything — you own the merge.",
|
|
422
|
+
].join("\n");
|
|
423
|
+
}
|
|
424
|
+
async function executeSwarmJob(jobId, prompt, objective, budgetMs, maxConcurrency, context,
|
|
425
|
+
/**
|
|
426
|
+
* Write mode (v1.4): when set, the kimi spawn cwd is the throwaway worktree
|
|
427
|
+
* (`spawnCwd`), the hook label is "swarm-write", and `workspaceRoot` (the same
|
|
428
|
+
* worktree) is exported as the trusted allowlist root + used to capture the
|
|
429
|
+
* patch. Undefined ⇒ read-only swarm (label "swarm", spawn cwd = job.cwd).
|
|
430
|
+
*/
|
|
431
|
+
writeMode) {
|
|
432
|
+
const paths = resolvePluginPaths(context.env);
|
|
433
|
+
await ensurePluginPaths(paths);
|
|
434
|
+
const store = new JobStore(paths);
|
|
435
|
+
const job = store.getJob(jobId);
|
|
436
|
+
if (!job) {
|
|
437
|
+
store.close();
|
|
438
|
+
throw new RuntimeError("JOB_NOT_FOUND", `Swarm job ${jobId} was not found.`, "swarm.worker");
|
|
439
|
+
}
|
|
440
|
+
// Swarm is read-only BY POLICY, but it fans out N subagents — without the
|
|
441
|
+
// PreToolUse hook there is no enforcement on ANY of them. Fail closed before
|
|
442
|
+
// spawning, matching the single-turn read-only commands.
|
|
443
|
+
if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK !== "1") {
|
|
444
|
+
const installStatus = await verifyHookInstalled(context.env);
|
|
445
|
+
if (!installStatus.installed) {
|
|
446
|
+
maybeWarnHookMissing(installStatus, "swarm", context.stderr);
|
|
447
|
+
const classified = new RuntimeError("SWARM_HOOK_NOT_INSTALLED", [
|
|
448
|
+
"/kimi:swarm refuses to run without the kimi-plugin-cc PreToolUse hook.",
|
|
449
|
+
"Swarm fans out multiple subagents; the hook is the ONLY thing keeping every",
|
|
450
|
+
"one of them read-only, so a missing hook means no enforcement across the fan-out.",
|
|
451
|
+
`Hook check failed: ${installStatus.reason ?? "unknown"}.`,
|
|
452
|
+
"Repair by running Claude Code /kimi:setup or Codex $kimi-setup, then retry.",
|
|
453
|
+
hookRefusalRetryProtocol(context.env),
|
|
454
|
+
].join(" "), "swarm.hook-check", { details: hookRefusalDetails(installStatus) });
|
|
455
|
+
try {
|
|
456
|
+
return await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
store.close();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const handlers = createCliCancellationHandlers();
|
|
464
|
+
try {
|
|
465
|
+
const executionPlan = executionPlanFromPersisted(job);
|
|
466
|
+
store.updateRunningJob(job.job_id, { phase: "turn-running" });
|
|
467
|
+
const result = await runCliPromptWithBudget({
|
|
468
|
+
// Read mode spawns in the user's cwd; WRITE mode spawns in the throwaway
|
|
469
|
+
// worktree so coder subagents edit there (job.cwd stays the user's real
|
|
470
|
+
// cwd for lineage). This is the load-bearing isolation seam.
|
|
471
|
+
cwd: writeMode?.spawnCwd ?? job.cwd,
|
|
472
|
+
env: context.env,
|
|
473
|
+
command: executionPlan.command,
|
|
474
|
+
prefixArgs: [...executionPlan.prefixArgs],
|
|
475
|
+
executionPlan,
|
|
476
|
+
prompt,
|
|
477
|
+
// "swarm" → read-only-plus-AgentSwarm allowlist; "swarm-write" → the same
|
|
478
|
+
// PLUS rescue-grade write confinement to the trusted worktree root. The
|
|
479
|
+
// label drives the hook for the coordinator AND every spawned subagent.
|
|
480
|
+
commandLabel: writeMode ? "swarm-write" : "swarm",
|
|
481
|
+
// The HARD concurrency ceiling on kimi-code 0.18.0+ (exported as
|
|
482
|
+
// KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY; ignored by older binaries).
|
|
483
|
+
// ALWAYS set: defaults to SWARM_DEFAULT_MAX_CONCURRENCY (read) /
|
|
484
|
+
// SWARM_WRITE_DEFAULT_MAX_CONCURRENCY (write) when the user passes no
|
|
485
|
+
// --max-concurrency, so a fan-out is never unbounded. Distinct from --cap
|
|
486
|
+
// (the soft total-count prompt hint).
|
|
487
|
+
swarmMaxConcurrency: maxConcurrency,
|
|
488
|
+
// Trusted allowlist root for the swarm-write hook (forge-proof; ignored
|
|
489
|
+
// by other labels). Only set in write mode.
|
|
490
|
+
trustedWorkspaceRoot: writeMode?.workspaceRoot,
|
|
491
|
+
model: job.model ?? undefined,
|
|
492
|
+
logPath: job.stream_log_path,
|
|
493
|
+
signal: handlers.signal,
|
|
494
|
+
}, budgetMs, "swarm.prompt");
|
|
495
|
+
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
496
|
+
if (handlers.cancelling) {
|
|
497
|
+
throw new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
|
|
498
|
+
}
|
|
499
|
+
assertCliResultSuccess(result, "swarm.runtime");
|
|
500
|
+
if (result.sessionId !== undefined &&
|
|
501
|
+
result.sessionId.length > 0 &&
|
|
502
|
+
result.sessionId !== job.kimi_session_id) {
|
|
503
|
+
store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
|
|
504
|
+
}
|
|
505
|
+
await syncKimiSessionTitle({
|
|
506
|
+
promptText: objective,
|
|
507
|
+
env: context.env,
|
|
508
|
+
cwd: job.cwd,
|
|
509
|
+
sessionId: result.sessionId ?? job.kimi_session_id,
|
|
510
|
+
title: buildKimiSessionTitle(writeMode ? "swarm-write" : "swarm", objective),
|
|
511
|
+
stderr: context.stderr,
|
|
512
|
+
});
|
|
513
|
+
warnIfSessionIdMissing(result, "swarm", job.job_id, context.stderr);
|
|
514
|
+
const prose = reassembleProseFromRecords(result.records);
|
|
515
|
+
// WRITE mode: capture the worktree change set as a .patch artifact (before
|
|
516
|
+
// runWriteSwarm's finally removes the worktree) and prepend the patch-handoff
|
|
517
|
+
// header so the consolidated report tells the main thread where the patch is.
|
|
518
|
+
const finalText = writeMode
|
|
519
|
+
? `${renderWriteSwarmHeader(await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr))}\n\n---\n\n${prose}`.trimEnd()
|
|
520
|
+
: prose;
|
|
521
|
+
const rendered = renderManagedJobOutput(job, finalText);
|
|
522
|
+
let artifactPath;
|
|
523
|
+
try {
|
|
524
|
+
artifactPath = await writeArtifact(paths, job, rendered.rendered);
|
|
525
|
+
}
|
|
526
|
+
catch (writeError) {
|
|
527
|
+
context.stderr.write(`[kimi-plugin-cc] swarm artifact write failed for job ${job.job_id}; raw output preserved in error details.\n`);
|
|
528
|
+
const classified = new RuntimeError("SWARM_ARTIFACT_WRITE_FAILED", `Failed to write swarm artifact: ${writeError.message ?? String(writeError)}`, "swarm.artifact", {
|
|
529
|
+
...(writeError instanceof Error ? { cause: writeError } : {}),
|
|
530
|
+
details: { rawOutput: finalText },
|
|
531
|
+
});
|
|
532
|
+
return await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
|
|
533
|
+
}
|
|
534
|
+
if (handlers.cancelling) {
|
|
535
|
+
throw new RuntimeError("SWARM_CANCELLED", "Swarm cancelled after artifact write.", "swarm.runtime");
|
|
536
|
+
}
|
|
537
|
+
return (store.markCompleted(job.job_id, {
|
|
538
|
+
summary: `[swarm] ${shorten(objective, SWARM_SUMMARY_MAX)}`,
|
|
539
|
+
phase: "done",
|
|
540
|
+
final_output_path: artifactPath,
|
|
541
|
+
error: null,
|
|
542
|
+
}) ?? job);
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
// WRITE mode: best-effort capture of any PARTIAL work before we mark the job
|
|
546
|
+
// and before runWriteSwarm's finally removes the worktree, so a cancel /
|
|
547
|
+
// budget-expiry doesn't silently discard edits the subagents already made.
|
|
548
|
+
if (writeMode) {
|
|
549
|
+
const captured = await capturePatchArtifact(paths, job.job_id, writeMode.workspaceRoot, context.stderr);
|
|
550
|
+
if (captured && !captured.empty) {
|
|
551
|
+
context.stderr.write(`[kimi-plugin-cc] swarm-write run ended early; partial patch captured at ${captured.path}.\n`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (handlers.cancelling) {
|
|
555
|
+
const cancelledError = new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime", error instanceof Error ? { cause: error } : undefined);
|
|
556
|
+
return await markJobCancelled(store, paths, job, "Swarm cancelled by user request.", cancelledError, {
|
|
557
|
+
phase: "cancelled",
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
const classified = classifyManagedCommandFailure(error, writeMode ? "rescue" : "review", job.job_id, {
|
|
561
|
+
preserveStage: true,
|
|
562
|
+
});
|
|
563
|
+
return await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
handlers.dispose();
|
|
567
|
+
store.close();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function shorten(text, max) {
|
|
571
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
572
|
+
if (normalized.length <= max) {
|
|
573
|
+
return normalized;
|
|
574
|
+
}
|
|
575
|
+
return `${normalized.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
|
|
576
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { RuntimeError } from "../errors.js";
|
|
2
|
+
import { executeRescueJob } from "./rescue.js";
|
|
3
|
+
import { executeAskJob } from "./ask.js";
|
|
4
|
+
export async function runWorker(argv, context) {
|
|
5
|
+
const [kind, jobId] = argv;
|
|
6
|
+
if (!jobId || (kind !== "rescue" && kind !== "ask")) {
|
|
7
|
+
throw new RuntimeError("INVALID_COMMAND", "worker expects the form: worker rescue <job-id> or worker ask <job-id>.", "worker");
|
|
8
|
+
}
|
|
9
|
+
if (kind === "rescue") {
|
|
10
|
+
const encodedPrompt = context.env.KIMI_PLUGIN_CC_RESCUE_PROMPT_B64;
|
|
11
|
+
if (!encodedPrompt) {
|
|
12
|
+
throw new RuntimeError("MISSING_RESCUE_PROMPT", "Background rescue prompt is missing.", "worker");
|
|
13
|
+
}
|
|
14
|
+
const prompt = Buffer.from(encodedPrompt, "base64").toString("utf8");
|
|
15
|
+
const reusedSession = context.env.KIMI_PLUGIN_CC_RESCUE_REUSED_SESSION === "1";
|
|
16
|
+
await executeRescueJob(jobId, prompt, context, { workerPid: process.pid, reusedSession });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// kind === "ask"
|
|
20
|
+
//
|
|
21
|
+
// v1.0 note: the ask worker no longer threads `reusedSession` or
|
|
22
|
+
// `rawPrompt` into executeAskJob — both were inputs to v0.4's
|
|
23
|
+
// `announceSessionTitle`, which is gone (kimi-web has no PATCH
|
|
24
|
+
// endpoint). The base64-encoded prompt + job id are sufficient.
|
|
25
|
+
const encodedPrompt = context.env.KIMI_PLUGIN_CC_ASK_PROMPT_B64;
|
|
26
|
+
if (!encodedPrompt) {
|
|
27
|
+
throw new RuntimeError("MISSING_ASK_PROMPT", "Background ask prompt is missing.", "worker");
|
|
28
|
+
}
|
|
29
|
+
const prompt = Buffer.from(encodedPrompt, "base64").toString("utf8");
|
|
30
|
+
await executeAskJob(jobId, prompt, context, { workerPid: process.pid });
|
|
31
|
+
}
|