kimi-plugin-cc 2.0.7 → 2.2.0

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 (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +50 -0
  4. package/README.md +22 -6
  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 +36 -13
  14. package/dist/commands/ask.js +16 -7
  15. package/dist/commands/cli-helpers.js +80 -0
  16. package/dist/commands/pursue.js +4 -2
  17. package/dist/commands/repair-sessions.js +2 -1
  18. package/dist/commands/replay.js +3 -2
  19. package/dist/commands/rescue.js +19 -9
  20. package/dist/commands/result.js +7 -1
  21. package/dist/commands/review-gate.js +2 -1
  22. package/dist/commands/review.js +8 -5
  23. package/dist/commands/setup.js +2 -1
  24. package/dist/commands/status.js +2 -2
  25. package/dist/commands/swarm.js +7 -3
  26. package/dist/companion.js +6 -0
  27. package/dist/help.js +40 -0
  28. package/dist/hooks/config-safety.js +18 -9
  29. package/dist/job-store.js +43 -6
  30. package/dist/jobs.js +7 -3
  31. package/dist/kimi-engine.js +1 -0
  32. package/dist/kimi-errors.js +35 -1
  33. package/dist/kimi-timeouts.js +7 -7
  34. package/dist/parsing.js +87 -13
  35. package/dist/render.js +19 -1
  36. package/dist/session-recovery.js +203 -0
  37. package/dist/vendor/smol-toml/date.js +50 -21
  38. package/dist/vendor/smol-toml/error.js +6 -2
  39. package/dist/vendor/smol-toml/extract.js +259 -66
  40. package/dist/vendor/smol-toml/parse.js +64 -48
  41. package/dist/vendor/smol-toml/primitive.js +107 -135
  42. package/dist/vendor/smol-toml/struct.js +106 -134
  43. package/dist/vendor/smol-toml/util.js +22 -78
  44. package/dist/version.js +1 -1
  45. package/docs/migration.md +3 -3
  46. package/docs/registry-releases.md +35 -1
  47. package/docs/safety.md +49 -2
  48. package/package.json +2 -2
  49. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  50. package/plugins/kimi-codex/dist/cli-client.js +36 -13
  51. package/plugins/kimi-codex/dist/commands/ask.js +16 -7
  52. package/plugins/kimi-codex/dist/commands/cli-helpers.js +80 -0
  53. package/plugins/kimi-codex/dist/commands/pursue.js +4 -2
  54. package/plugins/kimi-codex/dist/commands/repair-sessions.js +2 -1
  55. package/plugins/kimi-codex/dist/commands/replay.js +3 -2
  56. package/plugins/kimi-codex/dist/commands/rescue.js +19 -9
  57. package/plugins/kimi-codex/dist/commands/result.js +7 -1
  58. package/plugins/kimi-codex/dist/commands/review-gate.js +2 -1
  59. package/plugins/kimi-codex/dist/commands/review.js +8 -5
  60. package/plugins/kimi-codex/dist/commands/setup.js +2 -1
  61. package/plugins/kimi-codex/dist/commands/status.js +2 -2
  62. package/plugins/kimi-codex/dist/commands/swarm.js +7 -3
  63. package/plugins/kimi-codex/dist/companion.js +6 -0
  64. package/plugins/kimi-codex/dist/help.js +40 -0
  65. package/plugins/kimi-codex/dist/hooks/config-safety.js +18 -9
  66. package/plugins/kimi-codex/dist/job-store.js +43 -6
  67. package/plugins/kimi-codex/dist/jobs.js +7 -3
  68. package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
  69. package/plugins/kimi-codex/dist/kimi-errors.js +35 -1
  70. package/plugins/kimi-codex/dist/kimi-timeouts.js +7 -7
  71. package/plugins/kimi-codex/dist/parsing.js +87 -13
  72. package/plugins/kimi-codex/dist/render.js +19 -1
  73. package/plugins/kimi-codex/dist/session-recovery.js +203 -0
  74. package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +50 -21
  75. package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +6 -2
  76. package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +259 -66
  77. package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +64 -48
  78. package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +107 -135
  79. package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +106 -134
  80. package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +22 -78
  81. package/plugins/kimi-codex/dist/version.js +1 -1
  82. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +3 -1
  83. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +2 -1
  84. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +3 -1
  85. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +3 -2
@@ -1,4 +1,9 @@
1
1
  import { RuntimeError } from "../errors.js";
2
+ import { ResponseTimeoutError } from "../cli-client.js";
3
+ import { observedExecutionFields } from "../kimi-engine.js";
4
+ import { resolveKimiHome } from "../kimi-home.js";
5
+ import { recoverSessionIdFromStore } from "../session-recovery.js";
6
+ import { formatBudgetDuration } from "../render.js";
2
7
  /**
3
8
  * Reassemble prose final text from cli-client records.
4
9
  *
@@ -110,3 +115,78 @@ export function assertCliResultSuccess(result, stage) {
110
115
  });
111
116
  }
112
117
  }
118
+ /** Timeout-only capture. Cancellation takes precedence, including while the
119
+ * asynchronous store scan is in flight. No title sync on failure. */
120
+ export async function captureTimeoutPartialOutput(classified, store, job, env, cancellationSignal) {
121
+ if (cancellationSignal?.aborted)
122
+ return undefined;
123
+ const timeout = classified.cause;
124
+ if (!(timeout instanceof ResponseTimeoutError) || !timeout.partialResult)
125
+ return undefined;
126
+ const result = timeout.partialResult;
127
+ let sessionId = result.sessionId;
128
+ let source = sessionId ? "announced" : null;
129
+ const details = classified instanceof RuntimeError ? classified.details : timeout.details;
130
+ details.kimi_session_id_source = job.kimi_session_id_source;
131
+ // The persisted plan is plugin-owned. Recovery never runs on cancellation,
132
+ // v1, successful turns, teardown rejection, or an already-known resume id.
133
+ if (!sessionId && !job.kimi_session_id && job.intended_engine === "native-v2") {
134
+ // Without the system.version marker the row has no proven engine, and
135
+ // resume would refuse the id as lineage-unknown; do not advertise one.
136
+ if (result.observedEngine === null) {
137
+ details.session_recovery = "engine_unobserved";
138
+ }
139
+ else {
140
+ const recovered = await recoverSessionIdFromStore({
141
+ kimiHome: resolveKimiHome(env, job.cwd), cwd: job.cwd,
142
+ spawnedAt: result.spawnedAt, settledAt: result.settledAt, promptDigest: job.prompt_digest,
143
+ store, jobId: job.job_id,
144
+ });
145
+ sessionId = recovered.sessionId;
146
+ if (sessionId)
147
+ source = "recovered_session_dir";
148
+ else
149
+ details.session_recovery = recovered.reason;
150
+ }
151
+ }
152
+ if (cancellationSignal?.aborted)
153
+ return undefined;
154
+ // Best-effort metadata only. The row is still `running`, and a session id
155
+ // written here can collide with `jobs_running_session_idx` when a resume of
156
+ // the same session was started while this job ran. A store error must never
157
+ // prevent markJobFailed from running — the partial artifact matters more
158
+ // than the metadata, so swallow and fall through.
159
+ try {
160
+ store.updateRunningJob(job.job_id, observedExecutionFields(result));
161
+ if (sessionId) {
162
+ const updated = source === "recovered_session_dir"
163
+ ? store.claimRecoveredSessionId(job.job_id, sessionId)
164
+ : store.updateRunningJob(job.job_id, { kimi_session_id: sessionId, kimi_session_id_source: source });
165
+ if (updated?.kimi_session_id === sessionId && updated.kimi_session_id_source === source) {
166
+ details.kimi_session_id = sessionId;
167
+ details.kimi_session_id_source = source;
168
+ }
169
+ else
170
+ details[source === "recovered_session_dir" ? "session_recovery" : "session_id_persistence"] = "persistence_failed";
171
+ }
172
+ }
173
+ catch {
174
+ // `session_recovery` describes only the recovery branch; an announced id or
175
+ // the provenance update failing is a different fact.
176
+ details[source === "recovered_session_dir" ? "session_recovery" : "session_id_persistence"] = "persistence_failed";
177
+ // Diagnostics stay in the stream log; the failure transition proceeds.
178
+ }
179
+ return {
180
+ text: reassembleProseFromRecords(result.records),
181
+ toolCalls: result.records.some((record) => record.role === "tool" ||
182
+ (record.role === "assistant" && Array.isArray(record.tool_calls) &&
183
+ record.tool_calls.length > 0)),
184
+ recordCount: result.records.length,
185
+ budgetMs: timeout.details.budget_ms,
186
+ };
187
+ }
188
+ export function warnInheritedBudget(job, stderr) {
189
+ if (job.budget_source === "inherited" && job.budget_ms !== null) {
190
+ stderr.write(`[${job.command_type}] budget ${formatBudgetDuration(job.budget_ms)} inherited from job ${job.resumed_from_job_id}; pass --budget to override\n`);
191
+ }
192
+ }
@@ -143,6 +143,8 @@ 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,
147
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
146
148
  pid: process.pid,
147
149
  kimi_pid: null,
148
150
  status: "running",
@@ -168,7 +170,7 @@ export async function runPursue(argv, context) {
168
170
  await markJobFailed(store, paths, job, classified, "Pursue failed.", { phase: "failed" });
169
171
  throw classified;
170
172
  }
171
- const completed = await executePursueJob(job.job_id, prompt, objective, parsed.budgetMs ?? KIMI_PURSUE_DEFAULT_BUDGET_MS, context);
173
+ const completed = await executePursueJob(job.job_id, prompt, objective, job.budget_ms, context);
172
174
  if (!completed.final_output_path) {
173
175
  throw new RuntimeError("PURSUE_RESULT_MISSING", "Pursue finished without a rendered result.", "pursue.result");
174
176
  }
@@ -249,7 +251,7 @@ async function executePursueJob(jobId, prompt, objective, budgetMs, context) {
249
251
  if (result.sessionId !== undefined &&
250
252
  result.sessionId.length > 0 &&
251
253
  result.sessionId !== job.kimi_session_id) {
252
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
254
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
253
255
  }
254
256
  await syncKimiSessionTitle({
255
257
  promptText: objective,
@@ -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 { warnInheritedBudget, 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,10 +71,14 @@ 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,
75
+ budget_source: parsed.budgetMs !== undefined ? "explicit"
76
+ : sessionResolution.sourceJob?.operation_kind === "rescue" && sessionResolution.sourceJob.budget_ms !== null ? "inherited" : "default",
71
77
  pid: parsed.background ? null : process.pid,
72
78
  kimi_pid: null,
73
79
  status: "running",
74
80
  kimi_session_id: sessionResolution.kimiSessionId,
81
+ kimi_session_id_source: sessionResolution.sourceJob?.kimi_session_id_source ?? null,
75
82
  ...persistedExecutionPlanFields(executionPlan),
76
83
  agent_profile: RESCUE_AGENT_PROFILE_PLACEHOLDER,
77
84
  prompt_digest: digestPrompt(prompt),
@@ -93,6 +100,7 @@ export async function runRescue(argv, context) {
93
100
  await markJobFailed(store, paths, job, classified, rescueConfig.cancellation.failedSummary, { phase: "failed" });
94
101
  throw classified;
95
102
  }
103
+ warnInheritedBudget(job, context.stderr);
96
104
  if (parsed.background) {
97
105
  return startBackgroundJob(job, prompt, context, paths, {
98
106
  workerKind: "rescue",
@@ -185,18 +193,17 @@ export async function executeRescueJob(jobId, prompt, context, options) {
185
193
  signal: handlers.signal,
186
194
  // SIGKILL escalation defaults to 1500ms inside cli-client —
187
195
  // matches v0.4's cancellation.ts behavior for the wire client.
188
- }, KIMI_RESCUE_PROMPT_TIMEOUT_MS, "rescue.prompt");
196
+ }, job.budget_ms ?? KIMI_RESCUE_PROMPT_TIMEOUT_MS, "rescue.prompt");
189
197
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
190
198
  if (handlers.cancelling) {
191
199
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "rescue.runtime");
192
200
  }
193
201
  assertCliResultSuccess(result, "rescue.runtime");
194
202
  if (result.sessionId !== undefined &&
195
- result.sessionId.length > 0 &&
196
- result.sessionId !== job.kimi_session_id) {
203
+ result.sessionId.length > 0) {
197
204
  // length>0 guard: empty-string capture would poison the row. (Kimi
198
205
  // alpha.4 challenge finding #3.)
199
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
206
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
200
207
  }
201
208
  await syncKimiSessionTitle({
202
209
  promptText: prompt,
@@ -242,14 +249,17 @@ export async function executeRescueJob(jobId, prompt, context, options) {
242
249
  }) ?? job);
243
250
  }
244
251
  catch (error) {
252
+ const classified = classifyManagedCommandFailure(error, "rescue", job.job_id, {
253
+ preserveStage: true,
254
+ background: job.background,
255
+ budgetMs: job.budget_ms ?? KIMI_RESCUE_PROMPT_TIMEOUT_MS,
256
+ });
257
+ const partialOutput = await captureTimeoutPartialOutput(classified, store, job, context.env, handlers.signal);
245
258
  if (handlers.cancelling) {
246
259
  const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, "rescue.runtime", error instanceof Error ? { cause: error } : undefined);
247
260
  return await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError, { phase: "cancelled" });
248
261
  }
249
- const classified = classifyManagedCommandFailure(error, "rescue", job.job_id, {
250
- preserveStage: true,
251
- });
252
- return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed" });
262
+ return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed", partialOutput });
253
263
  }
254
264
  finally {
255
265
  handlers.dispose();
@@ -2,7 +2,7 @@ import { RuntimeError } from "../errors.js";
2
2
  import { resolveRepoIdentity } from "../git.js";
3
3
  import { sweepStaleJobs } from "../jobs.js";
4
4
  import { reconcileHistoricalJobProvenance } from "../kimi-engine.js";
5
- import { withJobStore } from "../job-store.js";
5
+ import { renderBudgetSource, withJobStore } from "../job-store.js";
6
6
  import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
7
7
  import { parseJobLookupArgs } from "../parsing.js";
8
8
  import { readArtifact, renderTerminalJobArtifact } from "../render.js";
@@ -42,6 +42,12 @@ 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
+ budget_source: renderBudgetSource(job),
47
+ kimi_session_id_source: job.kimi_session_id_source,
48
+ kimi_session_id: job.kimi_session_id,
49
+ // True when a budget timeout preserved assistant prose in the artifact.
50
+ partial_output: job.status === "failed" && Number(job.error?.details?.partial_output_chars ?? 0) > 0,
45
51
  summary: job.summary,
46
52
  error: job.error,
47
53
  artifact_path: artifactPath,
@@ -153,6 +153,7 @@ async function executeReviewGate(payload, assistantMessage, context) {
153
153
  kimi_pid: null,
154
154
  status: "running",
155
155
  kimi_session_id: null,
156
+ budget_source: "default",
156
157
  ...persistedExecutionPlanFields(executionPlan),
157
158
  agent_profile: REVIEW_GATE_AGENT_PROFILE_PLACEHOLDER,
158
159
  prompt_digest: digestPrompt(prompt),
@@ -195,7 +196,7 @@ async function executeReviewGate(payload, assistantMessage, context) {
195
196
  // can't synthesize empty strings today, but the guard locks the
196
197
  // SQLite contract independent of upstream behavior).
197
198
  activeStore.updateRunningJob(job.job_id, {
198
- kimi_session_id: result.sessionId,
199
+ kimi_session_id_source: "announced", kimi_session_id: result.sessionId,
199
200
  });
200
201
  }
201
202
  const finalText = reassembleProseFromRecords(result.records);
@@ -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,8 @@ 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,
85
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
84
86
  pid: process.pid,
85
87
  kimi_pid: null,
86
88
  status: "running",
@@ -113,7 +115,7 @@ export async function runReview(argv, context, commandType) {
113
115
  model: parsed.model,
114
116
  logPath,
115
117
  signal: handlers.signal,
116
- }, KIMI_REVIEW_PROMPT_TIMEOUT_MS, `${commandType}.prompt`);
118
+ }, job.budget_ms ?? KIMI_REVIEW_PROMPT_TIMEOUT_MS, `${commandType}.prompt`);
117
119
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
118
120
  if (handlers.cancelling) {
119
121
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, `${commandType}.runtime`);
@@ -127,7 +129,7 @@ export async function runReview(argv, context, commandType) {
127
129
  // poison the row — consumers reading SQLite distinguish "missing"
128
130
  // (NULL) from "captured" (non-empty UUID) only via NULL-ness.
129
131
  // (Kimi alpha.4 challenge finding #3.)
130
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
132
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
131
133
  }
132
134
  await syncKimiSessionTitle({
133
135
  promptText: previewPrompt,
@@ -158,13 +160,14 @@ export async function runReview(argv, context, commandType) {
158
160
  return rendered.output;
159
161
  }
160
162
  catch (error) {
163
+ const classified = classifyManagedCommandFailure(error, commandType, job.job_id, { background: job.background, budgetMs: job.budget_ms ?? KIMI_REVIEW_PROMPT_TIMEOUT_MS });
164
+ const partialOutput = await captureTimeoutPartialOutput(classified, store, job, context.env, handlers.signal);
161
165
  if (handlers.cancelling) {
162
166
  const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, `${commandType}.runtime`, error instanceof Error ? { cause: error } : undefined);
163
167
  await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError);
164
168
  throw cancelledError;
165
169
  }
166
- const classified = classifyManagedCommandFailure(error, commandType, job.job_id);
167
- await markJobFailed(store, paths, job, classified, cancel.failedSummary);
170
+ await markJobFailed(store, paths, job, classified, cancel.failedSummary, { partialOutput });
168
171
  throw classified;
169
172
  }
170
173
  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) {
@@ -2,7 +2,7 @@ import { RuntimeError } from "../errors.js";
2
2
  import { resolveRepoIdentity } from "../git.js";
3
3
  import { sweepStaleJobs } from "../jobs.js";
4
4
  import { reconcileHistoricalJobProvenance } from "../kimi-engine.js";
5
- import { withJobStore } from "../job-store.js";
5
+ import { renderBudgetSource, withJobStore } from "../job-store.js";
6
6
  import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
7
7
  import { parseJobLookupArgs } from "../parsing.js";
8
8
  export async function runStatus(argv, context) {
@@ -22,6 +22,6 @@ export async function runStatus(argv, context) {
22
22
  throw new RuntimeError("JOB_NOT_FOUND", "No matching job was found for status.", "status.lookup");
23
23
  }
24
24
  const job = await reconcileHistoricalJobProvenance(store, found);
25
- return `${JSON.stringify(job, null, 2)}\n`;
25
+ return `${JSON.stringify({ ...job, budget_source: renderBudgetSource(job) }, null, 2)}\n`;
26
26
  });
27
27
  }
@@ -210,6 +210,8 @@ 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,
214
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
213
215
  pid: process.pid,
214
216
  kimi_pid: null,
215
217
  status: "running",
@@ -235,7 +237,7 @@ async function runReadSwarm(parsed, objective, context) {
235
237
  await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
236
238
  throw classified;
237
239
  }
238
- const completed = await executeSwarmJob(job.job_id, prompt, objective, parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS, resolveSwarmMaxConcurrency(parsed.maxConcurrency), context);
240
+ const completed = await executeSwarmJob(job.job_id, prompt, objective, job.budget_ms, resolveSwarmMaxConcurrency(parsed.maxConcurrency), context);
239
241
  if (!completed.final_output_path) {
240
242
  throw new RuntimeError("SWARM_RESULT_MISSING", "Swarm finished without a rendered result.", "swarm.result");
241
243
  }
@@ -308,6 +310,8 @@ async function runWriteSwarm(parsed, objective, context) {
308
310
  model: parsed.model ?? null,
309
311
  thinking: parsed.thinking ?? null,
310
312
  background: false,
313
+ budget_ms: parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS,
314
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
311
315
  pid: process.pid,
312
316
  kimi_pid: null,
313
317
  status: "running",
@@ -335,7 +339,7 @@ async function runWriteSwarm(parsed, objective, context) {
335
339
  });
336
340
  throw classified;
337
341
  }
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 });
342
+ const completed = await executeSwarmJob(job.job_id, prompt, objective, job.budget_ms, resolveSwarmWriteMaxConcurrency(parsed.maxConcurrency), context, { spawnCwd: worktreePath, workspaceRoot: worktreePath });
339
343
  if (!completed.final_output_path) {
340
344
  throw new RuntimeError("SWARM_RESULT_MISSING", "Swarm (write) finished without a rendered result.", "swarm.result");
341
345
  }
@@ -500,7 +504,7 @@ writeMode) {
500
504
  if (result.sessionId !== undefined &&
501
505
  result.sessionId.length > 0 &&
502
506
  result.sessionId !== job.kimi_session_id) {
503
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
507
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
504
508
  }
505
509
  await syncKimiSessionTitle({
506
510
  promptText: objective,
package/dist/companion.js CHANGED
@@ -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 = {
package/dist/help.js ADDED
@@ -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
+ }
@@ -417,15 +417,23 @@ export function normalizeTopLevelInlineHooks(contents, lineEnding) {
417
417
  entryCount: hooks.length,
418
418
  };
419
419
  }
420
+ // Same defaults as smol-toml 1.9.1 parse(); this walker only locates assignments.
421
+ // tests/runtime/toml-walker-context.test.ts asserts these against the vendored parse().
422
+ export function assignmentParseContext(s) {
423
+ return { s, p: s.charCodeAt(0) === 0xfeff ? 1 : 0, d: 1_000, bi: false, ld: true, uk: 0 };
424
+ }
420
425
  function findTopLevelHooksAssignment(contents) {
421
- let ptr = skipVoid(contents, 0);
422
- while (ptr < contents.length) {
426
+ const ctx = assignmentParseContext(contents);
427
+ skipVoid(ctx);
428
+ while (ctx.p < contents.length) {
423
429
  // A root-level direct assignment cannot resume after entering a table.
424
- if (contents[ptr] === "[")
430
+ if (contents[ctx.p] === "[")
425
431
  return null;
426
- const declarationStart = ptr;
427
- const [key, valueStart] = parseKey(contents, ptr);
428
- const [, valueEnd] = extractValue(contents, valueStart, undefined, 1_000, undefined);
432
+ const declarationStart = ctx.p;
433
+ const key = parseKey(ctx);
434
+ skipVoid(ctx, true, true);
435
+ extractValue(ctx, undefined);
436
+ const valueEnd = ctx.p;
429
437
  if (key.length === 1 && key[0] === "hooks") {
430
438
  const lineStart = contents.lastIndexOf("\n", declarationStart - 1) + 1;
431
439
  const start = /^[\t ]*$/.test(contents.slice(lineStart, declarationStart))
@@ -445,7 +453,7 @@ function findTopLevelHooksAssignment(contents) {
445
453
  }
446
454
  return { start, end, hadLineEnding };
447
455
  }
448
- ptr = skipVoid(contents, valueEnd);
456
+ skipVoid(ctx);
449
457
  }
450
458
  return null;
451
459
  }
@@ -585,14 +593,15 @@ function validateVersionSensitiveEvents(events, version) {
585
593
  return invalidHookSet(`version-sensitive event ${JSON.stringify(first.event)} requires a verified kimi-code version`, first.entry, first.line);
586
594
  }
587
595
  // 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):
596
+ // 2.0.2 (9d07f634), 2.1.0 (52437299), 2.1.1 (f67e6398) and 2.2.0 (472192c3):
589
597
  // the additive events and HookDefSchema are unchanged. Require the full
590
598
  // version string AND matching components; no inferred 2.x compatibility.
591
599
  if (version.major === 2 && ((version.minor === 0 && ((version.version === "2.0.0" && version.patch === 0) ||
592
600
  (version.version === "2.0.1" && version.patch === 1) ||
593
601
  (version.version === "2.0.2" && version.patch === 2))) ||
594
602
  (version.minor === 1 && ((version.version === "2.1.0" && version.patch === 0) ||
595
- (version.version === "2.1.1" && version.patch === 1))))) {
603
+ (version.version === "2.1.1" && version.patch === 1))) ||
604
+ (version.minor === 2 && ((version.version === "2.2.0" && version.patch === 0))))) {
596
605
  return { valid: true };
597
606
  }
598
607
  if (version.major !== 0) {