fix(#1821): stop copying dead hook scripts for Kilo and ZCode (hooksSurface:none)

Kilo and ZCode both declare `hooksSurface: 'none'` and have no plugin surface,
so the GSD installer staged lifecycle hook scripts (hooks/*.js, hooks/*.sh,
hooks/lib/) plus a `{"type":"commonjs"}` package.json marker into their config
dirs where nothing ever invokes them — dead weight (#1821).

The installer's two hook-copy guards at bin/install.js were still on the legacy
hardcoded runtime-name list and never excluded Kilo or ZCode. Add
`&& !isKilo && !isZcode` to both (isZcode added to the install() runtimeFlags
destructure).

OpenCode — which #1821 also named — is deliberately NOT excluded: since the
issue was filed, #1914 shipped a native OpenCode plugin (plugins/gsd-core.js)
that spawns those exact staged hooks via OpenCode's event bus and requires both
the hook scripts and the CommonJS package.json marker. Excluding OpenCode would
regress #1914, so its hooks stay live. The genuinely-dead cases are Kilo & ZCode.

- bin/install.js: add `&& !isKilo && !isZcode` to the hooks/dist copy guard and
  the hooks/lib copy guard; document the OpenCode-vs-Kilo/ZCode split.
- tests/install-minimal-hooks.test.cjs: regression test asserting Kilo and ZCode
  receive no gsd-*.js/.sh hooks or hooks/lib, while OpenCode keeps its hooks +
  #1914 plugin and Claude keeps its hooks (over-exclusion guard).
- tests/fixtures/golden-install-parity/{kilo,zcode}.json: drop the 21 hooks/*
  entries and the package.json marker they no longer receive (opencode unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-07 08:28:15 -04:00
parent a3b9cbaeb8
commit 2b806904e2
6 changed files with 103 additions and 52 deletions

View File

@@ -8161,7 +8161,7 @@ function reportInstallerMigrationResult(result) {
}
function install(isGlobal, runtime = 'claude', options = {}) {
const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
const { isOpencode, isKilo, isZcode, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
const plan = resolveInstallPlan(runtime);
const dirName = getDirName(runtime);
const src = path.join(__dirname, '..');
@@ -9163,7 +9163,13 @@ function install(isGlobal, runtime = 'claude', options = {}) {
failures.push('VERSION');
}
if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi) {
// #1821: Kilo and ZCode declare hooksSurface:'none' AND have no plugin surface,
// so the staged hook scripts are dead weight for them — exclude both here.
// OpenCode also declares hooksSurface:'none' but is deliberately NOT excluded:
// its native plugin adapter (#1914, installed above under plugins/gsd-core.js)
// spawns the staged hooks/*.js scripts via OpenCode's event bus and needs both
// them and the CommonJS package.json marker written below.
if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi && !isKilo && !isZcode) {
// Write package.json to force CommonJS mode for GSD scripts
// Prevents "require is not defined" errors when project has "type": "module"
// Node.js walks up looking for package.json - this stops inheritance from project
@@ -9255,11 +9261,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
// Gate hooks/lib/ install on the same runtimes that receive hooks (see line ~8702).
// Codex/Copilot/Cursor/Windsurf/Trae/Cline do not use the shared hooks/lib/ helpers
// (Cursor uses standalone .js hook scripts registered via hooks.json; Codex uses
// hooks.json directly; the others skip hooks entirely), so they must not receive
// the hooks/lib/ helpers — otherwise the Codex comment downstream
// ("we deliberately do *not* copy hooks/lib/ for Codex") is contradicted in practice.
// hooks.json directly; the others skip hooks entirely); Kilo and ZCode also skip
// hooks entirely (hooksSurface:'none' with no plugin surface — #1821). OpenCode
// is NOT excluded: its #1914 plugin adapter spawns the staged hooks and requires
// hooks/lib/ helpers. None of the excluded runtimes must receive the hooks/lib/
// helpers — otherwise the Codex comment downstream ("we deliberately do *not*
// copy hooks/lib/ for Codex") is contradicted in practice.
const hooksLibSrc = path.join(src, 'hooks', 'lib');
if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi && fs.existsSync(hooksLibSrc)) {
if (!isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae && !isCline && !isKimi && !isKilo && !isZcode && fs.existsSync(hooksLibSrc)) {
const hooksLibDest = path.join(targetDir, 'hooks', 'lib');
fs.mkdirSync(hooksLibDest, { recursive: true });
copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);