diff --git a/CHANGELOG.md b/CHANGELOG.md index c0bda70..37fff7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,45 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`). ## Unreleased +### Fixed + +- **`pi-fork` was never registered — the `fork` tool has been missing since + v1.0.0.** `entrypoint-user.sh` registers the `/opt` pi packages with + `pi install ` and guarded that with a **whole-file substring + grep** on `~/.pi/agent/settings.json`. But `settings.example.json` carries a + top-level `"pi-fork"` **config block** (the fork effort profiles, added in + `pi-toolkit` `adb6907`, 2026-06-17), so `grep -q pi-fork settings.json` + matches on any settings file bootstrapped from — or template-merged with — + that template. The guard therefore concluded "already installed" and + `pi install /opt/pi-fork` never ran, on fresh *and* preserved volumes. + Compounding it, the non-destructive template merge runs **earlier in the same + startup** than the install loop, so the very mechanism that delivers new + template keys to an old volume is what plants the string that defeats the + guard. `pi-observational-memory` and `pi-studio` escaped only by luck: the + template key is `observational-memory` (no `pi-` prefix) and there is no + studio block. + + The guard now inspects the `packages` **array** (jq, with a grep fallback + matching the stored `…/opt/"` path form, which a config *key* can never + produce). Existing volumes self-heal on the next container start — the guard + returns false, `pi install /opt/pi-fork` runs, and `fork` registers on the + following pi start or `/reload`. No image rebuild is required to benefit if + you run `pi install /opt/pi-fork` by hand. + +- **Both test suites asserted the bug as green.** `scripts/smoke-test.sh` and + `scripts/recreate-sanity-check.sh` checked registration with the *same* + whole-file grep, so "pi-fork registered (fork tool)" passed on every build + and every recreate while the tool was absent. Both now assert against + `packages[]` with the same predicate as the entrypoint guard, and the labels + say `packages[]` so the distinction is visible in CI output. The smoke-test + readiness wait loop was switched to the array check too (and to + `docker exec -u developer` + `$HOME` instead of a hard-coded + `/home/developer` path). + + Detected by an agent session noticing `fork` was absent from its own tool + list on v1.6.3; zero `fork` calls exist across the 19 sessions on this + volume, confirming it never once loaded. + ### Changed - **Settings template now defaults to Claude Opus 5** (`pi-toolkit` @ `926f738`). diff --git a/entrypoint-user.sh b/entrypoint-user.sh index 1301913..af8fd3b 100755 --- a/entrypoint-user.sh +++ b/entrypoint-user.sh @@ -178,13 +178,38 @@ if command -v pi &>/dev/null; then # idempotent (no duplicate package entry on re-run), and stores a relative # path that resolves into the image-layer /opt so it survives volume # recreate. The tools/command register on the NEXT pi start (extensions - # bind at startup). Guard on settings.json so we only install once per - # volume. /opt/pi-studio is present only in the studio variant; the - # `[ -d ]` test makes this a no-op everywhere else. + # bind at startup) or on `/reload`. Guard on settings.json so we only + # install once per volume. /opt/pi-studio is present only in the studio + # variant; the `[ -d ]` test makes this a no-op everywhere else. + # + # The guard MUST inspect the `packages` ARRAY, not merely grep the whole + # file for the package name. settings.example.json ships a top-level + # "pi-fork" CONFIG block (the fork effort profiles, pi-toolkit adb6907, + # 2026-06-17), so a whole-file substring grep matches on any settings.json + # that was bootstrapped from — or template-merged with — that template. + # Worse, the merge above runs FIRST, so it plants the matching string in the + # same startup that the loop then reads: `pi install /opt/pi-fork` was + # skipped forever and the `fork` tool never registered (v1.0.0 → v1.6.3). + # Its siblings escaped only by luck — the template key is + # "observational-memory" (no pi- prefix) and there is no studio block. + # jq reads the array; the grep fallback matches the stored relative-path + # form ("…/opt/\""), which a config KEY can never produce. + _pi_pkg_registered() { + _pi_reg_settings="$HOME/.pi/agent/settings.json" + [ -f "$_pi_reg_settings" ] || return 1 + if command -v jq >/dev/null 2>&1; then + jq -e --arg n "$1" \ + '(.packages // []) | any((type == "string") and (. == "npm:" + $n or endswith("/" + $n)))' \ + "$_pi_reg_settings" >/dev/null 2>&1 + else + grep -q "opt/$1\"" "$_pi_reg_settings" + fi + } + for _pkg in /opt/pi-fork /opt/pi-observational-memory /opt/pi-studio; do [ -d "$_pkg" ] || continue _name=$(basename "$_pkg") - if ! grep -q "$_name" "$HOME/.pi/agent/settings.json" 2>/dev/null; then + if ! _pi_pkg_registered "$_name"; then pi install "$_pkg" >/dev/null 2>&1 || \ echo "WARN: pi install $_name failed (continuing)" fi diff --git a/scripts/recreate-sanity-check.sh b/scripts/recreate-sanity-check.sh index 57b9dbd..9eacb11 100755 --- a/scripts/recreate-sanity-check.sh +++ b/scripts/recreate-sanity-check.sh @@ -8,7 +8,8 @@ # nvim data, uv cache, ssh-local) # - pi runtime wiring is intact: keybindings symlink, AGENTS.md symlink, # ≥4 extensions, the mempalace.ts bridge, settings.json, and the pi-fork / -# pi-observational-memory / (studio variant) pi-studio package registrations +# pi-observational-memory / (studio variant) pi-studio package +# registrations in settings.json packages[] # - Shell defaults re-seeded from /etc/skel-devbox # - /tmp/sshcm exists with mode 700 (ssh ControlMaster dir) # - /opt toolkits intact @@ -199,21 +200,38 @@ if command -v jq >/dev/null 2>&1 && [ -f "$HOME/.pi/agent/settings.json" ]; then fi fi -# pi package registrations (pi install → recorded in settings.json) +# pi package registrations (pi install → recorded in settings.json). +# Check the `packages` ARRAY, not the whole file: the settings template ships a +# top-level "pi-fork" CONFIG block (asserted just above), so `grep -q pi-fork +# settings.json` is a guaranteed false green — which is how an un-registered +# fork tool went unnoticed from v1.0.0 through v1.6.3. Same array check the +# fixed entrypoint-user.sh guard uses. +_pkg_registered() { + _s="$HOME/.pi/agent/settings.json" + [ -f "$_s" ] || return 1 + if command -v jq >/dev/null 2>&1; then + jq -e --arg n "$1" \ + '(.packages // []) | any((type == "string") and (. == "npm:" + $n or endswith("/" + $n)))' \ + "$_s" >/dev/null 2>&1 + else + grep -q "opt/$1\"" "$_s" + fi +} + if [ -f "$HOME/.pi/agent/settings.json" ]; then for pkg in pi-fork pi-observational-memory; do - if grep -q "$pkg" "$HOME/.pi/agent/settings.json" 2>/dev/null; then - pass "$pkg registered in settings.json" + if _pkg_registered "$pkg"; then + pass "$pkg registered in settings.json packages[]" else - fail "$pkg not registered in settings.json" + fail "$pkg NOT in settings.json packages[] (tool will not load)" fi done if [ "$VARIANT" = "studio" ]; then - if grep -q "pi-studio" "$HOME/.pi/agent/settings.json" 2>/dev/null; then - pass "pi-studio registered in settings.json" + if _pkg_registered pi-studio; then + pass "pi-studio registered in settings.json packages[]" else - fail "pi-studio not registered in settings.json (studio variant)" + fail "pi-studio NOT in settings.json packages[] (studio variant)" fi fi fi diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index eae2830..6b1016d 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -18,7 +18,8 @@ # - entrypoint deploys ≥4 extensions # - mempalace bridge symlink present # - settings.json bootstrapped -# - pi-fork + pi-observational-memory registered via `pi install` +# - pi-fork + pi-observational-memory registered in settings.json packages[] +# via `pi install` # - pi-devbox-version command present + wraps the build manifest correctly # (human, --json, --quiet) # - (studio variant only, auto-detected) pi-studio cloned + prebuilt @@ -234,28 +235,40 @@ exec_test "mempalace skill linked (fallback)" 'test -L $HOME/.agents/skills # pi-fork + pi-observational-memory are registered by entrypoint-user.sh via # `pi install /opt/`, which runs slightly after the keybindings marker. +# +# Assert against the `packages` ARRAY, never a whole-file grep: the settings +# template ships a top-level "pi-fork" CONFIG block, so `grep -q pi-fork +# settings.json` passes even when `pi install /opt/pi-fork` never ran. That +# false green is exactly why the missing `fork` tool shipped unnoticed from +# v1.0.0 through v1.6.3. +pkg_registered_cmd() { + printf "jq -e --arg n %s '(.packages // []) | any((type == \"string\") and (. == \"npm:\" + \$n or endswith(\"/\" + \$n)))' \$HOME/.pi/agent/settings.json" "$1" +} + for i in $(seq 1 15); do - if docker exec "$CID" grep -q pi-observational-memory \ - /home/developer/.pi/agent/settings.json 2>/dev/null; then + if docker exec -u developer "$CID" sh -c "$(pkg_registered_cmd pi-observational-memory)" \ + >/dev/null 2>&1; then break fi sleep 1 done -exec_test "pi-fork registered (fork tool)" 'grep -q pi-fork $HOME/.pi/agent/settings.json && echo ok' -exec_test "pi-observational-memory registered (recall tool)" 'grep -q pi-observational-memory $HOME/.pi/agent/settings.json && echo ok' +exec_test "pi-fork registered in packages[] (fork tool)" \ + "$(pkg_registered_cmd pi-fork)" +exec_test "pi-observational-memory registered in packages[] (recall tool)" \ + "$(pkg_registered_cmd pi-observational-memory)" # pi-studio registration (studio variant only) — registered by the same # entrypoint-user.sh local-path install loop as fork/obsmem. if [ "${STUDIO_VARIANT:-0}" = "1" ]; then for i in $(seq 1 15); do - if docker exec "$CID" grep -q pi-studio \ - /home/developer/.pi/agent/settings.json 2>/dev/null; then + if docker exec -u developer "$CID" sh -c "$(pkg_registered_cmd pi-studio)" \ + >/dev/null 2>&1; then break fi sleep 1 done - exec_test "pi-studio registered (/studio command + studio_* tools)" \ - 'grep -q pi-studio $HOME/.pi/agent/settings.json && echo ok' + exec_test "pi-studio registered in packages[] (/studio command + studio_* tools)" \ + "$(pkg_registered_cmd pi-studio)" fi # ── /tmp/sshcm directory created by entrypoint ────────────────────────