* fix(#2305): stage shared guard hooks for Kilo — drop skipSharedHooksInstall Kilo's capability descriptor declared BOTH hostBehaviors.nativePlugin (a plugin that spawns the shared PreToolUse guard scripts as subprocesses) AND hostBehaviors.skipSharedHooksInstall:true, which suppresses staging of hooks/*.js into the Kilo config dir. The plugin's runHook treats an absent hook script as a silent allow, so every guard it spawned (gsd-prompt-guard, gsd-read-guard, gsd-worktree-path-guard) no-opped on every Kilo install. OpenCode uses the byte-identical plugin with hook staging on and is unaffected — it is the reference shape. The skip flag predates Kilo's plugin surface: it dates to #1821 (hooks were dead weight for a runtime with no hook consumer), and #2093 added the hooks-dependent nativePlugin without revisiting it. - capabilities/kilo/capability.json: remove skipSharedHooksInstall (regenerated gsd-core/bin/lib/capability-registry.cjs accordingly) - bin/install.js: correct the stale #1821 comments claiming Kilo has no plugin surface - tests/kilo-upgrades.test.cjs: install-fixture tests (global + local) asserting the guard scripts land where the plugin's walk-up resolves them; an end-to-end test driving a disallowed out-of-worktree write through the REAL installed Kilo tree and asserting the guard rejects it; a cross-runtime descriptor invariant (nativePlugin and skipSharedHooksInstall:true must never coexist) - tests/kilo-imperative-reference.test.cjs: flip the pinned assertion - golden fixtures regenerated (kilo now stages the 24 hook files, same set as OpenCode) Fixes #2305 * fix(#2305): warn loudly when a guard hook script is missing (runHook) runHook's absent-file branch returned a silent exit-0 allow — the mechanism that let #2305 ship undetected: with the hooks bundle never staged on Kilo, every PreToolUse guard the plugin spawned resolved to "file not found → allow" with zero signal anywhere. Keep the adapter's design contract (a missing hook must never break the tool call — pinned by the existing adapter test) but make the absence loud: console.error once per hook file, naming the unresolved path and the remediation. Applied identically to .kilo/ and .opencode/ plugin copies (byte-parity guard). Golden parity fixtures regenerated (the installed plugin file's hash changed). Fixes #2305 * chore(#2305): add changeset fragment * test(#2305): include gsd-workflow-guard.js in the staged-guards regression list The native plugin spawns four guards on write-like tool calls — the regression test's PLUGIN_GUARD_HOOKS list covered three. Staging itself was already asserted via the golden fixtures (the full bundle), but the named per-guard assertion should cover every guard the plugin actually dispatches. Surfaced by cross-AI review of PR #2327. * test(#2305): update the #1821 tests that encoded Kilo's false no-plugin premise The #1821 hook-copy test asserted Kilo must receive no staged hooks — the exact behavior this PR reverses (and the cause of all 8 CI failures). Kilo moves from the ZCode "no dead hooks" loop to the OpenCode group, with positive assertions on the new contract: the three guard hooks the plugin spawns, hooks/lib/git-cmd.js, and plugins/gsd-core.js all staged. The integration runtime contract flips kilo packageJson to true (the CommonJS marker ships with the bundle), and the pi contract comment no longer cites Kilo as a no-plugin runtime. * chore(#2305): scope the queued #1821 changeset fragment to ZCode only The fragment still claimed the installer skips hooks for Kilo — rendering both it and this PR's fragment into the same release would ship two contradictory statements about Kilo's install behavior. It now claims ZCode only and notes that #2327 reverses the Kilo half. * chore(#2305): rename changeset fragment to the generator naming convention 2305-kilo-stage-guard-hooks.md -> loud-guard-hooks.md, matching the <adjective>-<noun>-<noun> shape npm run changeset generates (review nit). --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
@@ -671,19 +671,23 @@ describe('#1755: .sh hooks are copied and executable after install', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #1821: Kilo/ZCode (hooksSurface:none, no plugin) receive no dead hooks ────
|
||||
// ─── #1821/#2305: hooks staged iff a surface consumes them ─────────────────────
|
||||
//
|
||||
// #1821 reported dead hook scripts staged for runtimes with hooksSurface:'none'.
|
||||
// OpenCode and pi ALSO declare hooksSurface:'none', but each has a native plugin
|
||||
// adapter that spawns the staged hooks/*.js scripts as subprocesses (OpenCode's
|
||||
// #1914 plugins/gsd-core.js via OpenCode's event bus; pi's #2102 Stage 2
|
||||
// pi/gsd.cjs → extensions/gsd.cjs via pi.on(...) bridges) — so for both, the
|
||||
// hooks are LIVE and must keep being copied. Kilo and ZCode have no plugin
|
||||
// surface at all, so their staged hooks are genuinely dead: this is the case
|
||||
// the fix removes. These tests assert the split: Kilo/ZCode get no hooks;
|
||||
// OpenCode/pi (and Claude) still do.
|
||||
// OpenCode, pi — and, corrected by #2305, Kilo — ALSO declare
|
||||
// hooksSurface:'none', but each has a native plugin adapter that spawns the
|
||||
// staged hooks/*.js scripts as subprocesses (OpenCode's #1914
|
||||
// plugins/gsd-core.js via OpenCode's event bus; pi's #2102 Stage 2 pi/gsd.cjs
|
||||
// → extensions/gsd.cjs via pi.on(...) bridges; Kilo's plugins/gsd-core.js,
|
||||
// byte-identical to OpenCode's) — so for all three, the hooks are LIVE and
|
||||
// must keep being copied. ZCode has no plugin surface at all, so its staged
|
||||
// hooks are genuinely dead: that is the case #1821's fix removes. (#1821
|
||||
// originally excluded Kilo too, on the false premise that it had no plugin
|
||||
// surface — #2305 reversed that: the skip flag silently no-opped every guard
|
||||
// hook Kilo's plugin spawns.) These tests assert the split: ZCode gets no
|
||||
// hooks; Kilo/OpenCode/pi (and Claude) do.
|
||||
|
||||
describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep their hooks', () => {
|
||||
describe('#1821/#2305: ZCode receives no dead hook files; Kilo/OpenCode/Claude keep their hooks', () => {
|
||||
function gsdHookFilesUnder(configDir) {
|
||||
const hooksDir = path.join(configDir, 'hooks');
|
||||
if (!fs.existsSync(hooksDir)) return [];
|
||||
@@ -716,10 +720,11 @@ describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep the
|
||||
}
|
||||
}
|
||||
|
||||
// Kilo and ZCode both declare hooksSurface:'none' with no plugin surface, so
|
||||
// their staged hooks are genuinely dead weight (#1821) — this is the case
|
||||
// the fix removes.
|
||||
for (const runtime of ['kilo', 'zcode']) {
|
||||
// ZCode declares hooksSurface:'none' with no plugin surface, so its staged
|
||||
// hooks are genuinely dead weight (#1821) — this is the case the fix
|
||||
// removes. (Kilo was originally in this loop; #2305 moved it to the
|
||||
// OpenCode group below — its native plugin spawns the staged guard hooks.)
|
||||
for (const runtime of ['zcode']) {
|
||||
test(`${runtime} --global install creates no gsd-*.js/.sh hook files or hooks/lib`, () => {
|
||||
const { hookFiles, hooksLibExists } = installAndCollect(runtime);
|
||||
assert.deepStrictEqual(hookFiles, [], `${runtime} install must not copy any gsd-*.js/.sh hook files, found: ${hookFiles.join(', ')}`);
|
||||
@@ -727,6 +732,26 @@ describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep the
|
||||
});
|
||||
}
|
||||
|
||||
// #2305: Kilo's native plugin (plugins/gsd-core.js, byte-identical to
|
||||
// OpenCode's) spawns the staged PreToolUse guard hooks as subprocesses, so
|
||||
// Kilo must receive the shared hooks bundle as a sibling of gsd-core/ —
|
||||
// the shape the plugin's resolveRepoRoot walk requires. #1821 excluded
|
||||
// Kilo on the false premise that it had no plugin surface; with the skip
|
||||
// flag set, every guard silently no-opped on every Kilo install.
|
||||
test('kilo --global install stages the guard hooks its plugin spawns, hooks/lib, and the plugin', () => {
|
||||
const { hookFiles, hooksLibExists, gitCmdExists, pluginExists } = installAndCollect('kilo');
|
||||
const basenames = hookFiles.map((f) => path.basename(f));
|
||||
for (const expected of ['gsd-prompt-guard.js', 'gsd-read-guard.js', 'gsd-worktree-path-guard.js']) {
|
||||
assert.ok(
|
||||
basenames.includes(expected),
|
||||
`kilo install must copy ${expected} (spawned by the plugin's runHook), found: ${basenames.join(', ')}`,
|
||||
);
|
||||
}
|
||||
assert.ok(hooksLibExists, 'kilo install must create hooks/lib/');
|
||||
assert.ok(gitCmdExists, 'kilo install must copy hooks/lib/git-cmd.js (required by the shared hooks)');
|
||||
assert.ok(pluginExists, 'kilo install must install plugins/gsd-core.js (the hook-spawning plugin)');
|
||||
});
|
||||
|
||||
// Regression guard for #1914: OpenCode's plugin adapter spawns the staged
|
||||
// hooks, so excluding OpenCode from the hook copy would break it. OpenCode
|
||||
// must KEEP its hooks and receive the plugin.
|
||||
|
||||
Reference in New Issue
Block a user