From aac4a1c323cb2632da361a983fa201a12718fd64 Mon Sep 17 00:00:00 2001 From: Joakim Persson Date: Wed, 26 Aug 2026 18:47:03 +0200 Subject: [PATCH] =?UTF-8?q?release:=20v1.8.9=20=E2=80=94=20the=20version?= =?UTF-8?q?=20flag=20that=20blamed=20the=20wrong=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two versions, two flags. `--expected-version` has only ever asserted `pi --version`, but AGENTS.md step 4 spelled it `X.Y.Z` inside a checklist where every other X.Y.Z is the pi-devbox tag. Run as documented for v1.8.8 the final runtime gate of the release printed ✗ pi version mismatch: expected 1.8.8, got 0.84.3 and exited 1 — a red accusing the image of being the wrong version. Not one reader's slip: the v1.8.8 release-readiness handoff from pi@emb-7kj4vr4g propagated the same wrong spelling twice while correctly calling step 4 "not ceremonial", so two independent readers converged on it. README.md had it right all along, which means the two documents disagreed. - new --expected-image-version asserts the pi-devbox release tag, read from release_tag in /etc/pi-devbox/build-manifest.json (no checkout, no network); leading `v` optional on either side - both flags detect being handed the other one's value, and the test is exact rather than heuristic: the value is compared against the other quantity the image itself reports, so it can only fire on a real mix-up - neither flag is required now. With none, live `pi --version` is asserted against the manifest's pi_version — not a tautology, since a stale pi in the ~/.pi/npm-global volume can shadow the baked one, exactly as a stale npm:pi-atelier can in packages[] - the header note replaced was stale and load-bearing: it claimed pi is resolved from 'latest' and cannot be self-derived, while Dockerfile.variant pins ARG PI_VERSION=0.84.3 and docker-publish.yml reads that ARG as its source of truth. The same withdrawn claim also sat in cli_utils' pi-devbox-sanity --help - argument parsing: a missing value, or a value that is another flag, is a usage error instead of silently consuming the next argument; --help works All fourteen flag combinations exercised by execution, including the two manifest-absent branches and the shadowing branch a healthy container cannot reach — mutation-tested with a doctored manifest so each failure branch was observed firing rather than assumed present. CHANGELOG also names what no commit here causes: mempalace-toolkit main moved e70bef2 -> 5b8d78f, so this tag ships the auto-delivered logstream mailbox because base_tag folds the resolved toolkit SHA. It would have landed either way; going unnamed is the 553d865 shape that already caused one cross-host misattribution. Component audit found nothing else to bump — pi, mempalace, pi-atelier all equal their upstream latest, and every other floating ref resolves to the commit already baked. --- AGENTS.md | 20 ++++- CHANGELOG.md | 141 +++++++++++++++++++++++++++++++ README.md | 15 +++- scripts/recreate-sanity-check.sh | 140 ++++++++++++++++++++++++++---- 4 files changed, 296 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fa4ff24..f355efa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,9 +98,23 @@ re-brand of opencode-devbox's `pi-only` variant. **post-recreate sanity check** inside the running container to confirm persisted volumes survived and the pi runtime wiring re-deployed (not just that the container booted): - `docker compose exec devbox bash scripts/recreate-sanity-check.sh --expected-version X.Y.Z` - (or just `pi-devbox-sanity --expected-version X.Y.Z` if `cli_utils/bin` is - on PATH). This is the runtime peer of the build-time `smoke-test.sh` gate. + `docker compose exec devbox bash scripts/recreate-sanity-check.sh --expected-image-version X.Y.Z` + (or just `pi-devbox-sanity --expected-image-version X.Y.Z` if + `cli_utils/bin` is on PATH). This is the runtime peer of the build-time + `smoke-test.sh` gate. + **`X.Y.Z` here is the pi-devbox release tag** you are shipping (e.g. + `1.8.9`), which is what the rest of this checklist means by `vX.Y.Z`. + `--expected-image-version` is the flag that asserts it. There is also an + `--expected-version`, and it means something else — the **pi coding agent** + version (e.g. `0.84.3`, the `ARG PI_VERSION` pin). Handing the release tag + to that one used to report *"pi version mismatch: expected 1.8.8, got + 0.84.3"*, i.e. a red on the final gate of the release accusing the wrong + component; it now tells you to use `--expected-image-version` instead, and + the reverse mix-up is caught too. Both flags are optional — with neither, + the live pi version is asserted against the version recorded in the image's + own build manifest (which catches a stale `pi` in the `~/.pi/npm-global` + volume shadowing the baked one) and the image tag is reported + informationally. 5. Push tag: `git tag vX.Y.Z && git push origin vX.Y.Z`. 6. Watch CI: smoke job builds amd64 only and asserts size + extensions + pi version + new-base-tooling presence. Variant build is multi-arch diff --git a/CHANGELOG.md b/CHANGELOG.md index 37d9249..715a106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,147 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`). --- +## v1.8.9 — 2026-08-26 + +The coordination log gets a reader, and the release checklist's last gate stops +accusing the wrong component. + +### The mailbox arrives — named here *because nothing in this repo caused it* + +**`mempalace-toolkit` main moves `e70bef2` → `5b8d78f` (exactly one commit, 281 +insertions / 9 deletions across `extensions/pi/mempalace.ts` and +`extensions/pi/README.md`), and that is what actually ships the auto-delivered +logstream mailbox.** No pi-devbox commit implements it. `docker-publish.yml` +resolves `MEMPALACE_TOOLKIT_REF=main` to a concrete SHA at build time and folds +that SHA into `base_tag`, so the mailbox would have landed in the next tagged +image **whether or not this section existed** — which is precisely why it exists. +That is the same shipped-undocumented shape as `553d865` in v1.8.7, and that one +caused a cross-host misattribution: an agent on another machine reasoned about +which image contained which behaviour from a CHANGELOG that never mentioned it. +The rule this release adopts: **if a floating ref will pull a behaviour change +into the image, name it in the CHANGELOG before tagging, not after.** + +What the mailbox does, from the shipped code rather than from the design +discussion: + +- **The bridge was write-only.** It stamped provenance on the way *out* and never + read the log back, so a directed ask reached an agent only if that agent + happened to run `mempalace_event_list` itself. The channel carried real + cross-machine traffic from 2026-08-18 onward with **zero readers** — every + delivery in that window happened because a human said "check your mailbox". +- **Doubly gated, exactly like the provenance stamper:** inert unless *both* + `MEMPALACE_PI_DEVICE` and `MEMPALACE_REMOTE_URL` are set. An unstamped client + has no address to be reached at, so there is nothing for it to read. +- **On by default, opt out with `MEMPALACE_MAILBOX=0`.** Deliberate: an opt-in + fix for a nobody-remembers-to-do-it problem only relocates the forgetting. + Tunables: `MEMPALACE_MAILBOX_POLL_MS` (min gap between mid-session polls, + default 300000) and `MEMPALACE_MAILBOX_RESURFACE_MS` (re-announce a still-owed + ask after, default 3600000). +- **Owed-ness is derived, never read off `status`.** `event_ack` appends and never + mutates, and `status` is written once, so a directed `open` keeps matching the + mailbox query forever — answered or not. A candidate counts as answered only + when one of this device's own events has a **higher `seq`**, joins via + `metadata.ack_of` or a shared `correlation_id`, and carries a terminal status + (`applied`, `superseded`, `failed`, `blocked`). `claimed` and `ready` are + deliberately **not** terminal — that is how "taken, but not finished" keeps + resurfacing. +- **`*` broadcasts are excluded from the owed set.** `to_agent: ` also matches + broadcasts per the tool contract, so without this a broadcast written with + `status="open"` would make every machine believe it personally owed the same + answer — and the code would contradict the skill that documents it. +- **The dedup map is in memory on purpose.** A restart forgets, so an already-seen + ask can resurface: visible noise a human corrects in one turn. The opposite + failure — suppressing an unanswered ask — is silent and permanent. Do not + "fix" the noise by persisting it. +- **Delivery queues, it never interrupts.** A sections push at + `before_agent_start` plus a second `agent_settled` handler behind the 300 s + floor, using `steer` and *not* `triggerTurn`: `agent_settled` means idle, so + nothing wakes a model on inbound fleet traffic. + +Measured on v1.8.8 (which bakes `e70bef2`, i.e. no mailbox) immediately before +this release: the wake-up mailbox query had to be run by hand, returned **3** +directed asks with `status="open"`, and the derivation above resolved **all +three** as already answered — the third independent confirmation that the raw +`status` filter never shrinks, and the first taken on a fresh container with no +memory of having answered them. + +### `--expected-image-version`: two versions, two flags + +**`scripts/recreate-sanity-check.sh --expected-version 1.8.8` reported +`✗ pi version mismatch: expected 1.8.8, got 0.84.3` and exit 1** — a red on the +final runtime gate of a release, accusing the image of being the wrong version, +when the flag had only ever asserted `pi --version`. `AGENTS.md` step 4 spelled +it `--expected-version X.Y.Z` inside a checklist where every *other* `X.Y.Z` is +the pi-devbox tag; `README.md` got it right, so the two documents disagreed. + +Not hypothetical, and not one reader's slip: the v1.8.8 release-readiness handoff +from `pi@emb-7kj4vr4g` (`evt_20260826T134919_a614ecfc2d4f`) propagated +`--expected-version 1.8.8` twice, in its body and in +`metadata.cannot_check_here`, while correctly calling step 4 "the runtime peer of +the smoke gate, so it is not ceremonial". Two independent readers, one on another +machine, converged on the wrong meaning. Left alone it puts a spurious red on +every release, and the intuitive remedy — re-pull, re-recreate — is pure waste. + +- **New `--expected-image-version X.Y.Z`** asserts the pi-devbox release tag, + read from `release_tag` in `/etc/pi-devbox/build-manifest.json` (the image's + own build-time ground truth — no checkout, no network, no Docker socket). A + leading `v` is optional on either side, so `1.8.9` and `v1.8.9` both work. +- **Both flags now detect being handed the other one's value**, and the test is + exact rather than heuristic: the value is compared against the *other* + quantity this image actually reports, so it can only fire when the mix-up is + real. `--expected-version 1.8.9` now says *"is the pi-devbox IMAGE version, + not the pi version — use `--expected-image-version`"*, and the reverse mix-up + is caught the same way. +- **Neither flag is required any more.** With none, the live `pi --version` is + asserted against `pi_version` in the build manifest. That is not a tautology: + `pi` resolves through `PATH`, and a stale install in the `~/.pi/npm-global` + volume can shadow the baked one — the same shadowing this script already + guards against for `npm:pi-atelier` in `packages[]`. Verified by mutating the + manifest to a different version, which made the new check fail as intended. +- **The header note it replaced was stale and load-bearing.** It claimed pi "is + resolved from `latest` at CI build time and is NOT pinned … cannot self-derive + an expected version". `Dockerfile.variant` pins `ARG PI_VERSION=0.84.3`, and + `docker-publish.yml` *reads that ARG* as its source of truth (refusing to + build on a floating value, checking it is published on npm, warning when npm + is ahead). The same withdrawn claim also sat in `cli_utils`'s + `pi-devbox-sanity --help`, the third place this confusion lived; fixed there + too, in that repo. +- Argument parsing hardened while in there: a flag whose value is missing — or + is another flag — is now a usage error (exit 2) instead of silently consuming + the next argument, and `--help` works. + +All fourteen flag combinations were exercised by execution, including the two +manifest-absent branches and the shadowing branch, which a healthy container +cannot reach naturally — mutation-tested with a doctored manifest path so that +each failure branch was observed *firing* rather than assumed present. + +### Component audit: no bumps, and that is the finding + +Checked before tagging, since a base rebuild was already forced: + +| Component | In v1.8.8 | Upstream now | Action | +|---|---|---|---| +| pi (npm) | `0.84.3` (pinned) | `0.84.3` is `latest` | none | +| mempalace (PyPI) | `3.8.0` (pinned) | `3.8.0` | none | +| pi-atelier | `v0.8.2` (pinned) | `v0.8.2` highest tag | none | +| pi-toolkit, pi-extensions, pi-fork, pi-observational-memory, pi-studio | floating | **identical to baked** | none | +| skillset snapshot | `6eb20af` | `6eb20af` | none | +| **mempalace-toolkit** | `e70bef2` | **`5b8d78f`** | ships the mailbox | + +So the whole ~67-minute base rebuild this tag pays for is attributable to the +toolkit SHA alone — `base_tag` folds it, and it moved. Every other floating ref +resolved to the commit already baked (verified with `git ls-remote` per repo, not +by reading a cached clone). + +One claim in this audit came from a fork that had fabricated its findings — six +plausible-looking toolkit commits with five nonexistent SHAs, a pi `0.84.4` that +npm has never published, a pi-studio commit `ls-remote` says does not exist, and +a compatibility floor of `0.8.2` where the code says `0.7.1`. Every row above was +therefore re-measured directly. Recorded because the failure mode is specific: +none of it looked wrong, and `git cat-file -e` is what caught it. + +--- + ## v1.8.8 — 2026-08-26 The vendored `mempalace` skill snapshot stops being anonymous, and the diff --git a/README.md b/README.md index 05e94bc..9ed2e98 100644 --- a/README.md +++ b/README.md @@ -1017,10 +1017,21 @@ After `docker compose up -d --force-recreate`, run the **runtime** peer of persisted volumes survived, and pi runtime wiring is intact: ```bash -./scripts/recreate-sanity-check.sh # auto-detects variant -./scripts/recreate-sanity-check.sh --expected-version 0.79.4 # assert pi version +./scripts/recreate-sanity-check.sh # auto-detects variant +./scripts/recreate-sanity-check.sh --expected-image-version 1.8.9 # assert the pi-devbox release tag +./scripts/recreate-sanity-check.sh --expected-version 0.84.3 # assert the pi coding agent version ``` +Those are **two different versions**, and the flags are not interchangeable: +`--expected-image-version` takes the pi-devbox release tag (`v` optional), +`--expected-version` takes `pi --version`. Hand one the other's value and it +says so by name instead of reporting a mismatch against the wrong component. +With neither flag, both values are read from the image's own build manifest +(`/etc/pi-devbox/build-manifest.json`): the live pi version is asserted against +the one recorded at build time — which catches a stale `pi` in the +`~/.pi/npm-global` volume shadowing the baked one — and the release tag is +reported informationally. + If `cli_utils` is on your PATH, the `pi-devbox-sanity` wrapper runs the same check by short name and locates the repo automatically (override with `PI_DEVBOX_REPO=/path/to/pi-devbox`). Like `smoke-test.sh`, this script is diff --git a/scripts/recreate-sanity-check.sh b/scripts/recreate-sanity-check.sh index d86f25e..5076a0d 100755 --- a/scripts/recreate-sanity-check.sh +++ b/scripts/recreate-sanity-check.sh @@ -2,8 +2,8 @@ # Runtime post-recreate verification for pi-devbox. # # Verifies that after `docker compose up -d --force-recreate`: -# - The new image is actually live (pi version matches, when an expected -# version is supplied — see the version note below) +# - The new image is actually live (both the pi version and — when asked — +# the pi-devbox image release tag; see the two version notes below) # - Persisted named volumes survived (~/.pi config, shell history, zoxide, # nvim data, uv cache, ssh-local) # - pi runtime wiring is intact: keybindings symlink, AGENTS.md symlink, @@ -25,13 +25,33 @@ # the pi-devbox repo (which a maintainer already has for CI builds). A plain # `docker pull` consumer is not the audience and will not have this file. # -# Version note: pi's version is resolved from `latest` at CI build time and is -# NOT pinned to a concrete value in Dockerfile.variant (ARG PI_VERSION=latest). -# So unlike opencode-devbox, this script cannot self-derive an expected version -# from the Dockerfile. Pass --expected-version to assert a match; without it the -# live pi version is reported as an informational WARN, not a failure. +# TWO DIFFERENT VERSIONS, TWO DIFFERENT FLAGS. This distinction has already +# cost a release day, so it is spelled out here and in AGENTS.md step 4: # -# Usage: ./scripts/recreate-sanity-check.sh [--expected-version X.Y.Z] [--variant studio|plain] +# --expected-version the PI CODING AGENT version, e.g. 0.84.3 +# (`pi --version`; pinned as ARG PI_VERSION in +# Dockerfile.variant, which CI reads as the source +# of truth) +# --expected-image-version the PI-DEVBOX IMAGE release tag, e.g. 1.8.9 or +# v1.8.9 (the `release_tag` baked into +# /etc/pi-devbox/build-manifest.json) +# +# Passing a release tag to --expected-version used to report +# "pi version mismatch: expected 1.8.8, got 0.84.3" — an accusation aimed at +# the wrong component, on the last gate of a release. Both flags now detect +# being handed the other one's value and say so instead. +# +# Neither flag is required. Both values are derivable from the image's own +# build manifest, so by default the script asserts the LIVE pi version against +# the version recorded at build time — which is not a tautology: a stale +# `pi` in the ~/.pi/npm-global volume can shadow the baked one, exactly the +# way a stale npm:pi-atelier can (see the packages[] check below). Pass the +# flags when you want an assertion against a value you name yourself, which +# is what a release checklist wants. +# +# Usage: ./scripts/recreate-sanity-check.sh [--expected-version X.Y.Z] +# [--expected-image-version X.Y.Z] +# [--variant studio|plain] # # Exit codes: # 0 all checks passed @@ -41,22 +61,61 @@ set -euo pipefail EXPECTED_VERSION="" +EXPECTED_IMAGE_VERSION="" VARIANT="" REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MANIFEST=/etc/pi-devbox/build-manifest.json -# Parse arguments +usage() { + cat >&2 <<'EOF' +usage: recreate-sanity-check.sh [--expected-version X.Y.Z] + [--expected-image-version X.Y.Z] + [--variant studio|plain] + + --expected-version pi coding agent version, e.g. 0.84.3 (`pi --version`) + --expected-image-version pi-devbox image release tag, e.g. 1.8.9 or v1.8.9 + --variant studio|plain (auto-detected when omitted) + +These are two different versions. Both are read from the image's own build +manifest when the corresponding flag is omitted. +EOF +} + +# Parse arguments. Every flag takes a value, so reject a missing one rather +# than swallowing the next flag as if it were the value. +need_value() { + case "${2:-}" in + ""|-*) + echo "$1 requires a value" >&2 + usage + exit 2 + ;; + esac +} while [[ $# -gt 0 ]]; do case "$1" in --expected-version) + need_value "$@" EXPECTED_VERSION="$2" shift 2 ;; + --expected-image-version) + need_value "$@" + EXPECTED_IMAGE_VERSION="$2" + shift 2 + ;; --variant) + need_value "$@" VARIANT="$2" shift 2 ;; + --help|-h) + usage + exit 0 + ;; *) - echo "usage: $0 [--expected-version X.Y.Z] [--variant studio|plain]" >&2 + echo "unknown option: $1" >&2 + usage exit 2 ;; esac @@ -67,6 +126,19 @@ pass() { echo " ✓ $1"; } fail() { echo " ✗ $1" >&2; FAILED=$((FAILED + 1)); } warn() { echo " ⚠ $1" >&2; } +# Read one top-level field from the build manifest, or print nothing. The +# manifest is the image's own ground truth (written at `docker build` time by +# Dockerfile.variant), so it needs no checkout and no network. Absent on an +# image built before it existed, hence every caller treats "" as unknown. +manifest_field() { + [ -f "$MANIFEST" ] || return 0 + command -v jq >/dev/null 2>&1 || return 0 + jq -r --arg k "$1" '.[$k] // empty' "$MANIFEST" 2>/dev/null || true +} +# Release tags are written with a leading v in the manifest and quoted without +# one in checklists; compare on the bare number so both spellings work. +strip_v() { printf '%s' "${1#v}"; } + # Auto-detect variant if not provided. The studio variant vendors pi-studio to # /opt/pi-studio; the plain variant does not. if [ -z "$VARIANT" ]; then @@ -86,21 +158,59 @@ else fi echo -echo "-- pi version --" +MANIFEST_PI_VERSION=$(manifest_field pi_version) +MANIFEST_RELEASE_TAG=$(manifest_field release_tag) + +echo "-- pi (coding agent) version --" if ACTUAL_VERSION=$(pi --version 2>&1 | head -1); then if [ -n "$EXPECTED_VERSION" ]; then - if [ "$ACTUAL_VERSION" = "$EXPECTED_VERSION" ]; then - pass "pi version $ACTUAL_VERSION" + if [ "$(strip_v "$EXPECTED_VERSION")" = "$(strip_v "$ACTUAL_VERSION")" ]; then + pass "pi version $ACTUAL_VERSION (matches --expected-version)" + elif [ -n "$MANIFEST_RELEASE_TAG" ] && + [ "$(strip_v "$EXPECTED_VERSION")" = "$(strip_v "$MANIFEST_RELEASE_TAG")" ]; then + # Exact, not heuristic: the value handed over IS this image's release + # tag, so it cannot be a pi version anyone meant. + fail "--expected-version $EXPECTED_VERSION is the pi-devbox IMAGE version, not the pi version — use --expected-image-version $EXPECTED_VERSION (live pi is $ACTUAL_VERSION)" else - fail "pi version mismatch: expected $EXPECTED_VERSION, got $ACTUAL_VERSION" + fail "pi version mismatch: expected $EXPECTED_VERSION, got $ACTUAL_VERSION (this flag asserts the pi coding agent version; for the image release tag use --expected-image-version)" + fi + elif [ -n "$MANIFEST_PI_VERSION" ]; then + # Not a tautology: the manifest records what pi reported at BUILD time, + # while `pi --version` resolves through PATH, which a stale npm-global + # volume install can shadow. + if [ "$MANIFEST_PI_VERSION" = "$ACTUAL_VERSION" ]; then + pass "pi version $ACTUAL_VERSION (matches this image's build manifest)" + else + fail "live pi $ACTUAL_VERSION != $MANIFEST_PI_VERSION recorded in $MANIFEST — a stale pi in the ~/.pi/npm-global volume is shadowing the baked one" fi else - warn "pi version $ACTUAL_VERSION (no --expected-version given; pi is built from 'latest', cannot self-derive — informational only)" + warn "pi version $ACTUAL_VERSION (no --expected-version and no build manifest to compare against — informational only)" fi else fail "pi --version failed" fi +echo +echo "-- pi-devbox image version --" +if [ -z "$MANIFEST_RELEASE_TAG" ]; then + if [ -n "$EXPECTED_IMAGE_VERSION" ]; then + fail "cannot verify --expected-image-version $EXPECTED_IMAGE_VERSION: no readable release_tag in $MANIFEST (image built before the manifest existed, or jq missing)" + else + warn "image release tag unknown (no readable $MANIFEST) — pi-devbox-version would say the same" + fi +elif [ -n "$EXPECTED_IMAGE_VERSION" ]; then + if [ "$(strip_v "$EXPECTED_IMAGE_VERSION")" = "$(strip_v "$MANIFEST_RELEASE_TAG")" ]; then + pass "image version $MANIFEST_RELEASE_TAG (matches --expected-image-version)" + elif [ -n "$MANIFEST_PI_VERSION" ] && + [ "$(strip_v "$EXPECTED_IMAGE_VERSION")" = "$MANIFEST_PI_VERSION" ]; then + fail "--expected-image-version $EXPECTED_IMAGE_VERSION is the pi version, not the image release tag — use --expected-version $EXPECTED_IMAGE_VERSION (this image is $MANIFEST_RELEASE_TAG)" + else + fail "image version mismatch: expected $EXPECTED_IMAGE_VERSION, got $MANIFEST_RELEASE_TAG — the recreate did not pick up the intended image" + fi +else + warn "image version $MANIFEST_RELEASE_TAG (no --expected-image-version given — informational only)" +fi + echo echo "-- Persisted named volumes (must survive --force-recreate) --"