kimi-plugin-cc 2.0.7 → 2.0.8

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 (64) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +29 -0
  4. package/README.md +21 -5
  5. package/agents/kimi-ask.md +3 -0
  6. package/agents/kimi-challenge.md +3 -1
  7. package/agents/kimi-rescue.md +3 -0
  8. package/agents/kimi-review.md +3 -1
  9. package/commands/ask.md +6 -2
  10. package/commands/challenge.md +6 -2
  11. package/commands/rescue.md +6 -2
  12. package/commands/review.md +6 -2
  13. package/dist/cli-client.js +29 -12
  14. package/dist/commands/ask.js +9 -4
  15. package/dist/commands/cli-helpers.js +31 -0
  16. package/dist/commands/pursue.js +2 -1
  17. package/dist/commands/repair-sessions.js +2 -1
  18. package/dist/commands/replay.js +3 -2
  19. package/dist/commands/rescue.js +10 -3
  20. package/dist/commands/result.js +4 -0
  21. package/dist/commands/review.js +6 -4
  22. package/dist/commands/setup.js +2 -1
  23. package/dist/commands/swarm.js +4 -2
  24. package/dist/companion.js +6 -0
  25. package/dist/help.js +40 -0
  26. package/dist/hooks/config-safety.js +3 -2
  27. package/dist/job-store.js +8 -3
  28. package/dist/jobs.js +7 -3
  29. package/dist/kimi-engine.js +1 -0
  30. package/dist/kimi-errors.js +35 -1
  31. package/dist/kimi-timeouts.js +7 -7
  32. package/dist/parsing.js +87 -13
  33. package/dist/render.js +19 -1
  34. package/dist/version.js +1 -1
  35. package/docs/migration.md +3 -3
  36. package/docs/registry-releases.md +14 -1
  37. package/package.json +2 -2
  38. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  39. package/plugins/kimi-codex/dist/cli-client.js +29 -12
  40. package/plugins/kimi-codex/dist/commands/ask.js +9 -4
  41. package/plugins/kimi-codex/dist/commands/cli-helpers.js +31 -0
  42. package/plugins/kimi-codex/dist/commands/pursue.js +2 -1
  43. package/plugins/kimi-codex/dist/commands/repair-sessions.js +2 -1
  44. package/plugins/kimi-codex/dist/commands/replay.js +3 -2
  45. package/plugins/kimi-codex/dist/commands/rescue.js +10 -3
  46. package/plugins/kimi-codex/dist/commands/result.js +4 -0
  47. package/plugins/kimi-codex/dist/commands/review.js +6 -4
  48. package/plugins/kimi-codex/dist/commands/setup.js +2 -1
  49. package/plugins/kimi-codex/dist/commands/swarm.js +4 -2
  50. package/plugins/kimi-codex/dist/companion.js +6 -0
  51. package/plugins/kimi-codex/dist/help.js +40 -0
  52. package/plugins/kimi-codex/dist/hooks/config-safety.js +3 -2
  53. package/plugins/kimi-codex/dist/job-store.js +8 -3
  54. package/plugins/kimi-codex/dist/jobs.js +7 -3
  55. package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
  56. package/plugins/kimi-codex/dist/kimi-errors.js +35 -1
  57. package/plugins/kimi-codex/dist/kimi-timeouts.js +7 -7
  58. package/plugins/kimi-codex/dist/parsing.js +87 -13
  59. package/plugins/kimi-codex/dist/render.js +19 -1
  60. package/plugins/kimi-codex/dist/version.js +1 -1
  61. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +3 -1
  62. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +2 -1
  63. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +3 -1
  64. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +3 -2
@@ -4,6 +4,19 @@ The npm package `kimi-plugin-cc` ships the Claude plugin at its root and the
4
4
  self-contained Codex plugin under `plugins/kimi-codex`. GitHub marketplaces
5
5
  remain available. The package has no install scripts and no separate CLI.
6
6
 
7
+ ## Publishing status
8
+
9
+ As of September 26, 2026, [2.0.7 is live on npm](https://www.npmjs.com/package/kimi-plugin-cc/v/2.0.7).
10
+ The first upload used the validated GitHub Actions artifact and interactive npm
11
+ authentication. Its public tarball matched that artifact byte-for-byte, and a
12
+ fresh registry install passed isolated setup/check for both hosts.
13
+
14
+ The trusted publisher is configured for `linxule/kimi-plugin-cc`, workflow
15
+ `publish.yml`, environment `npm`, with direct publishing allowed.
16
+ `NPM_TRUSTED_PUBLISHING=true` is enabled in GitHub Actions. Future GitHub releases
17
+ use short-lived OIDC credentials, without a stored npm token or interactive 2FA.
18
+ The first npm OIDC publication will be the next release; do not republish 2.0.7.
19
+
7
20
  ## Validation
8
21
 
9
22
  Use the repository's normal version bump and generated-surface procedure in
@@ -25,7 +38,7 @@ vendored licenses, and installs/checks the safety hook in isolated homes for
25
38
  both hosts. It also exercises unsupported-version refusal using a fake CLI.
26
39
  It does not invoke a model or read the maintainer's Kimi configuration.
27
40
 
28
- ## First publication
41
+ ## First publication (completed bootstrap reference)
29
42
 
30
43
  The package must first exist on npm before its package settings can be configured.
31
44
  After the release commit passes CI, create its immutable version tag and GitHub
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimi-plugin-cc",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Kimi for Claude Code and Codex: code questions, reviews, fixes, and parallel tasks through the local Kimi Code CLI.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -19,7 +19,7 @@
19
19
  "devDependencies": {
20
20
  "@types/bun": "^1.4.2",
21
21
  "@types/node": "^26.5.1",
22
- "smol-toml": "1.8.0",
22
+ "smol-toml": "1.9.1",
23
23
  "tsx": "^4.23.13",
24
24
  "typescript": "^7.0.2"
25
25
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimi",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Codex plugin that delegates review, challenge, ask, rescue, pursue, and swarm workflows to the local kimi-code CLI through the kimi-plugin-cc companion runtime.",
5
5
  "author": {
6
6
  "name": "linxule"
@@ -656,6 +656,25 @@ export function requireSessionId(result, context) {
656
656
  },
657
657
  });
658
658
  }
659
+ /** Settled timeout data; stream records are deliberately non-serializable. */
660
+ export class ResponseTimeoutError extends RuntimeError {
661
+ constructor(budgetMs, stage, commandLabel, partialResult, cause) {
662
+ super("RESPONSE_TIMEOUT", `${stage} timed out after ${budgetMs}ms.`, stage, {
663
+ ...(cause instanceof Error ? { cause } : {}),
664
+ details: {
665
+ budget_ms: budgetMs,
666
+ command_label: commandLabel ?? null,
667
+ record_count: partialResult?.records.length ?? 0,
668
+ partial_output_chars: partialResult?.records.reduce((count, record) => count + (record.role === "assistant" && typeof record.content === "string" ? record.content.length : 0), 0) ?? 0,
669
+ aborted: true,
670
+ ...(partialResult ? { settled_post_abort: true } : {}),
671
+ },
672
+ });
673
+ this.name = "ResponseTimeoutError";
674
+ // Never serialize stream records into job error details or error envelopes.
675
+ Object.defineProperty(this, "partialResult", { value: partialResult, enumerable: false });
676
+ }
677
+ }
659
678
  /**
660
679
  * Run `kimi -p` under a budget that also tells the subprocess to die
661
680
  * when the budget expires.
@@ -682,7 +701,8 @@ export function requireSessionId(result, context) {
682
701
  * subprocess/tree teardown, then reject with
683
702
  * `RuntimeError("RESPONSE_TIMEOUT", …)` matching the legacy
684
703
  * `withTimeout` code shape.
685
- * - Result success path is unchanged from `runCliPrompt`.
704
+ * - A clean non-aborted exit-0 result wins same-tick budget expiry.
705
+ * Otherwise a settled result on timeout is carried as partial output.
686
706
  */
687
707
  export async function runCliPromptWithBudget(opts, budgetMs, stage) {
688
708
  const controller = new AbortController();
@@ -698,12 +718,7 @@ export async function runCliPromptWithBudget(opts, budgetMs, stage) {
698
718
  controller.abort();
699
719
  }, budgetMs);
700
720
  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
- });
721
+ const timeoutError = (partialResult, cause) => new ResponseTimeoutError(budgetMs, stage, opts.commandLabel, partialResult, cause);
707
722
  try {
708
723
  let result;
709
724
  try {
@@ -714,13 +729,13 @@ export async function runCliPromptWithBudget(opts, budgetMs, stage) {
714
729
  }
715
730
  catch (error) {
716
731
  if (timeoutFired)
717
- throw timeoutError();
732
+ throw timeoutError(undefined, error);
718
733
  throw error;
719
734
  }
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);
735
+ if (timeoutFired && (result.aborted || result.exitCode !== 0)) {
736
+ // A clean, non-aborted result won the settle/timeout boundary and is success.
737
+ // Budget aborts normally resolve with records, after owned teardown settles.
738
+ throw timeoutError(result);
724
739
  }
725
740
  return result;
726
741
  }
@@ -770,6 +785,8 @@ function buildArgs(opts) {
770
785
  }
771
786
  function buildEnv(opts) {
772
787
  const env = { ...opts.env };
788
+ // 2.2.0 audit: ambient trust starts project MCP stdio servers outside PreToolUse.
789
+ delete env.KIMI_CODE_TRUST_WORKSPACE;
773
790
  // kimi-code 0.33.0 inverted its CLI engine default: an unflagged `kimi -p`
774
791
  // now selects native agent-core-v2, while this upstream-documented flag pins
775
792
  // the legacy-v1 path. Native v2 still has a final-allow plan-file path that
@@ -1,3 +1,4 @@
1
+ import { companionHelp } from "../help.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import path from "node:path";
3
4
  import { createCliCancellationHandlers } from "../cli-cancellation.js";
@@ -16,7 +17,7 @@ import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.j
16
17
  import { classifyManagedCommandFailure } from "../kimi-errors.js";
17
18
  import { startBackgroundJob } from "../background-spawn.js";
18
19
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
19
- import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
21
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
21
22
  // v1.0 cutover note (PR 2):
22
23
  //
@@ -46,6 +47,8 @@ const ASK_PROMPT_PREFIX = [
46
47
  ].join("\n");
47
48
  export async function runAsk(argv, context) {
48
49
  const parsed = parseAskArgs(argv);
50
+ if (parsed.help)
51
+ return companionHelp(["ask", ...argv]);
49
52
  if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK !== "1") {
50
53
  await requireAskHookInstalled(context);
51
54
  }
@@ -76,6 +79,7 @@ export async function runAsk(argv, context) {
76
79
  model: parsed.model ?? null,
77
80
  thinking: parsed.thinking ?? null,
78
81
  background: parsed.background,
82
+ budget_ms: parsed.budgetMs ?? (sessionResolution.sourceJob?.operation_kind === "ask" ? sessionResolution.sourceJob.budget_ms : null) ?? null,
79
83
  pid: parsed.background ? null : process.pid,
80
84
  kimi_pid: null,
81
85
  status: "running",
@@ -169,7 +173,7 @@ export async function executeAskJob(jobId, prompt, context, options) {
169
173
  resumeSessionId: job.kimi_session_id ?? undefined,
170
174
  logPath: job.stream_log_path,
171
175
  signal: handlers.signal,
172
- }, KIMI_ASK_PROMPT_TIMEOUT_MS, "ask.prompt");
176
+ }, job.budget_ms ?? KIMI_ASK_PROMPT_TIMEOUT_MS, "ask.prompt");
173
177
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
174
178
  if (handlers.cancelling) {
175
179
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "ask.runtime");
@@ -227,8 +231,9 @@ export async function executeAskJob(jobId, prompt, context, options) {
227
231
  await markJobFailed(store, paths, job, error, cancel.failedSummary, { phase: "failed" });
228
232
  throw error;
229
233
  }
230
- const classified = classifyManagedCommandFailure(error, "ask", job.job_id);
231
- await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed" });
234
+ const classified = classifyManagedCommandFailure(error, "ask", job.job_id, { background: job.background, budgetMs: job.budget_ms ?? KIMI_ASK_PROMPT_TIMEOUT_MS });
235
+ const partialOutput = captureTimeoutPartialOutput(classified, store, job);
236
+ await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed", partialOutput });
232
237
  throw classified;
233
238
  }
234
239
  finally {
@@ -1,4 +1,6 @@
1
1
  import { RuntimeError } from "../errors.js";
2
+ import { ResponseTimeoutError } from "../cli-client.js";
3
+ import { observedExecutionFields } from "../kimi-engine.js";
2
4
  /**
3
5
  * Reassemble prose final text from cli-client records.
4
6
  *
@@ -110,3 +112,32 @@ export function assertCliResultSuccess(result, stage) {
110
112
  });
111
113
  }
112
114
  }
115
+ /** Timeout-only capture, called after cancellation has taken precedence. No title sync on failure. */
116
+ export function captureTimeoutPartialOutput(classified, store, job) {
117
+ const timeout = classified.cause;
118
+ if (!(timeout instanceof ResponseTimeoutError) || !timeout.partialResult)
119
+ return undefined;
120
+ const result = timeout.partialResult;
121
+ // Best-effort metadata only. The row is still `running`, and a session id
122
+ // written here can collide with `jobs_running_session_idx` when a resume of
123
+ // the same session was started while this job ran. A store error must never
124
+ // prevent markJobFailed from running — the partial artifact matters more
125
+ // than the metadata, so swallow and fall through.
126
+ try {
127
+ store.updateRunningJob(job.job_id, observedExecutionFields(result));
128
+ if (result.sessionId !== undefined && result.sessionId.length > 0 && result.sessionId !== job.kimi_session_id) {
129
+ store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
130
+ }
131
+ }
132
+ catch {
133
+ // Diagnostics stay in the stream log; the failure transition proceeds.
134
+ }
135
+ return {
136
+ text: reassembleProseFromRecords(result.records),
137
+ toolCalls: result.records.some((record) => record.role === "tool" ||
138
+ (record.role === "assistant" && Array.isArray(record.tool_calls) &&
139
+ record.tool_calls.length > 0)),
140
+ recordCount: result.records.length,
141
+ budgetMs: timeout.details.budget_ms,
142
+ };
143
+ }
@@ -143,6 +143,7 @@ export async function runPursue(argv, context) {
143
143
  model: parsed.model ?? null,
144
144
  thinking: parsed.thinking ?? null,
145
145
  background: false,
146
+ budget_ms: parsed.budgetMs ?? KIMI_PURSUE_DEFAULT_BUDGET_MS,
146
147
  pid: process.pid,
147
148
  kimi_pid: null,
148
149
  status: "running",
@@ -168,7 +169,7 @@ export async function runPursue(argv, context) {
168
169
  await markJobFailed(store, paths, job, classified, "Pursue failed.", { phase: "failed" });
169
170
  throw classified;
170
171
  }
171
- const completed = await executePursueJob(job.job_id, prompt, objective, parsed.budgetMs ?? KIMI_PURSUE_DEFAULT_BUDGET_MS, context);
172
+ const completed = await executePursueJob(job.job_id, prompt, objective, job.budget_ms, context);
172
173
  if (!completed.final_output_path) {
173
174
  throw new RuntimeError("PURSUE_RESULT_MISSING", "Pursue finished without a rendered result.", "pursue.result");
174
175
  }
@@ -1,3 +1,4 @@
1
+ import { REPAIR_SESSIONS_SUPPORTED_FLAGS } from "../parsing.js";
1
2
  import { constants } from "node:fs";
2
3
  import { lstat, open } from "node:fs/promises";
3
4
  import { createRequire } from "node:module";
@@ -15,7 +16,7 @@ export async function runRepairSessions(argv, context) {
15
16
  const seen = new Set();
16
17
  for (const arg of argv) {
17
18
  if ((arg !== "--apply" && arg !== "--all") || seen.has(arg)) {
18
- throw new RuntimeError("INVALID_ARGS", "Usage: repair-sessions [--apply] [--all]", "repair-sessions.args");
19
+ throw new RuntimeError("INVALID_ARGS", `Usage: repair-sessions. Supported flags: ${REPAIR_SESSIONS_SUPPORTED_FLAGS}.`, "repair-sessions.args");
19
20
  }
20
21
  seen.add(arg);
21
22
  }
@@ -1,3 +1,4 @@
1
+ import { REPLAY_SUPPORTED_FLAGS } from "../parsing.js";
1
2
  import { access, stat, readFile } from "node:fs/promises";
2
3
  import { constants as fsConstants } from "node:fs";
3
4
  import { RuntimeError } from "../errors.js";
@@ -9,8 +10,8 @@ import { sweepStaleJobs } from "../jobs.js";
9
10
  const MAX_REPLAY_LOG_BYTES = 32 * 1024 * 1024;
10
11
  export async function runReplay(argv, context) {
11
12
  const [jobId, ...rest] = argv;
12
- if (!jobId || rest.length > 0) {
13
- throw new RuntimeError("INVALID_ARGS", "replay expects exactly one job id: replay <job-id>.", "replay.parse");
13
+ if (!jobId || jobId.startsWith("-") || rest.length > 0) {
14
+ throw new RuntimeError("INVALID_ARGS", `replay expects exactly one job id: replay <job-id>. Supported flags: ${REPLAY_SUPPORTED_FLAGS}.`, "replay.parse");
14
15
  }
15
16
  const paths = resolvePluginPaths(context.env);
16
17
  await ensurePluginPaths(paths);
@@ -1,3 +1,4 @@
1
+ import { companionHelp } from "../help.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import path from "node:path";
3
4
  import { createCliCancellationHandlers } from "../cli-cancellation.js";
@@ -16,7 +17,7 @@ import { parseRescueArgs } from "../parsing.js";
16
17
  import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
17
18
  import { startBackgroundJob } from "../background-spawn.js";
18
19
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
19
- import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
21
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
21
22
  export { describeMissingResult } from "../background-spawn.js";
22
23
  // v1.0 cutover note (PR 3):
@@ -41,6 +42,8 @@ const RESCUE_SUMMARY_MAX = 120;
41
42
  const RESCUE_AGENT_PROFILE_PLACEHOLDER = "<cli-client>";
42
43
  export async function runRescue(argv, context) {
43
44
  const parsed = parseRescueArgs(argv);
45
+ if (parsed.help)
46
+ return companionHelp(["task", "rescue", ...argv]);
44
47
  const paths = resolvePluginPaths(context.env);
45
48
  await ensurePluginPaths(paths);
46
49
  const repoIdentity = await resolveRepoIdentity(context.cwd);
@@ -68,6 +71,7 @@ export async function runRescue(argv, context) {
68
71
  model: parsed.model ?? null,
69
72
  thinking: parsed.thinking ?? null,
70
73
  background: parsed.background,
74
+ budget_ms: parsed.budgetMs ?? (sessionResolution.sourceJob?.operation_kind === "rescue" ? sessionResolution.sourceJob.budget_ms : null) ?? null,
71
75
  pid: parsed.background ? null : process.pid,
72
76
  kimi_pid: null,
73
77
  status: "running",
@@ -185,7 +189,7 @@ export async function executeRescueJob(jobId, prompt, context, options) {
185
189
  signal: handlers.signal,
186
190
  // SIGKILL escalation defaults to 1500ms inside cli-client —
187
191
  // matches v0.4's cancellation.ts behavior for the wire client.
188
- }, KIMI_RESCUE_PROMPT_TIMEOUT_MS, "rescue.prompt");
192
+ }, job.budget_ms ?? KIMI_RESCUE_PROMPT_TIMEOUT_MS, "rescue.prompt");
189
193
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
190
194
  if (handlers.cancelling) {
191
195
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "rescue.runtime");
@@ -248,8 +252,11 @@ export async function executeRescueJob(jobId, prompt, context, options) {
248
252
  }
249
253
  const classified = classifyManagedCommandFailure(error, "rescue", job.job_id, {
250
254
  preserveStage: true,
255
+ background: job.background,
256
+ budgetMs: job.budget_ms ?? KIMI_RESCUE_PROMPT_TIMEOUT_MS,
251
257
  });
252
- return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed" });
258
+ const partialOutput = captureTimeoutPartialOutput(classified, store, job);
259
+ return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed", partialOutput });
253
260
  }
254
261
  finally {
255
262
  handlers.dispose();
@@ -42,6 +42,10 @@ function renderResultEnvelope(job, artifactPath, body) {
42
42
  job_id: job.job_id,
43
43
  kind: job.command_type,
44
44
  status: job.status,
45
+ budget_ms: job.budget_ms,
46
+ kimi_session_id: job.kimi_session_id,
47
+ // True when a budget timeout preserved assistant prose in the artifact.
48
+ partial_output: job.status === "failed" && Number(job.error?.details?.partial_output_chars ?? 0) > 0,
45
49
  summary: job.summary,
46
50
  error: job.error,
47
51
  artifact_path: artifactPath,
@@ -17,7 +17,7 @@ import { renderManagedJobOutput, writeArtifact } from "../render.js";
17
17
  import { RuntimeError } from "../errors.js";
18
18
  import { resolveRepoIdentity } from "../git.js";
19
19
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
20
- import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
21
21
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
22
22
  // v1.0 cutover note (PR 2):
23
23
  //
@@ -81,6 +81,7 @@ export async function runReview(argv, context, commandType) {
81
81
  model: parsed.model ?? null,
82
82
  thinking: parsed.thinking ?? null,
83
83
  background: false,
84
+ budget_ms: parsed.budgetMs ?? null,
84
85
  pid: process.pid,
85
86
  kimi_pid: null,
86
87
  status: "running",
@@ -113,7 +114,7 @@ export async function runReview(argv, context, commandType) {
113
114
  model: parsed.model,
114
115
  logPath,
115
116
  signal: handlers.signal,
116
- }, KIMI_REVIEW_PROMPT_TIMEOUT_MS, `${commandType}.prompt`);
117
+ }, job.budget_ms ?? KIMI_REVIEW_PROMPT_TIMEOUT_MS, `${commandType}.prompt`);
117
118
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
118
119
  if (handlers.cancelling) {
119
120
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, `${commandType}.runtime`);
@@ -163,8 +164,9 @@ export async function runReview(argv, context, commandType) {
163
164
  await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError);
164
165
  throw cancelledError;
165
166
  }
166
- const classified = classifyManagedCommandFailure(error, commandType, job.job_id);
167
- await markJobFailed(store, paths, job, classified, cancel.failedSummary);
167
+ const classified = classifyManagedCommandFailure(error, commandType, job.job_id, { background: job.background, budgetMs: job.budget_ms ?? KIMI_REVIEW_PROMPT_TIMEOUT_MS });
168
+ const partialOutput = captureTimeoutPartialOutput(classified, store, job);
169
+ await markJobFailed(store, paths, job, classified, cancel.failedSummary, { partialOutput });
168
170
  throw classified;
169
171
  }
170
172
  finally {
@@ -1,3 +1,4 @@
1
+ import { SETUP_SUPPORTED_FLAGS } from "../parsing.js";
1
2
  // Managed-block installer for the kimi-code PreToolUse hook.
2
3
  //
3
4
  // Replaces the v0.4 Wire-based setup probe. In v1.0 the load-bearing
@@ -127,7 +128,7 @@ function parseArgs(argv) {
127
128
  disableReviewGate = true;
128
129
  break;
129
130
  default:
130
- throw new RuntimeError("INVALID_ARGS", `Unknown setup flag ${token}. Supported flags: --check, --uninstall, --all (with --uninstall), --enable-review-gate, --disable-review-gate.`, "setup.parse");
131
+ throw new RuntimeError("INVALID_ARGS", `Unknown setup flag ${token}. Supported flags: ${SETUP_SUPPORTED_FLAGS}.`, "setup.parse");
131
132
  }
132
133
  }
133
134
  if (enableReviewGate && disableReviewGate) {
@@ -210,6 +210,7 @@ async function runReadSwarm(parsed, objective, context) {
210
210
  model: parsed.model ?? null,
211
211
  thinking: parsed.thinking ?? null,
212
212
  background: false,
213
+ budget_ms: parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS,
213
214
  pid: process.pid,
214
215
  kimi_pid: null,
215
216
  status: "running",
@@ -235,7 +236,7 @@ async function runReadSwarm(parsed, objective, context) {
235
236
  await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
236
237
  throw classified;
237
238
  }
238
- const completed = await executeSwarmJob(job.job_id, prompt, objective, parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS, resolveSwarmMaxConcurrency(parsed.maxConcurrency), context);
239
+ const completed = await executeSwarmJob(job.job_id, prompt, objective, job.budget_ms, resolveSwarmMaxConcurrency(parsed.maxConcurrency), context);
239
240
  if (!completed.final_output_path) {
240
241
  throw new RuntimeError("SWARM_RESULT_MISSING", "Swarm finished without a rendered result.", "swarm.result");
241
242
  }
@@ -308,6 +309,7 @@ async function runWriteSwarm(parsed, objective, context) {
308
309
  model: parsed.model ?? null,
309
310
  thinking: parsed.thinking ?? null,
310
311
  background: false,
312
+ budget_ms: parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS,
311
313
  pid: process.pid,
312
314
  kimi_pid: null,
313
315
  status: "running",
@@ -335,7 +337,7 @@ async function runWriteSwarm(parsed, objective, context) {
335
337
  });
336
338
  throw classified;
337
339
  }
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 });
340
+ const completed = await executeSwarmJob(job.job_id, prompt, objective, job.budget_ms, resolveSwarmWriteMaxConcurrency(parsed.maxConcurrency), context, { spawnCwd: worktreePath, workspaceRoot: worktreePath });
339
341
  if (!completed.final_output_path) {
340
342
  throw new RuntimeError("SWARM_RESULT_MISSING", "Swarm (write) finished without a rendered result.", "swarm.result");
341
343
  }
@@ -1,3 +1,4 @@
1
+ import { companionHelp } from "./help.js";
1
2
  import { runAsk } from "./commands/ask.js";
2
3
  import { runModelInventory } from "./commands/models.js";
3
4
  import { runCancel } from "./commands/cancel.js";
@@ -15,6 +16,11 @@ import { runWorker } from "./commands/worker.js";
15
16
  import { formatError, RuntimeError } from "./errors.js";
16
17
  import { resolveKimiHome } from "./kimi-home.js";
17
18
  async function main(argv) {
19
+ const help = companionHelp(argv);
20
+ if (help !== undefined) {
21
+ process.stdout.write(`${help}\n`);
22
+ return;
23
+ }
18
24
  const [command, ...rest] = argv;
19
25
  const cwd = process.env.KIMI_PLUGIN_CC_WORKSPACE_CWD || process.cwd();
20
26
  const context = {
@@ -0,0 +1,40 @@
1
+ import { hasHelpFlag, ASK_SUPPORTED_FLAGS, RESCUE_SUPPORTED_FLAGS, REVIEW_SUPPORTED_FLAGS, PURSUE_SUPPORTED_FLAGS, SWARM_SUPPORTED_FLAGS, JOB_LOOKUP_SUPPORTED_FLAGS, SETUP_SUPPORTED_FLAGS, REPAIR_SESSIONS_SUPPORTED_FLAGS, REPLAY_SUPPORTED_FLAGS, } from "./parsing.js";
2
+ const SUBCOMMANDS = "setup, review, task, ask, status, result, cancel, replay, repair-sessions";
3
+ const TASK_KINDS = "rescue, challenge, pursue, swarm";
4
+ /** Help is a dispatch-only, side-effect-free exception to normal argument parsing. */
5
+ export function companionHelp(argv) {
6
+ const [command, kind] = argv;
7
+ // Scan only the argument region: the subcommand (and a task kind) are
8
+ // positional names, not prose, so they must not end the flag region.
9
+ const argumentStart = command === "--help" || command === "-h" ? 0
10
+ : command === "task" && kind !== undefined && !kind.startsWith("-") ? 2
11
+ : 1;
12
+ if (!hasHelpFlag(argv.slice(argumentStart)))
13
+ return undefined;
14
+ const suffix = " Use --help or -h among the leading flags (before any prompt text or --) to show this usage.";
15
+ const usage = (name, positionals, flags, description) => `Usage: companion.sh ${name}${positionals ? ` ${positionals}` : ""}. ${description} Supported flags: ${flags}.${suffix}`;
16
+ switch (command) {
17
+ case "--help":
18
+ case "-h":
19
+ return `Usage: companion.sh <subcommand> [arguments]. Subcommands: ${SUBCOMMANDS}. Use <subcommand> --help for usage.`;
20
+ case "task":
21
+ switch (kind) {
22
+ case "rescue": return usage("task rescue", "[options] [--] <task>", RESCUE_SUPPORTED_FLAGS, "Write-capable task; default budget 45m. Backgrounding does not extend the budget.");
23
+ case "challenge": return usage("task challenge", "[options] [--] [focus]", REVIEW_SUPPORTED_FLAGS, "Read-only challenge; default budget 30m. Foreground-only (no --background/--wait).");
24
+ case "pursue": return usage("task pursue", "[options] [--] <objective>", PURSUE_SUPPORTED_FLAGS, "Autonomous goal; default budget 45m. Runtime foreground-only.");
25
+ case "swarm": return usage("task swarm", "[options] [--] <objective>", SWARM_SUPPORTED_FLAGS, "Parallel task; default budget 30m. Runtime foreground-only; --write produces a patch.");
26
+ default: return `Usage: companion.sh task <kind> [arguments]. Task kinds: ${TASK_KINDS}. Use task <kind> --help for usage.`;
27
+ }
28
+ case "ask": return usage(command, "[options] [--] [prompt]", ASK_SUPPORTED_FLAGS, "Read-only question; default budget 30m. Backgrounding does not extend the budget.");
29
+ case "review": return usage(command, "[options] [--] [focus]", REVIEW_SUPPORTED_FLAGS, "Read-only review; default budget 30m. Foreground-only (no --background/--wait).");
30
+ case "setup": return usage(command, "[options]", SETUP_SUPPORTED_FLAGS, "Install, check, or remove this host's managed hook, or list configured models.");
31
+ case "status":
32
+ case "result":
33
+ case "cancel":
34
+ return usage(command, "[job-id] [options]", JOB_LOOKUP_SUPPORTED_FLAGS, command === "status" ? "Show job state as JSON." : command === "result" ? "Read a terminal artifact, optionally in a JSON envelope." : "Cancel a running job; without an id, target the repository's latest running job.");
35
+ case "replay": return usage(command, "<job-id>", REPLAY_SUPPORTED_FLAGS, "Replay a completed job's persisted stream log.");
36
+ case "repair-sessions": return usage(command, "[options]", REPAIR_SESSIONS_SUPPORTED_FLAGS, "Preview session metadata repair; --apply writes, --all selects all host-owned repositories.");
37
+ case "worker": return "Usage: companion.sh worker <ask|rescue> <job-id>. Internal detached-worker entry point; launched by ask/rescue, not manually.";
38
+ default: return undefined;
39
+ }
40
+ }
@@ -585,14 +585,15 @@ function validateVersionSensitiveEvents(events, version) {
585
585
  return invalidHookSet(`version-sensitive event ${JSON.stringify(first.event)} requires a verified kimi-code version`, first.entry, first.line);
586
586
  }
587
587
  // Exact schemas reviewed at 2.0.0 (1b89e4b0), 2.0.1 (caf7d4e2),
588
- // 2.0.2 (9d07f634), 2.1.0 (52437299), and 2.1.1 (f67e6398):
588
+ // 2.0.2 (9d07f634), 2.1.0 (52437299), 2.1.1 (f67e6398) and 2.2.0 (472192c3):
589
589
  // the additive events and HookDefSchema are unchanged. Require the full
590
590
  // version string AND matching components; no inferred 2.x compatibility.
591
591
  if (version.major === 2 && ((version.minor === 0 && ((version.version === "2.0.0" && version.patch === 0) ||
592
592
  (version.version === "2.0.1" && version.patch === 1) ||
593
593
  (version.version === "2.0.2" && version.patch === 2))) ||
594
594
  (version.minor === 1 && ((version.version === "2.1.0" && version.patch === 0) ||
595
- (version.version === "2.1.1" && version.patch === 1))))) {
595
+ (version.version === "2.1.1" && version.patch === 1))) ||
596
+ (version.minor === 2 && ((version.version === "2.2.0" && version.patch === 0))))) {
596
597
  return { valid: true };
597
598
  }
598
599
  if (version.major !== 0) {
@@ -58,6 +58,7 @@ export class JobStore {
58
58
  prompt_digest TEXT NOT NULL,
59
59
  summary TEXT NOT NULL,
60
60
  phase TEXT,
61
+ budget_ms INTEGER,
61
62
  final_output_path TEXT,
62
63
  stream_log_path TEXT NOT NULL,
63
64
  error TEXT
@@ -66,6 +67,9 @@ export class JobStore {
66
67
  if (!tableHasColumn(this.db, "phase")) {
67
68
  this.db.exec(`ALTER TABLE jobs ADD COLUMN phase TEXT;`);
68
69
  }
70
+ if (!tableHasColumn(this.db, "budget_ms")) {
71
+ this.db.exec(`ALTER TABLE jobs ADD COLUMN budget_ms INTEGER;`);
72
+ }
69
73
  // Additive provenance migration. Existing rows deliberately remain NULL:
70
74
  // v1.9.5 plus kimi-code 0.33/0.34 could have routed to native v2 before the
71
75
  // legacy pin landed, so a blanket historical v1 backfill would invent fact.
@@ -162,14 +166,14 @@ export class JobStore {
162
166
  background, pid, kimi_pid, status, kimi_session_id,
163
167
  operation_kind, intended_engine, observed_engine, kimi_version, system_version,
164
168
  kimi_command, kimi_prefix_args, plan_certification, resumed_from_job_id, safety_profile,
165
- agent_profile, prompt_digest, summary, phase, final_output_path, stream_log_path, error
169
+ agent_profile, prompt_digest, summary, phase, budget_ms, final_output_path, stream_log_path, error
166
170
  )
167
171
  VALUES (
168
172
  @job_id, @repo_id, @command_type, @created_at, @updated_at, @cwd, @model, @thinking,
169
173
  @background, @pid, @kimi_pid, @status, @kimi_session_id,
170
174
  @operation_kind, @intended_engine, @observed_engine, @kimi_version, @system_version,
171
175
  @kimi_command, @kimi_prefix_args, @plan_certification, @resumed_from_job_id, @safety_profile,
172
- @agent_profile, @prompt_digest, @summary, @phase, @final_output_path, @stream_log_path, @error
176
+ @agent_profile, @prompt_digest, @summary, @phase, @budget_ms, @final_output_path, @stream_log_path, @error
173
177
  )
174
178
  `, {
175
179
  ...serializeRecord(input),
@@ -276,7 +280,7 @@ export class JobStore {
276
280
  return this.updateWhere(jobId, { ...patch, status: "cancelled", pid: null, kimi_pid: null }, "status = 'running'");
277
281
  }
278
282
  static JOBS_COLUMNS = new Set([
279
- "status", "summary", "phase", "pid", "kimi_pid",
283
+ "status", "summary", "phase", "budget_ms", "pid", "kimi_pid",
280
284
  "final_output_path", "stream_log_path", "kimi_session_id",
281
285
  "error", "command_type", "prompt_digest", "repo_id",
282
286
  "cwd", "model", "thinking", "background", "agent_profile",
@@ -320,6 +324,7 @@ function serializeRecord(record) {
320
324
  return {
321
325
  ...record,
322
326
  phase: record.phase ?? null,
327
+ budget_ms: record.budget_ms ?? null,
323
328
  operation_kind: record.operation_kind ?? null,
324
329
  intended_engine: record.intended_engine ?? null,
325
330
  observed_engine: record.observed_engine ?? null,
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { RuntimeError } from "./errors.js";
3
- import { renderTerminalJobArtifact, writeArtifact } from "./render.js";
3
+ import { formatBudgetDuration, renderTerminalJobArtifact, writeArtifact } from "./render.js";
4
4
  export function digestPrompt(prompt) {
5
5
  return createHash("sha256").update(prompt).digest("hex");
6
6
  }
@@ -30,10 +30,14 @@ export function normalizeJobError(error) {
30
30
  };
31
31
  }
32
32
  export async function markJobFailed(store, paths, job, error, summary = "Job failed.", options) {
33
+ if (options?.partialOutput) {
34
+ const label = job.command_type[0].toUpperCase() + job.command_type.slice(1);
35
+ summary = `${label} timed out after ${formatBudgetDuration(options.partialOutput.budgetMs)}; partial output captured (${options.partialOutput.text.trim().length} chars).`;
36
+ }
33
37
  const normalized = normalizeJobError(error);
34
38
  const phase = options?.phase;
35
39
  const artifactJob = {
36
- ...job,
40
+ ...(store.getJob(job.job_id) ?? job),
37
41
  status: "failed",
38
42
  summary,
39
43
  error: normalized,
@@ -42,7 +46,7 @@ export async function markJobFailed(store, paths, job, error, summary = "Job fai
42
46
  kimi_pid: null,
43
47
  ...(phase !== undefined ? { phase } : {}),
44
48
  };
45
- const artifactPath = await writeArtifact(paths, artifactJob, renderTerminalJobArtifact(artifactJob));
49
+ const artifactPath = await writeArtifact(paths, artifactJob, renderTerminalJobArtifact(artifactJob, options?.partialOutput));
46
50
  return (store.markFailed(job.job_id, {
47
51
  summary,
48
52
  error: normalized,
@@ -38,6 +38,7 @@ export const NATIVE_V2_CERTIFIED_VERSIONS = Object.freeze([
38
38
  "2.0.2",
39
39
  "2.1.0",
40
40
  "2.1.1",
41
+ "2.2.0",
41
42
  ]);
42
43
  /** Per-operation native-v2 certification. One green operation never certifies another. */
43
44
  export const NATIVE_V2_CERTIFIED = new Map([