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,224 @@
1
+ import { spawn } from "node:child_process";
2
+ import { access, appendFile, constants as fsConstants } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { RuntimeError, formatError } from "./errors.js";
6
+ import { preferStableNodePath } from "./hooks/install-paths.js";
7
+ import { markJobFailed, waitForTerminalJob } from "./jobs.js";
8
+ import { JobStore } from "./job-store.js";
9
+ import { readArtifact } from "./render.js";
10
+ // Resolves to runtime/companion.ts in dev (via tsx) and dist/companion.js in production (compiled
11
+ // with tsc to the same relative layout). The extension is derived from the running module so the
12
+ // child-process spawn in startBackgroundJob targets the right entrypoint in both modes.
13
+ const isCompiledRuntime = import.meta.url.endsWith(".js");
14
+ const companionEntrypoint = fileURLToPath(new URL(isCompiledRuntime ? "./companion.js" : "./companion.ts", import.meta.url));
15
+ const companionProjectRoot = path.resolve(path.dirname(companionEntrypoint), "..");
16
+ const FAILURE_PERSIST_RETRY_DELAYS_MS = [100, 200, 400];
17
+ export async function startBackgroundJob(job, prompt, context, paths, options) {
18
+ // MUST be the same resolution the hook verifier uses. The worker re-verifies
19
+ // the hook (rescue.ts / ask.ts, deliberately, so a background job cannot
20
+ // outlive or bypass the check), and the verifier rebuilds the canonical
21
+ // command from ITS OWN `process.argv0`. Spawning with a different spelling of
22
+ // node than setup pinned makes the worker compute a different expected
23
+ // command than the config holds → byte-exact mismatch → every background
24
+ // rescue/ask refuses forever, with `/kimi:setup` unable to converge the two
25
+ // sides (setup runs via companion.sh, whose argv0 is the stable symlink).
26
+ //
27
+ // Concretely: `spawn(process.execPath)` gives the child an argv0 equal to the
28
+ // fully symlink-resolved path (…/Cellar/node/<ver>/bin/node), whereas setup
29
+ // pins /opt/homebrew/bin/node. Caught by Kimi's pre-release review of v1.9.0.
30
+ // NOTE: this is deliberately NOT `resolveNodeBinary`, which additionally
31
+ // REQUIRES an absolute override because the hook command runs under
32
+ // `/bin/sh -c` with a possibly sanitized PATH. Spawning has no such
33
+ // constraint — a bare name is resolved by spawn() via PATH — so the override
34
+ // is honored verbatim here. Only the no-override default needs to change:
35
+ // it must match what the verifier computes, hence `preferStableNodePath`.
36
+ const override = context.env.KIMI_PLUGIN_CC_NODE_BIN;
37
+ const nodeBinary = override !== undefined && override.length > 0
38
+ ? override
39
+ : preferStableNodePath(process.execPath);
40
+ // Only fast-path the `fs.access` check for absolute/relative paths. A bare
41
+ // name like "node" is resolved by spawn() via PATH; the spawn `error` listener
42
+ // will still surface ENOENT on nextTick for that path, classified as
43
+ // spawnFailedErrorCode via the listener below.
44
+ if (nodeBinary.includes("/") || nodeBinary.includes("\\")) {
45
+ try {
46
+ await access(nodeBinary, fsConstants.X_OK);
47
+ }
48
+ catch (accessError) {
49
+ const code = accessError.code;
50
+ if (code === "ENOENT" || code === "EACCES" || code === "EPERM") {
51
+ const classified = new RuntimeError(options.nodeBinInvalidErrorCode, `Configured Node binary is not executable: ${nodeBinary}. Set KIMI_PLUGIN_CC_NODE_BIN to a valid Node >=22.5 executable and retry.`, options.spawnStage, accessError instanceof Error ? { cause: accessError } : undefined);
52
+ await persistBackgroundFailure(paths, job.job_id, classified, options.failedSummary, "node-bin-invalid");
53
+ throw classified;
54
+ }
55
+ // Any other errno (EIO, etc.) — fall through to the spawn attempt and let the
56
+ // existing spawn error path surface it.
57
+ }
58
+ }
59
+ const b64 = Buffer.from(prompt, "utf8").toString("base64");
60
+ const spawnArgs = isCompiledRuntime
61
+ ? [companionEntrypoint, "worker", options.workerKind, job.job_id]
62
+ : ["--import", "tsx", companionEntrypoint, "worker", options.workerKind, job.job_id];
63
+ const child = spawn(nodeBinary, spawnArgs, {
64
+ cwd: companionProjectRoot,
65
+ detached: true,
66
+ stdio: "ignore",
67
+ env: {
68
+ ...context.env,
69
+ KIMI_PLUGIN_CC_WORKSPACE_CWD: context.cwd,
70
+ [options.promptEnvVar]: b64,
71
+ [options.reusedSessionEnvVar]: options.reusedSession ? "1" : "0",
72
+ ...(options.extraEnv ?? {}),
73
+ // A worker must reopen the exact store its parent selected, regardless of cwd.
74
+ KIMI_PLUGIN_CC_DATA: path.resolve(paths.claudePluginData),
75
+ },
76
+ });
77
+ let spawnReportedFailure = false;
78
+ const spawnErrorPromise = new Promise((resolve) => {
79
+ let settled = false;
80
+ const settle = (value) => {
81
+ if (settled) {
82
+ return;
83
+ }
84
+ settled = true;
85
+ resolve(value);
86
+ };
87
+ child.once("error", (spawnError) => {
88
+ spawnReportedFailure = true;
89
+ settle(new RuntimeError(options.spawnFailedErrorCode, `Background ${options.workerKind} worker failed to spawn: ${spawnError.message}`, options.spawnStage, { cause: spawnError }));
90
+ });
91
+ // Give the event loop one tick to deliver a synchronous spawn error
92
+ // (ENOENT fires on process.nextTick). If nothing fires by then, the
93
+ // child is live and we can proceed.
94
+ setImmediate(() => settle(null));
95
+ });
96
+ child.on("close", (exitCode, signal) => {
97
+ if (spawnReportedFailure) {
98
+ return;
99
+ }
100
+ if (exitCode === null || exitCode === 0) {
101
+ return;
102
+ }
103
+ // The worker exited non-zero before any state update. If the job is
104
+ // still in its spawn phase, the worker never reached executeJob.
105
+ const classified = new RuntimeError(options.earlyExitErrorCode, `Background ${options.workerKind} worker exited with code ${exitCode}${signal ? ` (signal ${signal})` : ""} before reporting a result.`, options.spawnStage);
106
+ void markEarlyExit(paths, job.job_id, classified, options.failedSummary);
107
+ });
108
+ child.unref();
109
+ const spawnError = await spawnErrorPromise;
110
+ if (spawnError) {
111
+ await markSpawnFailure(paths, job.job_id, spawnError, options.failedSummary);
112
+ throw spawnError;
113
+ }
114
+ const store = new JobStore(paths);
115
+ try {
116
+ store.updateRunningJob(job.job_id, {
117
+ pid: child.pid ?? null,
118
+ phase: "worker-spawned",
119
+ });
120
+ }
121
+ finally {
122
+ store.close();
123
+ }
124
+ if (options.wait) {
125
+ const completed = await waitForTerminalJob(() => new JobStore(paths), job.job_id);
126
+ if (!completed.final_output_path) {
127
+ throw new RuntimeError(options.missingResultErrorCode, describeMissingResult(completed, options.workerKind), options.waitStage);
128
+ }
129
+ return readArtifact(completed.final_output_path);
130
+ }
131
+ return `${JSON.stringify({
132
+ job_id: job.job_id,
133
+ command_type: job.command_type,
134
+ }, null, 2)}\n`;
135
+ }
136
+ async function markSpawnFailure(paths, jobId, classified, failedSummary) {
137
+ await persistBackgroundFailure(paths, jobId, classified, failedSummary, "spawn-failed");
138
+ }
139
+ async function markEarlyExit(paths, jobId, classified, failedSummary) {
140
+ await persistBackgroundFailure(paths, jobId, classified, failedSummary, "early-exit", (current) => {
141
+ // Only mark failure if the worker exited before advancing past the spawn phase.
142
+ // Once the worker reaches worker-running or turn-running, the child owns the
143
+ // job state and the parent's close listener must not race with it.
144
+ return current.phase === "worker-spawned" || current.phase === "queued";
145
+ });
146
+ }
147
+ async function persistBackgroundFailure(paths, jobId, classified, failedSummary, event, shouldMark = () => true) {
148
+ let lastWriteError;
149
+ for (let attempt = 0; attempt <= FAILURE_PERSIST_RETRY_DELAYS_MS.length; attempt += 1) {
150
+ let store;
151
+ try {
152
+ store = new JobStore(paths);
153
+ const current = store.getJob(jobId);
154
+ if (!current || current.status !== "running" || !shouldMark(current)) {
155
+ return;
156
+ }
157
+ await markJobFailed(store, paths, current, classified, failedSummary, { phase: "failed" });
158
+ return;
159
+ }
160
+ catch (writeError) {
161
+ lastWriteError = writeError;
162
+ if (attempt < FAILURE_PERSIST_RETRY_DELAYS_MS.length) {
163
+ await sleep(FAILURE_PERSIST_RETRY_DELAYS_MS[attempt]);
164
+ }
165
+ }
166
+ finally {
167
+ try {
168
+ store?.close();
169
+ }
170
+ catch {
171
+ // The persistence attempt outcome above is the signal that matters.
172
+ }
173
+ }
174
+ }
175
+ await appendStuckJobTelemetry(paths, {
176
+ event,
177
+ job_id: jobId,
178
+ failed_summary: failedSummary,
179
+ error: {
180
+ code: classified.code,
181
+ stage: classified.stage,
182
+ message: classified.message,
183
+ details: classified.details,
184
+ },
185
+ persistence_error: formatError(lastWriteError),
186
+ });
187
+ }
188
+ async function appendStuckJobTelemetry(paths, entry) {
189
+ const stuckPath = path.join(paths.pluginRoot, "stuck-jobs.jsonl");
190
+ try {
191
+ await appendFile(stuckPath, `${JSON.stringify({ ts: new Date().toISOString(), ...entry })}\n`, "utf8");
192
+ }
193
+ catch (appendError) {
194
+ process.stderr.write(`[kimi-plugin-cc] failed to append stuck job telemetry at ${stuckPath}: ${formatError(appendError)}\n`);
195
+ }
196
+ }
197
+ function sleep(ms) {
198
+ return new Promise((resolve) => {
199
+ setTimeout(resolve, ms);
200
+ });
201
+ }
202
+ /**
203
+ * Builds a descriptive message for the rare edge case where `waitForTerminalJob`
204
+ * returns a terminal job whose `final_output_path` is null (e.g. `markJobFailed`'s
205
+ * own artifact write failed). The normal background --wait failure path returns
206
+ * the rendered failure artifact via `readArtifact` — that artifact already
207
+ * carries the code, stage, and message via `renderTerminalJobArtifact`. This
208
+ * helper is belt-and-suspenders for the near-unreachable case where even the
209
+ * failure artifact could not be written. Intended for terminal job records only;
210
+ * a running record would render the generic "finished without a rendered result."
211
+ * sentence, which would be misleading.
212
+ */
213
+ export function describeMissingResult(job, workerKind = "rescue") {
214
+ const base = `Background ${workerKind} job ${job.job_id} finished without a rendered result`;
215
+ if (job.status === "failed" || job.status === "cancelled") {
216
+ const code = job.error?.code || "unknown";
217
+ const stage = job.error?.stage || "unknown";
218
+ // `||` (not `??`) intentionally: empty-string error.message or summary
219
+ // should fall through to the next fallback, not render as blank.
220
+ const detail = job.error?.message || job.summary || "no further detail";
221
+ return `${base} (${job.status}, ${code}, stage ${stage}): ${detail}`;
222
+ }
223
+ return `${base}.`;
224
+ }
@@ -0,0 +1,43 @@
1
+ import process from "node:process";
2
+ export function createCliCancellationHandlers() {
3
+ const controller = new AbortController();
4
+ let cancelling = false;
5
+ let disposed = false;
6
+ const onSignal = () => {
7
+ if (cancelling)
8
+ return;
9
+ cancelling = true;
10
+ // Abort BEFORE running through dispose — the cli-client may still be
11
+ // in its pre-spawn signal check or its post-spawn wait, both of which
12
+ // honor signal.aborted.
13
+ try {
14
+ controller.abort();
15
+ }
16
+ catch {
17
+ // AbortController.abort() can't throw in standard runtimes, but
18
+ // belt-and-suspenders for runtime quirks (e.g. test harnesses
19
+ // overriding global AbortController).
20
+ }
21
+ };
22
+ // Use `process.on` (not `once`) so dispose() can remove the listener
23
+ // via the function reference. `process.once` wraps the callback
24
+ // internally and depending on Node version removeListener may miss it.
25
+ // Idempotency comes from the `if (cancelling) return` guard.
26
+ process.on("SIGTERM", onSignal);
27
+ process.on("SIGINT", onSignal);
28
+ return {
29
+ get cancelling() {
30
+ return cancelling;
31
+ },
32
+ get signal() {
33
+ return controller.signal;
34
+ },
35
+ dispose() {
36
+ if (disposed)
37
+ return;
38
+ disposed = true;
39
+ process.off("SIGTERM", onSignal);
40
+ process.off("SIGINT", onSignal);
41
+ },
42
+ };
43
+ }