kimi-plugin-cc 2.5.4 → 2.5.5

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.
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Kimi for Claude Code: code questions, reviews, fixes, parallel tasks, and MCP tasks through the local Kimi Code CLI.",
8
- "version": "2.5.4"
8
+ "version": "2.5.5"
9
9
  },
10
10
  "plugins": [
11
11
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kimi",
3
3
  "description": "Kimi for Claude Code: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP tasks through the local Kimi Code CLI, with PreToolUse safety hooks.",
4
- "version": "2.5.4",
4
+ "version": "2.5.5",
5
5
  "homepage": "https://github.com/linxule/kimi-plugin-cc",
6
6
  "repository": "https://github.com/linxule/kimi-plugin-cc",
7
7
  "license": "Apache-2.0",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  > **Post-1.0 release history (v1.0.1 -> present) lives in [ROADMAP-TO-GA.md § Post-GA audit log](./ROADMAP-TO-GA.md#post-ga-audit-log)** and the "Version" / "Upstream compat" lines of [AGENTS.md](./AGENTS.md). Docs-only kimi-code compat checkups that don't bump the plugin version (e.g. the 0.14.2 / 0.14.3 patches) are recorded there, not here. Notable releases are summarized below; the GA entry and full pre-GA detail follow.
4
4
 
5
+ ## 2.5.5 — 2026-11-07
6
+
7
+ - Keep every host's managed hook block in one contiguous run: setup inserts a new block directly after the last managed END when only blanks/comments follow it and the parser proves the result equals the appended form, otherwise appends. A block previously appended after Magpie's model tables lost its BEGIN marker when Magpie rewrote them (yetone/magpie#5614), leaving an `END-without-BEGIN` orphan.
8
+ - Scope orphan markers per host. Markers pair only as an adjacent same-host BEGIN→END; another host's orphan is reported by `setup --check` and no longer fails this host, while own and legacy orphans still fail closed. The orphan remedy is plain `/kimi:setup`, never a scoped `--uninstall`, which would disarm a healthy block and leave the other host's orphan.
9
+ - Parse the whole config for the marker-less (absent-state) fallback instead of deleting managed-block lines first. Deleting them could hide a `matcher` that TOML binds to this host's bare hook table from a key written before another host's `[[hooks]]` header, reporting a disabled hook as installed; this predates 2.5.5 and was found in pre-tag review. Host isolation now counts clean exact-command entries against managed blocks carrying that command.
10
+ - Name the native-v2 certified versions in the out-of-range version notice and measure "newer" against the newest exact one, instead of the legacy-v1 0.41.x bound.
11
+ - Pass the full repository gate with 1,421 tests and 18,320 assertions, 330-file package checks across all three distributions, isolated consumer security, 24 real-binary smoke tests (167 assertions, no skips) on exact 2.5.1, and independent Codex and Kimi pre-tag review. Exact CLI certification remains unchanged through 2.5.1; publication does not update installed hosts or operator pins.
12
+
5
13
  ## 2.5.4 — 2026-11-06
6
14
 
7
15
  - Restore the original platform-neutral bounded-close settlement for swarm agent-state verification after follow-up CI showed that `wasClean` can both accept an early close and reject a valid one. The published 2.5.3 artifact contains the trial condition; this patch carries its revert without re-tagging or republishing it.
@@ -26,9 +26,9 @@ import { readPluginConfig, writePluginConfig } from "../config.js";
26
26
  import { RuntimeError } from "../errors.js";
27
27
  import { normalizeTopLevelInlineHooks, validateKimiHookSetForEnvironment, withKimiConfigLock, } from "../hooks/config-safety.js";
28
28
  import { buildHookShellCommand, hostIdFromHookCommand, resolveHookScriptPath, resolveHostId, resolveNodeBinary, } from "../hooks/install-paths.js";
29
- import { decodeManagedCommandLine, effectiveHost, evaluateInstalled, findUnmanagedApprovalHookBlocks, markerCommentLines, MARKERS, parseManagedBlock, } from "../hooks/managed-block.js";
29
+ import { decodeManagedCommandLine, effectiveHost, evaluateInstalled, findUnmanagedApprovalHookBlocks, markerCommentLines, MARKERS, ORPHAN_MARKER_REMEDY, parseManagedBlock, } from "../hooks/managed-block.js";
30
30
  import { formatVersionOutOfRangeWarning, probeKimiVersion, } from "../kimi-version-probe.js";
31
- import { isNativeV2CertifiedVersion } from "../kimi-engine.js";
31
+ import { isNativeV2CertifiedVersion, NATIVE_V2_CERTIFIED_VERSIONS } from "../kimi-engine.js";
32
32
  import { resolveKimiHome } from "../kimi-home.js";
33
33
  import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
34
34
  import { KIMI_PLUGIN_CC_VERSION } from "../version.js";
@@ -180,7 +180,7 @@ async function runInstall(configPath, hookScriptPath, hostId, reviewGateEnabled,
180
180
  const block = buildManagedBlock(hookScriptPath, context.env, lineEnding, hostId);
181
181
  const next = state.kind === "found"
182
182
  ? spliceBlock(existing, state.beginLine, state.endLine, block, lineEnding)
183
- : appendBlock(existing, block, lineEnding);
183
+ : insertBlockNearManagedBlocks(existing, blocks, block, lineEnding);
184
184
  await assertKimiHookSetValid(next, configPath, "setup.install", context.env);
185
185
  const blockWritten = next !== original;
186
186
  if (blockWritten)
@@ -256,7 +256,10 @@ async function runCheck(configPath, hookScriptPath, hostId, reviewGateEnabled, w
256
256
  nodeExists: (binPath) => existsSync(binPath),
257
257
  });
258
258
  // Non-blocking coexistence + cruft notices.
259
- const { blocks: allBlocks } = parseManagedBlock(existing, hostId);
259
+ const { blocks: allBlocks, foreignOrphans } = parseManagedBlock(existing, hostId);
260
+ for (const orphan of foreignOrphans) {
261
+ warnings.push(`Host "${orphan.hostId}" has an orphaned ${orphan.detail} managed marker at line ${orphan.line + 1}; that host's commands refuse until it is repaired. ${ORPHAN_MARKER_REMEDY}`);
262
+ }
260
263
  const otherHosts = [
261
264
  ...new Set(allBlocks.map((b) => effectiveHost(b, hostId)).filter((h) => h !== hostId)),
262
265
  ];
@@ -303,7 +306,9 @@ async function runCheck(configPath, hookScriptPath, hostId, reviewGateEnabled, w
303
306
  reviewGateEnabled,
304
307
  nextStep: installedCheck.state.kind === "absent"
305
308
  ? "Run /kimi:setup (without --check) to install the managed block."
306
- : "Run /kimi:setup --uninstall, then /kimi:setup to repair.",
309
+ : installedCheck.state.kind === "orphan"
310
+ ? ORPHAN_MARKER_REMEDY
311
+ : "Run /kimi:setup --uninstall, then /kimi:setup to repair.",
307
312
  details: buildDetails({
308
313
  configPath,
309
314
  hookScriptPath,
@@ -535,6 +540,52 @@ function spliceBlock(contents, beginLine, endLine, replacement, lineEnding) {
535
540
  const replacementWithEnding = replacement.endsWith(lineEnding) ? replacement : `${replacement}${lineEnding}`;
536
541
  return `${beforeWithSep}${replacementWithEnding}${after}`;
537
542
  }
543
+ // Keep every host's block in one contiguous run. Magpie's table rewrite drops
544
+ // comments that follow a table it owns (yetone/magpie#5614): a block appended
545
+ // after such a table loses its BEGIN marker and header, leaving an orphan END.
546
+ // Insert after the last managed END only when nothing but blanks/comments
547
+ // precedes the next header AND the real parser proves the result equals the
548
+ // appended form apart from hook order, so no field can rebind and no value can
549
+ // be split; otherwise append.
550
+ function insertBlockNearManagedBlocks(contents, blocks, block, lineEnding) {
551
+ if (blocks.length === 0)
552
+ return appendBlock(contents, block, lineEnding);
553
+ const anchor = Math.max(...blocks.map((b) => b.endLine));
554
+ const lines = splitPreservingEnding(contents);
555
+ for (let i = anchor + 1; i < lines.length; i += 1) {
556
+ const trimmed = lines[i].trim();
557
+ if (trimmed.startsWith("["))
558
+ break;
559
+ if (trimmed.length > 0 && !trimmed.startsWith("#"))
560
+ return appendBlock(contents, block, lineEnding);
561
+ }
562
+ const rawLines = contents.split(/(?<=\n)/);
563
+ const head = rawLines.slice(0, anchor + 1).join("");
564
+ const tail = rawLines.slice(anchor + 1).join("");
565
+ const appended = appendBlock(contents, block, lineEnding);
566
+ if (tail.length === 0)
567
+ return appended;
568
+ const headWithEnding = head.endsWith("\n") ? head : `${head}${lineEnding}`;
569
+ const body = block.endsWith(lineEnding) ? block : `${block}${lineEnding}`;
570
+ const tailSeparator = /^\r?\n/.test(tail) ? "" : lineEnding;
571
+ const inserted = `${headWithEnding}${lineEnding}${body}${tailSeparator}${tail}`;
572
+ return sameTomlIgnoringHookOrder(inserted, appended) ? inserted : appended;
573
+ }
574
+ function sameTomlIgnoringHookOrder(a, b) {
575
+ const canonical = (text) => {
576
+ const parsed = parseToml(text);
577
+ const hooks = Array.isArray(parsed.hooks)
578
+ ? parsed.hooks.map((entry) => JSON.stringify(entry)).sort()
579
+ : parsed.hooks;
580
+ return JSON.stringify({ ...parsed, hooks });
581
+ };
582
+ try {
583
+ return canonical(a) === canonical(b);
584
+ }
585
+ catch {
586
+ return false;
587
+ }
588
+ }
538
589
  function appendBlock(contents, block, lineEnding) {
539
590
  const base = contents.length === 0 || contents.endsWith(lineEnding) ? contents : `${contents}${lineEnding}`;
540
591
  const separator = base.length === 0 ? "" : lineEnding;
@@ -928,7 +979,7 @@ async function collectKimiVersionWarnings(env, warnings) {
928
979
  // v2-certified binary would wrongly warn "not certified, commands will refuse".
929
980
  if (probe.inTestedRange || isNativeV2CertifiedVersion(probe.version))
930
981
  return;
931
- warnings.push(formatVersionOutOfRangeWarning(probe, KIMI_PLUGIN_CC_VERSION));
982
+ warnings.push(formatVersionOutOfRangeWarning(probe, KIMI_PLUGIN_CC_VERSION, NATIVE_V2_CERTIFIED_VERSIONS));
932
983
  }
933
984
  /**
934
985
  * Non-blocking notice for kimi-code's own enabled plugins from
@@ -26,6 +26,12 @@
26
26
  import { parse as parseToml } from "../vendor/smol-toml/parse.js";
27
27
  import { classifyHookCommandDrift, describeHookCommandDrift, hostIdFromHookCommand, isOurApprovalHookCommand, } from "./install-paths.js";
28
28
  const MARKER_TAG = "kimi-plugin-cc-managed";
29
+ /**
30
+ * Plain install removes unpaired marker comments for every host and leaves each
31
+ * host's hook table in place. A scoped uninstall removes only this host's
32
+ * markers, so it would disarm a healthy block and keep another host's orphan.
33
+ */
34
+ export const ORPHAN_MARKER_REMEDY = "Run /kimi:setup (without --uninstall or --check) to repair: install removes orphaned managed marker comments for every host and keeps each host's hook table. A scoped --uninstall does not remove another host's orphan marker. Another tool rewriting ~/.kimi-code/config.toml usually causes this.";
29
35
  /**
30
36
  * Strict BEGIN matcher. Accepts `# === BEGIN kimi-plugin-cc-managed`
31
37
  * with an optional `:<host-id>` suffix, an optional ` (vX.Y.Z)` suffix,
@@ -135,40 +141,41 @@ function findMarkers(lines) {
135
141
  return hits;
136
142
  }
137
143
  /**
138
- * Pair BEGIN/END markers into well-formed blocks. Return the first orphan
139
- * (BEGIN-without-END or END-without-BEGIN); verification fails closed on either.
144
+ * Pair BEGIN/END markers into well-formed blocks. A block is a BEGIN followed
145
+ * immediately (in marker order) by an END with the same host suffix; every
146
+ * other marker is an orphan. Nested or interleaved markers therefore orphan
147
+ * both sides rather than being guessed into a block. Callers decide which
148
+ * orphans fail closed (see `parseManagedBlock`).
140
149
  */
141
150
  function pairBlocks(lines) {
142
151
  const markers = findMarkers(lines);
143
152
  const blocks = [];
144
- let open = null;
145
- for (const marker of markers) {
146
- if (marker.kind === "begin") {
147
- // A second BEGIN before the previous one closed → the previous is orphaned.
148
- if (open !== null)
149
- return { orphan: "BEGIN-without-END" };
150
- open = marker;
153
+ const orphans = [];
154
+ let i = 0;
155
+ while (i < markers.length) {
156
+ const marker = markers[i];
157
+ const next = markers[i + 1];
158
+ if (marker.kind === "begin" && next !== undefined && next.kind === "end" && next.hostId === marker.hostId) {
159
+ const body = validateBlockBody(lines.slice(marker.index + 1, next.index));
160
+ blocks.push({
161
+ hostId: marker.hostId,
162
+ beginLine: marker.index,
163
+ endLine: next.index,
164
+ commandPath: body.commandPath,
165
+ valid: body.valid,
166
+ invalidReason: body.invalidReason,
167
+ });
168
+ i += 2;
151
169
  continue;
152
170
  }
153
- // end
154
- if (open === null)
155
- return { orphan: "END-without-BEGIN" };
156
- if (open.hostId !== marker.hostId)
157
- return { orphan: "BEGIN-without-END" };
158
- const body = validateBlockBody(lines.slice(open.index + 1, marker.index));
159
- blocks.push({
160
- hostId: open.hostId,
161
- beginLine: open.index,
162
- endLine: marker.index,
163
- commandPath: body.commandPath,
164
- valid: body.valid,
165
- invalidReason: body.invalidReason,
171
+ orphans.push({
172
+ line: marker.index,
173
+ detail: marker.kind === "begin" ? "BEGIN-without-END" : "END-without-BEGIN",
174
+ hostId: marker.hostId,
166
175
  });
167
- open = null;
176
+ i += 1;
168
177
  }
169
- if (open !== null)
170
- return { orphan: "BEGIN-without-END" };
171
- return { blocks };
178
+ return { blocks, orphans };
172
179
  }
173
180
  /** Validate the lines BETWEEN a BEGIN/END pair against the safety grammar. */
174
181
  function validateBlockBody(rawBody) {
@@ -274,24 +281,30 @@ function validateBlockBody(rawBody) {
274
281
  export function parseManagedBlock(contents, hostId) {
275
282
  const rawLines = contents.split("\n");
276
283
  const lines = rawLines.map((line) => line.replace(/\r$/, ""));
277
- const paired = pairBlocks(lines);
278
- if ("orphan" in paired) {
279
- return { state: { kind: "orphan", detail: paired.orphan }, blocks: [], lines };
284
+ const { blocks, orphans } = pairBlocks(lines);
285
+ // An orphan suffixed for ANOTHER host cannot be part of this host's block:
286
+ // this host's verdict still rests on its own well-formed pair, the whole-file
287
+ // hook-set validation, and the real-parser cleanliness check. Legacy and own
288
+ // orphans stay fail-closed because they may be this host's damaged block.
289
+ const ownOrphan = orphans.find((o) => o.hostId === null || o.hostId === hostId);
290
+ const foreignOrphans = orphans.filter((o) => o.hostId !== null && o.hostId !== hostId);
291
+ if (ownOrphan !== undefined) {
292
+ return { state: { kind: "orphan", detail: ownOrphan.detail }, blocks: [], lines, foreignOrphans };
280
293
  }
281
- const blocks = paired.blocks;
282
294
  // A block belongs to this host when its marker suffix matches or, for a
283
295
  // legacy un-suffixed block, its command derives to this host. An unattributable
284
296
  // legacy command is claimable by the next installer; a command attributed to
285
297
  // another host is ignored so its block remains untouched.
286
298
  const relevant = blocks.filter((b) => effectiveHost(b, hostId) === hostId);
287
299
  if (relevant.length === 0) {
288
- return { state: { kind: "absent" }, blocks, lines };
300
+ return { state: { kind: "absent" }, blocks, lines, foreignOrphans };
289
301
  }
290
302
  if (relevant.length > 1) {
291
303
  return {
292
304
  state: { kind: "duplicate", beginLines: relevant.map((b) => b.beginLine) },
293
305
  blocks,
294
306
  lines,
307
+ foreignOrphans,
295
308
  };
296
309
  }
297
310
  const block = relevant[0];
@@ -307,6 +320,7 @@ export function parseManagedBlock(contents, hostId) {
307
320
  },
308
321
  blocks,
309
322
  lines,
323
+ foreignOrphans,
310
324
  };
311
325
  }
312
326
  /**
@@ -335,27 +349,6 @@ export function decodeManagedCommandLine(trimmedLine) {
335
349
  return literal[1];
336
350
  return null;
337
351
  }
338
- /**
339
- * Return `contents` with every well-formed managed BEGIN/END block removed
340
- * (inclusive), so what remains is only the bare/root TOML. Used by
341
- * `hasCleanEnforcingHookEntry` to enforce host isolation. If the markers are
342
- * unbalanced (orphan), returns the input unchanged — the caller is only reached
343
- * in the `absent` state, where a well-formed foreign block is the norm.
344
- */
345
- function stripManagedBlockLines(contents) {
346
- const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
347
- const paired = pairBlocks(lines);
348
- if (!("blocks" in paired))
349
- return contents;
350
- const inBlock = new Set();
351
- for (const block of paired.blocks) {
352
- for (let i = block.beginLine; i <= block.endLine; i += 1)
353
- inBlock.add(i);
354
- }
355
- if (inBlock.size === 0)
356
- return contents;
357
- return lines.filter((_, i) => !inBlock.has(i)).join("\n");
358
- }
359
352
  /**
360
353
  * True if a parsed `hooks[]` array contains a clean enforcing entry for
361
354
  * `expectedCommand`: `event="PreToolUse"`, byte-exact `command`, NO `matcher`
@@ -364,10 +357,11 @@ function stripManagedBlockLines(contents) {
364
357
  * kimi-code's strict schema). Each host's hook path is distinct, so the command
365
358
  * uniquely identifies this host's entry.
366
359
  */
367
- function hooksArrayHasCleanEntry(hooks, expectedCommand) {
360
+ function countCleanEntries(hooks, expectedCommand) {
368
361
  if (!Array.isArray(hooks))
369
- return false;
362
+ return 0;
370
363
  const allowedKeys = new Set(["event", "command", "timeout"]);
364
+ let count = 0;
371
365
  for (const entry of hooks) {
372
366
  if (entry === null || typeof entry !== "object")
373
367
  continue;
@@ -380,29 +374,37 @@ function hooksArrayHasCleanEntry(hooks, expectedCommand) {
380
374
  continue;
381
375
  if (!Object.keys(rec).every((k) => allowedKeys.has(k)))
382
376
  continue;
383
- return true;
377
+ count += 1;
384
378
  }
385
- return false;
379
+ return count;
380
+ }
381
+ function hooksArrayHasCleanEntry(hooks, expectedCommand) {
382
+ return countCleanEntries(hooks, expectedCommand) > 0;
386
383
  }
387
384
  /**
388
- * Parser-based fallback used only in the absent state. Strip every managed
389
- * block first so another host's live hook cannot count for this host, then parse
390
- * the remaining bare/root TOML with the real parser rather than the prune line
391
- * scanner. `hooksArrayHasCleanEntry` requires the exact event and command, no
392
- * matcher in any spelling, and no foreign keys. Parse errors fail closed.
393
- * Callers run whole-file schema validation first, including timeout ranges.
385
+ * Parser-based fallback used only in the absent state. Parse the WHOLE file
386
+ * exactly as kimi-code does: deleting managed-block lines first would change
387
+ * which table their keys bind to (a `matcher` written before another host's
388
+ * `[[hooks]]` header belongs to the preceding table, possibly this host's bare
389
+ * one). Host isolation is kept by counting: every managed block whose body
390
+ * carries this exact command may account for one clean entry, so only a clean
391
+ * entry beyond those can be this host's marker-less table. Requires the exact
392
+ * event and command, no matcher in any spelling, and no foreign keys. Parse
393
+ * errors fail closed. Callers run whole-file schema validation first.
394
394
  */
395
395
  export function hasCleanEnforcingHookEntry(contents, expectedCommand) {
396
396
  let parsed;
397
397
  try {
398
- parsed = parseToml(stripManagedBlockLines(contents));
398
+ parsed = parseToml(contents);
399
399
  }
400
400
  catch {
401
401
  return false;
402
402
  }
403
403
  if (parsed === null || typeof parsed !== "object")
404
404
  return false;
405
- return hooksArrayHasCleanEntry(parsed.hooks, expectedCommand);
405
+ const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
406
+ const managedWithCommand = pairBlocks(lines).blocks.filter((block) => block.commandPath === expectedCommand).length;
407
+ return countCleanEntries(parsed.hooks, expectedCommand) > managedWithCommand;
406
408
  }
407
409
  /**
408
410
  * Whole-file parser check for the FOUND (marked-block present) path. The line
@@ -468,7 +470,7 @@ export function evaluateInstalled(contents, expectedCommand, opts) {
468
470
  if (state.kind === "orphan") {
469
471
  return {
470
472
  installed: false,
471
- reason: `${state.detail} marker. Run /kimi:setup --uninstall to clear.`,
473
+ reason: `${state.detail} marker. ${ORPHAN_MARKER_REMEDY}`,
472
474
  state,
473
475
  };
474
476
  }
@@ -516,12 +518,9 @@ export function findBareApprovalHookTables(contents) {
516
518
  const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
517
519
  const tableLines = scanTomlLines(contents).tables;
518
520
  const managed = new Set();
519
- const paired = pairBlocks(lines);
520
- if ("blocks" in paired) {
521
- for (const block of paired.blocks) {
522
- for (let i = block.beginLine; i <= block.endLine; i += 1)
523
- managed.add(i);
524
- }
521
+ for (const block of pairBlocks(lines).blocks) {
522
+ for (let i = block.beginLine; i <= block.endLine; i += 1)
523
+ managed.add(i);
525
524
  }
526
525
  const ranges = [];
527
526
  for (let i = 0; i < lines.length; i += 1) {
@@ -194,23 +194,41 @@ export function maxTestedMinor() {
194
194
  ? entry
195
195
  : max);
196
196
  }
197
+ function compareVersions(a, b) {
198
+ const pa = a.split(".").map((part) => Number.parseInt(part, 10));
199
+ const pb = b.split(".").map((part) => Number.parseInt(part, 10));
200
+ for (let i = 0; i < 3; i += 1) {
201
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
202
+ if (diff !== 0)
203
+ return diff;
204
+ }
205
+ return 0;
206
+ }
197
207
  /**
198
208
  * Format a user-facing warning line for an out-of-range version probe.
199
209
  * Setup itself may complete, but the execution-plan gate will refuse model
200
210
  * spawns until the operator selects a certified binary or updates the plugin.
211
+ * `nativeV2Certified` is the exact native-v2 table; both engines are named so
212
+ * the newest certified version is not misreported as the legacy-v1 bound.
201
213
  */
202
- export function formatVersionOutOfRangeWarning(probe, pluginVersion) {
214
+ export function formatVersionOutOfRangeWarning(probe, pluginVersion, nativeV2Certified = []) {
203
215
  const tested = KIMI_TESTED_MINORS.map((entry) => `${entry.major}.${entry.minor}.x`).join(", ");
204
216
  const max = maxTestedMinor();
205
- const aboveMax = probe.major > max.major || (probe.major === max.major && probe.minor > max.minor);
217
+ const newestNative = [...nativeV2Certified].sort(compareVersions).at(-1);
218
+ const probeRaw = `${probe.major}.${probe.minor}.${probe.patch}`;
219
+ const aboveMax = newestNative !== undefined
220
+ ? compareVersions(probeRaw, newestNative) > 0
221
+ : probe.major > max.major || (probe.major === max.major && probe.minor > max.minor);
222
+ const certified = nativeV2Certified.length > 0
223
+ ? `legacy-v1 ${tested}; native-v2 exact ${nativeV2Certified.join(", ")}`
224
+ : tested;
206
225
  const lines = [
207
- `WARNING: kimi-code version ${probe.version} is outside the range kimi-plugin-cc ${pluginVersion} was tested against (${tested}).`,
226
+ `WARNING: kimi-code version ${probe.version} is outside the versions kimi-plugin-cc ${pluginVersion} certifies (${certified}).`,
208
227
  ];
209
228
  if (aboveMax) {
210
- // Above the legacy-v1 tested upper bound is a release newer than the last
211
- // legacy compatibility audit, commonly from out-of-band auto-upgrade drift.
212
- lines.push(` This is NEWER than the newest version we have tested (${max.major}.${max.minor}.x) and is not yet certified for model execution.`);
229
+ // Commonly out-of-band auto-upgrade drift past the last certification.
230
+ lines.push(` This is NEWER than the newest certified version (${newestNative ?? `${max.major}.${max.minor}.x`}) and is not yet certified for model execution.`);
213
231
  }
214
- lines.push(` Setup can complete, but model-spawning commands will refuse before Kimi starts rather than run an unreviewed engine/version route.`, ` Update kimi-plugin-cc to a release that certifies this minor, or point KIMI_PLUGIN_CC_KIMI_BIN at a certified kimi-code binary and retry setup.`, ` KIMI_PLUGIN_CC_SKIP_VERSION_PROBE is a test/smoke seam, not a production compatibility override.`);
232
+ lines.push(` Setup can complete, but model-spawning commands will refuse before Kimi starts rather than run an unreviewed engine/version route.`, ` Update kimi-plugin-cc to a release that certifies this version, or point KIMI_PLUGIN_CC_KIMI_BIN at a certified kimi-code binary and retry setup.`, ` KIMI_PLUGIN_CC_SKIP_VERSION_PROBE is a test/smoke seam, not a production compatibility override.`);
215
233
  return lines.join("\n");
216
234
  }
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  // and AGENTS.md on every release. A future improvement would read this from
5
5
  // package.json at build time, but that adds a build-step dependency we
6
6
  // don't want yet.
7
- export const KIMI_PLUGIN_CC_VERSION = "2.5.4";
7
+ export const KIMI_PLUGIN_CC_VERSION = "2.5.5";
@@ -8,9 +8,11 @@ no separate CLI.
8
8
 
9
9
  ## Publishing status
10
10
 
11
- Plugin **2.5.4** restores the original platform-neutral bounded-close settlement for swarm agent-state verification after follow-up CI showed that `wasClean` can both accept an early close and reject a valid one. The immutable 2.5.3 artifact contains that trial condition; 2.5.4 carries its revert with deterministic no-close-event coverage and a protocol-correct mock handshake, without changing exact CLI certification through 2.5.1. Activation validation passed 71 tests with 244 assertions, the full repository check passed 1,412 tests with 18,282 assertions, and 330-file package and consumer-security validation passed. Release CI, OIDC publication, registry artifact identity, and the final Discover timestamp are recorded here after publication.
11
+ Plugin **2.5.5** keeps every host's managed hook block contiguous in the shared `~/.kimi-code/config.toml`, so a comment-preserving third-party writer that regroups tables cannot strip a block's BEGIN marker, and scopes orphan-marker verification per host: another host's orphan warns, own and legacy orphans still fail closed, and the remedy is plain setup. Exact CLI certification remains 2.5.0 and 2.5.1; publication does not update installed hosts or operator pins.
12
12
 
13
- Plugin **2.5.3** certifies exact Kimi Code **2.5.0 and 2.5.1** for all eight operations, adding local-execution/workspace-trust closures and authenticated WebSocket swarm activation with deterministic close-timeout coverage while preserving the no-plan construction and legacy-v1 0.41.x boundary. See the [2.5.0](./upstream-2.5.0-certification.md) and [2.5.1](./upstream-2.5.1-certification.md) certification records. The 2.5.1 source scan passed 18 tests with 153 assertions; candidate and production matrices each passed 24 tests with 167 assertions and no skips. The full check passed 1,412 tests with 18,282 assertions, package validation passed 330 files across all three distributions, isolated consumer security passed, and independent read-only `claude -p` review returned APPROVE. [Release v2.5.3](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.3) is anchored at `11d862a3fd9dae006c6888bfa221ea73101fb171`. [Final release-commit CI](https://github.com/linxule/kimi-plugin-cc/actions/runs/42237176295) and [OIDC publication](https://github.com/linxule/kimi-plugin-cc/actions/runs/42238045923) passed. The 330-file registry tarball has SHA-1 `6aa5ab4a2c862347e14354698b2ba6f7f8b1defd`, matches the validated workflow artifact byte-for-byte, and carries provenance (sigstore log index `3719667960`). A fresh registry npm installation has zero production advisories; isolated Claude Code, Codex, and DSH setup/checks pass, and all compiled runtime mirrors match. The Discover timestamp records npm publication at `2026-11-06T15:01:13.468Z`. Follow-up CI showed that `wasClean` is inconsistent and has been reverted on `main`; the published 2.5.3 artifact contains the trial condition, so carrying the revert requires a separately authorized patch release.
13
+ Plugin **2.5.4** restores the original platform-neutral bounded-close settlement for swarm agent-state verification after follow-up CI showed that `wasClean` can both accept an early close and reject a valid one. The immutable 2.5.3 artifact contains that trial condition; 2.5.4 carries its revert with deterministic no-close-event coverage and a protocol-correct mock handshake, without changing exact CLI certification through 2.5.1. Activation validation passed 71 tests with 244 assertions, the full repository check passed 1,412 tests with 18,282 assertions, and 330-file package and consumer-security validation passed. [Release v2.5.4](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.4) is anchored at `836da262ae303f51c9049b2711c9d957d7692ed2`. [Release-commit CI](https://github.com/linxule/kimi-plugin-cc/actions/runs/42249517714) and [OIDC publication](https://github.com/linxule/kimi-plugin-cc/actions/runs/42251575637) passed. The 330-file registry tarball has SHA-1 `b9e23fe6b1ceb54fdc5ee26447da050653526b7f`, matches the validated workflow artifact byte-for-byte, and carries provenance (sigstore log index `3721306580`). A fresh registry npm installation has zero production advisories; isolated Claude Code, Codex, and DSH setup/checks pass, and all compiled runtime mirrors match. The Discover timestamp records npm publication at `2026-11-06T16:29:28.637Z`.
14
+
15
+ Plugin **2.5.3** certifies exact Kimi Code **2.5.0 and 2.5.1** for all eight operations, adding local-execution/workspace-trust closures and authenticated WebSocket swarm activation with deterministic close-timeout coverage while preserving the no-plan construction and legacy-v1 0.41.x boundary. See the [2.5.0](./upstream-2.5.0-certification.md) and [2.5.1](./upstream-2.5.1-certification.md) certification records. The 2.5.1 source scan passed 18 tests with 153 assertions; candidate and production matrices each passed 24 tests with 167 assertions and no skips. The full check passed 1,412 tests with 18,282 assertions, package validation passed 330 files across all three distributions, isolated consumer security passed, and independent read-only `claude -p` review returned APPROVE. [Release v2.5.3](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.3) is anchored at `11d862a3fd9dae006c6888bfa221ea73101fb171`. [Final release-commit CI](https://github.com/linxule/kimi-plugin-cc/actions/runs/42237176295) and [OIDC publication](https://github.com/linxule/kimi-plugin-cc/actions/runs/42238045923) passed. The 330-file registry tarball has SHA-1 `6aa5ab4a2c862347e14354698b2ba6f7f8b1defd`, matches the validated workflow artifact byte-for-byte, and carries provenance (sigstore log index `3719667960`). A fresh registry npm installation has zero production advisories; isolated Claude Code, Codex, and DSH setup/checks pass, and all compiled runtime mirrors match. The Discover timestamp records npm publication at `2026-11-06T15:01:13.468Z`. Follow-up CI showed that `wasClean` is inconsistent and has been reverted on `main`; the published 2.5.3 artifact contains the trial condition. [Plugin 2.5.4](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.4) carries the correction.
14
16
 
15
17
  Plugin **2.5.2** hardens setup recovery and DSH dependency ownership. Its real Kimi Code **2.4.1** smoke passed **24 tests and 167 assertions with no skips**; the exact-source audit passed **16 tests and 97 assertions**. Normal npm installation keeps DSH host services optional. DSH use requires explicitly patched host/profile dependencies and both installation audits described below. Exact CLI certification remains unchanged. [Release v2.5.2](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.2) is anchored at `8b303bb6e49c581ad96c72bd031e8fd1643c9121`. [Release-commit CI](https://github.com/linxule/kimi-plugin-cc/actions/runs/41361753223) and [npm publication](https://github.com/linxule/kimi-plugin-cc/actions/runs/41362262009) passed. The 328-file registry tarball has SHA-1 `e620040dd35986a6929be7ad677642729a3126e7`, matches the validated workflow artifact byte-for-byte, and carries provenance (sigstore log index `3636981979`). A fresh registry npm installation has zero production advisories; isolated Claude Code, Codex, and DSH setup/checks pass, and all compiled runtime mirrors match. The Discover timestamp records npm publication at `2026-11-01T23:35:04.270Z`.
16
18
 
@@ -48,3 +48,5 @@ After the original certification, Linux release CI exposed a platform-specific e
48
48
  A subsequent CI attempt showed that native WebSocket close scheduling and `wasClean` semantics vary by platform and cannot supply a deterministic no-close-event precondition. That timeout case now uses a controlled socket which completes the protocol exchange but never emits `close`, and asserts the exact `Kimi web agent-state verification timed out.` message. The refusal is therefore tested without relying on incidental host close timing; production behavior is unchanged by this test-only follow-up.
49
49
 
50
50
  **Follow-up correction:** a later normal activation/deactivation fixture showed that `wasClean` can also reject a valid close. The production condition has been reverted on `main`, restoring the original certified bounded-close settlement; the controlled no-close-event test and protocol-correct mock handshake remain. The immutable 2.5.3 artifact contains the trial condition, so carrying this revert requires a new patch artifact rather than a re-tag or republish.
51
+
52
+ The correction ships as plugin 2.5.4 ([release](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.4)), with registry artifact and fresh-install verification recorded in [registry releases](registry-releases.md).
@@ -43,3 +43,5 @@ After the original certification and independent review, Linux release CI expose
43
43
  A subsequent CI attempt showed that native WebSocket close scheduling and `wasClean` semantics vary by platform and cannot supply a deterministic no-close-event precondition. That timeout case now uses a controlled socket which completes the protocol exchange but never emits `close`, and asserts the exact `Kimi web agent-state verification timed out.` message. The refusal is therefore tested without relying on incidental host close timing; production behavior is unchanged by this test-only follow-up.
44
44
 
45
45
  **Follow-up correction:** a later normal activation/deactivation fixture showed that `wasClean` can also reject a valid close. The production condition has been reverted on `main`, restoring the original certified bounded-close settlement; the controlled no-close-event test and protocol-correct mock handshake remain. The immutable 2.5.3 artifact contains the trial condition, so carrying this revert requires a new patch artifact rather than a re-tag or republish.
46
+
47
+ The correction ships as plugin 2.5.4 ([release](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.4)), with registry artifact and fresh-install verification recorded in [registry releases](registry-releases.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimi-plugin-cc",
3
- "version": "2.5.4",
3
+ "version": "2.5.5",
4
4
  "description": "Kimi for Claude Code, Codex, and DeepSeek Harness: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP tools through the local Kimi Code CLI.",
5
5
  "type": "module",
6
6
  "main": "dist/dsh/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimi",
3
- "version": "2.5.4",
3
+ "version": "2.5.5",
4
4
  "description": "Kimi for Codex: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP job controls through the local Kimi Code CLI.",
5
5
  "author": {
6
6
  "name": "linxule"
@@ -26,9 +26,9 @@ import { readPluginConfig, writePluginConfig } from "../config.js";
26
26
  import { RuntimeError } from "../errors.js";
27
27
  import { normalizeTopLevelInlineHooks, validateKimiHookSetForEnvironment, withKimiConfigLock, } from "../hooks/config-safety.js";
28
28
  import { buildHookShellCommand, hostIdFromHookCommand, resolveHookScriptPath, resolveHostId, resolveNodeBinary, } from "../hooks/install-paths.js";
29
- import { decodeManagedCommandLine, effectiveHost, evaluateInstalled, findUnmanagedApprovalHookBlocks, markerCommentLines, MARKERS, parseManagedBlock, } from "../hooks/managed-block.js";
29
+ import { decodeManagedCommandLine, effectiveHost, evaluateInstalled, findUnmanagedApprovalHookBlocks, markerCommentLines, MARKERS, ORPHAN_MARKER_REMEDY, parseManagedBlock, } from "../hooks/managed-block.js";
30
30
  import { formatVersionOutOfRangeWarning, probeKimiVersion, } from "../kimi-version-probe.js";
31
- import { isNativeV2CertifiedVersion } from "../kimi-engine.js";
31
+ import { isNativeV2CertifiedVersion, NATIVE_V2_CERTIFIED_VERSIONS } from "../kimi-engine.js";
32
32
  import { resolveKimiHome } from "../kimi-home.js";
33
33
  import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
34
34
  import { KIMI_PLUGIN_CC_VERSION } from "../version.js";
@@ -180,7 +180,7 @@ async function runInstall(configPath, hookScriptPath, hostId, reviewGateEnabled,
180
180
  const block = buildManagedBlock(hookScriptPath, context.env, lineEnding, hostId);
181
181
  const next = state.kind === "found"
182
182
  ? spliceBlock(existing, state.beginLine, state.endLine, block, lineEnding)
183
- : appendBlock(existing, block, lineEnding);
183
+ : insertBlockNearManagedBlocks(existing, blocks, block, lineEnding);
184
184
  await assertKimiHookSetValid(next, configPath, "setup.install", context.env);
185
185
  const blockWritten = next !== original;
186
186
  if (blockWritten)
@@ -256,7 +256,10 @@ async function runCheck(configPath, hookScriptPath, hostId, reviewGateEnabled, w
256
256
  nodeExists: (binPath) => existsSync(binPath),
257
257
  });
258
258
  // Non-blocking coexistence + cruft notices.
259
- const { blocks: allBlocks } = parseManagedBlock(existing, hostId);
259
+ const { blocks: allBlocks, foreignOrphans } = parseManagedBlock(existing, hostId);
260
+ for (const orphan of foreignOrphans) {
261
+ warnings.push(`Host "${orphan.hostId}" has an orphaned ${orphan.detail} managed marker at line ${orphan.line + 1}; that host's commands refuse until it is repaired. ${ORPHAN_MARKER_REMEDY}`);
262
+ }
260
263
  const otherHosts = [
261
264
  ...new Set(allBlocks.map((b) => effectiveHost(b, hostId)).filter((h) => h !== hostId)),
262
265
  ];
@@ -303,7 +306,9 @@ async function runCheck(configPath, hookScriptPath, hostId, reviewGateEnabled, w
303
306
  reviewGateEnabled,
304
307
  nextStep: installedCheck.state.kind === "absent"
305
308
  ? "Run /kimi:setup (without --check) to install the managed block."
306
- : "Run /kimi:setup --uninstall, then /kimi:setup to repair.",
309
+ : installedCheck.state.kind === "orphan"
310
+ ? ORPHAN_MARKER_REMEDY
311
+ : "Run /kimi:setup --uninstall, then /kimi:setup to repair.",
307
312
  details: buildDetails({
308
313
  configPath,
309
314
  hookScriptPath,
@@ -535,6 +540,52 @@ function spliceBlock(contents, beginLine, endLine, replacement, lineEnding) {
535
540
  const replacementWithEnding = replacement.endsWith(lineEnding) ? replacement : `${replacement}${lineEnding}`;
536
541
  return `${beforeWithSep}${replacementWithEnding}${after}`;
537
542
  }
543
+ // Keep every host's block in one contiguous run. Magpie's table rewrite drops
544
+ // comments that follow a table it owns (yetone/magpie#5614): a block appended
545
+ // after such a table loses its BEGIN marker and header, leaving an orphan END.
546
+ // Insert after the last managed END only when nothing but blanks/comments
547
+ // precedes the next header AND the real parser proves the result equals the
548
+ // appended form apart from hook order, so no field can rebind and no value can
549
+ // be split; otherwise append.
550
+ function insertBlockNearManagedBlocks(contents, blocks, block, lineEnding) {
551
+ if (blocks.length === 0)
552
+ return appendBlock(contents, block, lineEnding);
553
+ const anchor = Math.max(...blocks.map((b) => b.endLine));
554
+ const lines = splitPreservingEnding(contents);
555
+ for (let i = anchor + 1; i < lines.length; i += 1) {
556
+ const trimmed = lines[i].trim();
557
+ if (trimmed.startsWith("["))
558
+ break;
559
+ if (trimmed.length > 0 && !trimmed.startsWith("#"))
560
+ return appendBlock(contents, block, lineEnding);
561
+ }
562
+ const rawLines = contents.split(/(?<=\n)/);
563
+ const head = rawLines.slice(0, anchor + 1).join("");
564
+ const tail = rawLines.slice(anchor + 1).join("");
565
+ const appended = appendBlock(contents, block, lineEnding);
566
+ if (tail.length === 0)
567
+ return appended;
568
+ const headWithEnding = head.endsWith("\n") ? head : `${head}${lineEnding}`;
569
+ const body = block.endsWith(lineEnding) ? block : `${block}${lineEnding}`;
570
+ const tailSeparator = /^\r?\n/.test(tail) ? "" : lineEnding;
571
+ const inserted = `${headWithEnding}${lineEnding}${body}${tailSeparator}${tail}`;
572
+ return sameTomlIgnoringHookOrder(inserted, appended) ? inserted : appended;
573
+ }
574
+ function sameTomlIgnoringHookOrder(a, b) {
575
+ const canonical = (text) => {
576
+ const parsed = parseToml(text);
577
+ const hooks = Array.isArray(parsed.hooks)
578
+ ? parsed.hooks.map((entry) => JSON.stringify(entry)).sort()
579
+ : parsed.hooks;
580
+ return JSON.stringify({ ...parsed, hooks });
581
+ };
582
+ try {
583
+ return canonical(a) === canonical(b);
584
+ }
585
+ catch {
586
+ return false;
587
+ }
588
+ }
538
589
  function appendBlock(contents, block, lineEnding) {
539
590
  const base = contents.length === 0 || contents.endsWith(lineEnding) ? contents : `${contents}${lineEnding}`;
540
591
  const separator = base.length === 0 ? "" : lineEnding;
@@ -928,7 +979,7 @@ async function collectKimiVersionWarnings(env, warnings) {
928
979
  // v2-certified binary would wrongly warn "not certified, commands will refuse".
929
980
  if (probe.inTestedRange || isNativeV2CertifiedVersion(probe.version))
930
981
  return;
931
- warnings.push(formatVersionOutOfRangeWarning(probe, KIMI_PLUGIN_CC_VERSION));
982
+ warnings.push(formatVersionOutOfRangeWarning(probe, KIMI_PLUGIN_CC_VERSION, NATIVE_V2_CERTIFIED_VERSIONS));
932
983
  }
933
984
  /**
934
985
  * Non-blocking notice for kimi-code's own enabled plugins from