Let a Nix configuration declare gitea-axi's ambient context instead of
running a command that writes it. `setup` and `setup hooks` are write-only
against files the operator is assumed to own, so an operator whose agent
configuration is generated cannot use them at all.
The package installs the bundled Agent Skill to share/gitea-axi/skills and
publishes it as `passthru.skill`, alongside `passthru.sessionStartHook` read
from `session-start-hook.json` — a committed declaration the fast tier reads
too, so a test drives `setup hooks` and asserts the two agree.
On top of that, `homeModules.gitea-axi` declares both from those attributes
through home-manager's own Claude Code options, so an operator's existing
skills and SessionStart hooks compose rather than collide. Importing it
without enabling it yields a byte-identical generation.
The spec's Out of Scope entry deferring a home-manager module is deleted;
ADR 0020 records the reversal, and INSTALL.md describes both paths.
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.
Resolve the nix-flake-packaging spec's open verification item by observation
rather than inference, and act on the unfavourable answer.
`resolvePortableHookCommand` in axi-sdk-js returns the bare binary name only
when a PATH entry realpath-matches the entrypoint. npm symlinks its bin entry
straight at dist/main.js and satisfies that; Nix installs a generated wrapper
script whose realpath is the wrapper, so the absolute store path is recorded
instead. Verified by probing the SDK with both install shapes and by driving
the flake-built binary against a temporary HOME.
The path is content-addressed, so it moves on every rebuild and is eventually
collected, and a session-start hook that cannot execute fails silently. The
mitigation is documentation, per the decision recorded when the item was
opened: the setup help text now says to re-run `setup hooks` after an upgrade.
How the setup command constructs the hook is deliberately unchanged. Preferring
the bare name is the right answer for every wrapper-based install, not a Nix
special case, so it belongs in a successor task with its own ADR alongside the
related `isManagedHook` substring defect. package.nix's postUnpack rename
therefore stays; its comment no longer promises this task will remove it.
`nix develop` now yields the toolchain the repository actually needs — Node,
`git`, `tea`, and `curl` — giving a declarative answer to "what do I need to
work on this", which the repository previously specified nowhere.
The shell takes its Node from the package's `passthru` rather than naming
`pkgs.nodejs` a second time, so development and the shipped artifact cannot
drift onto different majors and cannot be set independently. `package.nix`
declares that `passthru` as an interface rather than leaving the shell to read
an incidental build attribute; it does not enter the derivation, so the store
path is unchanged.
The checks output aliases the package, so `nix flake check` builds it and
thereby runs both its verification phases instead of being a silent no-op. No
per-stage checks: the only stage adding coverage is the full typecheck, which
spans `test/` and `bench/` and would drag the benchmark harness into the
derivation's inputs, undoing the source filtering. It stays in CI.
The shell deliberately omits `gitea-axi` itself. The benchmark's arm resolves
that binary by name off PATH and must get the locally built `dist/main.js`, so
supplying the packaged one would silently substitute the wrong artifact.
The Nix build now runs the installed-binary facet of the packaging tier
against the binary it has just produced, via an `installCheckPhase` that
sets `GITEA_AXI_INSTALLED_BIN` to `$out/bin/gitea-axi` and runs the new
`test:installed` script. Running after `fixupPhase` means the binary
under test is the wrapped one an operator actually gets, and naming it is
all the phase does — the assertions stay in the shared tier, so the npm
and Nix installation paths cannot drift apart in what they guarantee.
`npmInstallHook` prunes dev dependencies out of the build tree before the
check runs, so `preInstall` snapshots `node_modules` with `cp -al` and the
check restores it by copying, leaving the snapshot intact for a replayed
phase.
Two defects fixed in passing, both surfaced by review:
`test:installed` pins a test file by path while the packaging runner sets
`passWithNoTests: true`, so moving that file would have taken the build
green having asserted nothing — the same silently-inert trap `doCheck`
sprang in task 0037. The script now passes `--passWithNoTests=false`.
`checkPhase`'s vitest left a timestamped run cache under
`node_modules/.vite`, which `npmInstallHook` copied into `$out`, shipping
a stray cache and making the derivation non-reproducible. It is now
removed before the install phase; `nix build --rebuild` passes.
Add a flake at the repository root exposing gitea-axi as a package, with the
derivation in its own callable expression so it stays buildable outside a flake
context and usable in an overlay unchanged.
Dependencies are fetched from the lockfile's integrity fields via importNpmLock
rather than a committed fixed-output hash, and the version is read from the
package manifest, so neither a dependency bump nor a release edits any Nix
expression. The source is an explicit allowlist, keeping ADR, spec, task and
bench churn out of the derivation's inputs.
The installed binary is wrapped with --suffix PATH per ADR 0018: the operator's
own git and tea win, and the closure's are a fresh-machine fallback.
Two things differ from the plan. Systems coverage is three targets, not four:
nixpkgs 26.11 dropped x86_64-darwin and now throws on evaluating it, which
would break nix flake show and nix flake check for every system at once. And
buildNpmPackage supplies no check hook, so doCheck alone was silently inert and
produced a green build whose tests never ran; running the fast tier needs an
explicit checkPhase.