feat(vpn): converge the fleet skill library onto the pinned private rev (NCFG#110) #1210

Merged
hermes merged 1 commit from kb/NCFG-110-skill-library-git into main 2026-09-29 15:27:55 -06:00
Collaborator

The converge half of Kanban nix-config#110, slice 1. The other half — the skill CONTENT — is nix-config-private/skills/** (private PR #253); this PR is what installs it on vpn.

What it adds

  • hermesSkills (hosts/vpn/default.nix) — a derivation that copies ${inputs.nix-config-private}/skills into the store, files 0444 / dirs 0555, with SOURCE_REV naming the rev it was built from. The kanboardScripts shape, for the same reasons: content that can only change through a merge, read-only on the host, identifiable by blob at verify time (the pin is an integ: merge commit, so a rev comparison would read a false negative).
  • hosts/vpn/hermes-skills-converge.sh — the converge, as a real file rather than a Nix string, so shellcheck, the contract test and the host run the same bytes. For every skill the store provides it makes $HERMES_HOME/.hermes/skills/<group>/<skill> a symlink into the store; a previous generation's link is PRUNED when this generation no longer provides that skill (that is how a skill deleted in git leaves the host); a live skill directory at a managed name is REPLACED (the first converge on the real host finds 133 of them).
  • system.activationScripts.hermes-skills — calls it with the store path, the shared tree, the rev and --owner hermes:hermes, after hermes-agent-setup.
  • hermes-profiles-setup now depends on hermes-skills — the profile gate links the declared groups out of the shared tree and FAILS the switch on a declared group the shared tree cannot provide (hermes-profile-skills.sh's MISSING class). On a fresh host those group dirs only exist once the library converge has run, so the order is load-bearing, not cosmetic.
  • .forgejo/tests/test-fleet-skills.sh, run by nix-build-check.

What it must not do — and is tested not to

The shared tree is shared, so the interesting cases are the ones where a converger could damage it:

  • an empty store is FATAL (exit 2) and prunes nothing (asserted: the live links and agent-authored/ are still there afterwards) — a build that lost its content must not be able to delete the library;
  • agent-authored/ (hermes' own skills.create_dir, the write path nix-config#101 built), the runtime dotfiles (.usage.json, .curator_ledger.jsonl, .hub/, .bundled_manifest), the app's seeded bundled skills and any unmanaged skill directory are left exactly where they are;
  • a managed name occupied by something that is neither a skill directory nor a link the script made FAILS the run (exit 1) instead of being deleted;
  • a link at a skill slot pointing outside the library, and a library-shaped link whose target is not a skill directory, are both kept;
  • the second run is a no-op (activations re-run on every switch).

The contract test runs the real script against fixture trees for all of the above, asserts the wiring (the store comes from the pinned private rev, the activation passes the real shared tree and the rev) and asserts the ORDER against the profile gate as an order, with three mutations that must turn it red, plus bash -n and shellcheck (-S warning -x, the flags the repo uses) in CI. Local: 21/21 cases, test-profile-skills.sh still 31/31.

Ordering of the merge, and the pin

This PR's eval reads ${inputs.nix-config-private}/skills, so the pinned private rev must already carry it: the private PR (#253) merges first, the CD chain bumps the pin in nix-config, and this branch is rebased onto that bump before its CI runs. The private repo's .forgejo/deploy/skills-scope.sh (in #253) is what makes the chain classify skills/** as a vpn-reader path — without it the chain gate would set PR_SKIP, the pin would never move, and this converge would deploy against content that is not there.

What changes on the host once this lands

Managed skills become read-only store symlinks. skill_manage PATCH on one is then refused (Path.rglob does not descend a symlinked directory), which is the intended "stricter than willy-nilly" state and is documented in the library doc and in two skills that ship with it — the git path (a PR to nix-config-private) is the way to change a fleet skill from now on, while agent-authored/ stays writable for a new one.

Verification (after merge and deploy)

readlink -f an installed skill → the hermes-skills-<rev> store path; sha256sum of an installed SKILL.md == git cat-file blob <pinned rev>:skills/… == the blob on origin/main of the private repo; hermes-profile-skills.sh still passes; a curated skill loads (skill_view) from the coding profile through the profile symlink chain.

The converge half of Kanban nix-config#110, slice 1. The other half — the skill CONTENT — is `nix-config-private/skills/**` (private PR #253); this PR is what installs it on vpn. ## What it adds * **`hermesSkills`** (`hosts/vpn/default.nix`) — a derivation that copies `${inputs.nix-config-private}/skills` into the store, files 0444 / dirs 0555, with `SOURCE_REV` naming the rev it was built from. The `kanboardScripts` shape, for the same reasons: content that can only change through a merge, read-only on the host, identifiable by blob at verify time (the pin is an `integ:` merge commit, so a rev comparison would read a false negative). * **`hosts/vpn/hermes-skills-converge.sh`** — the converge, as a real file rather than a Nix string, so shellcheck, the contract test and the host run the same bytes. For every skill the store provides it makes `$HERMES_HOME/.hermes/skills/<group>/<skill>` a symlink into the store; a previous generation's link is PRUNED when this generation no longer provides that skill (that is how a skill deleted in git leaves the host); a live skill directory at a managed name is REPLACED (the first converge on the real host finds 133 of them). * **`system.activationScripts.hermes-skills`** — calls it with the store path, the shared tree, the rev and `--owner hermes:hermes`, after `hermes-agent-setup`. * **`hermes-profiles-setup` now depends on `hermes-skills`** — the profile gate links the declared groups out of the shared tree and FAILS the switch on a declared group the shared tree cannot provide (`hermes-profile-skills.sh`'s MISSING class). On a fresh host those group dirs only exist once the library converge has run, so the order is load-bearing, not cosmetic. * **`.forgejo/tests/test-fleet-skills.sh`**, run by `nix-build-check`. ## What it must not do — and is tested not to The shared tree is shared, so the interesting cases are the ones where a converger could damage it: * an **empty store is FATAL (exit 2) and prunes nothing** (asserted: the live links and `agent-authored/` are still there afterwards) — a build that lost its content must not be able to delete the library; * `agent-authored/` (hermes' own `skills.create_dir`, the write path nix-config#101 built), the runtime dotfiles (`.usage.json`, `.curator_ledger.jsonl`, `.hub/`, `.bundled_manifest`), the app's seeded bundled skills and any unmanaged skill directory are left exactly where they are; * a managed name occupied by something that is neither a skill directory nor a link the script made FAILS the run (exit 1) instead of being deleted; * a link at a skill slot pointing outside the library, and a library-shaped link whose target is not a skill directory, are both kept; * the second run is a no-op (activations re-run on every switch). The contract test runs the real script against fixture trees for all of the above, asserts the wiring (the store comes from the pinned private rev, the activation passes the real shared tree and the rev) and asserts the ORDER against the profile gate as an order, with three mutations that must turn it red, plus `bash -n` and shellcheck (`-S warning -x`, the flags the repo uses) in CI. Local: 21/21 cases, `test-profile-skills.sh` still 31/31. ## Ordering of the merge, and the pin This PR's eval reads `${inputs.nix-config-private}/skills`, so the pinned private rev must already carry it: the private PR (#253) merges first, the CD chain bumps the pin in `nix-config`, and this branch is rebased onto that bump before its CI runs. The private repo's `.forgejo/deploy/skills-scope.sh` (in #253) is what makes the chain classify `skills/**` as a vpn-reader path — without it the chain gate would set `PR_SKIP`, the pin would never move, and this converge would deploy against content that is not there. ## What changes on the host once this lands Managed skills become read-only store symlinks. `skill_manage` PATCH on one is then refused (`Path.rglob` does not descend a symlinked directory), which is the intended "stricter than willy-nilly" state and is documented in the library doc and in two skills that ship with it — the git path (a PR to `nix-config-private`) is the way to change a fleet skill from now on, while `agent-authored/` stays writable for a new one. ## Verification (after merge and deploy) `readlink -f` an installed skill → the `hermes-skills-<rev>` store path; `sha256sum` of an installed `SKILL.md` == `git cat-file blob <pinned rev>:skills/…` == the blob on `origin/main` of the private repo; `hermes-profile-skills.sh` still passes; a curated skill loads (`skill_view`) from the coding profile through the profile symlink chain.
feat(vpn): converge the fleet skill library onto the pinned private rev (NCFG#110)
Some checks reported warnings
PR base guard / Base drift / conflict check (pull_request) Successful in 3s
Secret Scan / gitleaks scan (pull_request) Successful in 7s
NixOS Build Check / Scope changed deployable hosts (pull_request) Successful in 25s
NixOS Build Check / Flake input regression guard (pull_request) Successful in 2s
foremerge intent (advisory) 2 of 4 changed path(s) have no live foremerge intent: .forgejo/tests/test-fleet-skills.sh, hosts/vpn/hermes-skills-converge.sh
NixOS Build Check / Build changed host closures (native, one eval) (pull_request) Successful in 47s
0b793d46a7
The public half of nix-config#110 slice 1 (the content is nonpublic/private PR
#253). The fleet's skill library was the last big surface on vpn that was pure
host state: 176 skills, unversioned, unreviewable and unreproducible.

* `hermesSkills` — a derivation that copies `${inputs.nix-config-private}/skills`
  into the store (files 0444, dirs 0555) and writes `SOURCE_REV`; the
  `kanboardScripts` shape, for the same reasons.
* `hosts/vpn/hermes-skills-converge.sh` — the converge, a real .sh file so
  shellcheck, the contract test and the host run the same bytes. Every skill the
  store provides becomes a symlink at the same relative path in
  $HERMES_HOME/.hermes/skills; a previous generation's link is pruned when this
  generation stops providing that skill; a live skill directory at a managed name
  is replaced (chmod -R u+w first: 7 of the 133 real ones are store-seeded
  r-xr-xr-x trees whose parent must be writable to unlink). An EMPTY store is
  FATAL (exit 2) and prunes nothing — a build that lost its content must not be
  able to delete the library. agent-authored/ (hermes' own create_dir), the
  runtime dotfiles, the app's seeded bundled skills and any unmanaged skill
  directory are never touched.
* `system.activationScripts.hermes-skills` runs it after `hermes-agent-setup`;
  `hermes-profiles-setup` now depends on it, because the profile gate links the
  declared groups out of this tree and fails the switch (MISSING) on one the
  converge has not created yet.
* `.forgejo/tests/test-fleet-skills.sh` (22 cases, wired into
  nix-build-check.yml): both store layouts, idempotence, REPLACED incl. a
  read-only store-seeded tree, PRUNED, KEPT, the empty-store fatality asserting
  the live tree survives, the collision failing instead of deleting, the
  bad-invocation paths, the wiring and the load-bearing order, three mutations.
Author
Collaborator

foremerge intent coverage (advisory) — 2 of 4 changed path(s) in ymrtech/nix-config#1210 have no live foremerge intent:

  • .forgejo/tests/test-fleet-skills.sh
  • hosts/vpn/hermes-skills-converge.sh

Coverage was decided by reading the repo's foremerge ledger (48 in-flight intent(s) considered) against this PR's changed paths — no branch or session name is involved, so a force-push does not change the answer. Paths that legitimately need no claim (docs, .md, flake.lock) are excluded.

This is advisory and does not block the merge. To clear it, publish intent for the paths above (foremerge intent publish --agent <your agent id> --task <task> --summary <what and why> --scope file:<path>=modify), or ignore it if the work genuinely collides with nobody.

**foremerge intent coverage (advisory)** — 2 of 4 changed path(s) in ymrtech/nix-config#1210 have no live foremerge intent: - `.forgejo/tests/test-fleet-skills.sh` - `hosts/vpn/hermes-skills-converge.sh` Coverage was decided by reading the repo's foremerge ledger (48 in-flight intent(s) considered) against this PR's changed paths — no branch or session name is involved, so a force-push does not change the answer. Paths that legitimately need no claim (docs, `.md`, `flake.lock`) are excluded. This is advisory and does **not** block the merge. To clear it, publish intent for the paths above (`foremerge intent publish --agent <your agent id> --task <task> --summary <what and why> --scope file:<path>=modify`), or ignore it if the work genuinely collides with nobody.
hermes force-pushed kb/NCFG-110-skill-library-git from 0b793d46a7
Some checks reported warnings
PR base guard / Base drift / conflict check (pull_request) Successful in 3s
Secret Scan / gitleaks scan (pull_request) Successful in 7s
NixOS Build Check / Scope changed deployable hosts (pull_request) Successful in 25s
NixOS Build Check / Flake input regression guard (pull_request) Successful in 2s
foremerge intent (advisory) 2 of 4 changed path(s) have no live foremerge intent: .forgejo/tests/test-fleet-skills.sh, hosts/vpn/hermes-skills-converge.sh
NixOS Build Check / Build changed host closures (native, one eval) (pull_request) Successful in 47s
to a6dde177df
All checks were successful
PR base guard / Base drift / conflict check (pull_request) Successful in 3s
NixOS Build Check / Scope changed deployable hosts (pull_request) Successful in 14s
Secret Scan / gitleaks scan (pull_request) Successful in 8s
NixOS Build Check / Flake input regression guard (pull_request) Successful in 3s
NixOS Build Check / Build changed host closures (native, one eval) (pull_request) Successful in 34s
NixOS Build Check / Hermes dispatcher worktree base (pull_request) Successful in 13s
foremerge intent (advisory) all 4 changed path(s) covered by a live foremerge intent
2026-09-29 15:22:32 -06:00
Compare
Author
Collaborator

foremerge intent coverage (advisory) — update for ymrtech/nix-config#1210: all 4 claimable changed path(s) are now covered by a live foremerge intent. Nothing to do; this comment closes the earlier gap warning.

**foremerge intent coverage (advisory)** — update for ymrtech/nix-config#1210: all 4 claimable changed path(s) are now covered by a live foremerge intent. Nothing to do; this comment closes the earlier gap warning.
hermes merged commit 3898accc45 into main 2026-09-29 15:27:55 -06:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ymrtech/nix-config!1210
No description provided.