@staix/agent-hub 0.34.0 → 0.35.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 (161) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +35 -6
  3. package/docs/adding-an-ai-tool.md +2 -2
  4. package/docs/addons/claude-mem.md +4 -4
  5. package/docs/addons/open-code-review.md +1 -1
  6. package/docs/agent-notes/README.md +6 -0
  7. package/docs/agent-notes/adapters.md +9 -5
  8. package/docs/agent-notes/budget.md +1 -1
  9. package/docs/agent-notes/bus.md +1 -1
  10. package/docs/agent-notes/daemon.md +4 -3
  11. package/docs/agent-notes/local-worker.md +1 -1
  12. package/docs/agent-notes/models.md +2 -0
  13. package/docs/agent-notes/tasks.md +1 -1
  14. package/docs/agent-notes/tests.md +5 -0
  15. package/docs/design/adapters.md +110 -2
  16. package/docs/design/agent-hub-architecture.md +63 -1
  17. package/docs/design/agent-hub.md +6 -0
  18. package/docs/design/bus.md +18 -0
  19. package/docs/design/execution-reference.md +146 -0
  20. package/docs/design/task-board.md +10 -0
  21. package/docs/dogfood.md +33 -12
  22. package/docs/events.md +38 -20
  23. package/docs/memory.md +21 -7
  24. package/docs/operations.md +539 -24
  25. package/docs/quickstart.md +18 -4
  26. package/docs/research.md +71 -13
  27. package/docs/roles.md +1 -1
  28. package/docs/sdlc.md +9 -7
  29. package/docs/security.md +87 -11
  30. package/docs/smoke.md +14 -0
  31. package/docs/specs/2026-09-19-agent-hub-design.md +8 -3
  32. package/docs/verification/2026-11-07-0.34.1.md +54 -0
  33. package/docs/verification/2026-11-07-compat-python-startup-1248.md +51 -0
  34. package/docs/verification/2026-11-07-compat-receipt-harness-1248.md +105 -0
  35. package/docs/verification/2026-11-07-compat-receipt-producers-1248.md +96 -0
  36. package/docs/verification/2026-11-07-compat-workflow-1248.md +94 -0
  37. package/docs/verification/2026-11-07-daemon-startup-1232.md +169 -0
  38. package/docs/verification/2026-11-07-jcode-receipt-reconstruction-1248.md +118 -0
  39. package/docs/verification/2026-11-07-kimi-delayed-usage.md +34 -0
  40. package/docs/verification/2026-11-07-kimi-review-watch-1284.md +115 -0
  41. package/docs/verification/2026-11-08-agy-1.5.3.md +96 -0
  42. package/docs/verification/2026-11-08-agy-acp-t0.md +56 -0
  43. package/docs/verification/2026-11-08-hub-visual-1058.md +161 -0
  44. package/docs/verification/2026-11-08-jcode-0991-model-setter-1317.md +143 -0
  45. package/docs/verification/2026-11-08-kimi-252-model-setter-1317.md +137 -0
  46. package/docs/verification/2026-11-08-kimi-tui-completion-1282.md +45 -0
  47. package/docs/verification/2026-11-08-kimi-wire-yield-1283.md +31 -0
  48. package/docs/verification/2026-11-08-live-model-override-1317.md +250 -0
  49. package/docs/verification/2026-11-08-qwen-0269-acp.md +138 -0
  50. package/docs/verification/2026-11-08-qwen-0269-model-setter-1317.md +91 -0
  51. package/docs/verification/2026-11-09-0.35.0.md +129 -0
  52. package/docs/verification/2026-11-09-headless-records-1320.md +157 -0
  53. package/docs/verification/claude-permission-transport-native.py +10 -3
  54. package/docs/verification/claude-shell-wrapper-native.py +8 -8
  55. package/docs/verified.json +245 -15
  56. package/package.json +3 -3
  57. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  58. package/plugins/agent-hub/kimi-hook.js +20397 -0
  59. package/plugins/agent-hub/kimi-review-policy-worker.js +217 -0
  60. package/plugins/agent-hub/server.js +2701 -664
  61. package/scripts/check-docs.mjs +12 -1
  62. package/src/adapters/acp-model-policy.ts +56 -0
  63. package/src/adapters/acp.ts +175 -5
  64. package/src/adapters/agy/entry.ts +8 -5
  65. package/src/adapters/agy/hooks.ts +65 -15
  66. package/src/adapters/channel-peer.ts +33 -1
  67. package/src/adapters/claude-implementer.ts +16 -3
  68. package/src/adapters/codex-appserver.ts +118 -8
  69. package/src/adapters/codex-review-isolation.ts +3 -1
  70. package/src/adapters/compat.ts +11 -6
  71. package/src/adapters/definitions/builtin-metadata.ts +8 -1
  72. package/src/adapters/definitions/native-cli-entry-kimi.ts +76 -7
  73. package/src/adapters/definitions/native-cli-preview-common.ts +2 -2
  74. package/src/adapters/definitions/native-cli-preview-kimi.ts +9 -2
  75. package/src/adapters/definitions/reviewer-variants.ts +3 -1
  76. package/src/adapters/kimi-review-policy-worker.js +2 -0
  77. package/src/adapters/kimi-review-policy-worker.ts +85 -0
  78. package/src/adapters/kimi-review-policy.ts +68 -0
  79. package/src/adapters/kimi-review.ts +41 -41
  80. package/src/adapters/kimi-tui-facts.ts +153 -0
  81. package/src/adapters/kimi-wire-usage.ts +33 -8
  82. package/src/adapters/local-worker.ts +59 -20
  83. package/src/adapters/model-token.ts +3 -0
  84. package/src/adapters/pi.ts +27 -5
  85. package/src/adapters/registry-types.ts +34 -3
  86. package/src/adapters/registry.ts +1 -0
  87. package/src/adapters/reviewer-policy-watch.ts +86 -0
  88. package/src/adapters/reviewer-print.ts +80 -18
  89. package/src/cli/agents.ts +1 -1
  90. package/src/cli/facts-hook.ts +4 -2
  91. package/src/cli/help.ts +3 -2
  92. package/src/cli/identity.ts +3 -0
  93. package/src/cli/kimi-facts-hook.ts +11 -0
  94. package/src/cli/kimi-plugin.ts +157 -0
  95. package/src/cli/main.ts +48 -9
  96. package/src/cli/model.ts +52 -0
  97. package/src/cli/output.ts +23 -1
  98. package/src/cli/recovery-package.ts +15 -7
  99. package/src/cli/reset.ts +1 -1
  100. package/src/cli/status-lines.ts +8 -4
  101. package/src/hub/budget.ts +1 -1
  102. package/src/hub/bus.ts +40 -2
  103. package/src/hub/code-facts.ts +10 -3
  104. package/src/hub/completions.ts +3 -1
  105. package/src/hub/config-apply.ts +2 -1
  106. package/src/hub/control-client.ts +1 -1
  107. package/src/hub/daemon.ts +1050 -106
  108. package/src/hub/deliverable-files.ts +38 -5
  109. package/src/hub/dogfood-profile.ts +1 -1
  110. package/src/hub/envelope.ts +1 -1
  111. package/src/hub/events.ts +28 -18
  112. package/src/hub/execution-lineage.ts +120 -0
  113. package/src/hub/execution-reference.ts +36 -0
  114. package/src/hub/hub-tools.ts +21 -2
  115. package/src/hub/instances.ts +4 -3
  116. package/src/hub/kimi-events.ts +46 -0
  117. package/src/hub/kimi-usage.ts +132 -29
  118. package/src/hub/lifecycle.ts +31 -15
  119. package/src/hub/limits.ts +13 -4
  120. package/src/hub/manager.ts +15 -2
  121. package/src/hub/memory-policy.ts +2 -1
  122. package/src/hub/memory-sync-attestation.ts +54 -0
  123. package/src/hub/memory-sync-state.ts +20 -5
  124. package/src/hub/model-overrides.ts +115 -0
  125. package/src/hub/native-hook-launch.ts +12 -3
  126. package/src/hub/operational-view.ts +2 -2
  127. package/src/hub/peers.ts +27 -0
  128. package/src/hub/permission-mode.ts +1 -1
  129. package/src/hub/person-figures.ts +88 -0
  130. package/src/hub/report.ts +38 -4
  131. package/src/hub/research-dataset.ts +158 -0
  132. package/src/hub/research-memory.ts +68 -0
  133. package/src/hub/research-quality.ts +118 -0
  134. package/src/hub/research-record.ts +279 -0
  135. package/src/hub/research.ts +126 -27
  136. package/src/hub/restart.ts +14 -0
  137. package/src/hub/settings.ts +3 -0
  138. package/src/hub/startup-event-seeds.ts +51 -0
  139. package/src/hub/startup-readiness.ts +65 -0
  140. package/src/hub/startup-timing.ts +38 -0
  141. package/src/hub/tasks.ts +29 -9
  142. package/src/hub/ui.ts +9 -5
  143. package/src/hub/visual-assets.ts +50 -0
  144. package/src/hub/visual.ts +257 -0
  145. package/src/memory/brief.ts +169 -14
  146. package/src/memory/capture.ts +2 -1
  147. package/src/memory/recall.ts +46 -5
  148. package/src/memory/record-provenance.ts +82 -0
  149. package/src/memory/records.ts +116 -0
  150. package/src/models/route/adec.ts +190 -0
  151. package/src/models/route/config.ts +37 -3
  152. package/src/models/route/decision-evidence.ts +83 -0
  153. package/src/models/route/decision.ts +143 -0
  154. package/src/models/route/runtime.ts +13 -6
  155. package/src/ui/index.html +134 -6
  156. package/src/ui/plugin.ts +65 -4
  157. package/templates/AGENTS.block.md +1 -1
  158. package/templates/config.json +1 -1
  159. package/templates/profiles/general.json +1 -1
  160. package/templates/profiles/research.json +1 -1
  161. package/templates/profiles/writing.json +1 -1
@@ -1,8 +1,41 @@
1
1
  import { retainedLaunchWarning } from "./native-cli-retained.ts";
2
- import { resolve } from "node:path";
3
- import { spawnSync } from "node:child_process";
2
+ import { join, resolve } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { spawn, spawnSync } from "node:child_process";
5
+ import { mkdirSync } from "node:fs";
6
+ import { assessVersion, COMPAT } from "../compat.ts";
4
7
  import type { NativeCliEntryContext } from "./native-cli-entry-types.ts";
5
8
  import { bundleInStatus, rosterInStatus } from "../../hub/status-roster.ts";
9
+ import { cleanupKimiPlugin, ensureKimiPlugin, type KimiPluginOptions } from "../../cli/kimi-plugin.ts";
10
+
11
+ export function kimiTuiVersionGate(stdout: string): { ok: true; message?: string } | { ok: false; message: string } {
12
+ const verdict = assessVersion("kimi-tui", COMPAT["kimi-tui"].parse(stdout));
13
+ if (!verdict.ok) return { ok: false, message: verdict.reason };
14
+ return verdict.note ? { ok: true, message: verdict.note } : { ok: true };
15
+ }
16
+
17
+ export function kimiTuiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv) {
18
+ return { cwd, env, stdio: "inherit" as const };
19
+ }
20
+
21
+ export function kimiTuiVersionProbe(launch: { cmd: string; args: string[] }): { cmd: string; args: string[] } {
22
+ return { cmd: launch.cmd, args: [...launch.args, "--version"] };
23
+ }
24
+
25
+ function runKimiTui(cmd: string, args: string[], cwd: string, env: NodeJS.ProcessEnv): Promise<{ error?: Error; status: number | null }> {
26
+ const child = spawn(cmd, args, kimiTuiSpawnOptions(cwd, env));
27
+ return new Promise(resolveExit => {
28
+ const forward = (signal: NodeJS.Signals) => {
29
+ if (child.exitCode !== null || child.signalCode !== null) return;
30
+ try { child.kill(signal); } catch { /* the native child may exit as the signal arrives */ }
31
+ };
32
+ const onInterrupt = () => forward("SIGINT"), onTerminate = () => forward("SIGTERM"), onHangup = () => forward("SIGHUP");
33
+ const cleanup = () => { process.off("SIGINT", onInterrupt); process.off("SIGTERM", onTerminate); process.off("SIGHUP", onHangup); };
34
+ process.on("SIGINT", onInterrupt); process.on("SIGTERM", onTerminate); process.on("SIGHUP", onHangup);
35
+ child.once("error", error => { cleanup(); resolveExit({ error, status: null }); });
36
+ child.once("close", code => { cleanup(); resolveExit({ status: code }); });
37
+ });
38
+ }
6
39
 
7
40
  export async function runKimiEntry(ctx: NativeCliEntryContext): Promise<void> {
8
41
  const peer = ctx.instance ?? "kimi";
@@ -42,20 +75,56 @@ export async function runKimiEntry(ctx: NativeCliEntryContext): Promise<void> {
42
75
  if (!admission.ok) fail(admission.error);
43
76
  const runningCommand = admission.retainedLaunchConfig?.kimi_cmd;
44
77
  let launch: ReturnType<typeof buildKimiTuiLaunch>;
45
- let result: ReturnType<typeof spawnSync>;
78
+ let result: { error?: Error; status: number | null };
46
79
  try {
47
80
  // Inside the try: a kimi_cmd with no TUI form throws here, and the admission must still be released (#549).
48
81
  // The running hub's value wins over the file's while a reload is retained (#654): when it has no TUI form, say whose it is, so the
49
82
  // error is not read as a fault in the file. The words themselves are never echoed.
50
83
  const running = Array.isArray(runningCommand) && runningCommand.every((arg: unknown) => typeof arg === "string");
51
- try { launch = buildKimiTuiLaunch(running ? runningCommand : projectConfig().kimi_cmd, model); }
84
+ const command = running ? runningCommand : projectConfig().kimi_cmd;
85
+ try { launch = buildKimiTuiLaunch(command, model); }
52
86
  catch (error) {
53
87
  throw running ? new Error(`${(error as Error).message} (the command is the running hub's kimi_cmd: the value in the config file is retained until ahub settings reload)`) : error;
54
88
  }
55
- // The daemon's own bundle, so the MCP server matches the hub it talks to (#502); this CLI's copy only for an older hub.
56
- const written = configureKimiMcp(cwd, stateDir, bundleInStatus(snapshot) ?? resolve(cliDir, "../../plugins/agent-hub/server.js"), "tui");
89
+ const probe = kimiTuiVersionProbe(buildKimiTuiLaunch(command));
90
+ const version = spawnSync(probe.cmd, probe.args, { encoding: "utf8", timeout: 5000, maxBuffer: 4096, env: nativeLaunchEnv("kimi", childEnv()) });
91
+ const gate = kimiTuiVersionGate(version.status === 0 ? version.stdout : "");
92
+ if (version.status === 0 && version.stdout.trim() && gate.message) console.error(`${gate.message}. Completion receipts are off; the TUI launches as before.`);
93
+ // The daemon's own bundles, so the MCP server and native hook match the hub they talk to (#502, #1282).
94
+ const serverBundle = bundleInStatus(snapshot) ?? resolve(cliDir, "../../plugins/agent-hub/server.js");
95
+ const written = configureKimiMcp(cwd, stateDir, serverBundle, "tui");
57
96
  if (written.tracked) console.error(".kimi-code/mcp.json is tracked; the agent-hub entry contains machine paths. Review this local change before committing it.");
58
- result = spawnSync(launch.cmd, launch.args, { cwd, stdio: "inherit", env: { ...nativeLaunchEnv("kimi", childEnv()), AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } });
97
+ const nativeEnv: NodeJS.ProcessEnv = { ...nativeLaunchEnv("kimi", childEnv()), AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd };
98
+ if (!gate.ok) {
99
+ result = await runKimiTui(launch.cmd, launch.args, cwd, nativeEnv);
100
+ } else {
101
+ const nativeHome = nativeEnv.KIMI_CODE_HOME ?? join(nativeEnv.HOME ?? homedir(), ".kimi-code");
102
+ let nativeLaunchId: string | undefined, completionEnv: NodeJS.ProcessEnv | undefined, plugin: KimiPluginOptions | undefined;
103
+ try {
104
+ mkdirSync(nativeHome, { recursive: true, mode: 0o700 });
105
+ const native = await hub.request({ t: "native_hook_launch", op: "begin", peer, nativeHome });
106
+ if (!native.ok || typeof native.launchId !== "string" || typeof native.instanceId !== "string") throw new Error(native.error ?? "native hook launch was not admitted");
107
+ nativeLaunchId = native.launchId;
108
+ plugin = { home: nativeHome, hookScript: resolve(serverBundle, "../kimi-hook.js"), launchId: native.launchId, replaceStale: true };
109
+ console.error(`Kimi TUI completion facts: installing a launch-scoped native plugin under ${join(nativeHome, "plugins", "managed", "agent-hub")}; it will be removed after this TUI exits`);
110
+ ensureKimiPlugin(plugin);
111
+ completionEnv = { ...nativeEnv, AGENTHUB_INSTANCE_ID: native.instanceId, AGENTHUB_LAUNCH_ID: native.launchId, AGENTHUB_HOOK_PURPOSE: "facts" };
112
+ } catch (error) {
113
+ console.error(`Kimi TUI completion facts are unavailable: ${(error as Error).message}. Continuing without completion receipts; resolve the cause and restart to enable them.`);
114
+ if (nativeLaunchId) await hub.request({ t: "native_hook_launch", op: "end", peer, launchId: nativeLaunchId }).catch(() => {});
115
+ if (plugin) try { cleanupKimiPlugin(plugin); } catch { console.error(`Managed Kimi plugin cleanup refused: files or ownership changed; inspect ${join(nativeHome, "plugins", "managed", "agent-hub")} before removing it`); }
116
+ }
117
+ if (completionEnv) {
118
+ try {
119
+ result = await runKimiTui(launch.cmd, launch.args, cwd, completionEnv);
120
+ } finally {
121
+ if (nativeLaunchId) await hub.request({ t: "native_hook_launch", op: "end", peer, launchId: nativeLaunchId }).catch(() => {});
122
+ if (plugin) try { cleanupKimiPlugin(plugin); } catch { console.error(`Managed Kimi plugin cleanup refused: files or ownership changed; inspect ${join(nativeHome, "plugins", "managed", "agent-hub")} before removing it`); }
123
+ }
124
+ } else {
125
+ result = await runKimiTui(launch.cmd, launch.args, cwd, nativeEnv);
126
+ }
127
+ }
59
128
  } finally {
60
129
  if (admission.admissionId) await hub.request({ t: "kimi_launch_end", admissionId: admission.admissionId }).catch(() => {});
61
130
  }
@@ -26,10 +26,10 @@ export interface NativePreviewContext {
26
26
  /** The declared extra instance a `--as <id>` launch selected; its own model fields belong in the preview (#685). */
27
27
  instance?: string;
28
28
  }
29
- export interface NativePreviewMetadata { mode: "tui" | "headless"; mcp: { file: string; action: string; changed: boolean; tracked: boolean } }
29
+ export interface NativePreviewMetadata { mode: "tui" | "headless"; mcp: { file: string; action: string; changed: boolean; tracked: boolean }; completionPlugin?: { file: string; action: string; changed: boolean } }
30
30
  export type NativePreviewBuilder = (ctx: NativePreviewContext) => NativePreview;
31
31
  export interface NativePreview {
32
- tool: string; mode?: "tui" | "headless"; mcp?: NativePreviewMetadata["mcp"];
32
+ tool: string; mode?: "tui" | "headless"; mcp?: NativePreviewMetadata["mcp"]; completionPlugin?: NativePreviewMetadata["completionPlugin"];
33
33
  cmd: string; args: string[]; settings: string | undefined; envNames: string[]; warning?: string;
34
34
  unresolved: { field: string; reason: string }[]; redaction: string; needs: string[];
35
35
  }
@@ -1,4 +1,6 @@
1
1
  import { join } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { kimiPluginPreview } from "../../cli/kimi-plugin.ts";
2
4
  import type { Launch } from "../../cli/launch.ts";
3
5
  import { REDACTED, nativeCommand, previewState, finishPreview, type NativePreviewContext, type NativePreviewMetadata } from "./native-cli-preview-common.ts";
4
6
 
@@ -9,6 +11,7 @@ export function previewKimi(ctx: NativePreviewContext) {
9
11
  let envNames = state.envNames;
10
12
  let launch: Launch;
11
13
  let kimiMetadata: NativePreviewMetadata | undefined;
14
+ let completionPlugin: NativePreviewMetadata["completionPlugin"];
12
15
  const model = value("--model");
13
16
  const mode = value("--mode") ?? startModeOf(config.peers, "kimi");
14
17
  if (mode !== "tui" && mode !== "headless") throw new Error("invalid Kimi mode");
@@ -19,10 +22,14 @@ export function previewKimi(ctx: NativePreviewContext) {
19
22
  } else {
20
23
  const native = buildKimiTuiLaunch(config.kimi_cmd, model ? REDACTED : undefined);
21
24
  launch = { ...native, cmd: native.cmd === "kimi" ? "kimi" : REDACTED, args: native.args.map(arg => arg === "--model" || arg === REDACTED ? arg : REDACTED) };
22
- envNames = Object.keys(nativeLaunchEnv("kimi", childEnv())).concat(["AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR"]);
25
+ const nativeEnv = nativeLaunchEnv("kimi", childEnv());
26
+ envNames = Object.keys(nativeEnv).concat(["AGENTHUB_STATE_DIR", "AGENTHUB_PROJECT_DIR", "AGENTHUB_INSTANCE_ID", "AGENTHUB_LAUNCH_ID", "AGENTHUB_HOOK_PURPOSE"]);
27
+ unresolved.push({ field: "nativeLaunchId", reason: "native completion facts bind to the launch admitted by the running hub" });
28
+ const home = nativeEnv.KIMI_CODE_HOME ?? join(nativeEnv.HOME ?? homedir(), ".kimi-code");
29
+ completionPlugin = kimiPluginPreview(home, join(cliDir, "../../plugins/agent-hub/kimi-hook.js"));
23
30
  }
24
31
  const { file, action, changed, tracked } = kimiMcpPlan(cwd, stateDir, join(cliDir, "../../plugins/agent-hub/server.js"), mode);
25
- kimiMetadata = { mode, mcp: { file, action, changed, tracked } };
32
+ kimiMetadata = { mode, mcp: { file, action, changed, tracked }, ...(completionPlugin ? { completionPlugin } : {}) };
26
33
 
27
34
  return finishPreview(ctx, launch, envNames, unresolved, kimiMetadata);
28
35
  }
@@ -16,11 +16,13 @@ function reviewerVariant(kind: string, displayName: string, reviewer: NonNullabl
16
16
  permission: { unmanaged: "fresh read-only reviewer; an isolation receipt is required before startup" },
17
17
  session: { idField: "sessionId", currentId: () => undefined, freshOnRestore: true, launchKind: "reviewer-print", crashPlan: () => ({ resume: {}, how: "starts a fresh isolated review session" }) },
18
18
  turnEnd: "adapter", budget: { account: kind }, routing: { reviewOnly: true },
19
+ // A fresh reviewer never inherits a long-lived ACP setter through a base/variant merge.
20
+ modelOverride: undefined,
19
21
  reviewer, cli: { command: kind, startWords: () => [] }, display,
20
22
  };
21
23
  }
22
24
  const codexVariant = reviewerVariant("codex", "Codex reviewer", { backend: codexReviewerBackend, defaultPool: true, declaration: "explicit", scope: "code" }, CODEX_DISPLAY);
23
- export const CODEX_REVIEWER_VARIANT: AdapterDefinition = { ...codexVariant, start: { ...codexVariant.start, create: createCodexReviewer } };
25
+ export const CODEX_REVIEWER_VARIANT: AdapterDefinition = { ...codexVariant, modelOverride: { boundary: "turn" }, start: { ...codexVariant.start, create: createCodexReviewer } };
24
26
  export const KIMI_REVIEWER_VARIANT = reviewerVariant("kimi", "Kimi reviewer", { backend: kimiReviewerBackend, defaultPool: true, declaration: "explicit", scope: "code" }, KIMI_DISPLAY);
25
27
  /** Data-only declared placeholder for route explanations; no factory, backend or native argv. */
26
28
  export const AGY_REVIEWER_VARIANT: AdapterDefinition = {
@@ -0,0 +1,2 @@
1
+ // Source entry; the plugin carries a self-contained bundle at this same basename.
2
+ import "./kimi-review-policy-worker.ts";
@@ -0,0 +1,85 @@
1
+ import { isMainThread, parentPort, workerData, Worker } from "node:worker_threads";
2
+ import { checkKimiReviewPolicy, projectKimiReviewPolicyBinding, type KimiReviewPolicyBinding } from "./kimi-review-policy.ts";
3
+ import type { ReviewerPolicyProbe } from "./reviewer-policy-watch.ts";
4
+
5
+ /** Trusted host fixture only: two numeric cells, entered count then release flag. */
6
+ export interface KimiReviewPolicyWorkerOptions { gate?: SharedArrayBuffer }
7
+ export type KimiReviewPolicyFactory = (binding: KimiReviewPolicyBinding) => ReviewerPolicyProbe;
8
+
9
+ export function startKimiReviewPolicyWatch(binding: KimiReviewPolicyBinding, options: KimiReviewPolicyWorkerOptions = {}): ReviewerPolicyProbe {
10
+ const projected = projectKimiReviewPolicyBinding(binding);
11
+ if (options.gate && options.gate.byteLength !== 8) throw new Error("review policy fixture gate is invalid");
12
+ let worker: Worker;
13
+ try {
14
+ worker = new Worker(new URL("./kimi-review-policy-worker.js", import.meta.url), {
15
+ env: {}, execArgv: [], stdout: true, stderr: true,
16
+ workerData: { binding: projected, ...(options.gate ? { gate: options.gate } : {}) },
17
+ });
18
+ } catch { throw new Error("review policy worker could not start; retry the review"); }
19
+ let failed = false, stopping = false, exited = false, sequence = 0;
20
+ let pending: { id: number; resolve: (current: boolean) => void } | undefined;
21
+ let stopWork: Promise<void> | undefined;
22
+ let resolveExit!: () => void, resolveFailure!: () => void;
23
+ const exit = new Promise<void>(resolve => { resolveExit = resolve; });
24
+ const failure = new Promise<void>(resolve => { resolveFailure = resolve; });
25
+ const settle = (current: boolean) => { const request = pending; pending = undefined; request?.resolve(current); };
26
+ const fail = () => { failed = true; resolveFailure(); settle(false); };
27
+ worker.on("error", fail);
28
+ worker.on("exit", () => { exited = true; fail(); resolveExit(); });
29
+ try {
30
+ // Never inherit/forward worker output. Unexpected output invalidates the proof.
31
+ worker.stdout?.on("data", fail); worker.stderr?.on("data", fail);
32
+ worker.on("message", (message: unknown) => {
33
+ if (stopping || failed) return;
34
+ if (!message || typeof message !== "object" || Array.isArray(message)) return fail();
35
+ const reply = message as Record<string, unknown>;
36
+ if (Object.keys(reply).sort().join() !== "current,id" || !pending || reply.id !== pending.id || typeof reply.current !== "boolean") return fail();
37
+ if (!reply.current) return fail();
38
+ settle(true);
39
+ });
40
+ } catch { fail(); } // Return ownership even if runtime stream setup failed.
41
+ return {
42
+ failure,
43
+ check() {
44
+ if (failed || stopping || exited) return Promise.resolve(false);
45
+ if (pending || sequence >= Number.MAX_SAFE_INTEGER) { fail(); return Promise.resolve(false); }
46
+ return new Promise<boolean>(resolve => {
47
+ pending = { id: ++sequence, resolve };
48
+ try { worker.postMessage({ id: sequence }); } catch { fail(); }
49
+ });
50
+ },
51
+ stop() {
52
+ if (stopWork) return stopWork;
53
+ stopping = true; fail();
54
+ stopWork = (async () => {
55
+ if (!exited) await worker.terminate();
56
+ await exit;
57
+ })().catch(() => {
58
+ stopWork = undefined; // The same owner may explicitly retry an unconfirmed join.
59
+ throw new Error("review policy worker stop is unconfirmed; retry stopping the review");
60
+ });
61
+ return stopWork;
62
+ },
63
+ };
64
+ }
65
+
66
+ if (!isMainThread) {
67
+ const port = parentPort;
68
+ if (!port || Object.keys(process.env).length !== 0 || process.execArgv.length !== 0) throw new Error("review policy worker environment is invalid");
69
+ const data = workerData as { binding: KimiReviewPolicyBinding; gate?: SharedArrayBuffer };
70
+ const binding = projectKimiReviewPolicyBinding(data.binding);
71
+ const gate = data.gate ? new Int32Array(data.gate) : undefined;
72
+ if (gate && gate.length !== 2) throw new Error("review policy fixture gate is invalid");
73
+ let last = 0;
74
+ port.on("message", (message: unknown) => {
75
+ if (!message || typeof message !== "object" || Array.isArray(message)) throw new Error("review policy request is invalid");
76
+ const request = message as Record<string, unknown>;
77
+ if (Object.keys(request).join() !== "id" || request.id !== last + 1) throw new Error("review policy request is invalid");
78
+ last++;
79
+ if (gate) {
80
+ Atomics.add(gate, 0, 1); Atomics.notify(gate, 0);
81
+ while (Atomics.load(gate, 1) === 0) Atomics.wait(gate, 1, 0);
82
+ }
83
+ port.postMessage({ id: last, current: checkKimiReviewPolicy(binding) });
84
+ });
85
+ }
@@ -0,0 +1,68 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstatSync, readFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { realPath } from "../hub/project.ts";
5
+
6
+ export interface KimiReviewPolicyBinding {
7
+ readonly lexicalCwd: string;
8
+ readonly physicalCwd: string;
9
+ readonly sourcePath: string;
10
+ readonly source: string;
11
+ readonly sourceConfig: string;
12
+ readonly sourceConfigHash: string;
13
+ readonly executable: string;
14
+ readonly executableStat: Readonly<{ dev: number; ino: number; size: number; mtimeMs: number; ctimeMs: number }>;
15
+ readonly privateFiles: readonly Readonly<{ path: string; hash: string }>[];
16
+ readonly tool: string;
17
+ readonly authTargets: readonly string[];
18
+ }
19
+ export const kimiReviewFingerprint = (text: string): string => createHash("sha256").update(text).digest("hex");
20
+ export function readKimiReviewFile(file: string, limit = 262_144): string {
21
+ const stat = lstatSync(file); if (!stat.isFile() || stat.size > limit) throw new Error("unsupported reviewer file");
22
+ return readFileSync(file, "utf8");
23
+ }
24
+ /** Native discovery can walk above cwd; check lexical aliases as well as the physical temporary root. */
25
+ export function assertCleanCwdAncestors(...candidates: readonly string[]): void {
26
+ const seen = new Set<string>();
27
+ for (const candidate of candidates) {
28
+ let ancestor = resolve(candidate);
29
+ for (;;) {
30
+ if (!seen.has(ancestor)) {
31
+ seen.add(ancestor);
32
+ for (const marker of [".git", ".kimi-code", "AGENTS.md"]) {
33
+ if (lstatSync(join(ancestor, marker), { throwIfNoEntry: false }) !== undefined) {
34
+ throw new Error("private cwd ancestor contains ambient configuration");
35
+ }
36
+ }
37
+ }
38
+ const parent = dirname(ancestor); if (parent === ancestor) break; ancestor = parent;
39
+ }
40
+ }
41
+ }
42
+ /** Closed projection: no input, config contents, environment or extra caller fields cross the worker boundary. */
43
+ export function projectKimiReviewPolicyBinding(binding: KimiReviewPolicyBinding): KimiReviewPolicyBinding {
44
+ return Object.freeze({
45
+ lexicalCwd: binding.lexicalCwd, physicalCwd: binding.physicalCwd,
46
+ sourcePath: binding.sourcePath, source: binding.source,
47
+ sourceConfig: binding.sourceConfig, sourceConfigHash: binding.sourceConfigHash,
48
+ executable: binding.executable,
49
+ executableStat: Object.freeze({ dev: binding.executableStat.dev, ino: binding.executableStat.ino,
50
+ size: binding.executableStat.size, mtimeMs: binding.executableStat.mtimeMs, ctimeMs: binding.executableStat.ctimeMs }),
51
+ privateFiles: Object.freeze(binding.privateFiles.map(file => Object.freeze({ path: file.path, hash: file.hash }))),
52
+ tool: binding.tool, authTargets: Object.freeze(binding.authTargets.map(target => target)),
53
+ });
54
+ }
55
+ /** The complete filesystem fence, shared by synchronous authority checks and the periodic worker. */
56
+ export function checkKimiReviewPolicy(binding: KimiReviewPolicyBinding): boolean {
57
+ try {
58
+ assertCleanCwdAncestors(binding.lexicalCwd, binding.physicalCwd);
59
+ const now = lstatSync(binding.executable), expected = binding.executableStat;
60
+ return realPath(binding.sourcePath) === binding.source
61
+ && now.dev === expected.dev && now.ino === expected.ino && now.size === expected.size
62
+ && now.mtimeMs === expected.mtimeMs && now.ctimeMs === expected.ctimeMs
63
+ && kimiReviewFingerprint(readKimiReviewFile(binding.sourceConfig)) === binding.sourceConfigHash
64
+ && binding.privateFiles.length === 3 && binding.privateFiles.every(file => kimiReviewFingerprint(readKimiReviewFile(file.path, 2_097_152)) === file.hash)
65
+ && binding.authTargets.length === 3 && ["credentials", "oauth", "device_id"].every((name, index) =>
66
+ realPath(join(binding.source, name)) === binding.authTargets[index] && realPath(join(binding.tool, name)) === binding.authTargets[index]);
67
+ } catch { return false; }
68
+ }
@@ -1,38 +1,16 @@
1
1
  import { spawn, type ChildProcess } from "node:child_process";
2
2
  import { assessVersion, COMPAT } from "./compat.ts";
3
- import { createHash } from "node:crypto";
4
- import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
4
  import { homedir, tmpdir } from "node:os";
6
- import { basename, dirname, join, resolve } from "node:path";
5
+ import { basename, join, resolve } from "node:path";
7
6
  import { stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
8
7
  import { realPath } from "../hub/project.ts";
9
- import type { ReviewerBackend, ReviewerPrepareContext } from "./reviewer-print.ts";
8
+ import type { ReviewerBackend, ReviewerLaunch, ReviewerPrepareContext } from "./reviewer-print.ts";
9
+ import { assertCleanCwdAncestors, checkKimiReviewPolicy, kimiReviewFingerprint as hash, projectKimiReviewPolicyBinding, readKimiReviewFile as read } from "./kimi-review-policy.ts";
10
+ import { startKimiReviewPolicyWatch, type KimiReviewPolicyFactory } from "./kimi-review-policy-worker.ts";
10
11
 
11
12
  const MANAGED = "managed:kimi-code";
12
13
  const object = (v: unknown): v is Record<string, unknown> => !!v && typeof v === "object" && !Array.isArray(v);
13
- const hash = (text: string) => createHash("sha256").update(text).digest("hex");
14
- /** Native discovery can walk above cwd; check lexical aliases as well as the physical temporary root. */
15
- function assertCleanCwdAncestors(...candidates: readonly string[]): void {
16
- const seen = new Set<string>();
17
- for (const candidate of candidates) {
18
- let ancestor = resolve(candidate);
19
- for (;;) {
20
- if (!seen.has(ancestor)) {
21
- seen.add(ancestor);
22
- for (const marker of [".git", ".kimi-code", "AGENTS.md"]) {
23
- if (lstatSync(join(ancestor, marker), { throwIfNoEntry: false }) !== undefined) {
24
- throw new Error("private cwd ancestor contains ambient configuration");
25
- }
26
- }
27
- }
28
- const parent = dirname(ancestor); if (parent === ancestor) break; ancestor = parent;
29
- }
30
- }
31
- }
32
- const read = (file: string, limit = 262_144) => {
33
- const stat = lstatSync(file); if (!stat.isFile() || stat.size > limit) throw new Error("unsupported reviewer file");
34
- return readFileSync(file, "utf8");
35
- };
36
14
  const scalar = (value: unknown, depth = 0): string => {
37
15
  if (depth > 4) throw new Error("managed model setting too deeply nested");
38
16
  if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
@@ -108,9 +86,7 @@ export async function stopKimiReview(child: ChildProcess, locks: readonly string
108
86
  await stopOwnedProcess(child, { group: true });
109
87
  }
110
88
 
111
- export const kimiReviewerBackend: ReviewerBackend = {
112
- id: "kimi", taskMcp: true, nativePolicy: true,
113
- async prepare(context) {
89
+ export async function prepareKimiReviewer(context: ReviewerPrepareContext, policyFactory: KimiReviewPolicyFactory = startKimiReviewPolicyWatch): Promise<ReviewerLaunch | { unavailable: string }> {
114
90
  let own: string | undefined;
115
91
  try {
116
92
  if (context.signal.aborted) throw new Error("cancelled");
@@ -138,19 +114,40 @@ export const kimiReviewerBackend: ReviewerBackend = {
138
114
  const verdict = assessVersion("kimi-review", COMPAT["kimi-review"].parse(await version(command, cwd, env, context.signal)));
139
115
  if (!verdict.ok) throw new Error(verdict.reason);
140
116
  const fingerprints = [configFile, mcpFile, agentFile].map(file => hash(read(file, 2_097_152)));
141
- let cleaned = false;
142
- const cleanup = () => { if (cleaned) return; rmSync(root, { recursive: true, force: true }); cleaned = true; };
117
+ const binding = projectKimiReviewPolicyBinding({
118
+ lexicalCwd: join(lexicalRoot, "workspace"), physicalCwd: cwd,
119
+ sourcePath, source, sourceConfig, sourceConfigHash: hash(original), executable,
120
+ executableStat: { dev: executableStat.dev, ino: executableStat.ino, size: executableStat.size, mtimeMs: executableStat.mtimeMs, ctimeMs: executableStat.ctimeMs },
121
+ privateFiles: [configFile, mcpFile, agentFile].map((path, index) => ({ path, hash: fingerprints[index]! })),
122
+ tool, authTargets: sources,
123
+ });
124
+ let policyProbe: ReturnType<KimiReviewPolicyFactory> | undefined;
125
+ let cleaned = false, cleaning: Promise<void> | undefined;
126
+ const remove = () => { if (cleaned) return; rmSync(root, { recursive: true, force: true }); cleaned = true; };
127
+ const cleanup = () => {
128
+ if (cleaned) return;
129
+ if (!policyProbe) { remove(); return; }
130
+ return cleaning ??= policyProbe.stop().then(remove).catch(error => { cleaning = undefined; throw error; });
131
+ };
143
132
  return { command, args: ["-p", "Review the supplied task facts and record the evidence-based verdict with hub_review.", "--output-format", "stream-json", "--agent-file", agentFile, "--skills-dir", skills],
144
133
  cwd, env, envMode: "replace", cleanup, watchCurrent: true,
145
134
  stop: child => stopKimiReview(child, [join(sources[1]!, "kimi-code.lock"), join(sources[1]!, `${selected.lock}.lock`)]),
146
- current: () => {
147
- try {
148
- assertCleanCwdAncestors(join(lexicalRoot,"workspace"),cwd);
149
- const now = lstatSync(executable);
150
- return !cleaned && !context.signal.aborted && realPath(sourcePath) === source && now.dev === executableStat.dev && now.ino === executableStat.ino && now.size === executableStat.size && now.mtimeMs === executableStat.mtimeMs && now.ctimeMs === executableStat.ctimeMs && hash(read(sourceConfig)) === hash(original)
151
- && [configFile, mcpFile, agentFile].every((file, index) => hash(read(file, 2_097_152)) === fingerprints[index])
152
- && ["credentials", "oauth", "device_id"].every((name, index) => realPath(join(source, name)) === sources[index] && realPath(join(tool, name)) === sources[index]);
153
- } catch { return false; }
135
+ current: () => !cleaned && !context.signal.aborted && checkKimiReviewPolicy(binding),
136
+ startPolicyWatch: () => {
137
+ if (cleaned || context.signal.aborted) throw new Error("review policy is no longer current; retry the review");
138
+ if (!policyProbe) {
139
+ const probe = policyFactory(projectKimiReviewPolicyBinding(binding));
140
+ policyProbe = {
141
+ failure: probe.failure,
142
+ async check() {
143
+ if (cleaned || context.signal.aborted) return false;
144
+ const current = await probe.check();
145
+ return !cleaned && !context.signal.aborted && current;
146
+ },
147
+ stop: () => probe.stop(),
148
+ };
149
+ }
150
+ return policyProbe;
154
151
  },
155
152
  isolation: { backend: "kimi", version: verdict.version, binary: executable, profile: "private-config-exact-two-tools-shared-native-oauth", readOnly: true, strictMcp: true, ephemeral: true,
156
153
  enforcement: "native-policy", freshSession: true, hubToolsOnly: true,
@@ -160,5 +157,8 @@ export const kimiReviewerBackend: ReviewerBackend = {
160
157
  if (own) rmSync(own, { recursive: true, force: true });
161
158
  return { unavailable: `Kimi reviewer unavailable: ${(error as Error).message}` };
162
159
  }
163
- },
160
+ }
161
+
162
+ export const kimiReviewerBackend: ReviewerBackend = {
163
+ id: "kimi", taskMcp: true, nativePolicy: true, prepare: context => prepareKimiReviewer(context),
164
164
  };
@@ -0,0 +1,153 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, readdirSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { basename, dirname, isAbsolute, join, relative } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { realPath } from "../hub/project.ts";
6
+ import type { NativeFactsInput, NativeFactsPort, NativeFactsTranscript } from "./registry-types.ts";
7
+
8
+ const EVENTS = ["TurnStarted", "Stop", "StopFailure", "Interrupt", "SessionEnd", "TaskStarted", "Notification"] as const;
9
+ const SESSION = /^session_[A-Za-z0-9_-]{1,120}$/;
10
+ const AGENT = /^[A-Za-z0-9._-]{1,64}$/;
11
+ const TASK = /^[A-Za-z0-9._:-]{1,128}$/;
12
+ const INDEX_CAP = 16 * 1024 * 1024;
13
+ const WIRE_CAP = 8 * 1024 * 1024;
14
+ const TASK_CAP = 64 * 1024;
15
+ const MAX_AGENTS = 256;
16
+ const MAX_TASKS = 256;
17
+ const record = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
18
+ const integer = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) >= 0;
19
+ const inside = (root: string, path: string) => { const rel = relative(root, path); return rel !== "" && rel !== ".." && !rel.startsWith("../") && !isAbsolute(rel); };
20
+
21
+ export function decodeKimiTuiHook(event: string, value: unknown): NativeFactsInput | undefined {
22
+ if (!(EVENTS as readonly string[]).includes(event) || !record(value) || value.hook_event_name !== event || value.client_type !== "kimi_code_cli"
23
+ || typeof value.session_id !== "string" || !SESSION.test(value.session_id) || typeof value.cwd !== "string" || !isAbsolute(value.cwd) || value.cwd.length > 4096) return;
24
+ if ((event === "TurnStarted" || event === "Interrupt") && !integer(value.turn_id)) return;
25
+ if (event === "TaskStarted" && (typeof value.task_id !== "string" || !TASK.test(value.task_id))) return;
26
+ const home = process.env.KIMI_CODE_HOME ?? join(process.env.HOME ?? homedir(), ".kimi-code");
27
+ const transcriptPath = value.agentHubTranscriptPath ?? join(home, "session_index.jsonl");
28
+ if (typeof transcriptPath !== "string" || !isAbsolute(transcriptPath) || transcriptPath.length > 4096) return;
29
+ const phase = ({ TurnStarted: "start", Stop: "stop", StopFailure: "stop", Interrupt: "stop", SessionEnd: "session", TaskStarted: "pre", Notification: "post" } as const)[event as typeof EVENTS[number]];
30
+ return { event: event as NativeFactsInput["event"], phase, sessionId: value.session_id, transcriptPath, workspacePaths: [value.cwd], tool: "", input: {} };
31
+ }
32
+
33
+ export function encodeKimiTuiHook(): undefined { return undefined; }
34
+
35
+ function readBytes(path: string, cap: number, tail = false): Buffer {
36
+ const link = lstatSync(path);
37
+ if (!link.isFile() || link.isSymbolicLink() || link.nlink !== 1) throw new Error("not a regular file");
38
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
39
+ try {
40
+ const stat = fstatSync(fd);
41
+ if (!stat.isFile() || stat.nlink !== 1 || (!tail && stat.size > cap)) throw new Error("invalid file");
42
+ const length = Math.min(stat.size, cap), start = tail ? stat.size - length : 0;
43
+ const bytes = Buffer.alloc(length);
44
+ let offset = 0;
45
+ while (offset < length) {
46
+ const count = readSync(fd, bytes, offset, length - offset, start + offset);
47
+ if (count <= 0) throw new Error("short read");
48
+ offset += count;
49
+ }
50
+ return bytes;
51
+ } finally { closeSync(fd); }
52
+ }
53
+
54
+ function sessionDirectory(index: string, session: string): string {
55
+ const home = dirname(index), sessions = realPath(join(home, "sessions"));
56
+ let listed: string | undefined;
57
+ for (const line of readBytes(index, INDEX_CAP).toString("utf8").split("\n")) {
58
+ if (!line.includes(session)) continue;
59
+ let row: unknown;
60
+ try { row = JSON.parse(line); } catch { continue; }
61
+ if (record(row) && row.sessionId === session && typeof row.sessionDir === "string") {
62
+ if (listed) throw new Error("duplicate session");
63
+ listed = row.sessionDir;
64
+ }
65
+ }
66
+ if (!listed) throw new Error("session not indexed");
67
+ const directory = realPath(listed);
68
+ if (!inside(sessions, directory)) throw new Error("session outside home");
69
+ return directory;
70
+ }
71
+
72
+ function taskInventory(agents: string): number {
73
+ let active = 0;
74
+ const names = readdirSync(agents, { withFileTypes: true });
75
+ if (names.length > MAX_AGENTS) throw new Error("too many agents");
76
+ for (const entry of names) {
77
+ if (!AGENT.test(entry.name)) continue;
78
+ if (!entry.isDirectory()) continue;
79
+ const tasks = join(agents, entry.name, "tasks");
80
+ const dir = lstatSync(tasks, { throwIfNoEntry: false });
81
+ if (!dir) continue;
82
+ if (!dir.isDirectory() || dir.isSymbolicLink()) throw new Error("invalid task directory");
83
+ const files = readdirSync(tasks).filter(name => name.endsWith(".json")).sort();
84
+ if (files.length > MAX_TASKS) throw new Error("too many tasks");
85
+ for (const name of files) {
86
+ if (!TASK.test(name.slice(0, -5))) throw new Error("invalid task name");
87
+ const row: unknown = JSON.parse(readBytes(join(tasks, name), TASK_CAP).toString("utf8"));
88
+ if (!record(row) || row.taskId !== name.slice(0, -5) || typeof row.status !== "string") throw new Error("invalid task");
89
+ if (row.status === "running" && row.endedAt === null) { active++; continue; }
90
+ const terminal = ["completed", "failed", "cancelled"].includes(row.status) && integer(row.endedAt);
91
+ if (!terminal) throw new Error("unknown task state");
92
+ }
93
+ }
94
+ return active;
95
+ }
96
+
97
+ interface Prompt { turn: number; id: string; origin: string }
98
+ interface End { turn: number; reason: "completed" | "failed" | "cancelled" }
99
+ const empty = (): NativeFactsTranscript => ({ factIds: [] });
100
+
101
+ export function readKimiTuiTranscript(session: string, index: string): NativeFactsTranscript {
102
+ if (!SESSION.test(session) || basename(index) !== "session_index.jsonl" || !isAbsolute(index)) return empty();
103
+ try {
104
+ const directory = sessionDirectory(index, session);
105
+ const wire = join(directory, "agents", "main", "wire.jsonl");
106
+ if (!inside(directory, realPath(wire))) return empty();
107
+ const bytes = readBytes(wire, WIRE_CAP, true);
108
+ if (bytes.length === 0 || bytes[bytes.length - 1] !== 10) return empty();
109
+ const lines = bytes.toString("utf8").split("\n");
110
+ if (bytes.length === WIRE_CAP && !lines[0]?.startsWith('{"type":"metadata"')) lines.shift();
111
+ const prompts = new Map<number, Prompt>();
112
+ const ends = new Map<number, End>();
113
+ const promptEnds = new Map<string, "completed" | "failed" | "cancelled">();
114
+ const promptEndTurnIds = new Map<number, string>();
115
+ let started: Prompt | undefined;
116
+ for (const line of lines) {
117
+ if (!line || !/"(?:turn\.prompt|turn\.ended|prompt\.completed|prompt\.aborted)"/.test(line)) continue;
118
+ let row: unknown;
119
+ try { row = JSON.parse(line); } catch { return empty(); }
120
+ if (!record(row)) return empty();
121
+ if (row.type === "turn.prompt" && integer(row.turnId) && record(row.origin) && typeof row.origin.kind === "string") {
122
+ const turn = row.turnId, id = typeof row.promptId === "string" ? row.promptId : typeof row.origin.notificationId === "string" ? row.origin.notificationId : `turn-${turn}`;
123
+ const prompt = { turn, id, origin: row.origin.kind };
124
+ prompts.set(turn, prompt); started = prompt;
125
+ } else if (row.type === "turn.ended" && integer(row.turnId) && ["completed", "failed", "cancelled"].includes(String(row.reason))) {
126
+ const turn = row.turnId;
127
+ ends.set(turn, { turn, reason: row.reason as End["reason"] });
128
+ } else if (row.type === "prompt.completed" && typeof row.promptId === "string" && ["completed", "failed"].includes(String(row.reason))) {
129
+ promptEnds.set(row.promptId, row.reason as "completed" | "failed");
130
+ if (integer(row.turnId)) promptEndTurnIds.set(row.turnId, row.promptId);
131
+ } else if (row.type === "prompt.aborted" && typeof row.promptId === "string") {
132
+ promptEnds.set(row.promptId, "cancelled");
133
+ if (integer(row.turnId)) promptEndTurnIds.set(row.turnId, row.promptId);
134
+ } else if (row.type === "turn.prompt" || row.type === "turn.ended") return empty();
135
+ }
136
+ let completion: NativeFactsTranscript["completion"];
137
+ for (const [turn, end] of ends) {
138
+ const prompt = prompts.get(turn), promptReason = prompt ? promptEnds.get(prompt.id) : undefined, turnPromptId = promptEndTurnIds.get(turn);
139
+ if (!prompt || promptReason !== undefined && promptReason !== end.reason || turnPromptId !== undefined && turnPromptId !== prompt.id) continue;
140
+ completion = { id: createHash("sha256").update(`${session}:${turn}:${prompt.id}:${end.reason}`).digest("hex"), index: turn, reason: end.reason };
141
+ }
142
+ let background: number | undefined;
143
+ try { background = taskInventory(join(directory, "agents")); } catch { /* an unknown inventory holds settlement, not the final turn evidence */ }
144
+ return {
145
+ factIds: [],
146
+ ...(completion ? { completion } : {}),
147
+ ...(started ? { started: { id: createHash("sha256").update(`${session}:${started.turn}:${started.id}`).digest("hex"), index: started.turn, origin: started.origin } } : {}),
148
+ ...(background === undefined ? {} : { background }),
149
+ };
150
+ } catch { return empty(); }
151
+ }
152
+
153
+ export const KIMI_TUI_FACTS_HOOK: NativeFactsPort = { decode: decodeKimiTuiHook, encode: encodeKimiTuiHook, read: readKimiTuiTranscript };