feat(nix): expose declarative outputs and a home-manager module (task 0045)
All checks were successful
CI / test (22) (pull_request) Successful in 49s
CI / test (true, 24) (pull_request) Successful in 1m5s
CI / flake (pull_request) Successful in 3s
CI / test (22) (push) Successful in 49s
CI / test (true, 24) (push) Successful in 1m5s
CI / flake (push) Successful in 3s

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.
This commit was merged in pull request #54.
This commit is contained in:
2026-07-20 13:51:19 -04:00
parent a1e68dc530
commit e77a1f5e22
10 changed files with 476 additions and 41 deletions

View File

@@ -19,6 +19,20 @@ let
# reading it here means a store path and a released version cannot disagree.
manifest = lib.importJSON ./package.json;
# Where the bundled Agent Skill lands in the output, and the one address a
# consumer may depend on. The Skill's other copy — inside the installed node
# modules tree, where `setup` resolves it relative to its own module — is an
# artefact of how the command finds it at runtime, and moves whenever the
# packaging method changes.
skillSubdir = "share/gitea-axi/skills/gitea-axi";
# The SessionStart hook entry, read from the committed specification rather
# than written out here. The imperative `setup hooks` writes this same entry
# through the agent SDK, and a test drives it and asserts the two agree — so
# declaring it a second time in Nix would be a second source of truth with
# nothing checking it against the first.
sessionStartHook = lib.importJSON ./session-start-hook.json;
# An explicit allowlist of what the build and its tests actually read. The
# repository's highest-churn directories — .claude, bench, prose docs — are
# all build-irrelevant, so a whole-repository source would let writing an ADR
@@ -43,10 +57,14 @@ let
./tsconfig.build.json
./vitest.config.ts
./vitest.packaging.config.ts
# Read by the fast tier, which asserts the imperative hook install writes
# what this declares. Also read at evaluation time above, but that read is
# of the flake source rather than of `src` and would not require it here.
./session-start-hook.json
];
};
in
buildNpmPackage {
buildNpmPackage (finalAttrs: {
pname = "gitea-axi";
inherit (manifest) version;
inherit src nodejs;
@@ -101,9 +119,17 @@ buildNpmPackage {
# ADR 0018: append, never prepend. The operator's own `tea` owns the
# credential store it refreshes in place, so the closure's copy is a
# fresh-machine fallback rather than an override.
#
# The Agent Skill is also published under a stable address (ADR 0020), so a
# Nix expression can install it declaratively without reaching into the node
# modules tree. `cp` failing on a missing source is the guard that this
# address keeps pointing at something.
postInstall = ''
wrapProgram $out/bin/gitea-axi \
--suffix PATH : ${lib.makeBinPath [ git tea ]}
mkdir -p "$(dirname "$out/${skillSubdir}")"
cp -R skills/gitea-axi "$out/${skillSubdir}"
'';
# Drive the binary that was just installed through the shared installed-binary
@@ -144,11 +170,26 @@ buildNpmPackage {
runHook postInstallCheck
'';
# The Node the package is built against, published as a declared interface
# rather than left to be read off the build environment. The flake's dev shell
# consumes exactly this, so the two cannot drift onto different majors — and
# this attribute is why that holds, so removing it breaks the shell.
passthru = { inherit nodejs; };
# The package's declared interface to Nix expressions, published rather than
# left to be read off the build environment or guessed at from the output's
# layout. Every attribute here has a consumer that breaks if it is removed.
passthru = {
# The Node the package is built against. The flake's dev shell consumes
# exactly this, so development and the shipped artifact cannot drift onto
# different majors.
inherit nodejs;
# The bundled Agent Skill's directory, for a configuration that installs it
# declaratively. A directory rather than the SKILL.md inside it, so a Skill
# that grows helper files stays one reference.
skill = "${finalAttrs.finalPackage}/${skillSubdir}";
# The SessionStart hook entry, verbatim as it belongs in a Claude Code
# settings.json. Evaluating this builds nothing: it is the committed
# specification, and the command it records is a name resolved on PATH
# rather than a store path (ADR 0019).
inherit sessionStartHook;
};
meta = {
inherit (manifest) description homepage;
@@ -166,4 +207,4 @@ buildNpmPackage {
# evaluate. Consumed against 26.05, x86_64-darwin builds fine from here.
platforms = lib.platforms.linux ++ lib.platforms.darwin;
};
}
})