skills: let the skillset own the skills it owns, and stop a dangling link from killing boot
Baked skill links won over the live skillset clone for all three vendored skills, so a pushed edit to skills/mempalace/SKILL.md was invisible in every container until the next image build -- measured on two hosts (live md5 129bcc4752 vs baked 5236024fef). Cause was ordering, not intent: the baked links are created early with a create-only-when-absent guard to close a smoke readiness race, and the skillset deploy runs last and treats them as foreign. The comment claimed the opposite of the behaviour. The fix is not "skillset always wins". Ownership is per-skill: pi-extensions is owned by its package repo and copied over the snapshot at build time, so the skillset's lagging duplicate must keep losing; pi-devbox-environment is authored here. Only mempalace is skillset-owned. devbox-skill-reconcile therefore runs after the deploy and repoints only the names in skills/skillset-owned.txt, replacing a link solely when it points into the baked tree, so a real directory or a link pointing elsewhere is never disturbed. Precedence is now user override -> live clone (owned names) -> baked snapshot, with the early links intact as the fallback so the readiness race stays closed. Reviewing that turned up a latent boot-abort in the pre-existing baked-link block: `[ ! -e "$link" ]` is TRUE for a dangling symlink, so once a link can point into /workspace/skillset, a vanished mount makes plain `ln -s` fail with "File exists" -- and under `set -euo pipefail` that aborts container start before `exec "$@"`. Reachable on `docker restart` or a host reboot, not on a recreate, since ~/.agents is not a volume on any host. Now `ln -sfn`, which heals the link back to the baked fallback. Smoke additions cover what let this ship: the stale-snapshot canary grepped a phrase present in BOTH the stale and fresh copies, so it passed throughout; it now pins the newest section. Link targets are asserted, not just `test -L`; the owned-list content is asserted both ways; and the reconciler's replace path -- which no CI container exercises, since none mounts a skillset -- is covered by fabricating one. A mutation test showed the obvious three assertions still pass with the "is this link ours?" guard deleted, so a discriminating case was added: an owned name whose link is a user override outside the baked tree. Also refreshes the mempalace snapshot to skillset 670f7f1 (without it the fix helps only hosts that mount skillset) and corrects README, which documented the old, wrong precedence in three places. Verified with 12 fixture cases plus 2 mutants: ownership respected against the real trees, user overrides preserved, relative/trailing-slash/CRLF/space/glob inputs handled, dangling link healed, read-only skills dir exits 0, idempotent.
This commit is contained in:
@@ -547,7 +547,8 @@ directory, and they compose:
|
||||
`~/.agents/skills/` by `entrypoint-user.sh` on every start. They need no
|
||||
external mount, survive volume recreate (the source is an image path, not a
|
||||
home dir a named volume would shadow), and are created only when absent so a
|
||||
same-named skillset skill or user override is never clobbered. The bundled
|
||||
user override is never clobbered. Precedence against a mounted `skillset` repo
|
||||
is per-skill, not blanket — see *Skillset repo* below. The bundled
|
||||
**`pi-devbox-environment`** skill is delivered this way — it teaches agents
|
||||
the container's persistence model, host/LAN SSH reachability, split-DNS
|
||||
mechanisms, the interactive-vs-tool-shell alias gotcha (`dssh`/`dscp`),
|
||||
@@ -558,8 +559,11 @@ directory, and they compose:
|
||||
pi session to read `~/.agents/skills/pi-extensions/SKILL.md` at start (to fix
|
||||
fork/recall under-utilisation). That pointer would dangle in a container
|
||||
started *without* the private `skillset` repo, so the image also bakes
|
||||
fallback copies of **`pi-extensions`** and **`mempalace`**. They are
|
||||
symlinked only when absent, so a mounted skillset always overrides them. The
|
||||
fallback copies of **`pi-extensions`** and **`mempalace`**. Whether a mounted
|
||||
skillset overrides them depends on who *owns* the skill (see *Skillset repo*):
|
||||
`mempalace` is skillset-owned, so the live clone wins; `pi-extensions` is
|
||||
owned by its package repo, so the baked copy keeps winning — the skillset's
|
||||
copy of it is a downstream duplicate that can lag. The
|
||||
`pi-extensions` skill is *layered*: a committed snapshot in `rootfs/` is the
|
||||
floor, and `Dockerfile.variant` copies the canonical, package-owned copy from
|
||||
the pinned `pi-extensions` clone (`/opt/pi-extensions/skill/`) over it at
|
||||
@@ -573,7 +577,16 @@ directory, and they compose:
|
||||
- **Skillset repo (optional).** If a `skillset` repo is mounted (at
|
||||
`$HOME/skillset` or `/workspace/skillset`, or via `SKILLSET_CONTAINER_PATH`),
|
||||
`deploy-skills.sh` symlinks its skills in too. Image-baked skills are
|
||||
classified as foreign-links by its `--prune-stale` pass and left untouched.
|
||||
classified as foreign-links by its `--prune-stale` pass and left untouched —
|
||||
which through v1.8.4 meant the baked copy *always* won, so an edit pushed to a
|
||||
skillset-owned skill was invisible until the next image build. Since v1.8.5
|
||||
`devbox-skill-reconcile` runs right after the deploy and repoints the links for
|
||||
skills the skillset owns, listed in
|
||||
`/usr/local/share/pi-devbox/skills/skillset-owned.txt` (today: `mempalace`).
|
||||
Effective precedence, highest first: **user override** (a real directory, or a
|
||||
symlink pointing outside the baked tree) → **live skillset clone** (owned names
|
||||
only) → **baked snapshot** (everything else, and every skill when no skillset
|
||||
is mounted). Check with `readlink -f ~/.agents/skills/<skill>`.
|
||||
|
||||
To make agents *proactively* load a baked skill at session start (rather than
|
||||
only on description match), the image appends a short, gated pointer to the
|
||||
|
||||
Reference in New Issue
Block a user