docs: record that the session-start hook stores an absolute path (task 0042) #51

Merged
alexion merged 2 commits from task-0042-verify-hook-path-resolution into main 2026-07-20 12:43:35 -04:00
5 changed files with 140 additions and 5 deletions
Showing only changes of commit 4e92dde4e4 - Show all commits

View File

@@ -156,16 +156,37 @@ _Avoid_: mock mode, stub mode
### Distribution ### Distribution
**Agent Skill**: The markdown file bundled inside the npm package and installed to `~/.claude/skills/` by the `setup` command. **Agent Skill**: The markdown file bundled inside the npm package and installed to `~/.claude/skills/` by the `setup` command, or declared from the package by the [[home-manager module]].
_Avoid_: skill file, Claude skill _Avoid_: skill file, Claude skill
**setup**: The explicit subcommand that installs the Agent Skill into `~/.claude/skills/`; gitea-axi's primary fulfillment of AXI Principle 7 (Ambient context). **setup**: The explicit subcommand that installs the Agent Skill into `~/.claude/skills/`; gitea-axi's primary fulfillment of AXI Principle 7 (Ambient context).
Idempotent: re-running reports already-installed/updated rather than failing. Idempotent: re-running reports already-installed/updated rather than failing.
There is no postinstall script — installation of the skill is always an explicit user action. There is no postinstall script — installation of the skill is always an explicit user action.
`setup hooks` additionally opts into the [[SessionStart hook]]. `setup hooks` additionally opts into the [[SessionStart hook]].
It is the [[imperative install path]], and works only where the operator owns the target files; against a read-only target it reports the condition rather than writing.
_Avoid_: postinstall, installer script _Avoid_: postinstall, installer script
**SessionStart hook**: An opt-in ambient-context mechanism installed by `setup hooks` via `axi-sdk-js`'s `installSessionStartHooks()` (Claude Code `settings.json`, Codex `hooks.json`, OpenCode plugin). **SessionStart hook**: An opt-in ambient-context mechanism installed by `setup hooks` via `axi-sdk-js`'s `installSessionStartHooks()` (Claude Code `settings.json`, Codex `hooks.json`, OpenCode plugin), or declared by the [[home-manager module]].
It runs the bare `gitea-axi` binary (the short [[dashboard]] tier) in the session's working directory at session start and injects the output into the agent's context. It runs the bare `gitea-axi` binary (the short [[dashboard]] tier) in the session's working directory at session start and injects the output into the agent's context.
The SDK's installer registers the binary with no arguments, so the hook always runs the short tier; outside a Gitea repo it produces the dashboard's `REPO_NOT_FOUND` error, an accepted noise trade-off. The SDK's installer registers the binary with no arguments, so the hook always runs the short tier; outside a Gitea repo it produces the dashboard's `REPO_NOT_FOUND` error, an accepted noise trade-off.
The recorded command is currently the entrypoint's absolute path on any wrapper-based install, which rots whenever that path moves; recording the bare binary name and resolving it through `PATH` is agreed and lands with task 0043.
_Avoid_: session hook, ambient hook, postinstall hook _Avoid_: session hook, ambient hook, postinstall hook
**imperative install path**: Installation of the Agent Skill and the [[SessionStart hook]] by running `setup`, which writes into the operator's agent configuration directory.
Requires the operator to own those files; a declaratively generated configuration renders them read-only and the command reports rather than writes.
Contrast the [[declarative install path]]. Both are supported and neither supersedes the other.
_Avoid_: manual install, imperative setup
**declarative install path**: Installation of the Agent Skill and the [[SessionStart hook]] by declaring them in a Nix configuration, which generates the agent configuration rather than mutating it.
Agreed and specified; the outputs it consumes land with task 0045.
Consumes the package's exposed Skill location and [[hook specification]], either directly or through the [[home-manager module]].
Chosen where the operator's agent configuration is generated and therefore read-only; contrast the [[imperative install path]].
_Avoid_: nix install, declarative setup
**home-manager module**: The flake output that declares the Agent Skill and the [[SessionStart hook]] from the package, as the [[declarative install path]]'s ergonomic front end (task 0045).
Importing it does nothing until enabled; it installs the package by default, with a null package the documented way to declare configuration without installing the binary, and carries a toggle per managed piece.
_Avoid_: nix module, HM module
**hook specification**: The single committed declaration of the [[SessionStart hook]]'s recorded shape — command, timeout, and matcher (task 0045).
Read by both the Nix expression and the test suite, so that the [[declarative install path]] and the [[imperative install path]] cannot disagree about what the hook is without failing a test.
_Avoid_: hook config, hook schema

View File

@@ -87,6 +87,12 @@ Leaving a statement the commit itself documents as untrue seemed worse than a on
### Follow-up ### Follow-up
The successor task is described in the spec's Further Notes but not yet written as a task file. The successor is task 0043, which covers both defects together — the absolute-path recording and `isManagedHook` recognising its hook by substring — since both trace to the same resolution line, and deletes `package.nix`'s `postUnpack` rename with them.
It should carry its own ADR and cover both defects together, since both trace to the same resolution line: the absolute-path recording, and `isManagedHook` recognising its hook by substring.
Deleting `package.nix`'s `postUnpack` rename belongs to it. Grilling the mitigation afterwards found the framing here too narrow.
The maintainer's agent configuration is generated declaratively, so `~/.claude/settings.json` and the installed Skill are both read-only symlinks into the Nix store: `setup hooks` cannot write at all, and `setup` crashes outright on an unhandled filesystem error.
The recorded path's *shape* is therefore not the whole defect — the deeper one is that `setup` is write-only against a target that some operators cannot let it write.
Tasks 0044 and 0045 follow from that: a clean failure on unwritable targets, and a declarative install path that generates the configuration instead of mutating it.
The help-text mitigation added by this task is deliberately left in place rather than pre-emptively reverted.
It is accurate until task 0043 lands, which removes it as an acceptance criterion.

View File

@@ -0,0 +1,34 @@
---
spec: nix-flake-packaging
---
## What to build
Make the SessionStart hook survive an upgrade by recording a name that does not move.
Task 0042 established by observation that the hook records the entrypoint's absolute path on every wrapper-based install, and documented a re-run-after-upgrade mitigation.
This task removes the need for that mitigation.
The SDK returns the bare binary name only when a `PATH` entry realpath-matches the entrypoint it is handed.
An npm install satisfies that by symlinking its `bin` entry straight at the entrypoint; a wrapper-based install cannot, because a script that *invokes* a file never resolves *to* that file.
Handing the SDK the location where the binary actually resolves on `PATH`, rather than the module-relative entrypoint, makes the match succeed and the bare name get recorded — using the SDK's own resolution rather than bypassing it.
When the binary is not on `PATH` there is nothing to hand it, and the existing absolute-path behaviour stands unchanged as the fallback.
This is not a Nix accommodation.
Any wrapper-based install has the same shape — a shim, a launcher, a generated `.cmd` — and the fix is the convention for tools that write into user-owned configuration: prior art records a bare name and lets `PATH` resolve it, reserving absolute paths for configuration that a package manager regenerates.
A second defect shares the same line and is fixed here.
The hook is recognised as its own by testing whether the recorded command string *contains* the marker, so an entrypoint path lacking that substring makes re-running `setup hooks` append a duplicate rather than update in place, contradicting the idempotency its help text promises.
Recording the bare name makes the marker match by construction, but the recognition itself should not depend on the recorded command's shape.
The Nix derivation renames its build tree solely to work around that substring coupling.
Once the coupling is gone the rename has no remaining purpose and goes with it.
## Acceptance criteria
- [ ] The recorded hook command is the bare binary name whenever that name resolves to the running program on `PATH`.
- [ ] The recorded hook command remains the absolute entrypoint path when the binary is not resolvable on `PATH`, and that fallback is exercised by a test.
- [ ] Re-running `setup hooks` updates the existing entry in place rather than appending a second one, including when the entrypoint path does not contain the marker.
- [ ] The `setup` help text no longer instructs the user to re-run hooks after an upgrade, that instruction having become false.
- [ ] The derivation no longer renames its build tree, and the build still passes with the tree at a path that does not contain the marker.
- [ ] The behaviour is verified against a real wrapper-based install, not only against a source checkout.

View File

@@ -0,0 +1,29 @@
---
spec: nix-flake-packaging
---
## What to build
Report an unwritable target as an error the user can act on, instead of crashing.
Both halves of `setup` assume the files they manage are writable.
When they are not — because a configuration manager owns them, because a file is flagged immutable, because the path is root-owned — the skill install raises a raw filesystem error with no handling at all, and the hook install surfaces the underlying message through its error collector without saying what a reader should do about it.
Neither failure is exotic.
Any tool that manages a user's agent configuration declaratively renders these paths read-only, and gitea-axi's own Nix install method encourages exactly that arrangement.
The error names the file and the condition, and points at the general remedy: the file appears to be managed elsewhere, so the skill or hook should be declared through that configuration rather than installed by this command.
It deliberately does not guess at the cause.
Read-only is not diagnostic of any particular manager, and naming one would be wrong for most users who hit this.
The failure follows the CLI's existing error convention rather than inventing a shape, so it carries a code and help lines like every other error the tool reports.
## Acceptance criteria
- [ ] An unwritable skill target produces a structured CLI error rather than an unhandled filesystem exception.
- [ ] An unwritable hook target produces the same class of error, with the same guidance.
- [ ] Both errors name the file that could not be written and state that it appears to be managed by another tool.
- [ ] Neither error names or infers a specific configuration manager.
- [ ] A skill target that is unwritable but already byte-identical to the bundled copy succeeds rather than failing, since nothing needs to be written.
- [ ] The errors carry a code and help lines consistent with the rest of the CLI's error surface.

View File

@@ -0,0 +1,45 @@
---
spec: nix-flake-packaging
blocked-by: 0043-hook-records-bare-binary-name
---
## What to build
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.
They install the Agent Skill and the SessionStart hook by writing into the user's agent configuration directory, which works only when the user owns those files imperatively.
An operator whose agent configuration is generated declaratively cannot use either: the targets are read-only, and the operator is left hand-copying the Skill into their own configuration, where it silently drifts from the package that ships it.
The spec currently lists a home-manager module under Out of Scope, deferring it until there was usage evidence that the trade-off was worth making.
That evidence now exists, and the deferral's stated reasoning does not survive it: the concern was the automatism that ADR 0009 rejected when it chose an explicit `setup` command over a postinstall script, and a module the operator explicitly imports and enables is the opposite of an implicit install.
Revising that Out of Scope entry, and recording the decision as an ADR, is part of this task.
Two layers, the second built on the first.
The package gains a stable, documented location for the bundled Agent Skill, and exposes both the Skill and the hook's specification as attributes a Nix expression can consume.
Today the Skill's only address is a path inside the installed node modules tree, which is an implementation detail no consumer should depend on.
On top of that, the flake exposes a home-manager module: a thin wiring layer that declares the Skill and the hook from those attributes.
It follows the conventions the home-manager module tree overwhelmingly uses — an enable option so that importing the module does nothing until it is switched on, an overridable package option, and installation of that package by default with a null value as the documented opt-out for an operator who supplies the binary another way.
Each managed piece has its own toggle, defaulting on, so an operator can take the Skill declaratively while continuing to write the hook by hand.
The hook's specification is declared once, in a committed file that both the Nix expression and the test suite read.
Declaring it in the Nix expression alone would create a second source of truth alongside the behaviour of the imperative install path, with nothing to keep them agreed; a test that hardcoded the same values a third time would verify nothing.
The test drives the imperative install against a temporary home directory and asserts that what it writes matches what the file declares, so a divergence — including one introduced by the SDK changing the envelope it writes — fails a test rather than passing silently into a release.
The two installation paths remain independent and both supported: the command for operators who own their configuration, the module for operators whose configuration owns them.
## Acceptance criteria
- [ ] The bundled Agent Skill is installed to a stable location in the package output that is not an internal implementation path.
- [ ] The package exposes the Skill and the hook specification as attributes consumable from a Nix expression without building or running anything.
- [ ] The hook specification is declared in a single committed file, read by both the Nix expression and the test suite.
- [ ] A test drives the imperative hook install and asserts that what it writes matches the declared specification, failing if either side drifts.
- [ ] The flake exposes a home-manager module that declares the Skill and the hook.
- [ ] Importing the module without enabling it changes nothing about the resulting configuration.
- [ ] The module installs the package by default, and accepts a null package as the documented way to declare the configuration without installing the binary.
- [ ] The Skill and the hook each have their own toggle, both defaulting to on.
- [ ] The module composes with an existing configuration that already declares its own SessionStart hooks and skills, rather than conflicting with it.
- [ ] The spec's Out of Scope entry excluding a home-manager module is revised, and the decision to reverse it is recorded as an ADR.
- [ ] The user-facing documentation describes both installation paths and when each applies.