* test(#2755): failing-first coverage for per-runtime kimi hooks root Install/uninstall filesystem-shape rows over a sandbox HOME (no permission tricks) plus resolver unit rows. Covers both uninstall directions, which is where a fix applied only to the install call site would drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2755): resolve the kimi hooks-TOML root per runtime resolveKimiHooksTomlDir took no runtime argument and hardcoded ~/.kimi, but both kimi and kimi-code route through the single hooksSurface=kimi-hooks-toml branch. A --kimi-code install therefore wrote its [[hooks]] block, hook bundle and CommonJS marker into Kimi CLI's config file, and a --kimi-code uninstall stripped Kimi CLI's block. Adds a runtime selector to the resolver -- kimi keeps ~/.kimi + KIMI_SHARE_DIR, kimi-code gets ~/.kimi-code + KIMI_CODE_HOME, per Kimi Code's own upstream data-locations and hooks docs -- and passes the runtime at both the install and uninstall call sites. An omitted or unrecognized runtime still resolves ~/.kimi, so the exported no-arg contract is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#2755): use centralized helpers and add a divergence guard Review findings, all fixed in-PR: - The new test block reimplemented runMinimalInstall, createTempDir and toPosixPath. Extends runMinimalInstall with optional root/extraEnv instead (back-compat: every existing caller passes neither) and uses the centralized helpers, per CONTRIBUTING's Use Centralized Test Helpers rule. - Adds a parity assertion between the capability registry and the resolver: a third runtime declaring hooksSurface kimi-hooks-toml would silently inherit ~/.kimi, re-creating this very defect. The guard fires the moment those two surfaces drift. - Adds an installer-level test proving KIMI_SHARE_DIR and KIMI_CODE_HOME do not interfere when both are set, which only the resolver unit covered before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#2755): track the kimi-code hooks root in the emitted-artifact gates The remote runner caught a real ripple: moving kimi-code hooks to ~/.kimi-code made 31 emitted paths unattributable and 58 emitted hashes unexplained, because three parallel surfaces keyed on the literal .kimi path. - HOOK_CONFIG_RELATIVE_PATHS excluded only .kimi/config.toml, so kimi-code's config.toml became manifest-visible; it embeds a platform-varying node-runner command and must stay out for both products. - HOOKS_ROOTS, the package.json-marker branch and the synthesized-install-metadata pattern each named .kimi only. - tests/fixtures/install-tree/kimi-code.json still recorded the old paths; regenerated via gen:install-tree. Adds the per-PR drift acknowledgment for the 58 paths whose bytes are unchanged but whose destination moved - a ripple no source diff can show, since no hook script was edited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2755): clear production-tree security advisories The remote runner's npm-integrity gate reported 2 high advisories in the production dependency tree. My diff touches neither package.json nor package-lock.json, so these come from the base -- but a red gate is not something to wave off as pre-existing, so it is fixed here rather than deferred. Lockfile-only, semver-in-range, via npm audit fix: fast-uri 3.1.4 -> 3.1.5 (host confusion via backslash authority introducer) ip-address 10.2.0 -> 10.4.0 (three SSRF / trust-boundary bypasses) hono 4.12.31 -> 4.13.0 (moderate; reverting it traded a high for a moderate, so the full remedy is taken) npm audit now reports 0 vulnerabilities at every severity, npm ci installs clean from the updated lockfile, and the build and the kimi behavior both re-verified afterwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#2755): backfill changeset pr numbers Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/mellow-pandas-rally.md
Normal file
5
.changeset/mellow-pandas-rally.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Security
|
||||
pr: 3032
|
||||
---
|
||||
**Production dependency tree is clear of known advisories** — three transitive packages reached by `@anthropic-ai/claude-agent-sdk` carried published advisories: `fast-uri` (host confusion via a backslash authority introducer), `ip-address` (three SSRF / trust-boundary bypasses via leading-zero octets, CIDR-suffix suppression, and IPv4-mapped address misclassification), and `hono`. All three are lockfile-only, semver-in-range updates. (#2755)
|
||||
5
.changeset/sturdy-zebras-purr.md
Normal file
5
.changeset/sturdy-zebras-purr.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3032
|
||||
---
|
||||
**A `--kimi-code` install now configures hooks in Kimi Code, not Kimi CLI** — installing GSD for Kimi Code wrote its lifecycle hooks, hook bundle and CommonJS marker into Kimi CLI's `~/.kimi/config.toml`, so Kimi Code itself received no hooks at all and a machine with only Kimi Code got a config file no product reads. Each Kimi product now uses its own root and its own environment override (`KIMI_SHARE_DIR` for Kimi CLI, `KIMI_CODE_HOME` for Kimi Code), and uninstalling one no longer removes the other's hooks. (#2755)
|
||||
@@ -8082,11 +8082,12 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
|
||||
|
||||
// 1a-kimi. Non-layout Kimi side-effect (#2095 EoS/kimi Upgrade 1): kimi's
|
||||
// native config.toml lives outside targetDir entirely (resolveKimiHooksTomlDir
|
||||
// resolves ~/.kimi, a sibling of targetDir's ~/.config/agents), so its
|
||||
// resolves ~/.kimi for kimi and ~/.kimi-code for kimi-code (#2755), a sibling
|
||||
// of targetDir's ~/.config/agents), so its
|
||||
// cleanup can't be driven by anything under targetDir the way every other
|
||||
// hook surface above is.
|
||||
if (resolveInstallPlan(runtime).hooksSurface === 'kimi-hooks-toml') {
|
||||
const kimiHooksRoot = resolveKimiHooksTomlDir();
|
||||
const kimiHooksRoot = resolveKimiHooksTomlDir({ runtime });
|
||||
const kimiHooksTomlPath = path.join(kimiHooksRoot, 'config.toml');
|
||||
const kimiHooksCleanup = removeKimiHooksToml(kimiHooksTomlPath);
|
||||
if (kimiHooksCleanup.changed) {
|
||||
@@ -11924,7 +11925,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
||||
// hooks needed. Kimi is also artifact-only for its INSTALL surface (skills +
|
||||
// kimi-agents, no settings.json) but #2095 Upgrade 1 gives it its own
|
||||
// independent hooksSurface: kimi's native config.toml [[hooks]] array, which
|
||||
// lives outside targetDir entirely (resolveKimiHooksTomlDir resolves ~/.kimi,
|
||||
// lives outside targetDir entirely (resolveKimiHooksTomlDir resolves the
|
||||
// per-runtime root — ~/.kimi for kimi, ~/.kimi-code for kimi-code, #2755 —
|
||||
// a sibling of targetDir's ~/.config/agents) — hence writing it here, inside
|
||||
// this early-return, rather than requiring installSurface to change.
|
||||
//
|
||||
@@ -11945,7 +11947,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
||||
// ~/.kimi/hooks/<script> rather than a script that doesn't exist under
|
||||
// targetDir/hooks (which kimi no longer receives).
|
||||
if (plan.hooksSurface === 'kimi-hooks-toml' && isGlobal) {
|
||||
const kimiHooksRoot = resolveKimiHooksTomlDir();
|
||||
const kimiHooksRoot = resolveKimiHooksTomlDir({ runtime });
|
||||
// Note: the `failures` array's hard-fail gate (`if (failures.length > 0)
|
||||
// process.exit(1)`) runs earlier in this function, before this
|
||||
// profile-marker-only branch is ever reached — pushing to it here would
|
||||
@@ -11961,7 +11963,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
|
||||
//
|
||||
// Done HERE rather than in installer-migration 007 (which retires the same
|
||||
// stale marker for every other runtime) because kimi's hook root is
|
||||
// ~/.kimi — resolved by resolveKimiHooksTomlDir, OUTSIDE kimi's configDir.
|
||||
// the per-runtime Kimi root — resolved by resolveKimiHooksTomlDir, OUTSIDE the configDir.
|
||||
// Migration relPaths are structurally confined to configDir, so the
|
||||
// framework cannot address this path at all. Same exact-content predicate
|
||||
// either way, so a user-authored ~/.kimi/package.json is never touched.
|
||||
|
||||
@@ -650,6 +650,8 @@ Documentation gaps:
|
||||
|
||||
> **`kimi-code` is a different product from `kimi` above — every axis below is sourced independently.** `kimi` is Moonshot's Python `kimi-cli` (`from kimi_cli.app import KimiCLI`, `~/.kimi/config.toml`, `--work-dir`); `kimi-code` is the TypeScript/Node **Kimi Code CLI** (`~/.kimi-code/config.toml`, `$KIMI_CODE_HOME`, process-cwd, `AgentSwarm`). None of the values here are inherited from the `kimi` section. See `docs/migration/kimi-to-kimi-code.md` for the user-facing split.
|
||||
|
||||
**GSD hook destination (#2755):** although `kimi` and `kimi-code` share `hooksSurface: "kimi-hooks-toml"`, they do **not** share a hooks root. GSD writes its `[[hooks]]` block, hook bundle and CommonJS marker into `~/.kimi-code/config.toml` for `kimi-code` (overridable via `KIMI_CODE_HOME`) and into `~/.kimi/config.toml` for `kimi` (overridable via `KIMI_SHARE_DIR`); each product's env var is scoped to that product and does not redirect the other. `resolveKimiHooksTomlDir({ runtime })` in `src/runtime-homes.cts` is the single seam that makes this choice, for both the install and uninstall paths. Until #2755 that function was unparameterized and every `kimi-code` install wrote into Kimi CLI's root, leaving Kimi Code with no hooks — this row is documented explicitly because its absence is what let that go unnoticed.
|
||||
|
||||
| Axis | Value | Source | Evidence |
|
||||
|---|---|---|---|
|
||||
| embeddingMode | declarative | https://github.com/moonshotai/kimi-code/blob/main/docs/en/customization/plugins.md | "Plugins package reusable Kimi Code CLI capabilities into installable units — they can add Agent Skills, automatically load a specified Skill at session start, and declare MCP servers to provide real tool capabilities." A plugin is a `kimi.plugin.json` manifest plus markdown Skills; real tool capability arrives via external MCP processes. No in-process programmatic extension API is documented — the same shape as `codex` above. |
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -3206,9 +3206,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
|
||||
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
|
||||
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -3573,9 +3573,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.31",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz",
|
||||
"integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==",
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.0.tgz",
|
||||
"integrity": "sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -3678,9 +3678,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz",
|
||||
"integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
|
||||
@@ -55,6 +55,16 @@ export interface ResolveKimiOpts {
|
||||
existsSync?: (p: string) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for `resolveKimiHooksTomlDir`. Separate from `ResolveKimiOpts` so the
|
||||
* `runtime` selector is not implied to affect `resolveKimiGlobalDir`, which
|
||||
* resolves the generic Agent-Skills root and is runtime-independent.
|
||||
*/
|
||||
export interface ResolveKimiHooksTomlOpts extends ResolveKimiOpts {
|
||||
/** Runtime id — `kimi` (default) or `kimi-code`. See #2755. */
|
||||
runtime?: string;
|
||||
}
|
||||
|
||||
export interface ResolveConfigHomeOpts {
|
||||
env?: Record<string, string | undefined>;
|
||||
home?: string;
|
||||
@@ -370,28 +380,45 @@ export function resolveKimiGlobalDir(opts: ResolveKimiOpts = {}): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the directory holding Kimi CLI's OWN native config.toml (the file
|
||||
* Kimi itself reads for providers/models/hooks/etc — see
|
||||
* moonshotai.github.io/kimi-cli/en/configuration/data-locations.html and
|
||||
* .../reference/kimi-command.html). Default `~/.kimi`, overridden by
|
||||
* `KIMI_SHARE_DIR` per Kimi's own upstream env-var (NOT `KIMI_CONFIG_DIR`,
|
||||
* which is a GSD-installer write-location override for the unrelated generic
|
||||
* Agent-Skills root resolved by resolveKimiGlobalDir above).
|
||||
* Resolve the directory holding the Kimi product's OWN native config.toml —
|
||||
* the file that product itself reads for providers/models/hooks/etc, and the
|
||||
* one GSD writes its `[[hooks]]` block, hooks bundle and CommonJS marker into.
|
||||
*
|
||||
* This is deliberately a SEPARATE directory from GSD's kimi configHome
|
||||
* (~/.config/agents): Kimi's own docs confirm the Agent-Skills search path is
|
||||
* independent of KIMI_SHARE_DIR ("This variable does not affect Agent Skills
|
||||
* search paths, which are handled separately"). #2095 Upgrade 1 writes GSD's
|
||||
* native [[hooks]] entries into `<this dir>/config.toml`, never into the
|
||||
* skills configDir.
|
||||
* The two Kimi runtimes share `hooksSurface: "kimi-hooks-toml"` but are
|
||||
* different products with different roots, and this must be selected by
|
||||
* `runtime` (#2755). Before that fix this function was unparameterized and a
|
||||
* `--kimi-code` install wrote its hooks into Kimi CLI's `~/.kimi`, leaving Kimi
|
||||
* Code with none:
|
||||
*
|
||||
* kimi → `~/.kimi`, overridden by `KIMI_SHARE_DIR`
|
||||
* (moonshotai.github.io/kimi-cli/en/configuration/data-locations.html)
|
||||
* kimi-code → `~/.kimi-code`, overridden by `KIMI_CODE_HOME`
|
||||
* (moonshotai/kimi-code docs/en/configuration/data-locations.md;
|
||||
* its hooks doc places `[[hooks]]` in `~/.kimi-code/config.toml`)
|
||||
*
|
||||
* Each product's env var is scoped to that product: `KIMI_SHARE_DIR` is Kimi
|
||||
* CLI's own upstream variable and must NOT redirect kimi-code, nor vice versa.
|
||||
*
|
||||
* An unrecognised `runtime` (and an omitted one) falls back to `~/.kimi`, which
|
||||
* preserves the pre-#2755 behaviour for every existing caller that passes no
|
||||
* runtime — this function is exported, so that default is a contract.
|
||||
*
|
||||
* For BOTH runtimes this is deliberately a SEPARATE directory from the generic
|
||||
* Agent-Skills root resolved by `resolveKimiGlobalDir` (`~/.config/agents`):
|
||||
* both vendors' docs confirm the Agent-Skills search path is independent of the
|
||||
* data-root env var. GSD's native `[[hooks]]` entries go in
|
||||
* `<this dir>/config.toml`, never into the skills configDir.
|
||||
*/
|
||||
export function resolveKimiHooksTomlDir(opts: ResolveKimiOpts = {}): string {
|
||||
export function resolveKimiHooksTomlDir(opts: ResolveKimiHooksTomlOpts = {}): string {
|
||||
const env: Record<string, string | undefined> = opts.env ?? process.env;
|
||||
const home = opts.home ?? os.homedir();
|
||||
return resolveConfigHomeFromDescriptor(
|
||||
{ kind: 'dot-home', name: '.kimi', env: ['KIMI_SHARE_DIR'] },
|
||||
{ env, home },
|
||||
);
|
||||
// Explicit comparison rather than an object lookup keyed on `runtime`: the
|
||||
// value originates from argv, and an index would resolve inherited keys
|
||||
// (`constructor`, `__proto__`) to something that is not a descriptor.
|
||||
const descriptor: DotHomeDescriptor = opts.runtime === 'kimi-code'
|
||||
? { kind: 'dot-home', name: '.kimi-code', env: ['KIMI_CODE_HOME'] }
|
||||
: { kind: 'dot-home', name: '.kimi', env: ['KIMI_SHARE_DIR'] };
|
||||
return resolveConfigHomeFromDescriptor(descriptor, { env, home });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
63
tests/emitted-drift-acks/2755-kimi-code-hooks-root.json
Normal file
63
tests/emitted-drift-acks/2755-kimi-code-hooks-root.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"version": 1,
|
||||
"paths": {
|
||||
".kimi-code/hooks/gsd-check-update-worker.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-check-update.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-config-reload.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-context-monitor.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-cursor-post-tool.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-cursor-pre-tool.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-cursor-session-start.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-cursor-stop.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-cursor-subagent-start.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-cursor-subagent-stop.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-ensure-canonical-path.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-graphify-update.sh": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-phase-boundary.sh": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-prompt-guard.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-read-guard.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-read-injection-scanner.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-session-state.sh": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-statusline.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-update-banner.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-validate-commit.sh": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-windsurf-pre-command.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-windsurf-pre-write.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-workflow-guard.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-worktree-path-guard.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/gsd-write-guard.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/lib/cursor-workspace.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/lib/git-cmd.js": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/lib/gsd-graphify-rebuild.sh": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi-code/hooks/managed-hooks-registry.cjs": "#2755: kimi-code installs its hook bundle into Kimi Code own root (~/.kimi-code) instead of Kimi CLI ~/.kimi. The script bytes are unchanged - identical to what ~/.kimi previously received - so no source file under hooks/ moved and the diff cannot attribute the ripple. Only the install destination changed, at resolveKimiHooksTomlDir in src/runtime-homes.cts.",
|
||||
".kimi/hooks/gsd-check-update-worker.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-check-update.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-config-reload.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-context-monitor.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-cursor-post-tool.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-cursor-pre-tool.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-cursor-session-start.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-cursor-stop.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-cursor-subagent-start.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-cursor-subagent-stop.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-ensure-canonical-path.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-graphify-update.sh": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-phase-boundary.sh": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-prompt-guard.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-read-guard.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-read-injection-scanner.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-session-state.sh": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-statusline.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-update-banner.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-validate-commit.sh": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-windsurf-pre-command.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-windsurf-pre-write.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-workflow-guard.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-worktree-path-guard.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/gsd-write-guard.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/lib/cursor-workspace.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/lib/git-cmd.js": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/lib/gsd-graphify-rebuild.sh": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR.",
|
||||
".kimi/hooks/managed-hooks-registry.cjs": "#2755: the mirror of the .kimi-code entries above. kimi-code no longer emits into Kimi CLI native root at all, so this path disappears from the kimi-code manifest. Content unchanged; kimi own manifest still emits the same path and is untouched by this PR."
|
||||
}
|
||||
}
|
||||
60
tests/fixtures/install-tree/kimi-code.json
vendored
60
tests/fixtures/install-tree/kimi-code.json
vendored
@@ -1,36 +1,36 @@
|
||||
[
|
||||
".gsd-profile",
|
||||
".gsd/defaults.json",
|
||||
".kimi/hooks/gsd-check-update-worker.js",
|
||||
".kimi/hooks/gsd-check-update.js",
|
||||
".kimi/hooks/gsd-config-reload.js",
|
||||
".kimi/hooks/gsd-context-monitor.js",
|
||||
".kimi/hooks/gsd-cursor-post-tool.js",
|
||||
".kimi/hooks/gsd-cursor-pre-tool.js",
|
||||
".kimi/hooks/gsd-cursor-session-start.js",
|
||||
".kimi/hooks/gsd-cursor-stop.js",
|
||||
".kimi/hooks/gsd-cursor-subagent-start.js",
|
||||
".kimi/hooks/gsd-cursor-subagent-stop.js",
|
||||
".kimi/hooks/gsd-ensure-canonical-path.js",
|
||||
".kimi/hooks/gsd-graphify-update.sh",
|
||||
".kimi/hooks/gsd-phase-boundary.sh",
|
||||
".kimi/hooks/gsd-prompt-guard.js",
|
||||
".kimi/hooks/gsd-read-guard.js",
|
||||
".kimi/hooks/gsd-read-injection-scanner.js",
|
||||
".kimi/hooks/gsd-session-state.sh",
|
||||
".kimi/hooks/gsd-statusline.js",
|
||||
".kimi/hooks/gsd-update-banner.js",
|
||||
".kimi/hooks/gsd-validate-commit.sh",
|
||||
".kimi/hooks/gsd-windsurf-pre-command.js",
|
||||
".kimi/hooks/gsd-windsurf-pre-write.js",
|
||||
".kimi/hooks/gsd-workflow-guard.js",
|
||||
".kimi/hooks/gsd-worktree-path-guard.js",
|
||||
".kimi/hooks/gsd-write-guard.js",
|
||||
".kimi/hooks/lib/cursor-workspace.js",
|
||||
".kimi/hooks/lib/git-cmd.js",
|
||||
".kimi/hooks/lib/gsd-graphify-rebuild.sh",
|
||||
".kimi/hooks/managed-hooks-registry.cjs",
|
||||
".kimi/hooks/package.json",
|
||||
".kimi-code/hooks/gsd-check-update-worker.js",
|
||||
".kimi-code/hooks/gsd-check-update.js",
|
||||
".kimi-code/hooks/gsd-config-reload.js",
|
||||
".kimi-code/hooks/gsd-context-monitor.js",
|
||||
".kimi-code/hooks/gsd-cursor-post-tool.js",
|
||||
".kimi-code/hooks/gsd-cursor-pre-tool.js",
|
||||
".kimi-code/hooks/gsd-cursor-session-start.js",
|
||||
".kimi-code/hooks/gsd-cursor-stop.js",
|
||||
".kimi-code/hooks/gsd-cursor-subagent-start.js",
|
||||
".kimi-code/hooks/gsd-cursor-subagent-stop.js",
|
||||
".kimi-code/hooks/gsd-ensure-canonical-path.js",
|
||||
".kimi-code/hooks/gsd-graphify-update.sh",
|
||||
".kimi-code/hooks/gsd-phase-boundary.sh",
|
||||
".kimi-code/hooks/gsd-prompt-guard.js",
|
||||
".kimi-code/hooks/gsd-read-guard.js",
|
||||
".kimi-code/hooks/gsd-read-injection-scanner.js",
|
||||
".kimi-code/hooks/gsd-session-state.sh",
|
||||
".kimi-code/hooks/gsd-statusline.js",
|
||||
".kimi-code/hooks/gsd-update-banner.js",
|
||||
".kimi-code/hooks/gsd-validate-commit.sh",
|
||||
".kimi-code/hooks/gsd-windsurf-pre-command.js",
|
||||
".kimi-code/hooks/gsd-windsurf-pre-write.js",
|
||||
".kimi-code/hooks/gsd-workflow-guard.js",
|
||||
".kimi-code/hooks/gsd-worktree-path-guard.js",
|
||||
".kimi-code/hooks/gsd-write-guard.js",
|
||||
".kimi-code/hooks/lib/cursor-workspace.js",
|
||||
".kimi-code/hooks/lib/git-cmd.js",
|
||||
".kimi-code/hooks/lib/gsd-graphify-rebuild.sh",
|
||||
".kimi-code/hooks/managed-hooks-registry.cjs",
|
||||
".kimi-code/hooks/package.json",
|
||||
"agents/gsd-advisor-researcher.md",
|
||||
"agents/gsd-ai-researcher.md",
|
||||
"agents/gsd-assumptions-analyzer.md",
|
||||
|
||||
@@ -65,7 +65,7 @@ const EXPECTED_MANIFEST_COUNT = MANIFEST_FAMILIES.length;
|
||||
// `skills/gsd/...`, and `.agents/skills` must win over `.agents`.
|
||||
|
||||
const SKILLS_ROOTS = ['skills/gsd', '.agents/skills', 'skills'];
|
||||
const HOOKS_ROOTS = ['.kimi/hooks', 'hooks'];
|
||||
const HOOKS_ROOTS = ['.kimi-code/hooks', '.kimi/hooks', 'hooks'];
|
||||
|
||||
/** Source-of-truth command dir every skill/command surface converts from. */
|
||||
const COMMANDS_SRC = 'commands/gsd';
|
||||
@@ -423,7 +423,11 @@ const PROVENANCE_RULES = [
|
||||
if (root === 'plugins' || root === 'extensions') {
|
||||
return [COMMONJS_MARKER_SRC, INSTALL_ENGINE_SRC];
|
||||
}
|
||||
if (root === '.kimi/hooks') return [COMMONJS_MARKER_SRC, INSTALLER_SRC];
|
||||
// Both Kimi products install the shared bundle into their own native hook
|
||||
// root rather than under the generic Agent-Skills configDir (#2755).
|
||||
if (root === '.kimi/hooks' || root === '.kimi-code/hooks') {
|
||||
return [COMMONJS_MARKER_SRC, INSTALLER_SRC];
|
||||
}
|
||||
// 'hooks' — written by the shared bundle for most runtimes and by the
|
||||
// #2717 dedicated paths for cursor/windsurf/codex.
|
||||
return [COMMONJS_MARKER_SRC, INSTALLER_SRC, HOOKS_WINDOWS_SHIM_SRC];
|
||||
@@ -528,10 +532,11 @@ const PROVENANCE_RULES = [
|
||||
id: 'synthesized-install-metadata',
|
||||
kind: 'synthesized',
|
||||
roots: null,
|
||||
// `.kimi/package.json` is the same literal `{"type":"commonjs"}` CommonJS-mode
|
||||
// marker as the root one, written into Kimi's separate hooks root
|
||||
// (installSharedHooksBundle, install.js:11044-11046).
|
||||
pattern: /^(\.gsd-profile|package\.json|\.kimi\/package\.json|gsd-core\/VERSION|gsd-core\/\.gsd-runtime)$/,
|
||||
// `.kimi/package.json` and `.kimi-code/package.json` are the same literal
|
||||
// `{"type":"commonjs"}` CommonJS-mode marker as the root one, written into
|
||||
// each Kimi product's separate hooks root (installSharedHooksBundle; the
|
||||
// per-runtime root split is #2755).
|
||||
pattern: /^(\.gsd-profile|package\.json|\.kimi(-code)?\/package\.json|gsd-core\/VERSION|gsd-core\/\.gsd-runtime)$/,
|
||||
sources: () => [],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -184,7 +184,11 @@ const HOOK_CONFIG_FILES = new Set(['settings.json', 'settings.local.json', 'hook
|
||||
// same temp root used as --config-dir, collapsing the two into one directory
|
||||
// for the isolated test run. So it is excluded by its exact relative path
|
||||
// under that collapsed root, not by basename.
|
||||
const HOOK_CONFIG_RELATIVE_PATHS = new Set(['.kimi/config.toml']);
|
||||
// Both Kimi products' native config.toml embeds a platform-varying node-runner
|
||||
// command, so neither belongs in the golden-tracked emitted manifest. kimi-code
|
||||
// resolves its own root since #2755 — listing only `.kimi/config.toml` here made
|
||||
// kimi-code's config.toml newly manifest-visible and unattributable.
|
||||
const HOOK_CONFIG_RELATIVE_PATHS = new Set(['.kimi/config.toml', '.kimi-code/config.toml']);
|
||||
|
||||
// Path prefixes excluded from the parity manifest. `gsd-core/bin/lib/` holds the
|
||||
// tsc-built runtime artifacts (compiled from src/*.cts) that the install COPIES
|
||||
@@ -521,9 +525,14 @@ function installerEnv(overrides = {}) {
|
||||
* measure a DIFFERENT tree's installer — e.g. the differential baseline builder
|
||||
* pointing at a `git worktree` checked out at the base ref, so the emitted manifest it
|
||||
* produces reflects that ref's own installer code, not the PR checkout's.
|
||||
* @param {string} [opts.root] - Reuse an existing sandbox HOME instead of creating one.
|
||||
* When supplied, the caller owns its lifetime and it is NOT removed on failure.
|
||||
* @param {object} [opts.extraEnv] - Extra environment variables merged over the
|
||||
* installer env (after HOME/USERPROFILE), e.g. a runtime's config-home override.
|
||||
*/
|
||||
function runMinimalInstall({ runtime, scope, extraArgs = [], installScript = INSTALL_SCRIPT }) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-${runtime}-${scope}-`));
|
||||
function runMinimalInstall({ runtime, scope, extraArgs = [], installScript = INSTALL_SCRIPT, root: providedRoot = null, extraEnv = {} }) {
|
||||
const ownsRoot = providedRoot === null;
|
||||
const root = providedRoot ?? fs.mkdtempSync(path.join(os.tmpdir(), `gsd-${runtime}-${scope}-`));
|
||||
try {
|
||||
const LOCAL_DIR_NAME = {
|
||||
claude: '.claude', opencode: '.opencode', kilo: '.kilo',
|
||||
@@ -545,7 +554,7 @@ function runMinimalInstall({ runtime, scope, extraArgs = [], installScript = INS
|
||||
args.push(...extraArgs);
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
cwd, encoding: 'utf8',
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root }),
|
||||
env: installerEnv({ HOME: root, USERPROFILE: root, ...extraEnv }),
|
||||
});
|
||||
assert.strictEqual(result.status, 0,
|
||||
`installer exited with status ${result.status} for ${runtime} --${scope}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
@@ -555,7 +564,10 @@ function runMinimalInstall({ runtime, scope, extraArgs = [], installScript = INS
|
||||
: null;
|
||||
return { manifest, configDir, root, stdout: result.stdout, stderr: result.stderr };
|
||||
} catch (err) {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
// Only reclaim a root this call created. A caller-supplied root may be
|
||||
// shared across several installs (e.g. two runtimes into one HOME), so
|
||||
// tearing it down here would destroy the caller's other fixtures.
|
||||
if (ownsRoot) fs.rmSync(root, { recursive: true, force: true });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const { runMinimalInstall, INSTALL_SCRIPT, installerEnv } = require('./helpers/install-shared.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { cleanup, createTempDir, toPosixPath } = require('./helpers.cjs');
|
||||
const {
|
||||
negotiateHostCapabilities,
|
||||
shouldFlattenDispatch,
|
||||
@@ -324,3 +324,166 @@ test('boundary: every capability-declared extendedHookEvent is wired as a real e
|
||||
`base claude-dialect event ${event} must also be wired for kimi`);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2755: the hooks-TOML root is per-runtime, not a shared ~/.kimi
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Whether GSD's managed block is present, decided by the module's OWN parser
|
||||
* rather than a substring probe: stripping is a no-op exactly when there is no
|
||||
* block to strip.
|
||||
*/
|
||||
function hasGsdHooksBlock(tomlPath) {
|
||||
if (!fs.existsSync(tomlPath)) return false;
|
||||
const content = fs.readFileSync(tomlPath, 'utf8');
|
||||
return stripKimiHooksTomlBlock(content) !== content;
|
||||
}
|
||||
|
||||
/** Spawn the real installer for one Kimi variant against a shared sandbox HOME. */
|
||||
function runKimiInstall(root, runtime, { extraEnv = {}, uninstall = false } = {}) {
|
||||
return runMinimalInstall({
|
||||
runtime,
|
||||
scope: 'global',
|
||||
root,
|
||||
extraEnv,
|
||||
extraArgs: uninstall ? ['--uninstall'] : [],
|
||||
});
|
||||
}
|
||||
|
||||
function sandboxHome(t, prefix = 'gsd-2755-') {
|
||||
const root = createTempDir(prefix);
|
||||
t.after(() => cleanup(root));
|
||||
return root;
|
||||
}
|
||||
|
||||
describe('kimi vs kimi-code hooks-TOML root (#2755)', () => {
|
||||
test('--kimi-code --global writes its hooks into ~/.kimi-code and never creates ~/.kimi', (t) => {
|
||||
const root = sandboxHome(t);
|
||||
runKimiInstall(root, 'kimi-code');
|
||||
|
||||
assert.ok(hasGsdHooksBlock(path.join(root, '.kimi-code', 'config.toml')),
|
||||
"the GSD [[hooks]] block must land in Kimi Code's own config.toml");
|
||||
assert.ok(!fs.existsSync(path.join(root, '.kimi')),
|
||||
"a --kimi-code install must not create Kimi CLI's ~/.kimi root at all");
|
||||
});
|
||||
|
||||
test('--kimi-code --global installs its hook bundle under ~/.kimi-code/hooks', (t) => {
|
||||
const root = sandboxHome(t);
|
||||
runKimiInstall(root, 'kimi-code');
|
||||
|
||||
const hooksDir = path.join(root, '.kimi-code', 'hooks');
|
||||
assert.ok(fs.existsSync(path.join(hooksDir, 'gsd-check-update.js')),
|
||||
'the shared hook bundle must be self-contained under the kimi-code root');
|
||||
assert.ok(fs.existsSync(path.join(hooksDir, 'lib')),
|
||||
'hooks/lib must ship alongside it');
|
||||
assert.ok(fs.existsSync(path.join(hooksDir, 'package.json')),
|
||||
"the CommonJS marker must sit under kimi-code's hooks/ dir (#2544 shape)");
|
||||
|
||||
// The emitted [[hooks]] command paths must reference the same root the
|
||||
// bundle was installed into, or every hook resolves to a missing script.
|
||||
const toml = fs.readFileSync(path.join(root, '.kimi-code', 'config.toml'), 'utf8');
|
||||
const managed = toml.slice(
|
||||
toml.indexOf(KIMI_HOOKS_TOML_MARKER_BEGIN),
|
||||
toml.indexOf(KIMI_HOOKS_TOML_MARKER_END),
|
||||
);
|
||||
const commandPaths = [...managed.matchAll(/^command = "(.*)"$/gm)].map((m) => m[1]);
|
||||
assert.ok(commandPaths.length > 0, 'the managed block must emit at least one command');
|
||||
for (const cmd of commandPaths) {
|
||||
assert.ok(toPosixPath(cmd).includes('/.kimi-code/hooks/'),
|
||||
`hook command must reference the kimi-code hooks dir: ${cmd}`);
|
||||
assert.ok(!toPosixPath(cmd).includes('/.kimi/hooks/'),
|
||||
`hook command must not reference Kimi CLI's hooks dir: ${cmd}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('--kimi --global still writes into ~/.kimi and never creates ~/.kimi-code', (t) => {
|
||||
const root = sandboxHome(t);
|
||||
runKimiInstall(root, 'kimi');
|
||||
|
||||
assert.ok(hasGsdHooksBlock(path.join(root, '.kimi', 'config.toml')),
|
||||
"kimi's own destination must be unchanged by #2755");
|
||||
assert.ok(!fs.existsSync(path.join(root, '.kimi-code')),
|
||||
"a --kimi install must not create Kimi Code's root");
|
||||
});
|
||||
|
||||
test('KIMI_CODE_HOME redirects the kimi-code hooks destination', (t) => {
|
||||
const root = sandboxHome(t);
|
||||
const altHome = sandboxHome(t, 'gsd-2755-kch-');
|
||||
runKimiInstall(root, 'kimi-code', { extraEnv: { KIMI_CODE_HOME: altHome } });
|
||||
|
||||
assert.ok(hasGsdHooksBlock(path.join(altHome, 'config.toml')),
|
||||
'KIMI_CODE_HOME must redirect the hooks block');
|
||||
assert.ok(!fs.existsSync(path.join(root, '.kimi-code')),
|
||||
'the default kimi-code root must not be used when the env var is set');
|
||||
assert.ok(!fs.existsSync(path.join(root, '.kimi')),
|
||||
"Kimi CLI's root must not be touched either");
|
||||
});
|
||||
|
||||
test('KIMI_SHARE_DIR still redirects the kimi hooks destination', (t) => {
|
||||
const root = sandboxHome(t);
|
||||
const altHome = sandboxHome(t, 'gsd-2755-ksd-');
|
||||
runKimiInstall(root, 'kimi', { extraEnv: { KIMI_SHARE_DIR: altHome } });
|
||||
|
||||
assert.ok(hasGsdHooksBlock(path.join(altHome, 'config.toml')),
|
||||
"KIMI_SHARE_DIR must keep redirecting kimi's hooks block");
|
||||
assert.ok(!fs.existsSync(path.join(root, '.kimi-code')),
|
||||
"kimi-code's root must not be touched");
|
||||
});
|
||||
|
||||
test('uninstalling kimi-code leaves kimi hooks intact', (t) => {
|
||||
const root = sandboxHome(t);
|
||||
runKimiInstall(root, 'kimi');
|
||||
runKimiInstall(root, 'kimi-code');
|
||||
|
||||
const kimiToml = path.join(root, '.kimi', 'config.toml');
|
||||
const kimiCodeToml = path.join(root, '.kimi-code', 'config.toml');
|
||||
const kimiBefore = fs.readFileSync(kimiToml, 'utf8');
|
||||
|
||||
runKimiInstall(root, 'kimi-code', { uninstall: true });
|
||||
|
||||
assert.equal(fs.readFileSync(kimiToml, 'utf8'), kimiBefore,
|
||||
"a --kimi-code uninstall must leave Kimi CLI's config.toml byte-identical");
|
||||
assert.ok(!hasGsdHooksBlock(kimiCodeToml),
|
||||
"kimi-code's own block must be removed by its own uninstall");
|
||||
});
|
||||
|
||||
test('uninstalling kimi leaves kimi-code hooks intact', (t) => {
|
||||
const root = sandboxHome(t);
|
||||
runKimiInstall(root, 'kimi');
|
||||
runKimiInstall(root, 'kimi-code');
|
||||
|
||||
const kimiToml = path.join(root, '.kimi', 'config.toml');
|
||||
const kimiCodeToml = path.join(root, '.kimi-code', 'config.toml');
|
||||
const kimiCodeBefore = fs.readFileSync(kimiCodeToml, 'utf8');
|
||||
|
||||
runKimiInstall(root, 'kimi', { uninstall: true });
|
||||
|
||||
assert.equal(fs.readFileSync(kimiCodeToml, 'utf8'), kimiCodeBefore,
|
||||
"a --kimi uninstall must leave Kimi Code's config.toml byte-identical");
|
||||
assert.ok(!hasGsdHooksBlock(kimiToml),
|
||||
"kimi's own block must be removed by its own uninstall");
|
||||
});
|
||||
|
||||
test('KIMI_SHARE_DIR and KIMI_CODE_HOME set together do not interfere', (t) => {
|
||||
// Both products' overrides live in one environment in practice. Each must
|
||||
// honor only its own variable — proven through the real installer, not just
|
||||
// the resolver unit.
|
||||
const root = sandboxHome(t);
|
||||
const kimiAlt = sandboxHome(t, 'gsd-2755-both-kimi-');
|
||||
const codeAlt = sandboxHome(t, 'gsd-2755-both-code-');
|
||||
const both = { KIMI_SHARE_DIR: kimiAlt, KIMI_CODE_HOME: codeAlt };
|
||||
|
||||
runKimiInstall(root, 'kimi', { extraEnv: both });
|
||||
runKimiInstall(root, 'kimi-code', { extraEnv: both });
|
||||
|
||||
assert.ok(hasGsdHooksBlock(path.join(kimiAlt, 'config.toml')),
|
||||
'kimi must honor KIMI_SHARE_DIR while KIMI_CODE_HOME is also set');
|
||||
assert.ok(hasGsdHooksBlock(path.join(codeAlt, 'config.toml')),
|
||||
'kimi-code must honor KIMI_CODE_HOME while KIMI_SHARE_DIR is also set');
|
||||
assert.ok(!fs.existsSync(path.join(root, '.kimi')),
|
||||
'neither default root may be used when both overrides are set');
|
||||
assert.ok(!fs.existsSync(path.join(root, '.kimi-code')),
|
||||
'neither default root may be used when both overrides are set');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ const {
|
||||
getGlobalSkillsBase,
|
||||
resolveAntigravityGlobalDir,
|
||||
resolveKimiGlobalDir,
|
||||
resolveKimiHooksTomlDir,
|
||||
resolveConfigHomeFromDescriptor,
|
||||
resolveSkillsBaseFromDescriptor,
|
||||
detectAntigravityDirAmbiguity,
|
||||
@@ -1226,3 +1227,95 @@ describe('bug #3126: init.cjs uses runtime-homes not hardcoded .claude', () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── resolveKimiHooksTomlDir: per-runtime hooks root (#2755) ──────────────────
|
||||
|
||||
describe('resolveKimiHooksTomlDir — per-runtime hooks root (#2755)', () => {
|
||||
// Pure path computation; the fixture home never needs to exist on disk.
|
||||
const FIXTURE_HOME = path.join(os.tmpdir(), 'gsd-2755-home-fixture');
|
||||
const at = (...seg) => path.join(FIXTURE_HOME, ...seg);
|
||||
|
||||
test('a bare call does not throw', () => {
|
||||
assert.doesNotThrow(() => resolveKimiHooksTomlDir());
|
||||
});
|
||||
|
||||
test('an omitted runtime still resolves Kimi CLI\'s ~/.kimi (back-compat)', () => {
|
||||
// The function is exported; callers and tests outside this diff pass no
|
||||
// runtime, and their destination must not move.
|
||||
assert.equal(resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: {} }), at('.kimi'));
|
||||
});
|
||||
|
||||
test('runtime "kimi" resolves ~/.kimi', () => {
|
||||
assert.equal(
|
||||
resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: {}, runtime: 'kimi' }),
|
||||
at('.kimi'),
|
||||
);
|
||||
});
|
||||
|
||||
test('runtime "kimi-code" resolves ~/.kimi-code', () => {
|
||||
assert.equal(
|
||||
resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: {}, runtime: 'kimi-code' }),
|
||||
at('.kimi-code'),
|
||||
);
|
||||
});
|
||||
|
||||
test('KIMI_SHARE_DIR overrides the kimi root', () => {
|
||||
assert.equal(
|
||||
String(resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: { KIMI_SHARE_DIR: '/share' }, runtime: 'kimi' })).replace(/\\/g, '/'),
|
||||
'/share',
|
||||
);
|
||||
});
|
||||
|
||||
test('KIMI_CODE_HOME overrides the kimi-code root', () => {
|
||||
assert.equal(
|
||||
String(resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: { KIMI_CODE_HOME: '/kcode' }, runtime: 'kimi-code' })).replace(/\\/g, '/'),
|
||||
'/kcode',
|
||||
);
|
||||
});
|
||||
|
||||
test('KIMI_SHARE_DIR does not hijack the kimi-code root', () => {
|
||||
// KIMI_SHARE_DIR is Kimi CLI's own upstream var. Before #2755 it silently
|
||||
// redirected kimi-code too — that accident was the issue's suggested
|
||||
// workaround, and the fix must make it inert.
|
||||
assert.equal(
|
||||
resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: { KIMI_SHARE_DIR: '/share' }, runtime: 'kimi-code' }),
|
||||
at('.kimi-code'),
|
||||
);
|
||||
});
|
||||
|
||||
test('KIMI_CODE_HOME does not hijack the kimi root', () => {
|
||||
assert.equal(
|
||||
resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: { KIMI_CODE_HOME: '/kcode' }, runtime: 'kimi' }),
|
||||
at('.kimi'),
|
||||
);
|
||||
});
|
||||
|
||||
test('an unknown runtime falls back to ~/.kimi', () => {
|
||||
assert.equal(
|
||||
resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: {}, runtime: 'not-a-kimi' }),
|
||||
at('.kimi'),
|
||||
);
|
||||
});
|
||||
|
||||
test('every kimi-hooks-toml runtime resolves a distinct hooks root', () => {
|
||||
// Divergence guard (CLAUDE.md → Generative Fix Divergence). The resolver
|
||||
// hardcodes the two Kimi roots while the capability registry independently
|
||||
// decides which runtimes use the kimi-hooks-toml surface. If a third one is
|
||||
// ever added without teaching the resolver its root, it silently inherits
|
||||
// ~/.kimi — which IS the #2755 defect, re-created. This fails the moment
|
||||
// those two surfaces drift apart.
|
||||
const capsDir = path.join(ROOT, 'capabilities');
|
||||
const ids = fs.readdirSync(capsDir).filter((id) => {
|
||||
const file = path.join(capsDir, id, 'capability.json');
|
||||
if (!fs.existsSync(file)) return false;
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'))?.runtime?.hooksSurface === 'kimi-hooks-toml';
|
||||
});
|
||||
|
||||
assert.deepEqual(ids.sort(), ['kimi', 'kimi-code'],
|
||||
'the kimi-hooks-toml runtime set changed — teach resolveKimiHooksTomlDir the new root, then update this list');
|
||||
|
||||
const roots = ids.map((id) => resolveKimiHooksTomlDir({ home: FIXTURE_HOME, env: {}, runtime: id }));
|
||||
assert.equal(new Set(roots).size, roots.length,
|
||||
`each kimi-hooks-toml runtime must resolve its own root; got ${JSON.stringify(roots)}`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user