#4632·gsd-core

Epic: registration is not activation — one runtime descriptor + an activation probe

Author: trek-eCreated Sep 11, 2026Updated Sep 21, 2026
Labelsarea: installertype: choreepic

An approved epic does not approve its children — each child is its own issue before code.

Epic: registration is not activation — one runtime descriptor, and an activation probe that proves a hook actually fires

The deliverable is the seam, not the individual fixes. The issues absorbed below are evidence that one piece of behavior has no single owner. They are the symptom list, not the work list. Repairing them where they were reported — N fixes for N issues — leaves the missing owner still missing, and the next occurrence is already being written somewhere else in the tree. An implementation that does that has not closed this epic, even with every symptom gone and CI green.

Closing this epic means three things land together: one runtime descriptor owning every runtime-specific vocabulary, env candidate and command surface exists; the duplicate implementations are deleted rather than kept in sync, because two copies that agree today are the same defect as two that disagree; and a runtime × registered-hook activation probe that reds CI when a registered hook does not actually fire fails on a reintroduced copy, demonstrated by watching it go red. The first two without the third un-consolidate quietly as soon as someone who has not read this issue touches the area.

A defect that cannot be expressed through the seam is a finding about the seam's design and belongs in this issue as a comment — one special case is how the previous consolidation grew its second copy.

This is the shape that worked in #1372 (markdown sectionizer), #2143 (table, bounded mutation, fail-loud) and #2121 (phase identifier): each closed its bug class with a seam, a migration and an anti-divergence guard, none of them by fixing the reported symptoms. A first pass at #4634 proposed four independent fixes and no seam, which is the failure mode this scoping exists to prevent. Splitting delivery across several PRs is expected and welcome; splitting it so that the seam never lands is not.

Four open confirmed-bug issues share a property that makes them uniquely hard to notice: the installer reports success, the artifact is registered, and the thing does not work. Nothing in install, update, or CI disagrees. #4557 states it exactly:

The result is a hook that is installed, registered, trusted, and silently non-functional.

And #4332 names the mechanism by which this family reproduces itself — a fix that landed for one runtime and never generalised:

This is #2304 ("guards match Kimi's WriteFile matcher but reject Kimi's tool_name — all four dormant on Kimi") reproduced on Antigravity. […] The KIMI_TOOL_NAMES map added by #2507 fixes it for Kimi. There is no Antigravity equivalent.

That is the whole thesis of this epic in two sentences. The per-runtime knowledge is hand-maintained, one runtime at a time, so every fix is local and the next runtime inherits the original bug.

The four failures, each a different hop that is never verified

Issue What is registered Why it does not function
#4332 13 hooks in ~/.gemini/antigravity/settings.json, 27 scripts staged event names were translated to Antigravity's dialect (BeforeTool/AfterTool/SessionStart) but matchers and payload reads were left in Claude Code's vocabulary — the guard spawns and its tool_name check exits 0. All 7 advisory guards dormant, and because ON_CRASH = ALLOW is the intended posture, nothing is logged
#4557 commandWindows in Codex's hooks.json written as a bare quoted path to the .cmd shim; PowerShell evaluates a lone quoted string as an expression and prints it. SessionStart injects the hook's own filename into the session as context; Stop receives a path where it expects JSON
#4567 Pi's native extension registers only the top-level /gsd, while the installer's completion message tells the user to run /gsd-new-project — a command Pi never registered
#4347 two runtime-home resolvers that disagree: the shell launcher still probes GEMINI_CONFIG_DIR, which the JS resolver dropped in #1928 and which a test pins as an invariant (tests/declarative-reference-antigravity.test.cjs:307)
#4438 convertGeminiToolName prefix-tests raw tool tokens without decodeToolScalar normalization — the same class PR #4238 fixed for zcodeKeepsGrant (zcode) and parseFrontmatterTools (kimi), with the Gemini/Antigravity surface left behind

#4438 and #4332 are the same sentence written twice: a normalization or vocabulary fix lands on the runtimes someone was looking at, and the next runtime inherits the original bug. #4238 fixed two surfaces; #4438 is the third. #2507 fixed one; #4332 is the second.

#4332 is doubly damning because the codebase predicted it in a comment. gsd-read-guard.js lines 31–40 already say that without normalizing the payload, "the matcher fires but the tool_name check below exits 0 and the guard is dormant on Kimi." The prediction was written down, fixed for one runtime, and left to come true on the next.

The seam this epic delivers

  1. One runtime descriptor owns everything runtime-specific — env-var candidates, event names, tool/payload vocabulary, command registration surface, and platform command construction. KIMI_TOOL_NAMES becomes one row of a table, not a special case. A new runtime supplies a descriptor; it does not hand-edit N call sites.
  2. A single resolver. #4347 is two implementations of one question; the shell launcher and the JS resolver must derive their candidate list from the same source, so "gemini was dropped" is true in both or neither.
  3. An activation probe — the load-bearing piece. For every runtime × every registered hook, drive a synthetic payload through the real registration and assert the guard body actually executes. This converts the entire family from silent to caught: registration that does not activate is a test failure, on every runtime, not just the one someone happened to test on.
  4. The installer's completion message is generated from the registered command surface, so it cannot advertise a command that was not registered (#4567).

The ratchet

A parity test across every runtime descriptor: for each runtime the installer supports, every guard it registers must pass the activation probe, and the advertised command list must equal the registered command list. Adding a runtime without a passing probe fails CI. This is the capability-precedence-parity / lint-*-drift shape the repo already uses — the mechanism that has actually held here, where per-runtime hand-maintenance has not.

Note the posture hazard this epic must respect: ON_CRASH = ALLOW is correct for advisory guards, so a dormant guard is invisible by design at runtime. The probe is therefore the only place this class can be caught.

Absorbed issues — evidence of the missing seam, NOT a work list

Issue Defect
#4332 Registered guards are matched and spawned but reject Antigravity's payload — all 7 dormant (the #2304 defect, one runtime over)
#4557 Windows hooks.json commandWindows is a bare quoted .cmd path, so PowerShell prints it instead of running the hook
#4567 Installer advertises /gsd-new-project but Pi only registers /gsd
#4347 Shell launcher still probes GEMINI_CONFIG_DIR, which the JS resolver is tested to ignore (#1928)
#4438 convertGeminiToolName prefix-tests raw tokens without decodeToolScalar — the class PR #4238 fixed for zcode and kimi, not for the Gemini/Antigravity surface

Done when

  • Runtime-specific vocabulary, env candidates, and command surface live in one descriptor per runtime, consumed by every site
  • Exactly one runtime-home resolver implementation backs both the shell launcher and the JS path
  • Every runtime × registered-hook pair passes an activation probe in CI
  • The installer's advertised commands are generated from the registered surface
  • Adding a new runtime requires no hand-edit of guard internals
  • Failing-first regression per absorbed issue

Non-goals

  • Repairing the absorbed issues individually at their existing call sites without building the seam. That is the pattern that produced them, and it leaves the epic open.

  • Adding support for any new runtime (see the standing "no new runtimes or add-ons in core" position)

  • Changing ON_CRASH posture for advisory guards

  • Re-litigating #1928's removal of the gemini runtime — this epic makes the two resolvers agree with that decision, it does not revisit it

Generated with Claude Code