skillnet 0.3.0

Reconcile and manage local AI skill mirrors; calibration data for the multi-phase-plan skill.
Documentation
# Phase 03 — HM module: declarative `settings` / `catalogSettings`

> **Recommended Codex model: GPT 5.5 medium**
>
> The interesting work is writing two TOML files out of attrset options
> using `pkgs.formats.toml`, plumbing them into XDG config, and pointing
> phase 02's env vars at them when the user opts in. Moderate complexity:
> attrset → TOML serialisation has gotchas (lists of tables, ordering),
> and the option type has to be permissive enough for users to declare
> arbitrary skillnet schema without us mirroring it in Nix.

## Working tree

Same repo: `/data/nvme0/can/Projects/skillnet`. Phase 02 must already be
merged.

## Goal

A user can write, in their HM config:

```nix
programs.skillnet = {
  enable = true;
  settings = {
    mirror_root = "/home/can/.agents/skills-mirror";
    scopes = [ { name = "global"; sources = [ ... ]; } ];
    # … etc, whatever skillnet.toml accepts
  };
  catalogSettings = { … };
  database.urlFile = "/run/agenix/skillnet-pg-url";
};
```

…and `skillnet status` works from `/tmp` without ever touching cwd. The
generated TOML files live under `$XDG_CONFIG_HOME/skillnet/` and are
pointed at by `SKILLNET_CONFIG` / `SKILLNET_CATALOG_CONFIG` automatically.

## Why this matters now

Phase 02 lets users hand-write their TOMLs and point `configFile` /
`catalogConfigFile` at them. That's the floor. The ceiling is declarative
config: the HM module renders the TOMLs from Nix, so the user's entire
skillnet setup is reproducible from a single flake.

Without this, the existing reserved no-op attrs placeholder remains a TODO,
and users have to maintain TOMLs out-of-band.

## Out of scope

- Mirroring skillnet's TOML schema as typed Nix options. The option type
  is `attrsOf anything` — we let users pass through, with a thin warning
  in the docs that schema validation happens server-side in skillnet
  itself.
- Migrating existing users of the reserved placeholder option. It is documented
  as "unused in 0.2.0", so nothing to migrate.
- Catalog regeneration logic. We only write the TOML files; `skillnet
  catalog generate` continues to run on demand.

## Plan

1. **Replace the reserved placeholder** with two real options:

   ```nix
   settings = lib.mkOption {
     type = lib.types.nullOr (pkgs.formats.toml {}).type;
     default = null;
     example = lib.literalExpression ''
       {
         mirror_root = "/home/alice/skills-mirror";
         scopes = [ { name = "global"; sources = [ "/home/alice/.claude/skills" ]; } ];
       }
     '';
     description = ''
       Declarative content of skillnet.toml, written to
       $XDG_CONFIG_HOME/skillnet/skillnet.toml and exported via
       SKILLNET_CONFIG. Pass-through: skillnet validates the schema at
       runtime.
     '';
   };

   catalogSettings = lib.mkOption {
     type = lib.types.nullOr (pkgs.formats.toml {}).type;
     default = null;
     description = "Declarative content of skillnet.catalog.toml.";
   };
   ```

   Delete the placeholder option, which is documented as unused in 0.2.0.

2. **Render to disk via `xdg.configFile`.** When the corresponding setting
   is non-null:

   ```nix
   xdg.configFile."skillnet/skillnet.toml" = lib.mkIf (cfg.settings != null) {
     source = (pkgs.formats.toml {}).generate "skillnet.toml" cfg.settings;
   };
   xdg.configFile."skillnet/skillnet.catalog.toml" = lib.mkIf (cfg.catalogSettings != null) {
     source = (pkgs.formats.toml {}).generate "skillnet.catalog.toml" cfg.catalogSettings;
   };
   ```

3. **Auto-point the env vars** when `settings` / `catalogSettings` is set
   and the explicit `configFile` / `catalogConfigFile` from phase 02 is
   *not* set:

   ```nix
   programs.skillnet.configFile = lib.mkIf (cfg.settings != null)
     (lib.mkDefault "${config.xdg.configHome}/skillnet/skillnet.toml");
   programs.skillnet.catalogConfigFile = lib.mkIf (cfg.catalogSettings != null)
     (lib.mkDefault "${config.xdg.configHome}/skillnet/skillnet.catalog.toml");
   ```

   Use `mkDefault` so a user who explicitly sets `configFile` overrides
   this auto-wiring. The env var export from phase 02 then fires off the
   `configFile` value, which keeps the precedence chain simple:
   declarative `settings` ⇒ generated file ⇒ `configFile` value ⇒
   `SKILLNET_CONFIG` env var ⇒ binary.

4. **Document the precedence** in the option descriptions:
   - `settings` wins, generating a file under `$XDG_CONFIG_HOME`.
   - `configFile` (phase 02) takes whatever path the user gives.
   - Both null → binary falls back to cwd `./skillnet.toml`.

5. **Eval:** `nix flake check --no-build` to confirm `pkgs.formats.toml`
   round-trips the user's example. Build the activation package and
   inspect the generated `~/.config/skillnet/skillnet.toml` (this happens
   end-to-end in phase 04's extended test).

## Acceptance criteria

- [ ] `programs.skillnet.settings = { … };` causes
  `~/.config/skillnet/skillnet.toml` to exist after activation, with
  content equal to the rendered TOML.
- [ ] `SKILLNET_CONFIG` in `hm-session-vars.sh` points at that file when
  the user did not also set `configFile`.
- [ ] Explicit `configFile = "/elsewhere/skillnet.toml"` overrides the
  auto-derived path (verify via `home-manager build` and inspecting
  `hm-session-vars.sh`).
- [ ] The placeholder option is removed; users who set it see an evaluation
  error pointing them at `settings` (acceptable because the option was
  documented as unused).
- [ ] `nix flake check` passes.

## Files likely touched

- [nix/hm-module.nix]../../../../nix/hm-module.nix — new options,
  `xdg.configFile` entries, `mkDefault` plumbing.
- (No source / test changes in this phase.)

## Pitfalls

- **`pkgs.formats.toml` ordering is unspecified.** Symptom: the file
  diff-flaps on rebuild because attrset keys serialise in different
  order. Cause: Nix attrsets are sorted alphabetically when rendered, but
  list-of-attrset ordering may surprise — confirm `scopes` (list of
  tables) serialises in declared order. Recovery: it does, by construction
  — lists preserve order in `pkgs.formats.toml`. Test by rendering twice
  and `diff`ing.
- **`(pkgs.formats.toml {}).type` rejects functions and derivations.**
  Symptom: eval error if the user puts a derivation reference into
  `settings`. Cause: TOML can't represent it. Recovery: this is the right
  behaviour — fail at eval time with a clear error.
- **`xdg.configFile` requires `xdg.enable = true` on some HM versions.**
  Symptom: file not written. Recovery: `xdg.enable` defaults to true on
  recent HM; if older versions are supported, add `xdg.enable =
  lib.mkDefault true;` when `settings != null`.
- **Removing the placeholder option breaks downstream evals.** Symptom: a
  third party who set the unused option fails to evaluate. Acceptable since
  the option was documented as a no-op, but record the removal in the
  CHANGELOG (phase 05).
- **Infinite-recursion risk with `mkDefault`-on-`configFile`.** Symptom:
  Nix complains about self-reference. Cause: writing
  `programs.skillnet.configFile = mkIf (settings != null) ...` inside the
  same `config` block that also reads `cfg.configFile`. Recovery: read
  `config.programs.skillnet.configFile` (the post-merge value) rather
  than `cfg.configFile` when exporting the env var in phase 02's block;
  this is how HM modules avoid recursion.

## Reference

- Phase 02 ([02-hm-env-and-fixes.md]./02-hm-env-and-fixes.md) — defines
  the `configFile` / `catalogConfigFile` options this phase populates.
- HM `xdg.configFile` reference:
  https://nix-community.github.io/home-manager/options.xhtml (search
  `xdg.configFile`).
- `pkgs.formats.toml` source:
  `pkgs/pkgs-lib/formats.nix` in nixpkgs.