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.
Files changed (190) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +16 -0
  3. package/.claude-plugin/plugin.json +32 -0
  4. package/CHANGELOG.md +1096 -0
  5. package/LICENSE +201 -0
  6. package/README.md +772 -0
  7. package/agents/kimi-ask.md +82 -0
  8. package/agents/kimi-challenge.md +82 -0
  9. package/agents/kimi-pursue.md +91 -0
  10. package/agents/kimi-rescue.md +85 -0
  11. package/agents/kimi-review.md +82 -0
  12. package/agents/kimi-swarm-write.md +92 -0
  13. package/agents/kimi-swarm.md +83 -0
  14. package/commands/README.md +16 -0
  15. package/commands/ask.md +32 -0
  16. package/commands/cancel.md +17 -0
  17. package/commands/challenge.md +29 -0
  18. package/commands/pursue.md +37 -0
  19. package/commands/replay.md +13 -0
  20. package/commands/rescue.md +32 -0
  21. package/commands/result.md +13 -0
  22. package/commands/review.md +29 -0
  23. package/commands/setup.md +23 -0
  24. package/commands/status.md +13 -0
  25. package/commands/swarm.md +38 -0
  26. package/dist/background-spawn.js +224 -0
  27. package/dist/cli-cancellation.js +43 -0
  28. package/dist/cli-client.js +845 -0
  29. package/dist/commands/ask.js +327 -0
  30. package/dist/commands/cancel.js +178 -0
  31. package/dist/commands/cli-helpers.js +112 -0
  32. package/dist/commands/models.js +140 -0
  33. package/dist/commands/not-implemented.js +4 -0
  34. package/dist/commands/pursue.js +311 -0
  35. package/dist/commands/registry.js +87 -0
  36. package/dist/commands/repair-sessions.js +159 -0
  37. package/dist/commands/replay.js +123 -0
  38. package/dist/commands/rescue.js +333 -0
  39. package/dist/commands/result.js +58 -0
  40. package/dist/commands/review-gate.js +393 -0
  41. package/dist/commands/review.js +218 -0
  42. package/dist/commands/setup.js +1132 -0
  43. package/dist/commands/status.js +27 -0
  44. package/dist/commands/swarm.js +576 -0
  45. package/dist/commands/worker.js +31 -0
  46. package/dist/companion.js +114 -0
  47. package/dist/config.js +22 -0
  48. package/dist/errors.js +40 -0
  49. package/dist/git.js +215 -0
  50. package/dist/hooks/approval-hook.js +112 -0
  51. package/dist/hooks/approval-policy.js +254 -0
  52. package/dist/hooks/config-safety.js +637 -0
  53. package/dist/hooks/install-paths.js +504 -0
  54. package/dist/hooks/install.js +281 -0
  55. package/dist/hooks/managed-block.js +745 -0
  56. package/dist/hooks/review-gate-stop.js +19 -0
  57. package/dist/job-store.js +559 -0
  58. package/dist/jobs.js +202 -0
  59. package/dist/kimi-command.js +72 -0
  60. package/dist/kimi-engine.js +624 -0
  61. package/dist/kimi-errors.js +220 -0
  62. package/dist/kimi-home.js +9 -0
  63. package/dist/kimi-timeouts.js +68 -0
  64. package/dist/kimi-version-probe.js +1410 -0
  65. package/dist/logging.js +22 -0
  66. package/dist/native-v2-preflight.js +417 -0
  67. package/dist/package.json +1 -0
  68. package/dist/parsing.js +553 -0
  69. package/dist/paths.js +27 -0
  70. package/dist/plugin-data.js +98 -0
  71. package/dist/process-tree.js +448 -0
  72. package/dist/render.js +159 -0
  73. package/dist/rescue-approval.js +951 -0
  74. package/dist/schemas/review-gate-output.js +43 -0
  75. package/dist/session-title.js +293 -0
  76. package/dist/stream-json.js +498 -0
  77. package/dist/types.js +1 -0
  78. package/dist/vendor/shell-quote/LICENSE +24 -0
  79. package/dist/vendor/shell-quote/index.js +1 -0
  80. package/dist/vendor/shell-quote/parse.js +229 -0
  81. package/dist/vendor/smol-toml/LICENSE +24 -0
  82. package/dist/vendor/smol-toml/date.js +127 -0
  83. package/dist/vendor/smol-toml/error.js +63 -0
  84. package/dist/vendor/smol-toml/extract.js +100 -0
  85. package/dist/vendor/smol-toml/parse.js +148 -0
  86. package/dist/vendor/smol-toml/primitive.js +179 -0
  87. package/dist/vendor/smol-toml/struct.js +184 -0
  88. package/dist/vendor/smol-toml/util.js +113 -0
  89. package/dist/version.js +7 -0
  90. package/docs/migration.md +215 -0
  91. package/docs/models.md +82 -0
  92. package/docs/registry-releases.md +70 -0
  93. package/docs/safety.md +318 -0
  94. package/docs/session-visibility.md +32 -0
  95. package/hooks/README.md +9 -0
  96. package/hooks/hooks.json +16 -0
  97. package/package.json +67 -0
  98. package/plugins/kimi-codex/.codex-plugin/plugin.json +39 -0
  99. package/plugins/kimi-codex/dist/background-spawn.js +224 -0
  100. package/plugins/kimi-codex/dist/cli-cancellation.js +43 -0
  101. package/plugins/kimi-codex/dist/cli-client.js +845 -0
  102. package/plugins/kimi-codex/dist/commands/ask.js +327 -0
  103. package/plugins/kimi-codex/dist/commands/cancel.js +178 -0
  104. package/plugins/kimi-codex/dist/commands/cli-helpers.js +112 -0
  105. package/plugins/kimi-codex/dist/commands/models.js +140 -0
  106. package/plugins/kimi-codex/dist/commands/not-implemented.js +4 -0
  107. package/plugins/kimi-codex/dist/commands/pursue.js +311 -0
  108. package/plugins/kimi-codex/dist/commands/registry.js +87 -0
  109. package/plugins/kimi-codex/dist/commands/repair-sessions.js +159 -0
  110. package/plugins/kimi-codex/dist/commands/replay.js +123 -0
  111. package/plugins/kimi-codex/dist/commands/rescue.js +333 -0
  112. package/plugins/kimi-codex/dist/commands/result.js +58 -0
  113. package/plugins/kimi-codex/dist/commands/review-gate.js +393 -0
  114. package/plugins/kimi-codex/dist/commands/review.js +218 -0
  115. package/plugins/kimi-codex/dist/commands/setup.js +1132 -0
  116. package/plugins/kimi-codex/dist/commands/status.js +27 -0
  117. package/plugins/kimi-codex/dist/commands/swarm.js +576 -0
  118. package/plugins/kimi-codex/dist/commands/worker.js +31 -0
  119. package/plugins/kimi-codex/dist/companion.js +114 -0
  120. package/plugins/kimi-codex/dist/config.js +22 -0
  121. package/plugins/kimi-codex/dist/errors.js +40 -0
  122. package/plugins/kimi-codex/dist/git.js +215 -0
  123. package/plugins/kimi-codex/dist/hooks/approval-hook.js +112 -0
  124. package/plugins/kimi-codex/dist/hooks/approval-policy.js +254 -0
  125. package/plugins/kimi-codex/dist/hooks/config-safety.js +637 -0
  126. package/plugins/kimi-codex/dist/hooks/install-paths.js +504 -0
  127. package/plugins/kimi-codex/dist/hooks/install.js +281 -0
  128. package/plugins/kimi-codex/dist/hooks/managed-block.js +745 -0
  129. package/plugins/kimi-codex/dist/hooks/review-gate-stop.js +19 -0
  130. package/plugins/kimi-codex/dist/job-store.js +559 -0
  131. package/plugins/kimi-codex/dist/jobs.js +202 -0
  132. package/plugins/kimi-codex/dist/kimi-command.js +72 -0
  133. package/plugins/kimi-codex/dist/kimi-engine.js +624 -0
  134. package/plugins/kimi-codex/dist/kimi-errors.js +220 -0
  135. package/plugins/kimi-codex/dist/kimi-home.js +9 -0
  136. package/plugins/kimi-codex/dist/kimi-timeouts.js +68 -0
  137. package/plugins/kimi-codex/dist/kimi-version-probe.js +1410 -0
  138. package/plugins/kimi-codex/dist/logging.js +22 -0
  139. package/plugins/kimi-codex/dist/native-v2-preflight.js +417 -0
  140. package/plugins/kimi-codex/dist/package.json +1 -0
  141. package/plugins/kimi-codex/dist/parsing.js +553 -0
  142. package/plugins/kimi-codex/dist/paths.js +27 -0
  143. package/plugins/kimi-codex/dist/plugin-data.js +98 -0
  144. package/plugins/kimi-codex/dist/process-tree.js +448 -0
  145. package/plugins/kimi-codex/dist/render.js +159 -0
  146. package/plugins/kimi-codex/dist/rescue-approval.js +951 -0
  147. package/plugins/kimi-codex/dist/schemas/review-gate-output.js +43 -0
  148. package/plugins/kimi-codex/dist/session-title.js +293 -0
  149. package/plugins/kimi-codex/dist/stream-json.js +498 -0
  150. package/plugins/kimi-codex/dist/types.js +1 -0
  151. package/plugins/kimi-codex/dist/vendor/shell-quote/LICENSE +24 -0
  152. package/plugins/kimi-codex/dist/vendor/shell-quote/index.js +1 -0
  153. package/plugins/kimi-codex/dist/vendor/shell-quote/parse.js +229 -0
  154. package/plugins/kimi-codex/dist/vendor/smol-toml/LICENSE +24 -0
  155. package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +127 -0
  156. package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +63 -0
  157. package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +100 -0
  158. package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +148 -0
  159. package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +179 -0
  160. package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +184 -0
  161. package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +113 -0
  162. package/plugins/kimi-codex/dist/version.js +7 -0
  163. package/plugins/kimi-codex/scripts/companion.sh +37 -0
  164. package/plugins/kimi-codex/scripts/review-gate-hook.sh +32 -0
  165. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +31 -0
  166. package/plugins/kimi-codex/skills/kimi-ask/agents/openai.yaml +6 -0
  167. package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +25 -0
  168. package/plugins/kimi-codex/skills/kimi-cancel/agents/openai.yaml +6 -0
  169. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +31 -0
  170. package/plugins/kimi-codex/skills/kimi-challenge/agents/openai.yaml +6 -0
  171. package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +30 -0
  172. package/plugins/kimi-codex/skills/kimi-pursue/agents/openai.yaml +6 -0
  173. package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +25 -0
  174. package/plugins/kimi-codex/skills/kimi-replay/agents/openai.yaml +6 -0
  175. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +30 -0
  176. package/plugins/kimi-codex/skills/kimi-rescue/agents/openai.yaml +6 -0
  177. package/plugins/kimi-codex/skills/kimi-result/SKILL.md +25 -0
  178. package/plugins/kimi-codex/skills/kimi-result/agents/openai.yaml +6 -0
  179. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +31 -0
  180. package/plugins/kimi-codex/skills/kimi-review/agents/openai.yaml +6 -0
  181. package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +32 -0
  182. package/plugins/kimi-codex/skills/kimi-setup/agents/openai.yaml +6 -0
  183. package/plugins/kimi-codex/skills/kimi-status/SKILL.md +25 -0
  184. package/plugins/kimi-codex/skills/kimi-status/agents/openai.yaml +6 -0
  185. package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +31 -0
  186. package/plugins/kimi-codex/skills/kimi-swarm/agents/openai.yaml +6 -0
  187. package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +32 -0
  188. package/plugins/kimi-codex/skills/kimi-swarm-write/agents/openai.yaml +6 -0
  189. package/scripts/companion.sh +37 -0
  190. package/scripts/review-gate-hook.sh +32 -0
@@ -0,0 +1,845 @@
1
+ // Subprocess client for `kimi -p --output-format stream-json`.
2
+ //
3
+ // Replaces runtime/wire/client.ts for v1.0. Both modules coexist during the
4
+ // PR 1-3 transition; PR 4 deletes wire/. The contract is intentionally
5
+ // narrower than the v0.4 wire client — one process per prompt, no in-band
6
+ // approvals (PreToolUse hook handles those), no resumable JSON-RPC channel.
7
+ //
8
+ // Where v0.4 streamed turn events to a TurnCapture state machine,
9
+ // stream-json gives us flattened OpenAI-shaped records that callers
10
+ // reassemble at their own seam. The buffered result is the primary return
11
+ // shape; callers needing incremental visibility (e.g., updating SQLite
12
+ // phase as tools run) can pass `onRecord` to observe records as they arrive
13
+ // without paying for a separate event-stream API.
14
+ //
15
+ // Per-job env (`KIMI_PLUGIN_CC_CMD`) is set on this spawn so the
16
+ // PreToolUse hook can branch per command (review vs rescue vs ask). It is
17
+ // passed via the spawn `env` option, not exported to a shell, so concurrent
18
+ // jobs never share an env block.
19
+ import { spawn } from "node:child_process";
20
+ import { appendFile, mkdir } from "node:fs/promises";
21
+ import path from "node:path";
22
+ import { RuntimeError, formatError } from "./errors.js";
23
+ import { assertNoUnsafeExperimentalSelector, assertExecutionPlanMatchesSpawn, } from "./kimi-engine.js";
24
+ import { assertPrefixArgsSafe } from "./kimi-command.js";
25
+ import { resolveKimiHome } from "./kimi-home.js";
26
+ import { assertNativeV2Preflight } from "./native-v2-preflight.js";
27
+ import { parseVersionLine } from "./kimi-version-probe.js";
28
+ import { StreamJsonParser, extractSessionIdFromStderr, } from "./stream-json.js";
29
+ import { collectDescendantIdentities, revalidateProcessIdentities, waitForProcessTreeExit, } from "./process-tree.js";
30
+ /** Bytes of stderr retained for diagnostics on completion. Rolling buffer. */
31
+ const STDERR_TAIL_BYTES = 8192;
32
+ /** Hard cap on awaiting diagnostics log drain before resolving the result. */
33
+ const LOG_DRAIN_TIMEOUT_MS = 250;
34
+ /**
35
+ * Default delay between SIGTERM and SIGKILL when an abort fires.
36
+ * Matches v0.4's `cancellation.ts` escalation timing for wire clients,
37
+ * so rescue/ask/review continue to feel identical to a stuck-kimi user
38
+ * after the v1.0 cutover.
39
+ */
40
+ const DEFAULT_ESCALATION_MS = 1_500;
41
+ /** Maximum time spent confirming POSIX tree exit after SIGKILL dispatch. */
42
+ const POST_KILL_QUIESCENCE_MS = 500;
43
+ /** Hard bound for the one-shot descendant identity snapshot. */
44
+ const DESCENDANT_SNAPSHOT_TIMEOUT_MS = 1_000;
45
+ /** Hard bound for each batched identity revalidation pass. */
46
+ const IDENTITY_REVALIDATION_TIMEOUT_MS = 250;
47
+ const DESCENDANT_SNAPSHOT_TIMEOUT = Symbol("descendant-snapshot-timeout");
48
+ export async function runCliPrompt(opts) {
49
+ // Pre-aborted signal: refuse to spawn at all.
50
+ if (opts.signal?.aborted === true) {
51
+ throw new RuntimeError("CLI_ABORTED", "kimi subprocess request cancelled before spawn", "cli-client.pre-spawn", { details: { command: opts.command } });
52
+ }
53
+ assertExecutionPlanMatchesSpawn(opts.executionPlan, opts.command, opts.prefixArgs ?? [], opts.env);
54
+ if ((opts.commandLabel === "rescue" || opts.commandLabel === "swarm-write") &&
55
+ !opts.trustedWorkspaceRoot) {
56
+ throw new RuntimeError("CLI_TRUSTED_WORKSPACE_ROOT_MISSING", `Refusing ${opts.commandLabel}: write-capable plugin launches require a plugin-owned trusted workspace root.`, "cli-client.pre-spawn", { details: { command_label: opts.commandLabel } });
57
+ }
58
+ // The master experimental flag enables v2-only features (tower, subagent
59
+ // fork) outside the certified no-plan profile. Refuse it at the final
60
+ // boundary too, in case the environment changed after plan creation.
61
+ assertNoUnsafeExperimentalSelector(opts.env, "cli-client.pre-spawn");
62
+ // Validate the ACTUAL prefix argv used for this spawn — not only the
63
+ // env-resolved value — so a forged/corrupt persisted plan cannot smuggle a
64
+ // reserved kimi flag (e.g. `-r <session>`, which would resume a session the
65
+ // v2 preflight never scanned for plan taint).
66
+ assertPrefixArgsSafe(opts.prefixArgs ?? []);
67
+ if (opts.executionPlan.intendedEngine === "native-v2") {
68
+ // Re-run the cheap no-plan preconditions at the final boundary: the
69
+ // operator's config.toml and the resume journal are mutable between plan
70
+ // creation (possibly in another process) and this spawn.
71
+ await assertNativeV2Preflight({
72
+ kimiHome: resolveKimiHome(opts.env, opts.cwd),
73
+ env: opts.env,
74
+ resumeSessionId: opts.resumeSessionId,
75
+ stage: "cli-client.spawn",
76
+ });
77
+ }
78
+ const args = buildArgs(opts);
79
+ const env = buildEnv(opts);
80
+ if (opts.logPath !== undefined) {
81
+ await mkdir(path.dirname(opts.logPath), { recursive: true }).catch(() => {
82
+ // Best-effort; if mkdir fails the appendFile below will error and be
83
+ // swallowed by the log chain (diagnostics are non-blocking).
84
+ });
85
+ }
86
+ let child;
87
+ try {
88
+ child = spawn(opts.command, args, {
89
+ cwd: opts.cwd,
90
+ env,
91
+ stdio: "pipe",
92
+ // POSIX gets a fresh process group so cancellation reaches kimi's
93
+ // descendants; Windows has no negative-pid process-group signaling.
94
+ detached: process.platform !== "win32",
95
+ });
96
+ }
97
+ catch (err) {
98
+ throw new RuntimeError("CLI_SPAWN_FAILED", `Failed to spawn kimi: ${formatError(err)}`, "cli-client.spawn", err instanceof Error
99
+ ? { cause: err, details: { command: opts.command, args } }
100
+ : { details: { command: opts.command, args } });
101
+ }
102
+ // Pre-attach an 'error' listener immediately. Bun (and Node) can emit
103
+ // 'error' asynchronously between spawn returning and our Promise body
104
+ // attaching the real listener — without a handler in place, EventEmitter
105
+ // crashes the process. Capture into an array (not a single slot) so two
106
+ // back-to-back errors don't lose the first; we replay the first one when
107
+ // the real handler installs. The replacement is atomic across two
108
+ // synchronous statements (flag-flip + .on attach), so no async work can
109
+ // interleave between them.
110
+ const earlyErrors = [];
111
+ let earlyErrorHandled = false;
112
+ child.on("error", function earlyErrorListener(err) {
113
+ if (earlyErrorHandled)
114
+ return;
115
+ earlyErrors.push(err);
116
+ });
117
+ const parser = new StreamJsonParser();
118
+ const records = [];
119
+ const malformed = [];
120
+ // Rolling stderr tail — keep only the trailing STDERR_TAIL_BYTES so a
121
+ // long-running kimi process emitting megabytes of stderr (tool.progress,
122
+ // thinking deltas) doesn't grow our RSS unboundedly.
123
+ let stderrTail = "";
124
+ let announcedSessionId;
125
+ let announcedGoalSummary;
126
+ let announcedSystemVersion;
127
+ let engineMismatchDetected = false;
128
+ let handleEngineMismatch;
129
+ const plannedEngine = opts.executionPlan.intendedEngine;
130
+ const plannedVersion = opts.executionPlan.kimiVersion === null
131
+ ? null
132
+ : (parseVersionLine(opts.executionPlan.kimiVersion)?.raw ?? opts.executionPlan.kimiVersion);
133
+ let logChain = Promise.resolve();
134
+ const appendLogLine = (payload) => {
135
+ if (opts.logPath === undefined)
136
+ return;
137
+ const line = `${JSON.stringify({ ts: new Date().toISOString(), ...payload })}\n`;
138
+ logChain = logChain.then(() => appendFile(opts.logPath, line, "utf8").catch(() => {
139
+ // Diagnostics best-effort; never crash on log failure.
140
+ }));
141
+ };
142
+ appendLogLine({
143
+ event: "spawn",
144
+ command: opts.command,
145
+ args,
146
+ cwd: opts.cwd,
147
+ command_label: opts.commandLabel ?? null,
148
+ operation_kind: opts.executionPlan.operationKind,
149
+ intended_engine: opts.executionPlan.intendedEngine,
150
+ kimi_version: opts.executionPlan.kimiVersion,
151
+ plan_certification: opts.executionPlan.certification,
152
+ safety_profile: opts.executionPlan.safetyProfile,
153
+ swarm_max_concurrency: opts.swarmMaxConcurrency ?? null,
154
+ legacy_v1_forced: env.KIMI_CODE_LEGACY_FLAG === KIMI_LEGACY_FORCED_VALUE,
155
+ });
156
+ const invokeOnRecord = (record) => {
157
+ if (opts.onRecord === undefined)
158
+ return;
159
+ try {
160
+ opts.onRecord(record);
161
+ }
162
+ catch {
163
+ // Caller-supplied callback must never destabilize the parse loop.
164
+ }
165
+ };
166
+ // A native-v2 plan must see agent-core-v2's pre-tool system.version marker
167
+ // before any consumer-facing output; a legacy plan must never see it.
168
+ const requireMarkerBeforeOutput = (what) => {
169
+ if (plannedEngine !== "native-v2" || announcedSystemVersion !== undefined)
170
+ return false;
171
+ handleEngineMismatch?.({
172
+ observedEngine: null,
173
+ systemVersion: undefined,
174
+ reason: `${what} arrived before the native-v2 system.version marker; the engine that started cannot be established.`,
175
+ });
176
+ return true;
177
+ };
178
+ const consumeOutcomes = (outcomes) => {
179
+ for (const outcome of outcomes) {
180
+ if (engineMismatchDetected)
181
+ return;
182
+ if (outcome.unknownRecord !== undefined) {
183
+ // H3 forward-compat: a stream-json line with a role we don't model
184
+ // (a future kimi-code role). Log it for diagnostics but keep it OUT of
185
+ // records[] and out of onRecord — consumers iterate records[] expecting
186
+ // assistant/tool only. Tolerated, not treated as malformed/error —
187
+ // EXCEPT that on a native-v2 plan nothing may precede the marker.
188
+ if (requireMarkerBeforeOutput(`an unknown-role (${outcome.unknownRecord.role}) record`)) {
189
+ return;
190
+ }
191
+ appendLogLine({
192
+ event: "unknown_record",
193
+ role: outcome.unknownRecord.role,
194
+ });
195
+ continue;
196
+ }
197
+ if (outcome.goalSummary !== undefined) {
198
+ if (requireMarkerBeforeOutput("goal.summary"))
199
+ return;
200
+ // Goal-mode summary is out-of-band metadata for our wrapper, not a
201
+ // consumer-facing record. First-announce wins (a run emits exactly one,
202
+ // at session end); capture and skip so records[] stays assistant/tool.
203
+ if (announcedGoalSummary === undefined) {
204
+ announcedGoalSummary = outcome.goalSummary;
205
+ appendLogLine({
206
+ event: "goal_summary",
207
+ goal_id: outcome.goalSummary.goalId,
208
+ status: outcome.goalSummary.status,
209
+ turns_used: outcome.goalSummary.turnsUsed,
210
+ tokens_used: outcome.goalSummary.tokensUsed,
211
+ wall_clock_ms: outcome.goalSummary.wallClockMs,
212
+ });
213
+ }
214
+ continue;
215
+ }
216
+ if (outcome.record !== undefined) {
217
+ // Meta records are out-of-band wrapper metadata, never consumer-facing
218
+ // assistant/tool records. Capture session.resume_hint (first-announce
219
+ // wins, mirroring the stderr capture below for kimi 0.1.x), and skip
220
+ // every recognized meta type, including 0.23.5's turn.step.retrying and
221
+ // agent-core-v2's system.version. This keeps records[] and onRecord
222
+ // stable while preserving the resume hint as the primary session-id
223
+ // source under kimi 0.2.0+.
224
+ if (outcome.record.role === "meta") {
225
+ // Contract: on a native-v2 plan the system.version marker is the
226
+ // FIRST stream-json line. Any other meta (e.g. session.resume_hint)
227
+ // arriving before it violates the ordering and means the started
228
+ // engine cannot be established — refuse before pinning a session id.
229
+ if (outcome.record.type !== "system.version" &&
230
+ requireMarkerBeforeOutput(`a ${outcome.record.type} record`)) {
231
+ return;
232
+ }
233
+ if (outcome.record.type === "session.resume_hint" &&
234
+ announcedSessionId === undefined) {
235
+ announcedSessionId = outcome.record.sessionId;
236
+ appendLogLine({
237
+ event: "session_announce",
238
+ source: "stream-json.meta",
239
+ session_id: outcome.record.sessionId,
240
+ });
241
+ }
242
+ else if (outcome.record.type === "system.version" &&
243
+ announcedSystemVersion === undefined) {
244
+ announcedSystemVersion = outcome.record.version;
245
+ appendLogLine({
246
+ event: "system_version",
247
+ source: "stream-json.meta",
248
+ version: outcome.record.version,
249
+ });
250
+ if (plannedEngine !== "native-v2") {
251
+ handleEngineMismatch?.({
252
+ observedEngine: "native-v2",
253
+ systemVersion: outcome.record.version,
254
+ reason: `the pre-tool system.version marker proves native-v2 ${outcome.record.version} started.`,
255
+ });
256
+ return;
257
+ }
258
+ // The marker's `version` is a structured field, not a CLI banner:
259
+ // require a single well-formed semver token. A multi-line or
260
+ // otherwise decorated value must not be normalized into a certified
261
+ // one by taking its first line.
262
+ const observedVersion = parseExactVersionToken(outcome.record.version);
263
+ if (observedVersion === undefined) {
264
+ handleEngineMismatch?.({
265
+ observedEngine: "native-v2",
266
+ systemVersion: outcome.record.version,
267
+ reason: `the system.version marker carries a malformed version value ${JSON.stringify(outcome.record.version)}; the started engine's version cannot be established.`,
268
+ });
269
+ return;
270
+ }
271
+ if (plannedVersion !== null && observedVersion !== plannedVersion) {
272
+ handleEngineMismatch?.({
273
+ observedEngine: "native-v2",
274
+ systemVersion: outcome.record.version,
275
+ reason: `the system.version marker reports ${outcome.record.version}, but the plan certified ${plannedVersion}.`,
276
+ });
277
+ return;
278
+ }
279
+ }
280
+ continue;
281
+ }
282
+ if (requireMarkerBeforeOutput(`a ${outcome.record.role} record`))
283
+ return;
284
+ records.push(outcome.record);
285
+ appendLogLine({ event: "record", record: outcome.record });
286
+ invokeOnRecord(outcome.record);
287
+ }
288
+ else if (outcome.malformedLine !== undefined) {
289
+ // Diagnostics-only in general, but on a native-v2 plan a line that is
290
+ // not the marker arriving first still violates marker-first: the
291
+ // engine that produced it cannot be established.
292
+ if (requireMarkerBeforeOutput("a malformed stream-json line"))
293
+ return;
294
+ const entry = {
295
+ line: truncateChars(outcome.malformedLine, 200),
296
+ reason: outcome.malformedReason ?? "unknown",
297
+ };
298
+ malformed.push(entry);
299
+ appendLogLine({ event: "malformed", ...entry });
300
+ }
301
+ }
302
+ };
303
+ const errorDetails = () => ({
304
+ command: opts.command,
305
+ args,
306
+ command_label: opts.commandLabel ?? null,
307
+ });
308
+ return await new Promise((resolve, reject) => {
309
+ let aborted = false;
310
+ let cancellationTeardown;
311
+ let settlementStarted = false;
312
+ const settle = (kind, payload) => {
313
+ if (settlementStarted)
314
+ return;
315
+ settlementStarted = true;
316
+ opts.signal?.removeEventListener("abort", onAbort);
317
+ // Abort teardown can outlive the direct child's close event while a
318
+ // detached descendant is still being enumerated or escalated. Never
319
+ // expose a result/error until that one owned teardown path completes.
320
+ void (cancellationTeardown ?? Promise.resolve({ ok: true })).then((teardown) => {
321
+ if (!teardown.ok) {
322
+ appendLogLine({
323
+ event: "cancellation_teardown_error",
324
+ message: formatError(teardown.error),
325
+ });
326
+ reject(new RuntimeError("CLI_CANCELLATION_TEARDOWN_FAILED", `kimi cancellation teardown failed: ${formatError(teardown.error)}`, "cli-client.cancellation", teardown.error instanceof Error
327
+ ? { cause: teardown.error, details: errorDetails() }
328
+ : { details: errorDetails() }));
329
+ return;
330
+ }
331
+ if (kind === "resolve")
332
+ resolve(payload);
333
+ else
334
+ reject(payload);
335
+ });
336
+ };
337
+ const escalationMs = opts.escalationMs ?? DEFAULT_ESCALATION_MS;
338
+ const identityReader = opts.processIdentityReader;
339
+ let processExited = false;
340
+ child.once("exit", () => {
341
+ processExited = true;
342
+ });
343
+ const captureDescendantIdentities = async (childPid) => {
344
+ let snapshot;
345
+ try {
346
+ const collected = await raceWithValueTimeout((opts.descendantIdentityCollector ?? collectDescendantIdentities)(childPid), DESCENDANT_SNAPSHOT_TIMEOUT_MS, DESCENDANT_SNAPSHOT_TIMEOUT);
347
+ if (collected === DESCENDANT_SNAPSHOT_TIMEOUT) {
348
+ appendLogLine({
349
+ event: "descendant_collection_timeout",
350
+ timeout_ms: DESCENDANT_SNAPSHOT_TIMEOUT_MS,
351
+ });
352
+ return [];
353
+ }
354
+ snapshot = collected;
355
+ }
356
+ catch (err) {
357
+ appendLogLine({
358
+ event: "descendant_collection_error",
359
+ message: formatError(err),
360
+ });
361
+ return [];
362
+ }
363
+ const identities = [];
364
+ const seen = new Set();
365
+ for (const identity of snapshot) {
366
+ const key = `${identity.pid}:${identity.startToken}:${identity.processGroupId}`;
367
+ if (Number.isInteger(identity.pid) &&
368
+ identity.pid > 0 &&
369
+ identity.pid !== childPid &&
370
+ Number.isInteger(identity.processGroupId) &&
371
+ identity.processGroupId > 0 &&
372
+ identity.startToken.length > 0 &&
373
+ !seen.has(key)) {
374
+ seen.add(key);
375
+ identities.push({ ...identity });
376
+ }
377
+ }
378
+ return identities;
379
+ };
380
+ const trySignalPid = (pid, signal) => {
381
+ try {
382
+ process.kill(pid, signal);
383
+ }
384
+ catch (err) {
385
+ const ignored = isErrnoException(err, "ESRCH") || isErrnoException(err, "EPERM");
386
+ void ignored;
387
+ }
388
+ };
389
+ const signalCapturedTree = async (childPid, identities, signal) => {
390
+ // The live ChildProcess anchors its detached group. Signal the group
391
+ // before the direct child can exit and make that numeric PGID stale.
392
+ if (!processExited) {
393
+ trySignalPid(-childPid, signal);
394
+ try {
395
+ child.kill(signal);
396
+ }
397
+ catch {
398
+ // Best-effort direct-child signaling; ChildProcess owns this pid.
399
+ }
400
+ }
401
+ // One bounded pass replaces up to 512 sequential ps calls. A pid whose
402
+ // start token or PGID changed belongs to another process and is skipped.
403
+ const pidValidation = await revalidateProcessIdentities(identities, {
404
+ identityReader,
405
+ timeoutMs: IDENTITY_REVALIDATION_TIMEOUT_MS,
406
+ });
407
+ for (const current of pidValidation.identities) {
408
+ trySignalPid(current.pid, signal);
409
+ }
410
+ // Revalidate again immediately before signaling non-root groups. Never
411
+ // send to an unanchored numeric PGID left over from collection.
412
+ const groupValidation = await revalidateProcessIdentities(identities, {
413
+ identityReader,
414
+ timeoutMs: IDENTITY_REVALIDATION_TIMEOUT_MS,
415
+ });
416
+ const activeGroupIds = new Set(groupValidation.identities.map((identity) => identity.processGroupId));
417
+ activeGroupIds.delete(childPid);
418
+ for (const processGroupId of activeGroupIds) {
419
+ trySignalPid(-processGroupId, signal);
420
+ }
421
+ };
422
+ const teardownChildTree = async () => {
423
+ await opts.cancellationTeardownHook?.();
424
+ const childPid = child.pid;
425
+ if (childPid === undefined || processExited)
426
+ return;
427
+ const finiteEscalation = Number.isFinite(escalationMs);
428
+ if (process.platform === "win32") {
429
+ // Preserve the documented win32 behavior: only the direct child is
430
+ // managed; descendant enumeration/reaping remains unsupported.
431
+ try {
432
+ child.kill("SIGTERM");
433
+ }
434
+ catch {
435
+ // Best-effort.
436
+ }
437
+ const exited = await waitForProcessTreeExit([], escalationMs, {
438
+ isRootAlive: () => !processExited,
439
+ });
440
+ if (!exited && finiteEscalation) {
441
+ try {
442
+ child.kill("SIGKILL");
443
+ }
444
+ catch {
445
+ // Best-effort.
446
+ }
447
+ }
448
+ return;
449
+ }
450
+ const identities = await captureDescendantIdentities(childPid);
451
+ await signalCapturedTree(childPid, identities, "SIGTERM");
452
+ // The grace interval starts only after SIGTERM has reached every
453
+ // revalidated target. Slow process enumeration never consumes the grace.
454
+ const exited = await waitForProcessTreeExit(identities, escalationMs, {
455
+ identityReader,
456
+ isRootAlive: () => !processExited,
457
+ });
458
+ if (!exited && finiteEscalation) {
459
+ await signalCapturedTree(childPid, identities, "SIGKILL");
460
+ const quiescent = await waitForProcessTreeExit(identities, POST_KILL_QUIESCENCE_MS, {
461
+ identityReader,
462
+ isRootAlive: () => !processExited,
463
+ });
464
+ if (!quiescent) {
465
+ appendLogLine({
466
+ event: "post_kill_quiescence_timeout",
467
+ timeout_ms: POST_KILL_QUIESCENCE_MS,
468
+ descendant_count: identities.length,
469
+ });
470
+ }
471
+ }
472
+ };
473
+ let engineMismatchError;
474
+ handleEngineMismatch = (mismatch) => {
475
+ if (engineMismatchError !== undefined)
476
+ return;
477
+ engineMismatchDetected = true;
478
+ engineMismatchError = new RuntimeError("CLI_ENGINE_PROVENANCE_MISMATCH", `Refusing kimi output: execution plan required ${plannedEngine}, but ${mismatch.reason}`, "cli-client.engine-provenance", {
479
+ details: {
480
+ operation_kind: opts.executionPlan.operationKind,
481
+ intended_engine: plannedEngine,
482
+ observed_engine: mismatch.observedEngine,
483
+ planned_kimi_version: opts.executionPlan.kimiVersion,
484
+ system_version: mismatch.systemVersion ?? null,
485
+ },
486
+ });
487
+ appendLogLine({
488
+ event: "engine_provenance_mismatch",
489
+ intended_engine: plannedEngine,
490
+ observed_engine: mismatch.observedEngine,
491
+ system_version: mismatch.systemVersion ?? null,
492
+ reason: mismatch.reason,
493
+ });
494
+ cancellationTeardown = teardownChildTree().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
495
+ settle("reject", engineMismatchError);
496
+ };
497
+ const onAbort = () => {
498
+ if (aborted)
499
+ return;
500
+ aborted = true;
501
+ cancellationTeardown = teardownChildTree().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
502
+ void cancellationTeardown.then((outcome) => {
503
+ if (outcome.ok)
504
+ return;
505
+ // A bug in teardown must neither strand the outer promise nor leave the
506
+ // owned root running. The controlled child anchors this emergency kill.
507
+ const childPid = child.pid;
508
+ if (childPid !== undefined && !processExited) {
509
+ if (process.platform !== "win32")
510
+ trySignalPid(-childPid, "SIGKILL");
511
+ try {
512
+ child.kill("SIGKILL");
513
+ }
514
+ catch {
515
+ // Best-effort; settlement still surfaces the teardown failure.
516
+ }
517
+ }
518
+ settle("reject", new RuntimeError("CLI_CANCELLATION_TEARDOWN_FAILED", "kimi cancellation teardown failed", "cli-client.cancellation"));
519
+ });
520
+ };
521
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
522
+ // Re-check `aborted` AFTER attaching the listener. `await mkdir` above
523
+ // is the first synchronous yield point after the pre-spawn aborted
524
+ // check — if abort fired during that await, the signal is already in
525
+ // an aborted state by the time we reach addEventListener, which does
526
+ // NOT re-fire for already-aborted signals (per the AbortSignal spec).
527
+ // Without this re-check the spawned child is orphaned: SIGTERM/SIGKILL
528
+ // is never sent and the process keeps holding the SQLite row + model
529
+ // tokens. Audit report 28 (Codex H1) tracked this fix.
530
+ if (opts.signal?.aborted === true) {
531
+ onAbort();
532
+ }
533
+ // Swap the pre-attached early listener for the real handler. These two
534
+ // statements are synchronous and execute as one unit; no event can
535
+ // interleave between marking earlyErrorHandled and attaching the new
536
+ // listener.
537
+ earlyErrorHandled = true;
538
+ child.on("error", (err) => {
539
+ appendLogLine({ event: "process_error", message: formatError(err) });
540
+ settle("reject", new RuntimeError("CLI_PROCESS_ERROR", `kimi subprocess error: ${formatError(err)}`, "cli-client.process", err instanceof Error
541
+ ? { cause: err, details: errorDetails() }
542
+ : { details: errorDetails() }));
543
+ });
544
+ if (earlyErrors.length > 0) {
545
+ const captured = earlyErrors[0];
546
+ queueMicrotask(() => {
547
+ appendLogLine({
548
+ event: "process_error",
549
+ message: formatError(captured),
550
+ early: true,
551
+ additional_count: earlyErrors.length - 1,
552
+ });
553
+ settle("reject", new RuntimeError("CLI_PROCESS_ERROR", `kimi subprocess error: ${formatError(captured)}`, "cli-client.process", { cause: captured, details: errorDetails() }));
554
+ });
555
+ }
556
+ child.on("close", async (exitCode, signal) => {
557
+ consumeOutcomes(parser.flush());
558
+ if (!engineMismatchDetected &&
559
+ exitCode === 0 &&
560
+ !aborted &&
561
+ requireMarkerBeforeOutput("a clean exit")) {
562
+ return;
563
+ }
564
+ // The stderr resume-hint channel is the kimi 0.1.x fallback and exists
565
+ // only for legacy-v1 plans; a native-v2 plan pins a session id from the
566
+ // stdout meta record alone (after the marker), so stderr can never pin
567
+ // an id ahead of, or different from, the provenance-checked stream.
568
+ const sessionId = announcedSessionId ??
569
+ (plannedEngine === "legacy-v1" ? extractSessionIdFromStderr(stderrTail) : undefined);
570
+ appendLogLine({
571
+ event: "exit",
572
+ exit_code: exitCode,
573
+ signal,
574
+ session_id: sessionId,
575
+ malformed_count: malformed.length,
576
+ record_count: records.length,
577
+ aborted,
578
+ });
579
+ // Cap the log drain wait so a wedged disk can't block result delivery.
580
+ // The result itself is in memory; diagnostics are best-effort.
581
+ await raceWithTimeout(logChain, LOG_DRAIN_TIMEOUT_MS);
582
+ settle("resolve", {
583
+ sessionId,
584
+ goalSummary: announcedGoalSummary,
585
+ systemVersion: announcedSystemVersion,
586
+ observedEngine: announcedSystemVersion !== undefined
587
+ ? "native-v2"
588
+ : plannedEngine === "legacy-v1" &&
589
+ (exitCode === 0 ||
590
+ records.length > 0 ||
591
+ sessionId !== undefined ||
592
+ announcedGoalSummary !== undefined)
593
+ ? "legacy-v1"
594
+ : null,
595
+ records,
596
+ malformed,
597
+ stderrTail,
598
+ exitCode: exitCode ?? -1,
599
+ signal,
600
+ aborted,
601
+ });
602
+ });
603
+ // Stdin: kimi -p reads from argv, not stdin. End it immediately so the
604
+ // child never blocks. Stream method calls on a failed-spawn child can
605
+ // throw synchronously — guard so the 'error' event still wins.
606
+ try {
607
+ child.stdout.setEncoding("utf8");
608
+ child.stderr.setEncoding("utf8");
609
+ child.stdout.on("data", (chunk) => {
610
+ consumeOutcomes(parser.push(chunk));
611
+ });
612
+ child.stderr.on("data", (chunk) => {
613
+ // First-announce-wins across both channels: once a session id is
614
+ // pinned (from the stdout meta record or an earlier stderr line),
615
+ // never let a later stderr line overwrite it. The stderr channel is
616
+ // the 0.1.x fallback and is consulted ONLY for legacy-v1 plans — on a
617
+ // native-v2 plan a stderr line could otherwise pin a session id before
618
+ // (or different from) the provenance-checked stdout stream.
619
+ if (plannedEngine === "legacy-v1" && announcedSessionId === undefined) {
620
+ const nextSessionId = extractSessionIdFromStderr(stderrTail + chunk);
621
+ if (nextSessionId !== undefined) {
622
+ announcedSessionId = nextSessionId;
623
+ }
624
+ }
625
+ stderrTail = appendToTail(stderrTail, chunk, STDERR_TAIL_BYTES);
626
+ });
627
+ child.stdin.end();
628
+ }
629
+ catch (err) {
630
+ settle("reject", new RuntimeError("CLI_PROCESS_ERROR", `kimi subprocess stream setup failed: ${formatError(err)}`, "cli-client.streams", err instanceof Error
631
+ ? { cause: err, details: errorDetails() }
632
+ : { details: errorDetails() }));
633
+ }
634
+ });
635
+ }
636
+ /**
637
+ * Helper for callers that require a session id (replay-capable commands).
638
+ * Throws CLI_NO_SESSION_ID if the result lacks one, embedding the command
639
+ * label and exit code in details so the failure is actionable in logs.
640
+ *
641
+ * Use this AT the call site rather than baking the requirement into
642
+ * runCliPrompt — some callers (setup probes, smoke tests) are fine with
643
+ * an undefined sessionId.
644
+ */
645
+ export function requireSessionId(result, context) {
646
+ if (result.sessionId !== undefined && result.sessionId.length > 0) {
647
+ return result.sessionId;
648
+ }
649
+ throw new RuntimeError("CLI_NO_SESSION_ID", "kimi subprocess completed without announcing a session id; replay and resume will be unavailable", "cli-client.session-id", {
650
+ details: {
651
+ command_label: context.commandLabel ?? null,
652
+ exit_code: result.exitCode,
653
+ signal: result.signal,
654
+ aborted: result.aborted,
655
+ stderr_tail_len: result.stderrTail.length,
656
+ },
657
+ });
658
+ }
659
+ /**
660
+ * Run `kimi -p` under a budget that also tells the subprocess to die
661
+ * when the budget expires.
662
+ *
663
+ * Why this exists separately from `withTimeout`:
664
+ *
665
+ * The generic `withTimeout` (runtime/kimi-timeouts.ts) only rejects
666
+ * the outer Promise — it has no handle on the kimi child process.
667
+ * review/ask/review-gate previously wrapped `runCliPrompt` with
668
+ * `withTimeout` and let the orphaned subprocess keep running until
669
+ * it crashed or finished on its own. The reviewers (reports 17 +
670
+ * 18) flagged this as critical for review_gate in particular: the
671
+ * 8 s budget fires inside Claude Code's Stop hook, the parent
672
+ * returns "allow stop", and a runaway kimi keeps holding the
673
+ * SQLite row + model tokens. Subsequent `/kimi:cancel` can't reach
674
+ * it because `kimi_pid` is null.
675
+ *
676
+ * Behavior:
677
+ *
678
+ * - Owns an internal AbortController. If the caller passes
679
+ * `opts.signal`, both signals are linked — abort on either side
680
+ * aborts the internal controller.
681
+ * - When the budget expires, abort the internal controller, await the
682
+ * subprocess/tree teardown, then reject with
683
+ * `RuntimeError("RESPONSE_TIMEOUT", …)` matching the legacy
684
+ * `withTimeout` code shape.
685
+ * - Result success path is unchanged from `runCliPrompt`.
686
+ */
687
+ export async function runCliPromptWithBudget(opts, budgetMs, stage) {
688
+ const controller = new AbortController();
689
+ if (opts.signal?.aborted === true) {
690
+ controller.abort();
691
+ }
692
+ const onParentAbort = () => controller.abort();
693
+ opts.signal?.addEventListener("abort", onParentAbort, { once: true });
694
+ let timeoutFired = false;
695
+ let timer;
696
+ timer = setTimeout(() => {
697
+ timeoutFired = true;
698
+ controller.abort();
699
+ }, budgetMs);
700
+ timer.unref();
701
+ const timeoutError = (raceDetected = false) => new RuntimeError("RESPONSE_TIMEOUT", `${stage} timed out after ${budgetMs}ms${raceDetected ? " (race detected post-result)" : ""}.`, stage, {
702
+ details: {
703
+ budget_ms: budgetMs,
704
+ command_label: opts.commandLabel ?? null,
705
+ },
706
+ });
707
+ try {
708
+ let result;
709
+ try {
710
+ // Deliberately do not race this promise against an early timeout
711
+ // rejection. The timer aborts it, and runCliPrompt owns the complete
712
+ // child/descendant teardown before it can settle.
713
+ result = await runCliPrompt({ ...opts, signal: controller.signal });
714
+ }
715
+ catch (error) {
716
+ if (timeoutFired)
717
+ throw timeoutError();
718
+ throw error;
719
+ }
720
+ if (timeoutFired) {
721
+ // The timer and close can become runnable in the same event-loop turn.
722
+ // Teardown has still completed because runCliPrompt settled first.
723
+ throw timeoutError(true);
724
+ }
725
+ return result;
726
+ }
727
+ finally {
728
+ if (timer !== undefined)
729
+ clearTimeout(timer);
730
+ opts.signal?.removeEventListener("abort", onParentAbort);
731
+ }
732
+ }
733
+ /**
734
+ * Parse a structured version FIELD (the system.version marker), not a CLI
735
+ * banner: the whole value must be one semver token (optional leading `v`,
736
+ * optional pre-release/build suffix) with no surrounding whitespace or extra
737
+ * lines. Returns the normalized `raw` form, or undefined when malformed.
738
+ */
739
+ function parseExactVersionToken(value) {
740
+ if (/\s/.test(value) || value.length === 0)
741
+ return undefined;
742
+ return parseVersionLine(value)?.raw;
743
+ }
744
+ const KIMI_LEGACY_ENV = "KIMI_CODE_LEGACY_FLAG";
745
+ const KIMI_LEGACY_FORCED_VALUE = "1";
746
+ function buildArgs(opts) {
747
+ const args = [...(opts.prefixArgs ?? [])];
748
+ args.push("--output-format", "stream-json");
749
+ if (opts.resumeSessionId !== undefined) {
750
+ args.push("-r", opts.resumeSessionId);
751
+ }
752
+ if (opts.model !== undefined) {
753
+ args.push("-m", opts.model);
754
+ }
755
+ if (opts.skillsDirs !== undefined) {
756
+ for (const dir of opts.skillsDirs) {
757
+ args.push("--skills-dir", dir);
758
+ }
759
+ }
760
+ // NOTE: `opts.thinking === false` is intentionally NOT translated into a
761
+ // `--no-thinking` argv flag. kimi-code 0.1.1 rejects unknown options and
762
+ // has no per-spawn thinking control — the Round 2 multi-agent review
763
+ // caught this. The option stays in CliClientOptions for future wiring
764
+ // when upstream adds the flag (see field doc).
765
+ // -p accepts the prompt as the option value. We pass it as a separate argv
766
+ // entry so prompts with leading dashes or whitespace are not interpreted as
767
+ // additional flags.
768
+ args.push("-p", opts.prompt);
769
+ return args;
770
+ }
771
+ function buildEnv(opts) {
772
+ const env = { ...opts.env };
773
+ // kimi-code 0.33.0 inverted its CLI engine default: an unflagged `kimi -p`
774
+ // now selects native agent-core-v2, while this upstream-documented flag pins
775
+ // the legacy-v1 path. Native v2 still has a final-allow plan-file path that
776
+ // can skip our later external PreToolUse hook, so the engine choice must be a
777
+ // plugin-owned spawn invariant rather than ambient operator state. Older
778
+ // kimi-code releases ignore the unknown flag. Always overwrite ambient false
779
+ // values; accepting one would silently reopen v2 on 0.33+.
780
+ if (opts.executionPlan.intendedEngine === "legacy-v1") {
781
+ env[KIMI_LEGACY_ENV] = KIMI_LEGACY_FORCED_VALUE;
782
+ }
783
+ // The update preflight runs on the `-p` path too; a plugin spawn must never
784
+ // swap the certified binary underneath the execution plan.
785
+ env.KIMI_CODE_NO_AUTO_UPDATE = "1";
786
+ // Rescue and pursue share the write label. Only the immutable execution
787
+ // plan can grant pursue's narrow goal-metadata access; never trust ambient
788
+ // state inherited from another plugin job.
789
+ env.KIMI_PLUGIN_CC_OPERATION = opts.executionPlan.operationKind;
790
+ env.KIMI_CODE_HOME = resolveKimiHome(opts.env, opts.cwd);
791
+ if (opts.commandLabel !== undefined) {
792
+ env.KIMI_PLUGIN_CC_CMD = opts.commandLabel;
793
+ }
794
+ if (opts.swarmMaxConcurrency !== undefined) {
795
+ // kimi-code 0.18.0+ caps AgentSwarm's normal-phase concurrency at this
796
+ // value; older binaries ignore it. See the field doc on CliClientOptions.
797
+ env.KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY = String(opts.swarmMaxConcurrency);
798
+ }
799
+ if (opts.trustedWorkspaceRoot !== undefined) {
800
+ // Trusted allowlist root for write-capable PreToolUse cases. Forge-proof
801
+ // (env on the spawned process; the model cannot alter it). See field doc.
802
+ env.KIMI_PLUGIN_CC_WORKSPACE_ROOT = opts.trustedWorkspaceRoot;
803
+ }
804
+ return env;
805
+ }
806
+ function appendToTail(tail, chunk, maxBytes) {
807
+ const combined = tail + chunk;
808
+ if (combined.length <= maxBytes)
809
+ return combined;
810
+ return combined.slice(combined.length - maxBytes);
811
+ }
812
+ function truncateChars(value, maxChars) {
813
+ return value.length <= maxChars ? value : value.slice(0, maxChars);
814
+ }
815
+ function isErrnoException(err, code) {
816
+ return typeof err === "object" && err !== null && "code" in err && err.code === code;
817
+ }
818
+ function raceWithTimeout(promise, timeoutMs) {
819
+ let timer;
820
+ const timeoutPromise = new Promise((resolve) => {
821
+ timer = setTimeout(resolve, timeoutMs);
822
+ });
823
+ return Promise.race([
824
+ promise.then(() => { }, () => { }),
825
+ timeoutPromise,
826
+ ]).finally(() => {
827
+ if (timer !== undefined)
828
+ clearTimeout(timer);
829
+ });
830
+ }
831
+ async function raceWithValueTimeout(promise, timeoutMs, timeoutValue) {
832
+ let timer;
833
+ try {
834
+ return await Promise.race([
835
+ promise,
836
+ new Promise((resolve) => {
837
+ timer = setTimeout(() => resolve(timeoutValue), Math.max(0, timeoutMs));
838
+ }),
839
+ ]);
840
+ }
841
+ finally {
842
+ if (timer !== undefined)
843
+ clearTimeout(timer);
844
+ }
845
+ }