The SDK records a bare, upgrade-stable binary name only when a PATH entry realpath-matches the execPath it is handed. From the module-relative entrypoint that can only succeed under npm, which symlinks its bin entry straight at it; a wrapper-based install never can, because a script that invokes a file does not resolve to that file. So every wrapper install -- Nix, a shim, a generated .cmd -- recorded an absolute path that moves on upgrade, and a session-start hook that cannot execute fails silently. `setup hooks` now resolves gitea-axi on PATH itself and hands that location to the SDK, so the bare name is recorded. A candidate qualifies only if it resolves to the running entrypoint -- by realpath for a symlink, or by naming it in its text for a wrapper, following the chain, since a Nix install is two hops. A same-named binary that is some other program does not qualify, and the absolute entrypoint path stands as the fallback exactly as before. The related defect on the same line goes too: the SDK recognises its hook by finding the marker inside the recorded command, so an entrypoint path without "gitea-axi" in it made a re-run append a duplicate rather than update in place. `setup hooks` now prunes duplicates by matching the exact command it records, which is independent of that command's shape and cannot mistake another tool's hook for its own. With the coupling gone, package.nix no longer renames its build tree; the fast tier runs from /build/source and its idempotency test passes there. The help text's instruction to re-run hooks after an upgrade is deleted, having become false. Verified against the built Nix binary and a globally npm-installed pack: both record the bare name, both fall back to the absolute path when the name is absent, an impostor on PATH is refused, and re-runs leave one entry. Decision recorded as ADR 0019; ADR 0009's addendum is amended.
4.6 KiB
Record the SessionStart hook as a search-path name, not an install-tree path
setup hooks resolves gitea-axi on PATH and hands that location to installSessionStartHooks, so the recorded hook command is the bare name gitea-axi.
When the name resolves nowhere on PATH, the module-relative entrypoint is handed over instead and the absolute path is recorded, exactly as before.
A PATH entry qualifies only when it resolves to the running entrypoint — a symlink pointing at it, or a generated wrapper naming it — so a same-named binary that is some other program does not count.
setup hooks also collapses duplicate managed entries itself, recognising its own hook by the exact command it records rather than by a substring of that command.
Context
The agent SDK's resolvePortableHookCommand returns a bare binary name only when some PATH entry realpath-matches the entrypoint it is handed, and the absolute path otherwise.
Task 0042 verified that this splits the two installation methods: npm symlinks its bin entry straight at dist/main.js so the match succeeds, while any wrapper-based install cannot match, because a script that invokes a file never resolves to that file.
Under Nix the recorded path is content-addressed — it changes on every rebuild and is eventually garbage-collected — and a SessionStart hook that cannot execute does not run and does not warn.
Considered Options
Document a re-run after upgrade (rejected; this was the task 0042 mitigation being replaced) — Documentation against a silent failure is the weakest kind of fix. It also does not work reliably: the re-run may leave the stale entry behind rather than replacing it, so the help text had to caveat its own remedy.
Detect store paths in application code (rejected) — Special-casing Nix in the CLI is the wrong shape.
The problem is not Nix; it is every wrapper-based install — a shim, a launcher, a generated .cmd.
Write the hook files directly, bypassing the SDK (rejected) — Would duplicate the SDK's handling of three integrations and four files to change one string, and would drift from it on every SDK change.
Hand the SDK the PATH location (chosen) — The SDK already prefers the search-path name; it was only ever reaching for it from the wrong end.
Resolving the name the way a shell does and handing that over makes the SDK's own realpath test succeed, so the preference becomes reachable for every installation method rather than only for the symlink shape npm happens to use.
Recording a bare name and letting PATH resolve it is also the convention for tools writing into user-owned configuration; absolute paths belong in configuration a package manager regenerates.
Consequences
- The hook survives an upgrade whenever the binary is on
PATH, sosetup hooksno longer needs re-running after one, and the help text saying so is gone. - The absolute path remains the documented fallback for a binary that is not on
PATH— a source checkout run throughnode dist/main.js, say — where it is the only thing that could work. - Which command gets recorded now depends on the invoking environment's
PATH, not only on how the package was installed.PATHis therefore read from the process rather than from the injected environment, since it must agree with the SDK's own probing. - A same-named binary on
PATHthat is not this program does not qualify. The name must resolve to the running entrypoint — by realpath for a symlink, or by the wrapper naming it — or the fallback applies. Accepting any file that merely bears the name would make the SDK's realpath test a tautology, since the path handed over would trivially match itself. - The SDK recognises its managed hook by finding the marker inside the recorded command, which made a re-run append a duplicate whenever the entrypoint path lacked the substring
gitea-axi.setup hooksnow prunes duplicates by matching the exact command it records, so idempotency no longer depends on the recorded command's shape, and another tool's hook can never be mistaken for ours. package.nixno longer renames its build tree inpostUnpack. That rename existed only to give the fast tier an entrypoint path containing the marker. With the coupling gone the build runs from/build/sourceand the idempotency test passes there, which is what demonstrates the coupling is actually broken.- ADR 0009's addendum claimed "the SDK registers the bare binary as the hook command".
That was true only for npm installs; as of this decision it is true for any install whose
PATHentry resolves to this entrypoint. ADR 0009 is amended accordingly.