#!/bin/sh # devbox-skill-reconcile — hand skillset-OWNED skills back to the live clone. # # WHY THIS EXISTS # --------------- # entrypoint-user.sh links the image-baked skills into ~/.agents/skills/ EARLY # (before pi-deploy), because the smoke readiness probe gates on markers that # only land later, and a link created after that gate produced a flaky # assertion. Those links are created with a `[ ! -e ]` guard — "only when # absent" — and the skillset deploy runs LAST, treating already-present links # as foreign and leaving them alone. Net effect through v1.8.4: the baked copy # always won, so an edit pushed to a skillset-owned skill was invisible in # every container until the next image build (measured on two hosts: live # skillset md5 129bcc4752 vs baked 5236024fef, the new section absent). # # The fix is NOT "the skillset always wins". Ownership is per-skill (see # rootfs/usr/local/share/pi-devbox/skills/VENDORED.md): # # pi-devbox-environment authored in pi-devbox → baked IS canonical # pi-extensions owned by the package repo, copied over the snapshot # at build time; skillset carries a DOWNSTREAM copy # that can lag → baked must keep winning # mempalace owned by the skillset repo; baked is a snapshot # fallback for containers with no skillset mounted # → the live clone must win when it is present # # So only skills listed in skills/skillset-owned.txt are handed over. Baked # links stay as the fallback (the early-link race fix is untouched), and a user # override always beats both: a real directory is never replaced, and neither is # a symlink that already points somewhere other than the baked tree. # # Usage: devbox-skill-reconcile [skills-dir] [baked-src] # skillset-root the mounted skillset repo (contains skills//) # skills-dir default $HOME/.agents/skills # baked-src default /usr/local/share/pi-devbox/skills # # Idempotent, and silent unless it changes something. Exits 0 when there is # nothing to do (no skillset, no list) so the entrypoint never fails on it. set -eu SKILLSET_ROOT="${1:-}" SKILLS_DIR="${2:-$HOME/.agents/skills}" BAKED_SRC="${3:-/usr/local/share/pi-devbox/skills}" BAKED_SRC="${BAKED_SRC%/}" # a trailing slash would make the prefix # match below ("$BAKED_SRC"/*) match nothing [ -n "$SKILLSET_ROOT" ] || exit 0 [ -d "$SKILLSET_ROOT/skills" ] || exit 0 [ -d "$SKILLS_DIR" ] || exit 0 # Absolutise BOTH roots before they are used, because each has its own way of # failing silently when relative: a relative symlink TARGET is resolved against # the link's directory (~/.agents/skills), not $PWD, so it would dangle on # creation; and a relative BAKED_SRC would never prefix-match the absolute # target that `readlink` reports, so every skill would be skipped and the fix # would look like it had simply done nothing. SKILLSET_ROOT=$(CDPATH= cd -- "$SKILLSET_ROOT" 2>/dev/null && pwd) || exit 0 BAKED_SRC=$(CDPATH= cd -- "$BAKED_SRC" 2>/dev/null && pwd) || exit 0 OWNED_LIST="$BAKED_SRC/skillset-owned.txt" [ -f "$OWNED_LIST" ] || exit 0 while IFS= read -r _line || [ -n "$_line" ]; do # strip comments and surrounding whitespace; skip blanks _name=$(printf '%s\n' "$_line" | sed -e 's/#.*$//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') [ -n "$_name" ] || continue # defensive: a list entry must be a plain skill name, never a path case "$_name" in */*|.*) continue ;; esac _live="$SKILLSET_ROOT/skills/$_name" _link="$SKILLS_DIR/$_name" # the skillset does not ship it → the baked fallback is all there is [ -d "$_live" ] || continue # a real directory is a user override → never touch [ -L "$_link" ] || continue # only ever replace OUR OWN link. readlink is deliberate: `readlink -f` # would resolve a link that already points into the skillset clone and, # since both trees hold a same-named skill, could not tell them apart. _target=$(readlink "$_link" 2>/dev/null || true) case "$_target" in "$BAKED_SRC"/*|"$BAKED_SRC") ;; # baked link → ours to replace *) continue ;; # user/foreign target → leave alone esac # -n so an existing symlink-to-directory is replaced rather than followed # (without it, ln would create $_link/$_name inside the baked tree). if ln -sfn "$_live" "$_link" 2>/dev/null; then printf 'skill %s: baked snapshot -> live skillset (%s)\n' "$_name" "$_live" fi done < "$OWNED_LIST"