skillnet 0.4.0

Reconcile and manage local AI skill mirrors; calibration data for the multi-phase-plan skill.
Documentation
# Phase 02 — HM module: env wiring + four orthogonal fixes

> **Recommended Codex model: GPT 5.5 medium**
>
> Four small but non-mechanical changes in one Nix module: export new env
> vars introduced in phase 01, wire `database.path` for sqlite, soften the
> `skillsRoot` activation, and add `database.urlFile` so passwords stop
> leaking into `hm-session-vars.sh`. Each fix is small individually but the
> module's `mkMerge`/`mkIf` plumbing needs careful editing to keep the
> existing test ([nix/test-hm-module.nix]../../../../nix/test-hm-module.nix)
> green. `medium` is correct; `low` risks subtle option-merging bugs.

## Working tree

Same repo: `/data/nvme0/can/Projects/skillnet`. Phase 01 must already be
merged so the env var names `SKILLNET_CONFIG` and `SKILLNET_CATALOG_CONFIG`
are honoured by the binary.

## Goal

[nix/hm-module.nix](../../../../nix/hm-module.nix) exports
`SKILLNET_CONFIG` and `SKILLNET_CATALOG_CONFIG` (the env hooks introduced
in phase 01), honours `programs.skillnet.database.path` end-to-end for the
sqlite backend, emits a warning instead of `exit 1` when `skillsRoot` is
absent, and offers `programs.skillnet.database.urlFile` so secrets stay out
of `hm-session-vars.sh`.

## Why this matters now

Phase 01 added env-var hooks in the CLI; this phase is what makes them
visible after `home-manager switch`. The other three fixes are gaps the
README of this plan documents:

- `database.path` is declared but ignored — the SQLite branch only sets
  `SKILLNET_DATA_DIR` from `dataDir`, not from `database.path`. Anyone who
  sets `database.path` silently gets a different file location than they
  asked for.
- `skillsRoot` activation aborts with `exit 1` when the directory is
  missing, bricking the first `home-manager switch` on a fresh machine.
- The Postgres URL goes into the world-readable
  `/nix/store/.../hm-session-vars.sh`. For users who embed credentials in
  the URL (the common case for self-hosted Postgres), that's a real leak.

## Out of scope

- Adding `programs.skillnet.settings` / `catalogSettings` options that
  *write* TOML — that's phase 03. This phase only points env vars at
  whatever the user gives it (an option type `nullOr str` for each path).
- Renaming `dataDir` or `database.url`. Back-compat must be preserved.
- Removing the lowercase `skillnet_DATA_DIR` alias. Adjust the option
  description to mark it deprecated; deletion can happen in a later
  release.

## Plan

1. **Add path options for the new env vars.** Add two new options under
   `options.programs.skillnet`:

   ```nix
   configFile = lib.mkOption {
     type = lib.types.nullOr lib.types.path;
     default = null;
     description = ''
       Absolute path to skillnet.toml. When set, exported as
       SKILLNET_CONFIG so the binary can be invoked from any directory.
       Leave null to keep the cwd-based default behaviour.
     '';
   };

   catalogConfigFile = lib.mkOption {
     type = lib.types.nullOr lib.types.path;
     default = null;
     description = ''
       Absolute path to skillnet.catalog.toml. Exported as
       SKILLNET_CATALOG_CONFIG when set.
     '';
   };
   ```

   In the `config` block, conditionally export:

   ```nix
   (lib.mkIf (cfg.configFile != null) {
     home.sessionVariables.SKILLNET_CONFIG = toString cfg.configFile;
   })
   (lib.mkIf (cfg.catalogConfigFile != null) {
     home.sessionVariables.SKILLNET_CATALOG_CONFIG = toString cfg.catalogConfigFile;
   })
   ```

2. **Wire `database.path` for the sqlite backend.** Today
   [nix/hm-module.nix:72-81]../../../../nix/hm-module.nix#L72-L81 only
   sets `SKILLNET_DATA_DIR` from `cfg.dataDir`. Make the sqlite branch
   prefer `database.path`'s parent when set, falling back to `dataDir`.
   Concretely: if `cfg.database.path != null`, set
   `SKILLNET_DATA_DIR = dirOf cfg.database.path` (or just export both vars
   if `src/calibration/db.rs` reads `database.path` distinctly — check
   [src/calibration/db.rs:120]../../../../src/calibration/db.rs#L120
   first; the precedence table in
   [docs/src/commands.md:44-53]../../../../docs/src/commands.md#L44-L53
   says `[database].path` is honoured directly from the TOML, so the HM
   side only needs to ensure the parent dir exists). The simpler fix:

   ```nix
   (lib.mkIf (cfg.database.backend == "sqlite") {
     home.sessionVariables = {
       SKILLNET_DATA_DIR = cfg.dataDir;
       skillnet_DATA_DIR = cfg.dataDir;  # deprecated alias
     };
     home.activation.skillnet-data-dir = lib.hm.dag.entryAfter ["writeBoundary"] ''
       $DRY_RUN_CMD mkdir -p ${lib.escapeShellArg cfg.dataDir}
       ${lib.optionalString (cfg.database.path != null) ''
         $DRY_RUN_CMD mkdir -p ${lib.escapeShellArg (builtins.dirOf cfg.database.path)}
       ''}
     '';
   })
   ```

   Add an assertion: `cfg.database.backend == "sqlite" -> cfg.database.path == null || lib.hasPrefix "/" cfg.database.path` (must be absolute).

3. **Soften `skillsRoot` activation.** Replace the `exit 1` block at
   [nix/hm-module.nix:90-96]../../../../nix/hm-module.nix#L90-L96 with a
   warning that does not abort:

   ```nix
   home.activation.skillnet-skills-root = lib.hm.dag.entryAfter ["writeBoundary"] ''
     if [ ! -d ${lib.escapeShellArg cfg.skillsRoot} ]; then
       echo "skillnet: programs.skillnet.skillsRoot does not exist: ${cfg.skillsRoot}" >&2
       echo "skillnet: clone or restore the ai-skills checkout at that path; skipping for now." >&2
     fi
   '';
   ```

   Update the test expectation in phase 04 accordingly (no longer asserts
   failure mode).

4. **Add `database.urlFile`.** New option:

   ```nix
   urlFile = lib.mkOption {
     type = lib.types.nullOr lib.types.path;
     default = null;
     description = ''
       Path to a file containing the Postgres connection URL. Preferred
       over database.url because url ends up in the world-readable
       hm-session-vars.sh. Read at shell init via a small wrapper that
       exports SKILLNET_DATABASE_URL=$(cat <urlFile>).
     '';
   };
   ```

   Implementation: when `urlFile != null`, do **not** set
   `home.sessionVariables.SKILLNET_DATABASE_URL`. Instead, inject a
   session-init snippet:

   ```nix
   programs.bash.initExtra = lib.mkIf (cfg.database.urlFile != null) ''
     if [ -r ${lib.escapeShellArg cfg.database.urlFile} ]; then
       export SKILLNET_DATABASE_URL="$(cat ${lib.escapeShellArg cfg.database.urlFile})"
     fi
   '';
   programs.zsh.initExtra = ...; programs.fish.shellInit = ...;
   ```

   (Match whichever shells the existing module already touches. If only
   bash is targeted, that's fine; document the limitation.) Update the
   Postgres-backend assertion to allow either `url` or `urlFile`:

   ```nix
   assertion = cfg.database.backend != "postgres"
     || cfg.database.url != null
     || cfg.database.urlFile != null;
   message = "programs.skillnet.database needs `url` or `urlFile` when backend = \"postgres\".";
   ```

5. **Update the existing test stub** so it still passes (full extension
   happens in phase 04). At minimum, verify `nix flake check` evaluates
   the module; the existing
   [nix/test-hm-module.nix]../../../../nix/test-hm-module.nix block that
   greps for the `exit 1` activation message must be relaxed (the grep on
   line 87 will fail because the new message no longer says `exit 1`). For
   now: change the grep to look for the new "skipping for now" line.

6. **Eval-check locally:** `nix flake check --no-build` (fast eval pass)
   and `nix build .#checks.x86_64-linux.hm-module-test` (full activation
   build).

## Acceptance criteria

- [ ] `programs.skillnet.configFile = "/abs/skillnet.toml"` produces
  `SKILLNET_CONFIG=/abs/skillnet.toml` in `hm-session-vars.sh`.
- [ ] `programs.skillnet.catalogConfigFile = ...` likewise sets
  `SKILLNET_CATALOG_CONFIG`.
- [ ] Sqlite branch: setting `programs.skillnet.database.path =
  "/abs/dir/db.sqlite"` causes the activation to `mkdir -p /abs/dir`.
- [ ] `programs.skillnet.skillsRoot = "/nonexistent"` no longer fails
  `home-manager switch`; warning is printed on stderr.
- [ ] Postgres branch with `database.urlFile` set: `hm-session-vars.sh`
  contains **no** `SKILLNET_DATABASE_URL` assignment, and an interactive
  shell sources `SKILLNET_DATABASE_URL` from the file at init.
- [ ] Assertion fires when `backend = "postgres"` and neither `url` nor
  `urlFile` is set.
- [ ] `nix flake check` passes (the existing
  [nix/test-hm-module.nix]../../../../nix/test-hm-module.nix is
  minimally updated; comprehensive extension is in phase 04).

## Files likely touched

- [nix/hm-module.nix]../../../../nix/hm-module.nix — new options,
  new sessionVariables conditionals, softened activation, urlFile shell
  init.
- [nix/test-hm-module.nix]../../../../nix/test-hm-module.nix — relax the
  `exit 1` grep on the `skills-root` activation; do **not** add new
  positive tests here (phase 04 does that).

## Pitfalls

- **`home.sessionVariables` does not run shell logic.** Symptom: you try
  to set `SKILLNET_DATABASE_URL = "$(cat ${urlFile})"` and end up with the
  literal `$(cat /path)` in `hm-session-vars.sh`. Cause: the file is a
  static `KEY=value` dump, not a shell script. Recovery: use
  `programs.<shell>.initExtra`, not `home.sessionVariables`, for any
  command-substitution.
- **`urlFile` made required without back-compat.** Symptom: existing
  configs with only `database.url = "postgres://..."` break. Recovery:
  the assertion in step 4 accepts either source; only mutually-exclusive
  case to add to docs is "if both are set, urlFile wins" (or just refuse
  both — pick one and document).
- **Activation order with `database.path`.** Symptom: skillnet writes to
  `/abs/dir/db.sqlite` and crashes because `/abs/dir` doesn't exist.
  Cause: the `mkdir -p` lives in a separate activation block that may run
  after sourcing. Recovery: keep the `entryAfter ["writeBoundary"]`
  ordering and ensure the `mkdir -p` is in the same activation block as
  the `dataDir` one (don't fork into a second block).
- **`skillsRoot` warning suppressed in batch HM runs.** Symptom: user
  doesn't notice the missing directory. Recovery: prefix the warning with
  `>&2 echo "WARNING:"` and ensure it's visible in `home-manager switch`
  output — HM passes stderr through.
- **Lowercase `skillnet_DATA_DIR` alias.** Don't delete it in this
  phase — there's a `grep -F skillnet_DATA_DIR` in the existing test
  ([nix/test-hm-module.nix:69]../../../../nix/test-hm-module.nix#L69).
  Add a doc-comment marking deprecation.

## Reference

- Phase 01 ([01-cli-env-vars.md]./01-cli-env-vars.md) — env var names.
- HM activation script reference:
  https://nix-community.github.io/home-manager/options.xhtml (search for
  `home.activation`).
- Phase 04 ([04-test-non-cwd-status.md]./04-test-non-cwd-status.md)
  consumes the env-var exports introduced here.