kimi-plugin-cc 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +16 -0
  3. package/.claude-plugin/plugin.json +32 -0
  4. package/CHANGELOG.md +1096 -0
  5. package/LICENSE +201 -0
  6. package/README.md +772 -0
  7. package/agents/kimi-ask.md +82 -0
  8. package/agents/kimi-challenge.md +82 -0
  9. package/agents/kimi-pursue.md +91 -0
  10. package/agents/kimi-rescue.md +85 -0
  11. package/agents/kimi-review.md +82 -0
  12. package/agents/kimi-swarm-write.md +92 -0
  13. package/agents/kimi-swarm.md +83 -0
  14. package/commands/README.md +16 -0
  15. package/commands/ask.md +32 -0
  16. package/commands/cancel.md +17 -0
  17. package/commands/challenge.md +29 -0
  18. package/commands/pursue.md +37 -0
  19. package/commands/replay.md +13 -0
  20. package/commands/rescue.md +32 -0
  21. package/commands/result.md +13 -0
  22. package/commands/review.md +29 -0
  23. package/commands/setup.md +23 -0
  24. package/commands/status.md +13 -0
  25. package/commands/swarm.md +38 -0
  26. package/dist/background-spawn.js +224 -0
  27. package/dist/cli-cancellation.js +43 -0
  28. package/dist/cli-client.js +845 -0
  29. package/dist/commands/ask.js +327 -0
  30. package/dist/commands/cancel.js +178 -0
  31. package/dist/commands/cli-helpers.js +112 -0
  32. package/dist/commands/models.js +140 -0
  33. package/dist/commands/not-implemented.js +4 -0
  34. package/dist/commands/pursue.js +311 -0
  35. package/dist/commands/registry.js +87 -0
  36. package/dist/commands/repair-sessions.js +159 -0
  37. package/dist/commands/replay.js +123 -0
  38. package/dist/commands/rescue.js +333 -0
  39. package/dist/commands/result.js +58 -0
  40. package/dist/commands/review-gate.js +393 -0
  41. package/dist/commands/review.js +218 -0
  42. package/dist/commands/setup.js +1132 -0
  43. package/dist/commands/status.js +27 -0
  44. package/dist/commands/swarm.js +576 -0
  45. package/dist/commands/worker.js +31 -0
  46. package/dist/companion.js +114 -0
  47. package/dist/config.js +22 -0
  48. package/dist/errors.js +40 -0
  49. package/dist/git.js +215 -0
  50. package/dist/hooks/approval-hook.js +112 -0
  51. package/dist/hooks/approval-policy.js +254 -0
  52. package/dist/hooks/config-safety.js +637 -0
  53. package/dist/hooks/install-paths.js +504 -0
  54. package/dist/hooks/install.js +281 -0
  55. package/dist/hooks/managed-block.js +745 -0
  56. package/dist/hooks/review-gate-stop.js +19 -0
  57. package/dist/job-store.js +559 -0
  58. package/dist/jobs.js +202 -0
  59. package/dist/kimi-command.js +72 -0
  60. package/dist/kimi-engine.js +624 -0
  61. package/dist/kimi-errors.js +220 -0
  62. package/dist/kimi-home.js +9 -0
  63. package/dist/kimi-timeouts.js +68 -0
  64. package/dist/kimi-version-probe.js +1410 -0
  65. package/dist/logging.js +22 -0
  66. package/dist/native-v2-preflight.js +417 -0
  67. package/dist/package.json +1 -0
  68. package/dist/parsing.js +553 -0
  69. package/dist/paths.js +27 -0
  70. package/dist/plugin-data.js +98 -0
  71. package/dist/process-tree.js +448 -0
  72. package/dist/render.js +159 -0
  73. package/dist/rescue-approval.js +951 -0
  74. package/dist/schemas/review-gate-output.js +43 -0
  75. package/dist/session-title.js +293 -0
  76. package/dist/stream-json.js +498 -0
  77. package/dist/types.js +1 -0
  78. package/dist/vendor/shell-quote/LICENSE +24 -0
  79. package/dist/vendor/shell-quote/index.js +1 -0
  80. package/dist/vendor/shell-quote/parse.js +229 -0
  81. package/dist/vendor/smol-toml/LICENSE +24 -0
  82. package/dist/vendor/smol-toml/date.js +127 -0
  83. package/dist/vendor/smol-toml/error.js +63 -0
  84. package/dist/vendor/smol-toml/extract.js +100 -0
  85. package/dist/vendor/smol-toml/parse.js +148 -0
  86. package/dist/vendor/smol-toml/primitive.js +179 -0
  87. package/dist/vendor/smol-toml/struct.js +184 -0
  88. package/dist/vendor/smol-toml/util.js +113 -0
  89. package/dist/version.js +7 -0
  90. package/docs/migration.md +215 -0
  91. package/docs/models.md +82 -0
  92. package/docs/registry-releases.md +70 -0
  93. package/docs/safety.md +318 -0
  94. package/docs/session-visibility.md +32 -0
  95. package/hooks/README.md +9 -0
  96. package/hooks/hooks.json +16 -0
  97. package/package.json +67 -0
  98. package/plugins/kimi-codex/.codex-plugin/plugin.json +39 -0
  99. package/plugins/kimi-codex/dist/background-spawn.js +224 -0
  100. package/plugins/kimi-codex/dist/cli-cancellation.js +43 -0
  101. package/plugins/kimi-codex/dist/cli-client.js +845 -0
  102. package/plugins/kimi-codex/dist/commands/ask.js +327 -0
  103. package/plugins/kimi-codex/dist/commands/cancel.js +178 -0
  104. package/plugins/kimi-codex/dist/commands/cli-helpers.js +112 -0
  105. package/plugins/kimi-codex/dist/commands/models.js +140 -0
  106. package/plugins/kimi-codex/dist/commands/not-implemented.js +4 -0
  107. package/plugins/kimi-codex/dist/commands/pursue.js +311 -0
  108. package/plugins/kimi-codex/dist/commands/registry.js +87 -0
  109. package/plugins/kimi-codex/dist/commands/repair-sessions.js +159 -0
  110. package/plugins/kimi-codex/dist/commands/replay.js +123 -0
  111. package/plugins/kimi-codex/dist/commands/rescue.js +333 -0
  112. package/plugins/kimi-codex/dist/commands/result.js +58 -0
  113. package/plugins/kimi-codex/dist/commands/review-gate.js +393 -0
  114. package/plugins/kimi-codex/dist/commands/review.js +218 -0
  115. package/plugins/kimi-codex/dist/commands/setup.js +1132 -0
  116. package/plugins/kimi-codex/dist/commands/status.js +27 -0
  117. package/plugins/kimi-codex/dist/commands/swarm.js +576 -0
  118. package/plugins/kimi-codex/dist/commands/worker.js +31 -0
  119. package/plugins/kimi-codex/dist/companion.js +114 -0
  120. package/plugins/kimi-codex/dist/config.js +22 -0
  121. package/plugins/kimi-codex/dist/errors.js +40 -0
  122. package/plugins/kimi-codex/dist/git.js +215 -0
  123. package/plugins/kimi-codex/dist/hooks/approval-hook.js +112 -0
  124. package/plugins/kimi-codex/dist/hooks/approval-policy.js +254 -0
  125. package/plugins/kimi-codex/dist/hooks/config-safety.js +637 -0
  126. package/plugins/kimi-codex/dist/hooks/install-paths.js +504 -0
  127. package/plugins/kimi-codex/dist/hooks/install.js +281 -0
  128. package/plugins/kimi-codex/dist/hooks/managed-block.js +745 -0
  129. package/plugins/kimi-codex/dist/hooks/review-gate-stop.js +19 -0
  130. package/plugins/kimi-codex/dist/job-store.js +559 -0
  131. package/plugins/kimi-codex/dist/jobs.js +202 -0
  132. package/plugins/kimi-codex/dist/kimi-command.js +72 -0
  133. package/plugins/kimi-codex/dist/kimi-engine.js +624 -0
  134. package/plugins/kimi-codex/dist/kimi-errors.js +220 -0
  135. package/plugins/kimi-codex/dist/kimi-home.js +9 -0
  136. package/plugins/kimi-codex/dist/kimi-timeouts.js +68 -0
  137. package/plugins/kimi-codex/dist/kimi-version-probe.js +1410 -0
  138. package/plugins/kimi-codex/dist/logging.js +22 -0
  139. package/plugins/kimi-codex/dist/native-v2-preflight.js +417 -0
  140. package/plugins/kimi-codex/dist/package.json +1 -0
  141. package/plugins/kimi-codex/dist/parsing.js +553 -0
  142. package/plugins/kimi-codex/dist/paths.js +27 -0
  143. package/plugins/kimi-codex/dist/plugin-data.js +98 -0
  144. package/plugins/kimi-codex/dist/process-tree.js +448 -0
  145. package/plugins/kimi-codex/dist/render.js +159 -0
  146. package/plugins/kimi-codex/dist/rescue-approval.js +951 -0
  147. package/plugins/kimi-codex/dist/schemas/review-gate-output.js +43 -0
  148. package/plugins/kimi-codex/dist/session-title.js +293 -0
  149. package/plugins/kimi-codex/dist/stream-json.js +498 -0
  150. package/plugins/kimi-codex/dist/types.js +1 -0
  151. package/plugins/kimi-codex/dist/vendor/shell-quote/LICENSE +24 -0
  152. package/plugins/kimi-codex/dist/vendor/shell-quote/index.js +1 -0
  153. package/plugins/kimi-codex/dist/vendor/shell-quote/parse.js +229 -0
  154. package/plugins/kimi-codex/dist/vendor/smol-toml/LICENSE +24 -0
  155. package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +127 -0
  156. package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +63 -0
  157. package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +100 -0
  158. package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +148 -0
  159. package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +179 -0
  160. package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +184 -0
  161. package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +113 -0
  162. package/plugins/kimi-codex/dist/version.js +7 -0
  163. package/plugins/kimi-codex/scripts/companion.sh +37 -0
  164. package/plugins/kimi-codex/scripts/review-gate-hook.sh +32 -0
  165. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +31 -0
  166. package/plugins/kimi-codex/skills/kimi-ask/agents/openai.yaml +6 -0
  167. package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +25 -0
  168. package/plugins/kimi-codex/skills/kimi-cancel/agents/openai.yaml +6 -0
  169. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +31 -0
  170. package/plugins/kimi-codex/skills/kimi-challenge/agents/openai.yaml +6 -0
  171. package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +30 -0
  172. package/plugins/kimi-codex/skills/kimi-pursue/agents/openai.yaml +6 -0
  173. package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +25 -0
  174. package/plugins/kimi-codex/skills/kimi-replay/agents/openai.yaml +6 -0
  175. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +30 -0
  176. package/plugins/kimi-codex/skills/kimi-rescue/agents/openai.yaml +6 -0
  177. package/plugins/kimi-codex/skills/kimi-result/SKILL.md +25 -0
  178. package/plugins/kimi-codex/skills/kimi-result/agents/openai.yaml +6 -0
  179. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +31 -0
  180. package/plugins/kimi-codex/skills/kimi-review/agents/openai.yaml +6 -0
  181. package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +32 -0
  182. package/plugins/kimi-codex/skills/kimi-setup/agents/openai.yaml +6 -0
  183. package/plugins/kimi-codex/skills/kimi-status/SKILL.md +25 -0
  184. package/plugins/kimi-codex/skills/kimi-status/agents/openai.yaml +6 -0
  185. package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +31 -0
  186. package/plugins/kimi-codex/skills/kimi-swarm/agents/openai.yaml +6 -0
  187. package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +32 -0
  188. package/plugins/kimi-codex/skills/kimi-swarm-write/agents/openai.yaml +6 -0
  189. package/scripts/companion.sh +37 -0
  190. package/scripts/review-gate-hook.sh +32 -0
@@ -0,0 +1,114 @@
1
+ import { runAsk } from "./commands/ask.js";
2
+ import { runModelInventory } from "./commands/models.js";
3
+ import { runCancel } from "./commands/cancel.js";
4
+ import { notImplementedCompanionCommand } from "./commands/not-implemented.js";
5
+ import { runReplay } from "./commands/replay.js";
6
+ import { runRepairSessions } from "./commands/repair-sessions.js";
7
+ import { runResult } from "./commands/result.js";
8
+ import { runReview } from "./commands/review.js";
9
+ import { runRescue } from "./commands/rescue.js";
10
+ import { runPursue } from "./commands/pursue.js";
11
+ import { runSwarm } from "./commands/swarm.js";
12
+ import { renderSetupResult, runSetup } from "./commands/setup.js";
13
+ import { runStatus } from "./commands/status.js";
14
+ import { runWorker } from "./commands/worker.js";
15
+ import { formatError, RuntimeError } from "./errors.js";
16
+ import { resolveKimiHome } from "./kimi-home.js";
17
+ async function main(argv) {
18
+ const [command, ...rest] = argv;
19
+ const cwd = process.env.KIMI_PLUGIN_CC_WORKSPACE_CWD || process.cwd();
20
+ const context = {
21
+ cwd,
22
+ env: {
23
+ ...process.env,
24
+ KIMI_CODE_HOME: resolveKimiHome(process.env, cwd),
25
+ },
26
+ stdout: process.stdout,
27
+ stderr: process.stderr,
28
+ };
29
+ switch (command) {
30
+ case "setup": {
31
+ if (rest.includes("--models")) {
32
+ const args = [...rest];
33
+ args.splice(args.indexOf("--models"), 1);
34
+ context.stdout.write(`${await runModelInventory(args, context)}\n`);
35
+ return;
36
+ }
37
+ const result = await runSetup(rest, context);
38
+ context.stdout.write(`${renderSetupResult(result)}\n`);
39
+ // A failed probe means the hook did not actually deny when exercised —
40
+ // enforcement is NOT in place, whichever action produced it. This must be
41
+ // nonzero for `install` too, not just `check`: since v1.9.0 an agent may
42
+ // run setup and then RETRY the refused command, and it keys that decision
43
+ // on the exit code. Exiting 0 after a failed probe would walk it straight
44
+ // past a proven-dead hook (a byte-exact command + a readable-but-broken
45
+ // script passes `verifyHookInstalled`, since only the probe can tell that
46
+ // the script cannot deny — e.g. a truncated `approval-hook.js`, which has
47
+ // happened here via iCloud sync, or a half-extracted plugin cache).
48
+ // `probe: "skipped"` (uninstall) is deliberately untouched.
49
+ if (result.probe === "failed") {
50
+ process.exitCode = 1;
51
+ }
52
+ return;
53
+ }
54
+ case "review": {
55
+ const result = await runReview(rest, context, "review");
56
+ context.stdout.write(`${result}\n`);
57
+ return;
58
+ }
59
+ case "task": {
60
+ const [taskType, ...taskArgs] = rest;
61
+ if (taskType === "rescue") {
62
+ const result = await runRescue(taskArgs, context);
63
+ context.stdout.write(result);
64
+ return;
65
+ }
66
+ if (taskType === "challenge") {
67
+ const result = await runReview(taskArgs, context, "challenge");
68
+ context.stdout.write(`${result}\n`);
69
+ return;
70
+ }
71
+ if (taskType === "pursue") {
72
+ const result = await runPursue(taskArgs, context);
73
+ context.stdout.write(result);
74
+ return;
75
+ }
76
+ if (taskType === "swarm") {
77
+ const result = await runSwarm(taskArgs, context);
78
+ context.stdout.write(result);
79
+ return;
80
+ }
81
+ notImplementedCompanionCommand(taskType ? `task ${taskType}` : "task");
82
+ return;
83
+ }
84
+ case "ask": {
85
+ const result = await runAsk(rest, context);
86
+ context.stdout.write(`${result}\n`);
87
+ return;
88
+ }
89
+ case "status":
90
+ context.stdout.write(await runStatus(rest, context));
91
+ return;
92
+ case "result":
93
+ context.stdout.write(await runResult(rest, context));
94
+ return;
95
+ case "cancel":
96
+ context.stdout.write(await runCancel(rest, context));
97
+ return;
98
+ case "replay":
99
+ context.stdout.write(await runReplay(rest, context));
100
+ return;
101
+ case "repair-sessions":
102
+ context.stdout.write(await runRepairSessions(rest, context));
103
+ return;
104
+ case "worker":
105
+ await runWorker(rest, context);
106
+ return;
107
+ default:
108
+ throw new RuntimeError("INVALID_COMMAND", `Unknown or missing companion subcommand: ${command ?? "<none>"}. Expected one of setup, review, task, ask, status, result, cancel, replay, repair-sessions.`, "companion");
109
+ }
110
+ }
111
+ main(process.argv.slice(2)).catch((error) => {
112
+ process.stderr.write(`${formatError(error)}\n`);
113
+ process.exitCode = 1;
114
+ });
@@ -0,0 +1,22 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ const DEFAULT_CONFIG = {
3
+ reviewGateEnabled: false,
4
+ };
5
+ export async function readPluginConfig(paths) {
6
+ try {
7
+ const contents = await readFile(paths.configPath, "utf8");
8
+ const parsed = JSON.parse(contents);
9
+ return {
10
+ reviewGateEnabled: parsed.reviewGateEnabled ?? DEFAULT_CONFIG.reviewGateEnabled,
11
+ };
12
+ }
13
+ catch (error) {
14
+ if (error.code === "ENOENT") {
15
+ return DEFAULT_CONFIG;
16
+ }
17
+ throw error;
18
+ }
19
+ }
20
+ export async function writePluginConfig(paths, config) {
21
+ await writeFile(paths.configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
22
+ }
@@ -0,0 +1,40 @@
1
+ export class RuntimeError extends Error {
2
+ code;
3
+ stage;
4
+ details;
5
+ constructor(code, message, stage, options) {
6
+ super(message, options);
7
+ this.name = "RuntimeError";
8
+ this.code = code;
9
+ this.stage = stage;
10
+ this.details = options?.details ?? {};
11
+ }
12
+ }
13
+ export function formatError(error) {
14
+ if (error instanceof RuntimeError) {
15
+ const base = `[${error.code}] [${error.stage}] ${error.message}`;
16
+ // Serialize structured context so an LLM caller can act on it. Without
17
+ // this, `details` existed only in the SQLite job row — reachable via
18
+ // `result <jobId> --json` for BACKGROUND jobs, but simply lost for
19
+ // foreground ones (review/challenge/ask), whose only output is this
20
+ // string. v1.9.0 needs `drift_axis` to reach the caller on every path, so
21
+ // it is rendered here as one deterministic JSON line. (Kimi's pre-release
22
+ // review caught that the agents were told to branch on a field no channel
23
+ // actually carried.)
24
+ const details = error.details;
25
+ if (details !== undefined && Object.keys(details).length > 0) {
26
+ try {
27
+ return `${base}\ndetails: ${JSON.stringify(details)}`;
28
+ }
29
+ catch {
30
+ // A non-serializable value must never suppress the error itself.
31
+ return base;
32
+ }
33
+ }
34
+ return base;
35
+ }
36
+ if (error instanceof Error) {
37
+ return error.message;
38
+ }
39
+ return String(error);
40
+ }
@@ -0,0 +1,215 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { realpath, rm } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { RuntimeError } from "./errors.js";
7
+ const execFileAsync = promisify(execFile);
8
+ const MAX_DIFF_CHARS = 80_000;
9
+ /**
10
+ * Buffer ceiling for a write-swarm capture patch. Unlike review diffs (capped at
11
+ * MAX_DIFF_CHARS for prompt budget), a capture patch MUST be byte-complete to
12
+ * `git apply`, so it is never truncated — only bounded by this generous buffer to
13
+ * avoid an unbounded RSS spike. A swarm patch larger than this is pathological.
14
+ */
15
+ const MAX_PATCH_BYTES = 64 * 1024 * 1024;
16
+ export async function collectReviewContext(cwd, base) {
17
+ const { repoRoot } = await resolveRepoIdentity(cwd);
18
+ const statusSummary = (await runGit(["status", "--short", "--untracked-files=all"], cwd)).trim();
19
+ if (base) {
20
+ const diffText = await getBaseDiff(cwd, base);
21
+ return {
22
+ repoRoot,
23
+ statusSummary,
24
+ diffText: truncateDiff(diffText),
25
+ targetDescription: `branch diff against ${base}`,
26
+ };
27
+ }
28
+ // Working-tree review is repo-wide. status --short is already repo-wide; limiting diff to
29
+ // the caller's subdirectory would mask unrelated-but-relevant changes elsewhere in the repo.
30
+ const headDiff = await runGitAllowFailure(["diff", "--no-ext-diff", "--unified=3", "HEAD", "--"], cwd);
31
+ const diffText = headDiff.ok && headDiff.stdout.trim().length > 0
32
+ ? headDiff.stdout
33
+ : await runGit(["diff", "--no-ext-diff", "--unified=3", "--"], cwd);
34
+ return {
35
+ repoRoot,
36
+ statusSummary,
37
+ diffText: truncateDiff(diffText),
38
+ targetDescription: "current working tree changes",
39
+ };
40
+ }
41
+ export async function resolveRepoIdentity(cwd) {
42
+ const workspaceRoot = await realpath(cwd);
43
+ const result = await runGitAllowFailure(["rev-parse", "--show-toplevel", "--git-common-dir"], cwd);
44
+ if (!result.ok) {
45
+ return {
46
+ repoId: sha256(workspaceRoot),
47
+ repoRoot: workspaceRoot,
48
+ gitCommonDir: null,
49
+ isGitRepo: false,
50
+ };
51
+ }
52
+ const [repoRootRaw, gitCommonDirRaw] = result.stdout
53
+ .trim()
54
+ .split("\n")
55
+ .map((line) => line.trim())
56
+ .filter(Boolean);
57
+ const repoRoot = repoRootRaw ? await realpath(repoRootRaw) : workspaceRoot;
58
+ const gitCommonDir = gitCommonDirRaw && gitCommonDirRaw.length > 0
59
+ ? await realpath(path.resolve(cwd, gitCommonDirRaw))
60
+ : null;
61
+ return {
62
+ repoId: sha256(`${gitCommonDir ?? repoRoot}::${repoRoot}`),
63
+ repoRoot,
64
+ gitCommonDir,
65
+ isGitRepo: true,
66
+ };
67
+ }
68
+ async function getBaseDiff(cwd, base) {
69
+ // --end-of-options is a git safety marker (since 2.24) telling git every token after it is
70
+ // a refspec, not a flag. Defense in depth against argument injection via --base.
71
+ const threeDot = await runGitAllowFailure(["diff", "--no-ext-diff", "--unified=3", "--end-of-options", `${base}...HEAD`, "--"], cwd);
72
+ if (threeDot.ok) {
73
+ return threeDot.stdout;
74
+ }
75
+ return runGit(["diff", "--no-ext-diff", "--unified=3", "--end-of-options", base, "--"], cwd);
76
+ }
77
+ async function runGit(args, cwd) {
78
+ const result = await runGitAllowFailure(args, cwd);
79
+ if (!result.ok) {
80
+ throw new RuntimeError("GIT_COMMAND_FAILED", `git ${args.join(" ")} failed: ${result.stderr || "unknown error"}`, "review.git");
81
+ }
82
+ return result.stdout;
83
+ }
84
+ async function runGitAllowFailure(args, cwd) {
85
+ try {
86
+ const { stdout, stderr } = await execFileAsync("git", args, {
87
+ cwd,
88
+ env: process.env,
89
+ maxBuffer: 10 * 1024 * 1024,
90
+ });
91
+ return { ok: true, stdout, stderr };
92
+ }
93
+ catch (error) {
94
+ const execError = error;
95
+ return {
96
+ ok: false,
97
+ stdout: execError.stdout ?? "",
98
+ stderr: execError.stderr ?? execError.message,
99
+ };
100
+ }
101
+ }
102
+ function truncateDiff(diffText) {
103
+ if (diffText.length <= MAX_DIFF_CHARS) {
104
+ return diffText;
105
+ }
106
+ return `${diffText.slice(0, MAX_DIFF_CHARS)}\n\n[diff truncated by companion after ${MAX_DIFF_CHARS} characters]`;
107
+ }
108
+ function sha256(value) {
109
+ return createHash("sha256").update(value).digest("hex");
110
+ }
111
+ // ---------------------------------------------------------------------
112
+ // Ephemeral worktree lifecycle — write-swarm (v1.4).
113
+ //
114
+ // A write-swarm spawns its coordinator `kimi -p` with cwd = a throwaway
115
+ // detached worktree off the user's HEAD, so `coder` subagents edit there and
116
+ // never touch the user's checkout. The worktree is a LINKED worktree: its
117
+ // working tree + index are isolated and disposable, but it SHARES the main
118
+ // repo's object DB and refs. The PreToolUse hook (swarm-write label) confines
119
+ // every subagent write to this worktree; the plugin captures the result as a
120
+ // patch and removes the worktree. See .claude/docs/write-swarm-spec.md.
121
+ // ---------------------------------------------------------------------
122
+ /**
123
+ * True if the repo at `cwd` has a born HEAD (at least one commit). `git worktree
124
+ * add --detach HEAD` fails on an unborn HEAD (fresh repo, no commits) and on a
125
+ * bare repo, so write-swarm gates on this before creating any job state.
126
+ */
127
+ export async function hasBornHead(cwd) {
128
+ const result = await runGitAllowFailure(["rev-parse", "--verify", "--quiet", "HEAD"], cwd);
129
+ return result.ok && result.stdout.trim().length > 0;
130
+ }
131
+ /**
132
+ * Create a throwaway detached worktree at `worktreePath` checked out to `base`
133
+ * (write-swarm hard-codes "HEAD"; never user input). `worktreePath` MUST be
134
+ * outside the repo (the plugin's worktrees dir). Throws GIT_WORKTREE_ADD_FAILED
135
+ * on failure. Caller owns removal (`removeWorktree`) on EVERY terminal path.
136
+ */
137
+ export async function createEphemeralWorktree(repoRoot, base, worktreePath) {
138
+ const result = await runGitAllowFailure(["worktree", "add", "--detach", worktreePath, base], repoRoot);
139
+ if (!result.ok) {
140
+ throw new RuntimeError("GIT_WORKTREE_ADD_FAILED", `git worktree add failed: ${result.stderr || "unknown error"}`, "swarm.worktree", { details: { repoRoot, worktreePath, base } });
141
+ }
142
+ }
143
+ /**
144
+ * Capture the full change set in a worktree as a byte-complete, applyable patch.
145
+ *
146
+ * `git add -N -- .` records intent-to-add index entries for untracked files so
147
+ * the diff includes their content — WITHOUT writing blob objects into the shared
148
+ * object DB (intent-to-add stores a zero-oid entry in the worktree's OWN index
149
+ * only). This keeps the "plugin never mutates git state" invariant literally
150
+ * true: no loose objects, no ref/HEAD movement.
151
+ *
152
+ * The diff is taken against **HEAD** (the worktree's base commit), NOT the index:
153
+ * `git diff HEAD` captures the complete change set regardless of index state, so
154
+ * the patch is invariant to any intermediate staging (e.g. if a tool ever
155
+ * `git add`s a file, a plain `git diff` would silently omit it; `git diff HEAD`
156
+ * still captures it). `--binary` makes binary changes applyable; the diff is
157
+ * NEVER truncated (a capture patch must apply cleanly) and uses a raised
158
+ * maxBuffer. Mirrors `collectReviewContext`'s HEAD-based diff.
159
+ *
160
+ * Returns the patch text (empty string if the swarm changed nothing).
161
+ */
162
+ export async function captureWorktreePatch(worktreePath) {
163
+ // Best-effort intent-to-add so untracked files surface in `git diff HEAD`
164
+ // (without -N, git diff ignores untracked files entirely). A failure here just
165
+ // means new files are omitted from the diff, surfaced as a smaller patch.
166
+ await runGitAllowFailure(["add", "-N", "--", "."], worktreePath);
167
+ try {
168
+ const { stdout } = await execFileAsync("git", ["diff", "--binary", "HEAD", "--"], {
169
+ cwd: worktreePath,
170
+ env: process.env,
171
+ maxBuffer: MAX_PATCH_BYTES,
172
+ });
173
+ return stdout;
174
+ }
175
+ catch (error) {
176
+ const execError = error;
177
+ throw new RuntimeError("GIT_WORKTREE_DIFF_FAILED", `git diff (worktree capture) failed: ${execError.stderr || execError.message || "unknown error"}`, "swarm.worktree", { details: { worktreePath } });
178
+ }
179
+ }
180
+ /**
181
+ * Remove a worktree (best-effort, idempotent). `--force` discards the dirty tree,
182
+ * so callers MUST capture the patch BEFORE calling this. Falls back to pruning the
183
+ * admin entry and removing the on-disk dir if `worktree remove` cannot (e.g. the
184
+ * dir was partially deleted). Never throws — cleanup must not mask the real error
185
+ * on a failure path.
186
+ */
187
+ export async function removeWorktree(repoRoot, worktreePath) {
188
+ const removed = await runGitAllowFailure(["worktree", "remove", "--force", worktreePath], repoRoot);
189
+ if (!removed.ok) {
190
+ // Prune the admin entry (handles the case where the dir is already gone) then
191
+ // force-delete any on-disk remnant so no orphan accretes in the plugin dir.
192
+ await runGitAllowFailure(["worktree", "prune"], repoRoot);
193
+ await rm(worktreePath, { recursive: true, force: true }).catch(() => {
194
+ // Best-effort; a residual dir is swept on the next run.
195
+ });
196
+ }
197
+ }
198
+ /**
199
+ * Prune stale worktree admin entries (entries whose on-disk dir is gone). Cheap;
200
+ * run at the start of every write-swarm. Does NOT remove on-disk worktrees whose
201
+ * dirs still exist — the caller sweeps those by path. Never throws.
202
+ */
203
+ export async function pruneWorktrees(repoRoot) {
204
+ await runGitAllowFailure(["worktree", "prune"], repoRoot);
205
+ }
206
+ /**
207
+ * True if the working tree at `cwd` has uncommitted changes (tracked or
208
+ * untracked). write-swarm bases its worktree on HEAD, so a dirty tree means the
209
+ * user's in-progress work is NOT visible to the swarm — the caller warns. Returns
210
+ * false on a git failure (don't block the run on a status probe).
211
+ */
212
+ export async function isWorkingTreeDirty(cwd) {
213
+ const result = await runGitAllowFailure(["status", "--porcelain", "--untracked-files=all"], cwd);
214
+ return result.ok && result.stdout.trim().length > 0;
215
+ }
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ // Entry script for the kimi-code PreToolUse hook.
3
+ //
4
+ // Installed by /kimi:setup as a managed block in ~/.kimi-code/config.toml:
5
+ //
6
+ // [[hooks]]
7
+ // event = "PreToolUse"
8
+ // command = "node /abs/path/to/dist/hooks/approval-hook.js"
9
+ // timeout = 15
10
+ //
11
+ // Hook protocol (kimi-code agent-core/src/session/hooks/runner.ts —
12
+ // path was agent/hooks/ in 0.4.0 and earlier; relocated to session/hooks/
13
+ // in 0.5.0 with byte-identical file contents):
14
+ //
15
+ // stdin: full hook payload as JSON
16
+ // stdout: optional structured JSON (only consulted when exit code is 0)
17
+ // stderr: free-form reason
18
+ // exit 0: allow (with optional structured override)
19
+ // exit 2: deny — `stderr.trim()` becomes the reason surfaced to the model
20
+ // any other exit code: fall through to allow (fail-open)
21
+ //
22
+ // We use exit-code signaling rather than structured JSON because:
23
+ //
24
+ // 1. Exit 2 + stderr is the simpler contract — fewer surfaces for a
25
+ // version skew between us and kimi-code to break safety.
26
+ // 2. The runner short-circuits exit-2 before structuredOutput() is
27
+ // called, so emitting both would be redundant.
28
+ // 3. A crash in our hook would normally exit non-zero and silently
29
+ // allow (fail-open). We compensate by wrapping every code path in
30
+ // try/catch and exiting 2 with a "hook misconfigured" reason on
31
+ // any unexpected error.
32
+ //
33
+ // Process boundary:
34
+ //
35
+ // kimi-code spawns this hook with `shell: true` (see runner.ts:46-53,
36
+ // where `spawn(command, { shell: true, ... })` is invoked).
37
+ // The TOML command is parsed by `/bin/sh -c "..."` so the resolved
38
+ // `node /abs/path/...` works regardless of where it's invoked from.
39
+ import { decideHookOutcome } from "./approval-policy.js";
40
+ import { evaluateRescueHookRequest } from "../rescue-approval.js";
41
+ const STDIN_TIMEOUT_MS = 5_000;
42
+ async function main() {
43
+ const stdinText = await readStdinWithTimeout(STDIN_TIMEOUT_MS);
44
+ let input;
45
+ try {
46
+ const parsed = JSON.parse(stdinText);
47
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
48
+ throw new Error("hook payload must be a JSON object");
49
+ }
50
+ input = parsed;
51
+ }
52
+ catch (err) {
53
+ failClosed(`stdin was not a JSON object: ${formatErr(err)}`);
54
+ return;
55
+ }
56
+ const decision = await decideHookOutcome(input, {
57
+ commandLabel: process.env.KIMI_PLUGIN_CC_CMD,
58
+ operationKind: process.env.KIMI_PLUGIN_CC_OPERATION,
59
+ rescueEvaluator: evaluateRescueHookRequest,
60
+ // Trusted root for rescue/pursue/swarm-write. Set by the plugin spawn; the
61
+ // model inside kimi cannot forge it and hook payload cwd is not trusted.
62
+ trustedWorkspaceRoot: process.env.KIMI_PLUGIN_CC_WORKSPACE_ROOT,
63
+ });
64
+ if (decision.decision === "deny") {
65
+ // stderr.trim() in runner.ts:118 becomes the model-visible reason.
66
+ process.stderr.write(`${decision.reason ?? "denied"}\n`);
67
+ process.exit(2);
68
+ return;
69
+ }
70
+ process.exit(0);
71
+ }
72
+ function failClosed(reason) {
73
+ process.stderr.write(`kimi-plugin-cc safety hook misconfigured: ${reason}\n`);
74
+ process.exit(2);
75
+ }
76
+ function formatErr(err) {
77
+ return err instanceof Error ? err.message : String(err);
78
+ }
79
+ function readStdinWithTimeout(timeoutMs) {
80
+ return new Promise((resolve, reject) => {
81
+ let buf = "";
82
+ let settled = false;
83
+ const finish = (value) => {
84
+ if (settled)
85
+ return;
86
+ settled = true;
87
+ clearTimeout(timer);
88
+ if (value instanceof Error)
89
+ reject(value);
90
+ else
91
+ resolve(value);
92
+ };
93
+ const timer = setTimeout(() => {
94
+ finish(new Error(`stdin did not close within ${timeoutMs}ms`));
95
+ }, timeoutMs);
96
+ timer.unref();
97
+ try {
98
+ process.stdin.setEncoding("utf8");
99
+ }
100
+ catch {
101
+ // some test harnesses pre-encode; fine to ignore
102
+ }
103
+ process.stdin.on("data", (chunk) => {
104
+ buf += typeof chunk === "string" ? chunk : chunk.toString("utf8");
105
+ });
106
+ process.stdin.on("end", () => finish(buf));
107
+ process.stdin.on("error", (err) => finish(err instanceof Error ? err : new Error(String(err))));
108
+ });
109
+ }
110
+ main().catch((err) => {
111
+ failClosed(`uncaught error: ${formatErr(err)}`);
112
+ });