Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93f986e90e | |||
| 26f223568d | |||
| 01abda3456 | |||
| b5810654f6 | |||
| 4f6f470518 | |||
| fbc1f86612 | |||
| 2ebf00d6d4 | |||
| c3b6d36778 | |||
| 3a509077c2 | |||
| a55f6369b3 | |||
| ffd54750b9 | |||
| d8b745c164 | |||
| ae13c2264e | |||
| a2f0a4a441 | |||
| 53b41cd76b | |||
| 29b62093f0 | |||
| cbd7cf5c67 | |||
| 7c00dd6001 | |||
| 7649d53f3b | |||
| ade58131d6 | |||
| ffd44ad9cf |
+75
-3
@@ -12,16 +12,81 @@ SSH_KEY_PATH=~/.ssh
|
||||
# ── MemPalace memory (local by default) ───────────────────────────
|
||||
# By default the mempalace.ts extension spawns a LOCAL mempalace-mcp stdio
|
||||
# server (palace at ~/.mempalace). Uncomment the devbox-palace volume in
|
||||
# docker-compose.yml to persist it across container recreation.
|
||||
# docker-compose.yml to persist it across container recreation — that one
|
||||
# volume now covers the mined conversation transcripts too, since the pi and
|
||||
# opencode feeders stage inside the palace root (<palace-root>/pi-stage), so
|
||||
# the staged files and the palace dedup keys pointing at them cannot be
|
||||
# separated.
|
||||
#
|
||||
# That palace root is resolved with mempalace's own precedence
|
||||
# ($MEMPALACE_PALACE_PATH -> $MEMPAL_PALACE_PATH -> ~/.mempalace/config.json ->
|
||||
# ~/.mempalace/palace), and the feeders derive their stage FROM it
|
||||
# (<palace-root>/pi-stage). Neither the image nor the entrypoint exports it, by
|
||||
# design: pinning the palace without carrying the stage along re-creates the
|
||||
# very split that a shared root removed. Override it only to move the palace off
|
||||
# the default -- e.g. onto a different mount -- and only to a path with the SAME
|
||||
# persistence as the palace itself. A stage that outlives its palace (or dies
|
||||
# first) makes a scoped `mempalace sync` prune conversation drawers, because
|
||||
# their dedup key is the staged path. Setting it to the default buys nothing.
|
||||
# Unlike WORKSPACE_PATH/SSH_KEY_PATH above, this is a path INSIDE the container.
|
||||
# MEMPALACE_PALACE_PATH=/home/developer/.mempalace/palace
|
||||
#
|
||||
# To instead share ONE MemPalace across containers/harnesses (pi + opencode
|
||||
# + native), set the URL below. When set, the extension connects over HTTP
|
||||
# and NO local mempalace-mcp is spawned; the devbox-palace volume is then
|
||||
# irrelevant. MEMPALACE_REMOTE_TOKEN, if set, is sent as a bearer token.
|
||||
# Serve it with: mempalace-mcp --transport http --host 0.0.0.0 --port 8765
|
||||
# MEMPALACE_REMOTE_URL=http://mempalace.lan:8765/mcp
|
||||
#
|
||||
# Serve it with: mempalace serve --host 172.17.0.1 --port 8765
|
||||
#
|
||||
# NOT `mempalace-mcp --transport http --host 0.0.0.0`: `serve` is the turnkey
|
||||
# wrapper that mints/keeps a bearer token (0600, passed via env so it stays out
|
||||
# of `ps`) and can terminate TLS. Two binds to avoid:
|
||||
# 0.0.0.0 - exposes the palace to the whole LAN.
|
||||
# 127.0.0.1 - behind a tunnel this 403s every proxied request (the Host pin
|
||||
# is only enforced on loopback binds) AND silently starts with
|
||||
# no token at all, since auto-minting is gated on the bind being
|
||||
# non-loopback. Bind the docker0 gateway: reachable from the host
|
||||
# and its containers (so a newt/proxy container works), not from
|
||||
# the LAN. Set MEMPALACE_MCP_HTTP_TOKEN explicitly server-side.
|
||||
# MEMPALACE_REMOTE_URL=https://mempalace.example.com/mcp
|
||||
# MEMPALACE_REMOTE_TOKEN=
|
||||
|
||||
# ── MemPalace: automatic capture of pi sessions ───────────────────────
|
||||
# The mempalace.ts extension feeds this container's pi transcripts into the
|
||||
# palace by itself: on session_shutdown, and on a debounced agent_settled so a
|
||||
# crash loses at most one window rather than the whole session. The entrypoint
|
||||
# also runs a catch-up at container start, which is the only thing that can
|
||||
# recover transcripts after a hard kill (no handler runs on SIGKILL).
|
||||
# Nothing below is required for the local-palace case; the defaults work.
|
||||
#
|
||||
# MEMPALACE_FEED=0 # disable automatic capture entirely
|
||||
# MEMPALACE_FEED_DEBOUNCE_MS=600000 # min gap between mid-session feeds (10 min)
|
||||
# MEMPALACE_FEED_WING=wing_conversations
|
||||
#
|
||||
# REMOTE PALACE ONLY (MEMPALACE_REMOTE_URL set above): the palace is on another
|
||||
# host, and `mempalace_mine` resolves its source path in the SERVER process, so
|
||||
# the server cannot see this container's transcripts. The feeder therefore
|
||||
# rsyncs its staged exports into a per-device inbox on the palace host and asks
|
||||
# the server to mine its own local copy. Without MEMPALACE_PI_SSH_TARGET the
|
||||
# feeder is skipped (a remote palace with no inbox has nothing to mine).
|
||||
# MEMPALACE_PI_SSH_TARGET where to rsync to, as user@host:path
|
||||
# MEMPALACE_PI_REMOTE_PATH what that inbox is called ON THE SERVER — i.e. the
|
||||
# path the SERVER PROCESS can open. If the palace
|
||||
# server runs in Docker, that is the container path
|
||||
# (see docker-compose.mempalace.yml). If it runs
|
||||
# NATIVELY (systemd unit / uv tool / plain
|
||||
# `mempalace serve`), it sees host paths, so this
|
||||
# must equal the path half of
|
||||
# MEMPALACE_PI_SSH_TARGET. Getting this wrong is
|
||||
# quiet: rsync still succeeds and only the mine
|
||||
# fails with "source directory not found", so
|
||||
# transcripts ship and are filed nowhere. The feeder
|
||||
# warns in preflight when the two paths disagree.
|
||||
# MEMPALACE_PI_DEVICE inbox subdirectory for this machine (default: hostname)
|
||||
# MEMPALACE_PI_SSH_TARGET=user@palace-host:/srv/mempalace-feed
|
||||
# MEMPALACE_PI_REMOTE_PATH=/data/feed
|
||||
# MEMPALACE_PI_DEVICE=
|
||||
|
||||
# ── LAN access from the container (host-OS-agnostic) ─────────────────
|
||||
# On VM-backed hosts (macOS OrbStack / Docker Desktop) the container can't
|
||||
# reach the host's directly-attached LAN peers by default. The entrypoint
|
||||
@@ -52,6 +117,13 @@ SSH_KEY_PATH=~/.ssh
|
||||
# DEVBOX_ATELIER=1
|
||||
|
||||
# ── Git Configuration ────────────────────────────────────────────────
|
||||
# Set BOTH. If unset, every repo inside the container fails with
|
||||
# "Author identity unknown" on first commit, and an agent asked to commit
|
||||
# will guess an identity from git log — often the wrong one. The e-mail is
|
||||
# per-machine (work machines use the corporate address, personal machines the
|
||||
# private one), so it belongs in this per-machine .env, never in a skill or a
|
||||
# repo-local override. Consumed by entrypoint-user.sh -> ~/.gitconfig, which is
|
||||
# NOT persistent across container recreate — this file is the source of truth.
|
||||
GIT_USER_NAME=
|
||||
GIT_USER_EMAIL=
|
||||
|
||||
|
||||
@@ -18,6 +18,14 @@ name: Publish Docker Image
|
||||
# 5. build-variant multi-arch push of latest + vX.Y.Z tags.
|
||||
# 6. promote-base-latest re-tag base-<hash> → base-latest with `crane copy`.
|
||||
# 7. update-description patch Docker Hub description.
|
||||
#
|
||||
# Note the trigger: `push: tags: v*` (plus workflow_dispatch). Nothing here runs
|
||||
# on a push to main, so a smoke assertion added outside a release is UNVALIDATED
|
||||
# until the next tag — which is exactly how v1.8.0 shipped a broken assertion
|
||||
# written three days earlier (it asserted a literal /home/developer stage path,
|
||||
# while `run` executes `docker run --entrypoint=""` as root with HOME=/root).
|
||||
# The `smoke_only` dispatch input exists to close that gap: it runs steps 1-4
|
||||
# against HEAD and stops before anything is published.
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -33,6 +41,10 @@ on:
|
||||
description: 'Update latest aliases (default true for tag-push, false for manual test runs)'
|
||||
required: false
|
||||
default: 'false'
|
||||
smoke_only:
|
||||
description: 'Build base + run both smoke jobs against HEAD, then stop. Publishes nothing. Use to validate smoke assertions without cutting a tag.'
|
||||
required: false
|
||||
default: 'false'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -165,6 +177,40 @@ jobs:
|
||||
fi
|
||||
}
|
||||
|
||||
# Read a commit SHA from Gitea, surviving a bad build token.
|
||||
#
|
||||
# These repos are public (see the note at the call sites), so auth is
|
||||
# a convenience, not a requirement — but Gitea REJECTS an invalid
|
||||
# token (401) rather than ignoring it, so a revoked or malformed
|
||||
# GITEA_BUILD_TOKEN could fail an entire release on reads that work
|
||||
# fine anonymously. An ABSENT secret was always safe (Gitea ignores an
|
||||
# empty `token ` value and serves the request, 200); a STALE one was
|
||||
# not. So: try authed, and on 401/403 retry anonymously.
|
||||
#
|
||||
# A non-200 after that emits nothing and returns 0 deliberately, so
|
||||
# require_sha raises the loud explicit abort rather than this helper
|
||||
# inventing a fallback ref.
|
||||
#
|
||||
# Messages go to STDERR, not as ::warning:: annotations: this
|
||||
# function's stdout IS the SHA, so anything written there would be
|
||||
# captured into the ref by the command substitution.
|
||||
gitea_sha() { # $1=repo
|
||||
local repo="$1" url resp code
|
||||
url="https://gitea.jordbo.se/api/v1/repos/joakimp/${repo}/commits?limit=1&sha=main"
|
||||
resp=$(curl -s -w '\n%{http_code}' -H "$AUTH_HEADER" "$url" || printf '\n000')
|
||||
code=${resp##*$'\n'}
|
||||
if [ "$code" = "401" ] || [ "$code" = "403" ]; then
|
||||
printf 'WARNING: Gitea rejected the build token for %s (HTTP %s); retrying anonymously. The read should succeed (public repo), but GITEA_BUILD_TOKEN is stale or malformed and should be rotated.\n' "$repo" "$code" >&2
|
||||
resp=$(curl -s -w '\n%{http_code}' "$url" || printf '\n000')
|
||||
code=${resp##*$'\n'}
|
||||
fi
|
||||
if [ "$code" != "200" ]; then
|
||||
printf 'WARNING: Gitea commit lookup for %s returned HTTP %s\n' "$repo" "$code" >&2
|
||||
return 0
|
||||
fi
|
||||
printf '%s' "${resp%$'\n'*}" | jq -r '.[0].sha // empty' 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ── pi version: from the PIN, not from npm `latest` ───────────
|
||||
# Until v1.7.0 this followed npm `latest`, which meant every release
|
||||
# silently adopted whatever pi had shipped that morning — unaudited —
|
||||
@@ -226,15 +272,27 @@ jobs:
|
||||
echo "atelier_ref=${ATELIER_REF}" >> "$GITHUB_OUTPUT"
|
||||
echo "atelier_tag=${ATELIER_TAG}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# pi-toolkit / pi-extensions (Gitea) → commit SHAs. Gitea API
|
||||
# requires auth even for public-repo commit listing.
|
||||
TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
|
||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-toolkit/commits?limit=1&sha=main" \
|
||||
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||
# pi-toolkit / pi-extensions (Gitea) → commit SHAs. All three Gitea
|
||||
# repos read in this step are PUBLIC: an unauthenticated GET of these
|
||||
# commit endpoints returns 200 with the IDENTICAL sha (verified
|
||||
# 2026-08-15 for pi-toolkit, pi-extensions and mempalace-toolkit).
|
||||
# The comment that used to sit here claimed the Gitea API "requires
|
||||
# auth even for public-repo commit listing" — it does not. Only
|
||||
# /api/v1/repos/*/actions/* refuses anonymous reads (401), which is
|
||||
# what that claim was almost certainly generalised from.
|
||||
#
|
||||
# The header is still passed on purpose: it keeps working if a repo is
|
||||
# ever flipped private, and an ABSENT secret degrades cleanly, because
|
||||
# Gitea ignores an empty `token ` value and serves the request
|
||||
# anonymously (200). The real hazard is the opposite one — a REVOKED or
|
||||
# malformed token returns 401 where anonymous would have returned 200,
|
||||
# so a stale GITEA_BUILD_TOKEN turns a healthy public read into a
|
||||
# require_sha failure that reads like an API or network fault. If this
|
||||
# step ever fails on a repo you can browse anonymously, suspect the
|
||||
# token before you suspect Gitea.
|
||||
TOOLKIT_REF=$(gitea_sha pi-toolkit)
|
||||
require_sha PI_TOOLKIT_REF "$TOOLKIT_REF"
|
||||
EXTENSIONS_REF=$(curl -sf -H "$AUTH_HEADER" \
|
||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-extensions/commits?limit=1&sha=main" \
|
||||
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||
EXTENSIONS_REF=$(gitea_sha pi-extensions)
|
||||
require_sha PI_EXTENSIONS_REF "$EXTENSIONS_REF"
|
||||
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
|
||||
@@ -244,9 +302,7 @@ jobs:
|
||||
# into the base-decide hash (see that job) to force a base rebuild
|
||||
# when the toolkit moves — otherwise a toolkit-only fix silently
|
||||
# fails to land unless Dockerfile.base itself changes.
|
||||
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
|
||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/mempalace-toolkit/commits?limit=1&sha=main" \
|
||||
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||
MEMPALACE_TOOLKIT_REF=$(gitea_sha mempalace-toolkit)
|
||||
require_sha MEMPALACE_TOOLKIT_REF "$MEMPALACE_TOOLKIT_REF"
|
||||
echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -482,6 +538,14 @@ jobs:
|
||||
# ── Phase 4: multi-arch publish ─────────────────────────────────────
|
||||
build-variant:
|
||||
needs: [base-decide, smoke, resolve-versions]
|
||||
# A `smoke_only` dispatch stops the pipeline here: base is probed/built and
|
||||
# both smoke jobs run, but nothing is published. Deliberately NOT wrapped in
|
||||
# always() — specifying `if:` keeps the implicit "all needs succeeded" gate,
|
||||
# so a failing smoke still blocks the release. On a tag push `inputs` is
|
||||
# unset, and `null != 'true'` is true, so releases are unaffected.
|
||||
# promote-base-latest and update-description need build-variant to have
|
||||
# succeeded, so they skip on their own — no extra guard required.
|
||||
if: inputs.smoke_only != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
@@ -574,6 +638,7 @@ jobs:
|
||||
# or fail independently of the core release.
|
||||
build-variant-studio:
|
||||
needs: [base-decide, smoke-studio, resolve-versions]
|
||||
if: inputs.smoke_only != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
|
||||
@@ -76,10 +76,15 @@ re-brand of opencode-devbox's `pi-only` variant.
|
||||
4. Push tag: `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
||||
5. Watch CI: smoke job builds amd64 only and asserts size + extensions +
|
||||
pi version + new-base-tooling presence. Variant build is multi-arch
|
||||
(amd64 + arm64) only after smoke passes. **A tag push produces two runs, not
|
||||
one** — `lint.yml` fires on every push (including tag refs) and
|
||||
`docker-publish.yml` fires on `v*` tags. Watch the **publish** run; see
|
||||
*Gitea API access* below for how to find it without picking lint by mistake.
|
||||
(amd64 + arm64) only after smoke passes. A tag push fires **only**
|
||||
`docker-publish.yml` — `lint.yml` is scoped to `branches: ['**']`, which
|
||||
excludes tag refs on purpose (the tagged tree was already linted when the
|
||||
commit hit `main`, and a fast lint run sorting above the slow publish run
|
||||
made releases look finished before anything shipped). Verified on v1.8.4:
|
||||
`refs/tags/v1.8.4` produced run 571 (publish) and nothing else. Still filter
|
||||
discovery on `head_sha` **and** the workflow `path` — see *Gitea API access*
|
||||
below — because that guard costs nothing and a future workflow added on `v*`
|
||||
would silently reintroduce the ambiguity.
|
||||
6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus
|
||||
base-latest if the base was rebuilt this run).
|
||||
7. **Revoke any short-lived Gitea PAT** used during the release at
|
||||
@@ -87,6 +92,34 @@ re-brand of opencode-devbox's `pi-only` variant.
|
||||
`GITEA_ACCESS_TOKEN` env var instead (see *Gitea API access* below) —
|
||||
its lifecycle is managed host-side, nothing to revoke.
|
||||
|
||||
## Verifying this repo's reality from inside a container
|
||||
|
||||
Most work on this repo happens **inside** a pi-devbox container, inspecting a
|
||||
host or a peer over SSH. That setup manufactures convincing false negatives, so
|
||||
when you are about to report that something is **absent, unreachable, or not
|
||||
running**, suspect your own command first. Recurring instances:
|
||||
|
||||
- **`docker` is not on the host's non-interactive SSH `PATH`.** `ssh mac 'docker
|
||||
ps'` says *command not found* on a host that plainly runs Docker; use
|
||||
`/usr/local/bin/docker` (or `command -v docker` first). Every step in the
|
||||
*Release-day checklist* that inspects a running container hits this.
|
||||
- **Don't `| head -N` a search whose answer you don't already know.** The host's
|
||||
`~/.ssh/config` is ~500 lines; a `head -20` "proved" a peer absent that was
|
||||
defined at line 454.
|
||||
- **The deployment compose file is not this repo's.** `docker-compose.yml` here
|
||||
is a template pinning `:latest`; a real host runs its own per-machine file
|
||||
(find it with `docker inspect <container> --format '{{ index .Config.Labels
|
||||
"com.docker.compose.project.config_files" }}'`). Recreating from the repo copy
|
||||
can silently move a host off `:latest-studio` onto `:latest`.
|
||||
- **A live SSH ControlMaster hides remote auth changes** — after editing a
|
||||
peer's `authorized_keys`, prove access with `-o ControlPath=none -o
|
||||
ControlMaster=no`, or the breakage surfaces in a later session instead.
|
||||
|
||||
Depth and further mechanisms: the repo-authored `pi-devbox-environment` skill
|
||||
(`rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/SKILL.md`) §2
|
||||
and §3 — that file is the one an agent actually loads mid-session, whereas this
|
||||
`AGENTS.md` is only auto-read when the cwd *is* this repo.
|
||||
|
||||
## Gitea API access (env token)
|
||||
|
||||
`GITEA_ACCESS_TOKEN` + `GITEA_HOST` are passed into the container from the
|
||||
|
||||
+921
@@ -11,6 +11,927 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
||||
|
||||
---
|
||||
|
||||
## v1.8.6 — 2026-08-25
|
||||
|
||||
Patch release. Adopts the drift that accumulated in the ~2 days since v1.8.5
|
||||
(pi `0.84.3`, mempalace core `3.8.0`), then closes the documentation and
|
||||
observability gaps that v1.8.5 itself listed as "Still open". No component
|
||||
was adopted without an audit note recording *why* it is safe.
|
||||
|
||||
All moving refs re-resolved immediately before tagging (2026-08-25T13:28Z):
|
||||
pi-toolkit `0e1369e6`, pi-extensions `20228878`, mempalace-toolkit `0fe64c48`
|
||||
and pi-observational-memory `ce9fc982` all unchanged since v1.8.5;
|
||||
pi-fork `f1ff8087` → `bf702b4c`; pi-atelier holds at `v0.8.2` (floor for
|
||||
pi ≥0.84 satisfied); pi-studio's CI-resolved newest tag has moved again to
|
||||
`v0.9.51`. Base rebuild is forced (Dockerfile.base changed), so the 16
|
||||
floating base-tooling ARGs re-roll — expect ~67 min as for v1.8.5.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`mempalace` core `3.7.1` → `3.8.0`.** Released 2026-08-23T21:19Z, hours
|
||||
after this project's own v1.8.5 tag the same day. Additive/reliability only
|
||||
— reviewed for MCP tool-schema changes before bumping, as always: none.
|
||||
`sync --apply` (PR #2320/#2322) no longer deletes a drawer solely because
|
||||
its `source_file` was unreachable *at that moment* — it asks for
|
||||
corroboration first. **This does not relax the standing landmine** against
|
||||
running `mempalace_sync` / `mempalace_delete_by_source` beyond dry-run on
|
||||
the shared central palace: that failure mode is paths *permanently* absent
|
||||
from whichever host runs the sync, not transient unavailability, and 3.8.0
|
||||
doesn't touch it. Server-side perf fix PR #2307 (long-running Chroma servers
|
||||
no longer invalidate their own HNSW cache on their own writes) likewise does
|
||||
not make `mempalace_reconnect` unnecessary — that tool covers *external*
|
||||
writes bypassing the in-process client, a different scenario. Full reasoning
|
||||
lives in the `Dockerfile.base` comment above `ARG MEMPALACE_VERSION`.
|
||||
**Deployment note:** synlig's central palace currently serves `3.7.1`
|
||||
server-side via `docker-compose.mempalace.yml` (which reuses this image) —
|
||||
this client bump introduces version skew until that stack is separately
|
||||
redeployed; sequence accordingly.
|
||||
|
||||
- **`pi` `0.84.2` → `0.84.3`.** Published 2026-08-24T11:09Z. Release notes
|
||||
carry one "Breaking Changes" line — `GoogleThinkingLevel` renamed to
|
||||
`GoogleApiThinkingLevel` — checked against all four vendored packages
|
||||
(`pi-fork`, `pi-observational-memory`, `pi-atelier`, `pi-studio`): zero
|
||||
references, inert here. 0.84.3 also fixes two skill-discovery bugs that
|
||||
land directly on this repo's own vendored-skill work: nested Markdown
|
||||
skills inside `.agents/skills/` grouping directories not being discovered,
|
||||
and root Markdown files (`README.md`/`AGENTS.md`) in skill directories being
|
||||
wrongly reported as broken skills.
|
||||
|
||||
### Added
|
||||
|
||||
- **Browser automation is now documented to humans, not just to agents.**
|
||||
`agent-browser` + Playwright + a headless Chromium (~625 MB — the single
|
||||
largest addition in the image) previously had zero mentions in `README.md`,
|
||||
`DOCKER_HUB.md` or `THIRD_PARTY.md`; it existed only in the agent-facing
|
||||
`AGENTS.md` managed block. Added a `README.md` "Browser automation"
|
||||
subsection, a `DOCKER_HUB.md` feature entry, and `THIRD_PARTY.md` license
|
||||
rows for `agent-browser` (Apache-2.0), Playwright (Apache-2.0), and Chromium
|
||||
(BSD-3-Clause for Chromium's own code plus a large set of bundled
|
||||
third-party components under their own licenses; the binary here is not
|
||||
compiled by this repo — it's Playwright's own "Chrome for Testing" download
|
||||
via `playwright install --with-deps chromium`).
|
||||
- **`THIRD_PARTY.md` gains rows for `pi-atelier` (MIT) and `mempalace` core
|
||||
(MIT per the GitHub repo; noted that the PyPI package's own metadata omits
|
||||
a license classifier, so verify against the repo's `LICENSE` rather than
|
||||
sdist/wheel metadata if clearance is needed from the artifact alone).**
|
||||
- **`typst` and `socat` added to `README.md`'s tooling inventory.** Both were
|
||||
already used in prose (typst as pandoc's `--pdf-engine`, socat by
|
||||
`studio-expose`) but missing from the "What's inside" lists, so the
|
||||
inventory didn't match what the image actually ships.
|
||||
- **`mempalace` core version recorded in `/etc/pi-devbox/build-manifest.json`.**
|
||||
Previously absent — a published image couldn't answer "which palace version
|
||||
shipped?", and a palace bug couldn't be correlated to an image version.
|
||||
Derived from the live installed binary (matching the manifest's existing
|
||||
ground-truth-not-build-args philosophy), degrading to `null` rather than
|
||||
failing the build if the binary is missing or its output format changes.
|
||||
Verified landed: new top-level `"mempalace_version"` key, sibling to
|
||||
`pi_version` rather than a member of `components{}` (that map is rendered
|
||||
truncated to 12 chars by `pi-devbox-version`, which would mangle a longer
|
||||
version string).
|
||||
- **New smoke assertions**, all landed in `scripts/smoke-test.sh`: (1) the
|
||||
`pi-observational-memory` clone is checked for the actual `ce9fc98`
|
||||
auth-fix markers pinned to their fix site, `src/runtime.ts`
|
||||
(`availability_recheck`, `providerCredentialConfigured`,
|
||||
`hasConfiguredAuth`) — not merely clone existence, and deliberately not a
|
||||
repo-wide grep: all three identifiers also appear under `tests/`, so a
|
||||
repo-wide search would stay green even with the fix reverted in
|
||||
`src/runtime.ts` alone; (2) the manifest's new `mempalace_version` field is
|
||||
asserted present, non-null, and equal to what `mempalace --version` reports
|
||||
live, so the manifest can't silently drift from the installed package —
|
||||
expected to fail against any pre-v1.8.6 image, by design; (3) a
|
||||
behavioural check for the mempalace-toolkit feeder's `--agent` default
|
||||
(see below — this one turned out to be possible after all).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **A false claim was being published to Docker Hub on every release.**
|
||||
`DOCKER_HUB.md` advertised "neovim (LazyVim defaults)". Nothing in this
|
||||
repo installs LazyVim — the only nvim configuration is a 19-line
|
||||
`sysinit.vim` that sets `termguicolors`. `update-description` pushes this
|
||||
file verbatim (with `{{PI_VERSION}}` substituted) to the Hub description, so
|
||||
the error was public, not internal. Corrected to describe what's actually
|
||||
there.
|
||||
|
||||
### Component audit for this release
|
||||
|
||||
Checked against upstream 2026-08-25 (two days after v1.8.5's own audit):
|
||||
`mempalace` core moved `3.7.1` → `3.8.0` (see Changed, above — timing is
|
||||
notable: released *hours after* v1.8.5 tagged, so v1.8.5 could not have caught
|
||||
it no matter how carefully it was audited). `pi` moved `0.84.2` → `0.84.3`
|
||||
(see Changed). `pi-toolkit` `0e1369e6`, `pi-extensions` `20228878`,
|
||||
`pi-observational-memory` `ce9fc982`, and `pi-atelier` `v0.8.2` are all
|
||||
**unchanged** from v1.8.5 — in particular `pi-observational-memory` still sits
|
||||
exactly at the auth-fix commit with nothing landed upstream since, and
|
||||
`pi-atelier` is still the newest tag with the `≥0.7.1` floor for `pi ≥ 0.84`
|
||||
trivially satisfied. `pi-fork` has one upstream commit not adopted this
|
||||
release: `f1ff8087` → `bf702b4c`, a text-only rewording of the fork task
|
||||
preamble (no code-path change) — **left un-pulled** for this release since it
|
||||
is a moving ref CI resolves fresh at every build anyway; it will be adopted
|
||||
automatically on the next build regardless of this entry. `pi-studio` (studio
|
||||
variant) has drifted two tags upstream, `v0.9.48` (pinned at build time via
|
||||
CI's newest-semver-tag resolution) → `v0.9.51` at tag time, purely additive
|
||||
(watched PDF previews, opening PDFs directly in Studio, Studio header
|
||||
hide) — nothing to bump in this repo since studio-tag resolution happens in
|
||||
CI, not the Dockerfile, but note it **will** auto-adopt `v0.9.51` on the next
|
||||
studio-variant build. `mempalace-toolkit` unchanged — this release's manifest
|
||||
and pi-bump work in `Dockerfile.variant` stayed within that file's ownership
|
||||
and did not require a toolkit-side change.
|
||||
|
||||
### Still open
|
||||
|
||||
- **`MEMPALACE_VERSION` has no CI-side audit equivalent to `PI_VERSION`'s.**
|
||||
`PI_VERSION` is verified published-on-npm and warns (never silently adopts)
|
||||
on drift; `MEMPALACE_VERSION` is a literal Dockerfile string with zero
|
||||
references in `.gitea/workflows/docker-publish.yml`. Flagged in v1.8.5's
|
||||
audit as a gap; still a gap.
|
||||
- **`pi-devbox-version`'s human-readable output does not display
|
||||
`mempalace_version`.** Its render path is a fixed sequence
|
||||
(`release_tag`, `build_date`, `source_revision`, `pi`, then `components{}`)
|
||||
and the new top-level field isn't in it — only `--json` mode (which `cat`s
|
||||
the manifest directly) surfaces it today. One line in
|
||||
`rootfs/usr/local/bin/pi-devbox-version` would fix this; deferred since the
|
||||
field's stated purpose (correlating a palace bug to an image) is already
|
||||
served by `--json`, but worth doing in a follow-up if this becomes a
|
||||
routine manual check.
|
||||
|
||||
**Resolved during this release, not left open:** the feeder `--agent`
|
||||
default behavioural hook initially looked like it might need a
|
||||
mempalace-toolkit change (a `--print-config` flag that doesn't exist). It
|
||||
didn't — `mempalace-pi-session` assigns `AGENT` before argument parsing and
|
||||
`--help` exits 0 with no side effects, so `bash -x mempalace-pi-session
|
||||
--help` observes the real resolution (env interpolation and fallback)
|
||||
without needing a source change. The new smoke assertion exploits exactly
|
||||
that, checked both ways: with `MEMPALACE_PI_DEVICE` set it must resolve to
|
||||
`pi@<device>`; with it unset it must NOT be `pi@*` (catches a regression to
|
||||
the old unconditional `$USER`/`mempalace` default).
|
||||
`mempalace-toolkit` commit `c64ffa1` changed the feeder's `--agent` default
|
||||
from `$USER` to `pi@<device>`, but there is still no way for smoke to assert
|
||||
this default is actually in effect from this repo alone, since
|
||||
`mempalace-toolkit` is a separate repo this release does not modify. If the
|
||||
concurrent smoke-test work could not find an honest assertion from the
|
||||
existing `/opt/mempalace-toolkit` surface (help text, `--self-test`), this
|
||||
remains open pending a toolkit-side `--print-config`-style hook — a
|
||||
toolkit-repo change, not a pi-devbox one.
|
||||
- **16 base-tooling `ARG *_VERSION=latest` pins remain unrecorded.** (Corrected
|
||||
count — v1.8.5's entry said "~14"; the actual count from `Dockerfile.base`
|
||||
is 16, plus 5 more that float with no ARG at all: `rustup-init`, AWS CLI v2,
|
||||
Chromium-via-Playwright, Node's minor version via `setup_22.x`, and
|
||||
`DEBIAN_VERSION=trixie-slim` itself.) None of these are recorded anywhere
|
||||
once the build completes — not in the manifest, not in a label — so a
|
||||
published image cannot answer "which nvim/uv/chromium shipped?" without
|
||||
exec-ing in and asking the binary.
|
||||
|
||||
### Documentation
|
||||
|
||||
- **`.env.example` documents `MEMPALACE_PALACE_PATH`.** It was the only MemPalace
|
||||
variable the template never mentioned, while being the one that silently moves
|
||||
the feeders' stage: the palace root resolves as `$MEMPALACE_PALACE_PATH` →
|
||||
`$MEMPAL_PALACE_PATH` → `~/.mempalace/config.json` → `~/.mempalace/palace`, and
|
||||
the stage is derived from it (`<palace-root>/pi-stage`). The comment states the
|
||||
precedence, says why neither the image nor the entrypoint exports it (pinning
|
||||
the palace without carrying the stage re-creates the split a shared root
|
||||
removed — see v1.8.2), warns that a stage whose persistence differs from the
|
||||
palace makes a scoped `mempalace sync` prune conversation drawers whose dedup
|
||||
key is the staged path, and notes it is a *container* path unlike the
|
||||
host-side `WORKSPACE_PATH`/`SSH_KEY_PATH` above it. Found while auditing a live
|
||||
host whose `.env` sets the variable redundantly to the default.
|
||||
|
||||
---
|
||||
|
||||
## v1.8.5 — 2026-08-23
|
||||
|
||||
Patch release with two fixes in the container's skill wiring — one behavioural,
|
||||
one a latent crash found while reviewing the first — plus the `mempalace-toolkit`
|
||||
change that makes palace writes carry provenance. **No component pin moved.**
|
||||
Every ref was re-resolved at tag time and is byte-identical to what v1.8.4
|
||||
shipped: `pi` `0.84.2` (still npm latest), `pi-atelier` `v0.8.2` → `159f34cf`
|
||||
(newest tag; the `≥0.7.1` floor for `pi ≥ 0.84` holds), `pi-studio` `v0.9.48` →
|
||||
`c3b83680`, `pi-fork` `f1ff8087`, `pi-observational-memory` `ce9fc982`,
|
||||
`pi-toolkit` `0e1369e6`, `pi-extensions` `20228878`, `MEMPALACE_VERSION` `3.7.1`
|
||||
(still PyPI latest, and the version the central palace serves — no client/server
|
||||
skew). The single moving part is `mempalace-toolkit` `fd8b15f5` → `0fe64c4`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Vendored skills no longer silently shadow their live skillset
|
||||
counterparts.** `~/.agents/skills` was *asymmetric*: `mempalace`,
|
||||
`pi-devbox-environment` and `pi-extensions` resolved to the baked
|
||||
`/usr/local/share/pi-devbox/skills/…`, while every other skill resolved to the
|
||||
live `/workspace/skillset/skills/…`. Root cause was precedence-by-ordering in
|
||||
`entrypoint-user.sh`: the baked links are created **early** (line 65 in
|
||||
v1.8.4; the loop moved down as this fix added comments) — deliberately so, to
|
||||
close a smoke-test readiness race — with `[ ! -e … ]` so they are
|
||||
"created only when absent", and the skillset deploy runs **last**, where it
|
||||
classifies the existing links as foreign and leaves them alone. The comment at
|
||||
line 61 claimed the goal was "a same-named skillset skill … is never
|
||||
clobbered" — but with baked-first plus create-when-absent, the *skillset* skill
|
||||
was precisely the one that lost. Comment now describes the actual behaviour.
|
||||
|
||||
Observed cost, on two hosts independently: an edit to
|
||||
`skillset/skills/mempalace/SKILL.md` (adding a drawer-attribution rule) was
|
||||
pushed and present in the live clone (`md5 129bcc4752`), yet both the
|
||||
EMB-7KJ4VR4G and tor-ms22 containers kept loading the baked copy
|
||||
(`md5 5236024fef`) with zero occurrences of the new rule. The tor-ms22 agent
|
||||
had to fetch the rule from the Gitea API to read it at all. Editing a skillset
|
||||
skill therefore *appeared* to work and silently did nothing until an image
|
||||
rebuild — for exactly the three skills most likely to be iterated on.
|
||||
|
||||
**The fix is not "the skillset always wins"**, because ownership is per-skill
|
||||
(`rootfs/usr/local/share/pi-devbox/skills/VENDORED.md`): `pi-extensions`'
|
||||
authoritative source is the *package* repo, copied over the snapshot at build
|
||||
time, and `skillset` carries a downstream copy that can lag — handing that one
|
||||
to the clone would regress the skill. So a new helper
|
||||
`devbox-skill-reconcile` runs immediately after the skillset deploy and
|
||||
repoints only the skills named in `skills/skillset-owned.txt` (today:
|
||||
`mempalace`). Precedence is now user override → live skillset clone (owned
|
||||
names only) → baked snapshot, with the early links untouched as the fallback,
|
||||
so the readiness race stays closed. It only ever replaces a symlink that
|
||||
points into the baked tree, so a real directory or a link pointing elsewhere
|
||||
is never disturbed. Verify with `readlink -f ~/.agents/skills/mempalace`, not
|
||||
by reading the entrypoint.
|
||||
|
||||
- **A latent boot-abort in the baked-link block, found while reviewing the fix
|
||||
above and fixed with it.** `[ ! -e "$link" ]` is TRUE for a *dangling* symlink
|
||||
(`-e` follows the link), so once a link may point into `/workspace/skillset`
|
||||
— which the fix above makes possible — a vanished mount turns the guard into
|
||||
"create over a broken link", and plain `ln -s` then fails with `File exists`.
|
||||
Under the entrypoint's `set -euo pipefail` that **aborts container start**
|
||||
before `exec "$@"`, with a cryptic `ln` error and no pi. Reachable on a
|
||||
`docker restart` or a host reboot under `restart: unless-stopped` (the writable
|
||||
layer survives and `~/.agents` is not a volume on any host), though not on a
|
||||
`compose up -d` recreate. Now `ln -sfn`, which heals the broken link back to
|
||||
the baked fallback; the reconciler re-points it in the same boot if the clone
|
||||
is back. A comment at the call site records why the `-f` must stay.
|
||||
|
||||
- **README's skill-precedence documentation was wrong** in the same way the
|
||||
entrypoint comment was: it claimed baked skills are "created only when absent
|
||||
so a same-named skillset skill … is never clobbered" and that "a mounted
|
||||
skillset always overrides them". Rewritten to state the real, per-skill
|
||||
precedence and to name `skillset-owned.txt` and `devbox-skill-reconcile`.
|
||||
|
||||
- **The smoke canary for a stale `mempalace` snapshot could not detect
|
||||
staleness.** It grepped `"Shared palace: multiple harnesses"` — a phrase
|
||||
present in *both* the stale and the fresh copy, so it passed throughout the
|
||||
shadowing bug above. It now pins the newest section
|
||||
(`"Attribute what you file yourself"`), and `VENDORED.md` records that
|
||||
updating this string is part of refreshing the snapshot. Three further
|
||||
assertions close the gaps that let the bug ship: skill link **targets** are
|
||||
asserted (not merely `test -L`), the `skillset-owned.txt` list is asserted to
|
||||
contain `mempalace` and *not* `pi-extensions`, and the reconciler's replace
|
||||
path — which CI never exercises, since no smoke container mounts a skillset —
|
||||
is covered by fabricating a skillset and asserting all three outcomes (owned
|
||||
skill repointed, unowned skill left baked, user override untouched) — plus a
|
||||
second case that a mutation test proved necessary: with the reconciler's
|
||||
"is this link ours?" guard deleted, all three of those assertions still
|
||||
passed, so the discriminating case is an *owned* name whose link is a user
|
||||
override pointing outside the baked tree.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Vendored `mempalace` skill snapshot refreshed** from `skillset` `936fed8` →
|
||||
`670f7f1` (`md5 5236024fef` → `129bcc4752`), which adds the "Attribute what
|
||||
you file yourself" rule: hand-filed drawers should carry
|
||||
`added_by="<harness>@<device>"`. Without this refresh the symlink fix above
|
||||
would only help hosts that mount `skillset`; a bare container would still ship
|
||||
the pre-attribution-rule skill.
|
||||
|
||||
- **Component audit for this release — no pin edits needed.** Every component
|
||||
except `pi`/`pi-atelier` is pinned to a moving ref that CI resolves at build
|
||||
time, and each was checked against upstream on 2026-08-23: `pi` `0.84.2`
|
||||
(still npm latest, published 2026-08-14), `pi-atelier` `v0.8.2` (newest tag;
|
||||
the `≥0.7.1` floor for `pi ≥ 0.84` is satisfied), `pi-fork` `f1ff8087`,
|
||||
`pi-observational-memory` `ce9fc982`, `pi-toolkit` `0e1369e6`,
|
||||
`pi-extensions` `20228878`, `pi-studio` `v0.9.48` → `c3b83680` — all
|
||||
**byte-identical to what v1.8.4 shipped**. `MEMPALACE_VERSION` stays `3.7.1`
|
||||
(still PyPI latest, and the version the central palace serves, so no
|
||||
client/server skew). The one component that moved is `mempalace-toolkit`
|
||||
`fd8b15f5` → `0fe64c4`, which is this release's other payload: the feeder now
|
||||
defaults `--agent` to `pi@$MEMPALACE_PI_DEVICE` so palace writes carry
|
||||
provenance, with `$USER` still the fallback when the variable is unset
|
||||
(`AGENT="${MEMPALACE_PI_DEVICE:+pi@${MEMPALACE_PI_DEVICE}}"`), so un-enrolled
|
||||
hosts are unaffected. Nothing landed upstream after the
|
||||
`pi-observational-memory` merge `ce9fc982`, so the eight-week-bug fix in
|
||||
v1.8.4 is not destabilised. All of the above was re-resolved immediately
|
||||
before the tag and was unchanged — worth repeating for any future release,
|
||||
because six of nine components are moving refs that CI resolves at build
|
||||
time, so the build, not the Dockerfile, decides what ships.
|
||||
|
||||
Two notes for whoever runs the build. This release changes
|
||||
`entrypoint-user.sh`, `rootfs/**` and the resolved toolkit SHA — all three feed
|
||||
the base-image hash — so expect a **full multi-arch base rebuild** (~95 min,
|
||||
as on v1.8.3/CI 562), not a fast variant-only publish. And that rebuild
|
||||
re-resolves the ~14 base-tooling `ARG *_VERSION=latest` pins; measured drift
|
||||
on 2026-08-23 was one patch (`nvim v0.12.4 → v0.12.5`), so the window is
|
||||
favourable, but it is not covered by version assertions.
|
||||
|
||||
- **`pi-devbox-environment` skill — new §2 subsection "A negative result is
|
||||
usually your own filter", plus ControlMaster masking in §3.** This is baked
|
||||
(`rootfs/usr/local/share/pi-devbox/skills/`, symlinked to
|
||||
`~/.agents/skills/`), so it is an image-behaviour change even though no
|
||||
package moved. Motivated by three false negatives an agent produced in a
|
||||
single session, each from its own filter rather than from the world: a
|
||||
`| head -20` "proved" an SSH peer absent that was defined at **line 454** of a
|
||||
~500-line config; `ssh mac 'docker ps'` "proved" the host had no Docker, when
|
||||
the non-interactive SSH `PATH` simply lacks `/usr/local/bin`; and a `grep 'ssh
|
||||
'` "proved" no ControlMaster was running, when master processes **rename
|
||||
themselves** to `ssh: <controlpath> [mux]`. The rule now stated: a positive
|
||||
result carries its own evidence, absence has to be *earned*. §3 additionally
|
||||
documents that a live master socket makes later commands authenticate **not at
|
||||
all**, so "it still works" proves nothing after editing a peer's
|
||||
`authorized_keys` — verify with `-o ControlPath=none -o ControlMaster=no`, or
|
||||
the breakage surfaces in a future session with no memory of the edit.
|
||||
- **`AGENTS.md`: a stale CI claim corrected.** It said "a tag push produces two
|
||||
runs, not one — `lint.yml` fires on every push (including tag refs)". That
|
||||
stopped being true when lint was scoped to `branches: ['**']`, which excludes
|
||||
tag refs by design; `refs/tags/v1.8.4` produced run 571 (publish) and nothing
|
||||
else. The `head_sha` + workflow-`path` filter advice stays, because it costs
|
||||
nothing and any future `v*`-triggered workflow would reintroduce the
|
||||
ambiguity. Also adds a short "Verifying this repo's reality from inside a
|
||||
container" section, including the trap that **this repo's `docker-compose.yml`
|
||||
is a template pinning `:latest`** while a real host runs its own per-machine
|
||||
file — so recreating from the repo copy can silently move a host off
|
||||
`:latest-studio`.
|
||||
|
||||
---
|
||||
|
||||
### Still open
|
||||
|
||||
- `build-manifest.json` records the `mempalace-toolkit` SHA but **not** the
|
||||
mempalace **core** version, so a palace bug cannot be correlated with an
|
||||
image. `mempalace --version` prints it; adding it is a one-line change to the
|
||||
manifest `RUN` in `Dockerfile.variant` plus one smoke assertion, and is
|
||||
variant-only (no base rebuild cost).
|
||||
- Smoke asserts the `pi-observational-memory` clone **exists** but not that it
|
||||
contains the ambient-auth fix. npm still ships pre-fix `3.0.4`, so an
|
||||
accidental switch from the `/opt` clone to an npm install would be a silent
|
||||
regression. Cheap guard: `grep -rl availability_recheck` must be ≥1.
|
||||
- The feeder's new `pi@<device>` default has no behavioural test hook
|
||||
(`--dry-run` never prints the agent; `--self-test` only covers the remote-mine
|
||||
response classifier). Cheapest available check is a source-shape grep for
|
||||
`MEMPALACE_PI_DEVICE:+pi@`.
|
||||
|
||||
---
|
||||
|
||||
## v1.8.4 — 2026-08-22
|
||||
|
||||
Patch release, and the one that ends an eight-week bug: **the baked
|
||||
`pi-observational-memory` finally records observations on a Bedrock host that
|
||||
uses ambient AWS credentials.** The fix is ours, but it is no longer a patch —
|
||||
upstream merged it, so this release picks it up through the ordinary
|
||||
`PI_OBSMEM_REF=master` path with no local carry. Also **bumps pi-atelier
|
||||
`v0.8.1` → `v0.8.2`** (audited below) and bakes the `todo` extension's new
|
||||
`edit` action. `pi` stays `0.84.2` (still the npm latest, published
|
||||
2026-08-14) and `MEMPALACE_VERSION` stays `3.7.1` (still the PyPI latest).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **om consolidation on request-time-signed providers — upstream, not patched
|
||||
(`pi-observational-memory` `37986b6` → `ce9fc98`).** Under `37986b6`, om's
|
||||
pre-flight gate treated "pi exposes no `apiKey` and no auth header" as
|
||||
*unauthenticated* and skipped every consolidation. On Bedrock with ambient
|
||||
AWS credentials that is the normal case — pi signs SigV4 at request time —
|
||||
so om recorded **nothing for eight weeks** with no error, no cost and no log
|
||||
line. Every pi-devbox image up to and including v1.8.3 has that behaviour.
|
||||
|
||||
Two commits, both authored here and now upstream verbatim: `6f694e6` fixes
|
||||
the gate itself (it must not require a credential *payload*), and `699ccc7`
|
||||
adds the second half of pi's own rule — `hasConfiguredAuth` reads an
|
||||
availability snapshot that stays empty when the startup availability pass was
|
||||
skipped/aborted/failed, so on the otherwise-fatal path om now asks pi to
|
||||
re-check the credential live (refresh scoped to the provider, network-free),
|
||||
rate-limited 60 s per provider, bounded by a raced timeout, logged as
|
||||
`resolve.availability_recheck`. Filed as upstream issue #51, merged as PR #52
|
||||
(`ce9fc98`, 2026-08-22T04:46:18Z), which also carries PR #49's `env`/`baseUrl`
|
||||
forwarding merged four minutes earlier; the maintainer resolved the textual
|
||||
conflict between them keeping both behaviours. Verified on `ce9fc98` here:
|
||||
`tsc --noEmit` clean, `vitest` 257 tests / 27 files green.
|
||||
|
||||
**Note for anyone carrying the local workaround:** the interim fix was a
|
||||
`packages[]` override in `~/.pi/agent/settings.json` pointing pi at a patched
|
||||
clone outside the image. From this release on, delete the override — the
|
||||
baked `/opt/pi-observational-memory` has the fix. Confirm with
|
||||
`/etc/pi-devbox/build-manifest.json` → `components.pi-observational-memory`
|
||||
before removing it. The npm-published `pi-observational-memory` is **still
|
||||
`3.0.4` and still broken**; the image does not use npm for this component, so
|
||||
the release cadence there is irrelevant to us.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`PI_ATELIER_REF` / `PI_ATELIER_VERSION` `v0.8.1` → `v0.8.2`, audited per the
|
||||
floor note above the ARG.** Only one version sits between old and new and its
|
||||
changelog is two lines, both Workspace-Pulse-internal: inspection requests are
|
||||
now coalesced and serialized so short Turns avoid duplicate Git work and
|
||||
overlapping inspections cannot run concurrently, and live tool-driven Pulse
|
||||
updates are preserved while a fresh inspection is guaranteed at Turn end and
|
||||
retired sessions can no longer publish stale results. **Nothing touches pi's
|
||||
private TUI renderer**, which is the coupling that produced the
|
||||
0.6.0/0.7.0-under-pi-0.84 startup hang, and `pi` is unchanged at `0.84.2`, so
|
||||
this bump does not re-enter that risk class. Both the seam and the pin floor
|
||||
(never pair pi-atelier < 0.7.1 with pi >= 0.84) are unaffected.
|
||||
- **`pi-studio` `65995fe` (0.9.44) → `v0.9.48`** — 14 commits, four releases.
|
||||
Studio-side only (`INSTALL_STUDIO=false` by default, so this lands in the
|
||||
studio variant): open Studio in Muxy's browser, local PDF preview actions,
|
||||
previews survive Pandoc probe failures, legacy LaTeX styles tolerated in
|
||||
Pandoc previews, native dialogs replaced in embedded browsers, and file-copy
|
||||
import fixes with an explicit fallback. CI resolves the highest semver tag,
|
||||
not `main`, so this is `v0.9.48` exactly.
|
||||
- **`pi-fork` `4a09af4` → `f1ff808`** — one commit, "Add fork runtime
|
||||
awareness" (2026-08-19).
|
||||
- **`mempalace-toolkit` `b609cf5` → `fd8b15f`** — two commits, **docs only**
|
||||
(backup/recovery + units; the convos-miner mtime correction finished). The
|
||||
`fix(pi-session)` false-success guard was already baked in v1.8.3 — checked
|
||||
by ancestry (`git merge-base --is-ancestor 6e1f4f3 b609cf5`), not by reading
|
||||
the log, because a commit's *date* does not tell you which side of a pin it
|
||||
fell on.
|
||||
- **`aws-cli` 2.36.24 → 2.36.29** and the other `*_VERSION=latest` tools
|
||||
(bat/eza/fzf/gitleaks/nvim/micro/zoxide/yq/typst/tealdeer/agent-browser/
|
||||
playwright/gosu/git-lfs/uv) refresh implicitly, as designed.
|
||||
- `pi-toolkit` unchanged (`0e1369e`, local `main` == baked).
|
||||
|
||||
### Added
|
||||
|
||||
- **`todo` extension: an `edit` action** (`pi-extensions` `98eb07b` →
|
||||
`2022887`). The tool is a verbatim vendored copy of pi's own
|
||||
`examples/extensions/todo.ts`, which offers list/add/toggle/clear and no way
|
||||
to change an item's text. On a long-lived list that forces either a "patch"
|
||||
item describing a *different* item, or clear-and-re-add of everything — both
|
||||
hit for real on 2026-08-17 while tracking a 17-item fleet plan, which ended up
|
||||
with `#18` correcting `#17`. `edit` takes `id` + `text` and keeps the id and
|
||||
the done status; `nextId` is untouched. Id stability is the point, because ids
|
||||
are the only handle a palace snapshot of a plan can refer to.
|
||||
- Verified live in-container before committing, by repointing
|
||||
`~/.pi/agent/extensions/todo.ts` at a working copy for one session (unknown
|
||||
id and missing text both error as intended; editing a completed item kept its
|
||||
id and its `done` state), then reverting the symlink to the image copy.
|
||||
|
||||
### Notes
|
||||
|
||||
- **No pi-atelier change was needed for the `todo` action.** Its `tool_result`
|
||||
hook only checks that `details.todos` is a well-shaped array and ignores the
|
||||
action string, so the new action flows through its normalizer and sidebar
|
||||
untouched. Worth knowing while reading agent transcripts: that hook
|
||||
*replaces* todo tool output with `N/M done · see sidebar` whenever the sidebar
|
||||
todo panel is visible (`showSidebarTodos`), so an agent sees only the counter
|
||||
and not the item text — upstream's `list` otherwise returns every item. That
|
||||
is a deliberate context saving, not a tool limitation.
|
||||
- The vendored copy now carries a numbered **LOCAL DELTAS** list in its header
|
||||
(the earlier `ctx.mode !== "tui"` → `!ctx.hasUI` API fix, and this action), so
|
||||
reconciling a future upstream version stays mechanical rather than
|
||||
archaeological.
|
||||
- **A second om fix is NOT in this image and will not be.** Upstream PR #24
|
||||
("advance coverage watermark when observer records nothing", head
|
||||
`joakimp:fix/observer-empty-coverage-watermark` `b577b29`) is open but
|
||||
design-rejected by the maintainer on 2026-07-03: an empty observer verdict is
|
||||
usually a *technical* failure, so advancing the watermark would leave a gap in
|
||||
the observed session, and in the genuinely-nothing-to-observe case the next
|
||||
observer simply gets more context. So the observer can still re-fire on a
|
||||
growing span after an empty verdict — that is upstream's intended behaviour,
|
||||
not an image defect. Do not "fix" it by rebasing that branch.
|
||||
|
||||
---
|
||||
|
||||
## v1.8.3 — 2026-08-16
|
||||
|
||||
Patch release. **Bumps mempalace to `3.7.1`** and closes the gap that made the
|
||||
baked mempalace skill go stale for four commits. `pi` stays `0.84.2` (still the
|
||||
npm latest) and pi-atelier stays `v0.8.1`; every git-ref component
|
||||
(pi-toolkit, pi-extensions, pi-fork, pi-observational-memory, pi-studio,
|
||||
mempalace-toolkit) was checked against its upstream head and is unchanged.
|
||||
|
||||
- **`MEMPALACE_VERSION` `3.6.0` → `3.7.1`.** Verified against the 3.7.1 source
|
||||
rather than its changelog, because the risk is to palaces users cannot
|
||||
reconstruct: legacy drawers lack the new `chunk_total` completion marker and
|
||||
**both** decision sites trust them (`if chunk_total is None: ... trust the
|
||||
match as before`), so there is **no mass re-mine**; `NORMALIZE_VERSION` is `2`
|
||||
in both versions, so the "pre-v2 drawers are stale" gate does not fire either;
|
||||
`chromadb<2,>=1.5.4` keeps the same major, so no index-format migration; there
|
||||
is no auto-migration (the source says *"We do NOT auto-migrate"* twice) and
|
||||
`rebuild_index` has exactly one call site, the explicit `repair rebuild`; the
|
||||
single new palace file (`logstream.sqlite3`) is created lazily on first
|
||||
logstream use. Downgrade stays possible — 3.6.0 has zero references to
|
||||
`chunk_total` and ignores it as unknown metadata.
|
||||
|
||||
Two behaviour changes worth knowing, both turning a silent condition into a
|
||||
hard refusal: `MEMPALACE_MCP_ALLOW_PEER_WRITER` **no longer works on
|
||||
local/chroma palaces** (it is now gated on `backend_requires_single_writer()`,
|
||||
and `_MULTI_PROCESS_WRITER_BACKENDS` is `{pgvector, qdrant}`), and writer-lock
|
||||
*setup* failures now **fail closed** (`refusing this mutating tool`) instead of
|
||||
proceeding with a warning. Neither affects this image's normal
|
||||
MCP-server-plus-CLI-feeder pattern, which already serialised on the same
|
||||
`mine_palace_*.lock` under 3.6.0 — "process-lifetime single-writer ownership"
|
||||
in the upstream changelog describes tightened escape hatches, not a new lease.
|
||||
|
||||
What 3.7.1 buys a **shared central** palace is the real motivation: the stale
|
||||
chromadb `SharedSystemClient` cache is now dropped on reconnect (under 3.6.0 a
|
||||
peer's writes could be overwritten by a stale in-memory HNSW segment, *"index
|
||||
count going backwards"*), the writer lease is released on SIGTERM/SIGHUP
|
||||
instead of leaking a lock naming a dead PID, and an interrupted mine is no
|
||||
longer permanently skipped as though complete.
|
||||
|
||||
**Upgrading a server requires restarting it** — 3.7.1 refuses mutating tools
|
||||
when the served library drifts from what is installed, and `mempalace_reconnect`
|
||||
cannot clear that (it reopens the database but cannot reload Python modules).
|
||||
The fleet primary was upgraded and restarted before this image was tagged.
|
||||
|
||||
Note: opencode-devbox still pins `3.6.0`. The two images are meant to move in
|
||||
lockstep, so that pin diverges until opencode-devbox cuts its own release.
|
||||
|
||||
- **Vendored `mempalace` skill snapshot refreshed** to skillset `936fed8` (was
|
||||
`63f3bf5`). This is the gap worth naming: `~/.agents/skills/mempalace`
|
||||
symlinks to the **image-baked** copy under
|
||||
`/usr/local/share/pi-devbox/skills/`, and `entrypoint-user.sh` creates that
|
||||
link *first* while the skillset deploy never clobbers an existing name — so in
|
||||
a devbox container the vendored snapshot always wins, and editing the skillset
|
||||
repo alone changes nothing a container reads. Two commits' worth of guidance
|
||||
had been invisible here: the multi-machine shared-palace section (device
|
||||
provenance in `source_path`, mined drawers carrying the *mine* date with
|
||||
UUIDv7 recovery, the naive-local vs UTC timestamp mismatch, `agent_name` not
|
||||
being device-scoped, single-writer/no-queue semantics) and the
|
||||
hand-crafted-provenance guard.
|
||||
|
||||
- **`pi-global-AGENTS.append.md`** gains `### If the palace is central, it is
|
||||
shared — three rules`: never run `mempalace sync` against a shared palace (it
|
||||
prunes drawers whose sources look missing, which on a central palace is most
|
||||
of the content, including other machines' — compounded by RFC-001 §7.2, since
|
||||
feeders stage *inside* the palace root); a client-side timeout is not a
|
||||
failure (single writer, one large mine blocks everyone, so
|
||||
`mine timed out after 30000ms` usually means the mine completed — verify
|
||||
before retrying or you file a duplicate); and the `mempalace` CLI is not
|
||||
remote-aware, so it always opens a local-disk palace and can silently
|
||||
disagree with the MCP tools.
|
||||
|
||||
- **`mempalace-census` is now on `PATH`.** It shipped inside the image at
|
||||
`/opt/mempalace-toolkit/bin/` but was never symlinked into `/usr/local/bin`
|
||||
like its three siblings, so RFC-002 Phase A censuses had to be invoked by
|
||||
absolute path. Added to the symlink set, the `chmod +x` set, and the
|
||||
build-time `--help` smoke chain.
|
||||
|
||||
---
|
||||
|
||||
## v1.8.2 — 2026-08-16
|
||||
|
||||
Patch release. **Ships the fix for a silent transcript-feed failure**, plus the
|
||||
smoke assertion that stops it coming back. No image pins changed from v1.8.1
|
||||
(pi `0.84.2`, pi-atelier `v0.8.1`); what moves is the baked `mempalace-toolkit`
|
||||
ref and one new smoke check.
|
||||
|
||||
**The bug this closes** (found on the first boot of the v1.8.1 image, on
|
||||
EMB-7KJ4VR4G, 2026-08-15): the container-start catch-up rsynced seven pi session
|
||||
transcripts to the palace host correctly, then asked the server to mine
|
||||
`/data/feed/<device>` — the feeder's default `MEMPALACE_PI_REMOTE_PATH`, which
|
||||
assumes a *containerized* palace server. That fleet's primary runs **natively**
|
||||
(a systemd user unit + uv tool), so it only ever sees host paths and the mine
|
||||
died with `source directory not found`. rsync had already succeeded, so the
|
||||
inbox looked healthy.
|
||||
|
||||
It stayed invisible because of the second half: the feeder decided success with
|
||||
`'"error"' in body`. MCP answers a hard tool failure with HTTP 200 and a
|
||||
JSON-RPC *result* whose `content[].text` carries the tool's own JSON as an
|
||||
**escaped string** — the bytes are `\"error\"`, so the substring could never
|
||||
match. `~/.pi/agent/mempalace-catchup.log` printed
|
||||
`Done. Wing 'wing_conversations' updated.` directly beneath the error JSON and
|
||||
exited 0. A feeder whose only artifact claims success is worse than one that
|
||||
crashes: nothing in the container disagreed with it.
|
||||
|
||||
Shipped here:
|
||||
|
||||
- **`mempalace-toolkit` ≥ `b609cf5`** baked (CI resolves the ref at build time):
|
||||
`classify()` parses the MCP envelope instead of grepping it (JSON-RPC error,
|
||||
MCP `isError`, inner `success=false`/`error`), and separates "verified ok"
|
||||
from "unverified: no JSON tool payload" rather than assuming the good case.
|
||||
A preflight warning fires when the rsync destination and
|
||||
`MEMPALACE_PI_REMOTE_PATH` disagree — in *preflight*, so `--dry-run` and
|
||||
`--prepare` surface it too. Remote mode also stops previewing NEW/SKIP from
|
||||
the *local* palace, which had been reporting "6 already filed" about a palace
|
||||
it was not feeding; the tags are now `[?]` and the summary names who decides.
|
||||
- **New smoke assertion** — `mempalace-pi-session --self-test` run against the
|
||||
**baked** toolkit. It replays six recorded MCP responses (fixture 1 is the
|
||||
verbatim 2026-08-15 failure body) plus a regression guard asserting the old
|
||||
substring check is blind to it. A stale or reverted `MEMPALACE_TOOLKIT_REF`
|
||||
can therefore no longer ship a feeder that mines nothing while reporting
|
||||
success.
|
||||
- **`.env.example`** now spells out that `MEMPALACE_PI_REMOTE_PATH` is the path
|
||||
the *server process* can open — the container path for a dockerized server,
|
||||
identical to the ssh-target path for a native one — and that a mismatch fails
|
||||
quietly, with rsync succeeding and only the mine failing.
|
||||
|
||||
**The `--self-test` assertion is deliberately bare** (`mempalace-pi-session
|
||||
--self-test`, no `HOME=…` prefix). `run()` invokes
|
||||
`docker run --entrypoint="" $IMAGE sh -c …` and no Dockerfile sets `USER` or
|
||||
`ENV HOME`, so it executes with **no `HOME` at all** — the same condition that
|
||||
made v1.8.0's stage assertion unsatisfiable. The feeder is `set -u` with
|
||||
HOME-anchored defaults, so it used to die with `HOME: unbound variable` there;
|
||||
`b609cf5` derives `HOME` from the passwd database (what python's `expanduser()`
|
||||
falls back to) instead. Keeping the call bare means smoke also proves the feeder
|
||||
runs in a bare container, rather than papering over it with an env prefix.
|
||||
|
||||
---
|
||||
|
||||
## v1.8.1 — 2026-08-15
|
||||
|
||||
Patch release. **Unblocks v1.8.0, which never shipped.** Its `smoke` and
|
||||
`smoke-studio` jobs each failed exactly one assertion (67/68 and 70/71 passed),
|
||||
so `build-variant` and everything downstream skipped: no `v1.8.0` tag reached
|
||||
Docker Hub and `latest` stayed on v1.7.0 from 2026-08-07. Image content is
|
||||
unchanged from what v1.8.0 intended — the pins here are identical (pi `0.84.2`,
|
||||
pi-atelier `v0.8.1`).
|
||||
|
||||
The failing assertion was `pi stage defaults next to the palace (not a cache
|
||||
dir)`, added three days earlier in 7c00dd6. **It was a test bug, not a product
|
||||
regression.** It asserted a literal path:
|
||||
|
||||
```sh
|
||||
echo "$out" | grep -q "stage=/home/developer/.mempalace/pi-stage/"
|
||||
```
|
||||
|
||||
but the `run` helper invokes `docker run --rm --entrypoint="" $IMAGE sh -c …`,
|
||||
and neither `Dockerfile.base` nor `Dockerfile.variant` sets `USER` or `ENV HOME`
|
||||
(the published base image config carries no `HOME` at all — `HOME` is normally
|
||||
set by `entrypoint-user.sh`, which `--entrypoint=""` deliberately skips). So the
|
||||
assertion ran as **root with `HOME=/root`**, `mempalace-pi-session` correctly
|
||||
resolved `stage=/root/.mempalace/pi-stage/…` (it is `$HOME`-relative by design:
|
||||
`$MEMPALACE_PALACE_PATH` → `$MEMPAL_PALACE_PATH` → `~/.mempalace/config.json` →
|
||||
`~/.mempalace/palace`), and the literal grep could never match under any
|
||||
circumstances. The tell was one line below it in the log: the sibling assertion
|
||||
`pi stage follows MEMPALACE_PALACE_PATH` **passed**, because it sets the variable
|
||||
explicitly and so never consults `HOME`. Default fails while explicit passes is
|
||||
the signature of a wrong `HOME`, not of broken staging.
|
||||
|
||||
Fixed by asserting the invariant that was actually meant — the stage sits beside
|
||||
the resolved palace, sharing its lifetime — which is user-independent:
|
||||
|
||||
```sh
|
||||
case "$stage" in
|
||||
"stage=$HOME/.mempalace/pi-stage/"*) exit 0 ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
```
|
||||
|
||||
`$HOME` is expanded by the container's own shell, so this holds as root, as
|
||||
`developer`, or under any future user, while a cache-dir default — the
|
||||
regression the assertion exists to catch — still fails it (verified against all
|
||||
three cases plus a simulated `MEMPALACE_PI_STAGE` cache pin). A second
|
||||
assertion, `pi stage is palace-adjacent for the developer user`, now covers the
|
||||
deployment-specific path properly, by *supplying* `HOME=/home/developer` instead
|
||||
of assuming it.
|
||||
|
||||
### Why it took a release to notice — and the `smoke_only` input
|
||||
|
||||
`docker-publish.yml` triggers on `push: tags: v*` only. 7c00dd6 was a push to
|
||||
**main**, so only `lint.yml` ran; v1.8.0 was the first tag afterwards and
|
||||
therefore the assertion's **first execution ever**. Any smoke assertion written
|
||||
outside a release was unvalidated until the next release consumed it — the
|
||||
worst possible moment to discover it.
|
||||
|
||||
New `workflow_dispatch` input **`smoke_only`** closes that: it probes/builds the
|
||||
base and runs both smoke jobs against HEAD, then stops before publishing
|
||||
anything. Implemented as `if: inputs.smoke_only != 'true'` on `build-variant`
|
||||
and `build-variant-studio`, deliberately *without* `always()` so the implicit
|
||||
"needs succeeded" gate survives and a red smoke still blocks a release;
|
||||
`promote-base-latest` and `update-description` already require `build-variant`
|
||||
success and so skip on their own. On a tag push `inputs` is unset and
|
||||
`null != 'true'` is true, so releases behave exactly as before. This release was
|
||||
validated with a `smoke_only` dispatch before the tag was cut.
|
||||
|
||||
### Smoke failures now explain themselves
|
||||
|
||||
`run` discarded all output (`>/dev/null 2>&1`), so a red ❌ carried zero
|
||||
diagnostic weight — explaining this one-line failure took a CI-log dig plus a
|
||||
registry image-config inspection, when the container had already printed the
|
||||
answer and thrown it away. It now captures output and prints the last few lines
|
||||
under a failed assertion only. Assertions that want a diagnostic echo it to
|
||||
stderr (the stage checks now report the resolved stage and the `HOME` they saw),
|
||||
which stays invisible while they pass.
|
||||
|
||||
---
|
||||
|
||||
## v1.8.0 — 2026-08-15
|
||||
|
||||
Minor release. Headline: **pi sessions now feed MemPalace by themselves.** The
|
||||
image already shipped `mempalace-toolkit`, but its pi feeder
|
||||
(`mempalace-pi-session`) was never symlinked onto `PATH`, so nothing ever mined
|
||||
pi's transcripts — the palace only ever contained what an agent remembered to
|
||||
file by hand. A container that gets recreated regularly has no other memory, so
|
||||
a missed wind-down was a permanently lost session.
|
||||
|
||||
Also here: **pi 0.84.1 → 0.84.2 and pi-atelier v0.8.0 → v0.8.1**, bumped
|
||||
together. The pi bump closes the Amazon Bedrock tool-argument poison pill that
|
||||
v1.6.4 recorded as unfixed upstream; the atelier bump is the matching companion,
|
||||
since both sides changed fullscreen input handling in the same fortnight. Audits
|
||||
for both are below.
|
||||
|
||||
*Why event-driven and not a timer:* there is nothing schedulable inside the
|
||||
container — PID 1 is `bash -l`, with no systemd and no cron — and anything
|
||||
installed would not survive recreate anyway. The triggers therefore live where
|
||||
the events already are: pi's own lifecycle, plus container start.
|
||||
|
||||
### Added
|
||||
|
||||
- **`mempalace-pi-session` symlinked onto `PATH`** (`Dockerfile.base`,
|
||||
alongside its `mempalace-session` / `mempalace-docs` siblings, with the same
|
||||
`--help` build-time check). `entrypoint-user.sh` also self-heals the symlink
|
||||
into `~/.local/bin` (already ahead of `/usr/local/bin` on `PATH`, and
|
||||
writable by `developer`) so the feature works on images whose base predates
|
||||
this change.
|
||||
- **Container-start catch-up feed** (`entrypoint-user.sh`, backgrounded). pi's
|
||||
mempalace extension feeds the palace on `session_shutdown` and on a debounced
|
||||
`agent_settled`, but a hard kill (`docker kill`, OOM, host reboot) runs no
|
||||
handler at all; this is the only trigger that can recover the previous life's
|
||||
transcripts. Skipped when a remote palace is configured without an inbox to
|
||||
ship to — **and the skip now says so** (see Changed) — and skippable entirely
|
||||
with `MEMPALACE_FEED=0`.
|
||||
- **`MEMPALACE_PI_STAGE` no longer needs pinning here — the feeder's default
|
||||
was fixed upstream instead.** It used to stage under `~/.cache`, which is
|
||||
disposable in a container; the first cut of this change pinned the env var
|
||||
into the persisted `~/.pi` volume. That was the wrong fix: it created a second
|
||||
convention that could still diverge from the palace (keep the palace volume,
|
||||
drop `devbox-pi-config`, and a scoped `mempalace sync` prunes every
|
||||
conversation drawer, because dedup keys on the *staged* path). The feeder now
|
||||
defaults to `<palace-root>/pi-stage`, resolved with mempalace's own
|
||||
precedence (`$MEMPALACE_PALACE_PATH` → `$MEMPAL_PALACE_PATH` →
|
||||
`~/.mempalace/config.json` → `~/.mempalace/palace`), so the stage inherits
|
||||
whatever persistence the palace has and the two cannot be separated by
|
||||
accident. No `ENV` and no entrypoint export: adding one back would
|
||||
re-introduce exactly the split it removes.
|
||||
- **Transcript inbox mount in `docker-compose.mempalace.yml`**
|
||||
(`${MEMPALACE_FEED_DIR:-./feed}:/data/feed:ro`). A client cannot mine into a
|
||||
remote palace directly: `mempalace_mine` expands its source path in the
|
||||
*server* process, so the server can only see paths inside its own container.
|
||||
Clients rsync their staged exports to a per-device subdirectory and then ask
|
||||
the server to mine `/data/feed/<device>`. Read-only because mining only reads
|
||||
sources — all locks live palace-side.
|
||||
- **Smoke tests** for the above: `mempalace-pi-session` on `PATH`, two
|
||||
assertions that the stage resolves next to the palace (default, and following
|
||||
`$MEMPALACE_PALACE_PATH`), and two behavioural guards that feed the exporter a
|
||||
synthetic pi session — one that must be captured, one abandoned session that
|
||||
must not be. The second matters because pi expands skills/context into the
|
||||
user prompt, so an abandoned session can look substantial by byte count while
|
||||
containing no assistant output; and if pi's JSONL shape ever changes, the
|
||||
exporter would silently capture nothing.
|
||||
- **`.env.example`**: documents `MEMPALACE_FEED`,
|
||||
`MEMPALACE_FEED_DEBOUNCE_MS`, `MEMPALACE_FEED_WING`, and the remote-palace
|
||||
shipping vars `MEMPALACE_PI_SSH_TARGET`, `MEMPALACE_PI_REMOTE_PATH`,
|
||||
`MEMPALACE_PI_DEVICE`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **The "remote palace, no inbox" skip announces itself instead of vanishing**
|
||||
(`entrypoint-user.sh`). When `MEMPALACE_REMOTE_URL` is set but
|
||||
`MEMPALACE_PI_SSH_TARGET` is not, there is genuinely nothing the feeder can
|
||||
ship to, so skipping is correct — but the branch was a bare `:`, and the skip
|
||||
happens *before* the subshell that writes `~/.pi/agent/mempalace-catchup.log`.
|
||||
A container in that state therefore contributed nothing to the palace and left
|
||||
**no artifact at all**, not even an empty log, to explain why — indistinguish-
|
||||
able from a healthy run that had nothing to file. Found while flipping the
|
||||
first client onto the shared palace (2026-08-12), where it is the single most
|
||||
likely way to end up quietly memory-less. The notice now goes to both the
|
||||
container start output and that log path, names the two variables that fix it,
|
||||
states that MCP tools still work (only *this* container's transcripts go
|
||||
nowhere), and points at `MEMPALACE_FEED=0` for anyone who meant it.
|
||||
Deliberately incapable of breaking startup: an unwritable `~/.pi` — root-owned
|
||||
volume, a classic Docker accident — would make `mkdir -p` fail under `set -e`
|
||||
and abort the whole entrypoint, so it degrades to stdout-only. That was a real
|
||||
new risk, since this branch previously touched no filesystem whatsoever.
|
||||
Covered by two smoke assertions against the entrypoint as shipped in the image
|
||||
(the branch only runs at container start, so a `docker run` one-shot cannot
|
||||
reach it).
|
||||
|
||||
### Bumped: pi 0.84.1 → 0.84.2
|
||||
|
||||
- **`ARG PI_VERSION=0.84.2`** (`Dockerfile.variant`), with the audit the pin
|
||||
policy in that file requires.
|
||||
|
||||
**Headline for this image: the Bedrock tool-argument poison pill is FIXED
|
||||
upstream.** The v1.6.4 entry below recorded it as *"Not fixed upstream … still
|
||||
replayed unsanitised"* — that note is now superseded. pi-ai 0.84.2 adds a
|
||||
recursive `sanitizeBedrockDocument()` and applies it at exactly the site that
|
||||
entry named ([#7882](https://github.com/earendil-works/pi/pull/7882)):
|
||||
|
||||
```diff
|
||||
- toolUse: { toolUseId: c.id, name: c.name, input: c.arguments },
|
||||
+ toolUse: { toolUseId: c.id, name: c.name, input: sanitizeBedrockDocument(c.arguments) },
|
||||
```
|
||||
|
||||
(`dist/api/bedrock-converse-stream.js` — line 692 in pi-ai 0.84.1, 704 in
|
||||
0.84.2; it was 644 in 0.83.0 and 634 in 0.82.1.) The sanitiser drops object
|
||||
members whose key is the empty string, recursing through arrays and nested
|
||||
objects and preserving every valid value. It runs while the request is built,
|
||||
so it covers the live turn *and* a resume: a session already bricked by an
|
||||
empty-key tool argument now replays instead of dying on a Bedrock
|
||||
`ValidationException`. **`pi-session-repair` (in `cli_utils`) is therefore no
|
||||
longer the recovery path on this image.** It stays useful for older images and
|
||||
for inspecting a transcript, because the stored `.jsonl` is still malformed —
|
||||
the fix sanitises what is *sent*, not what was *recorded*.
|
||||
|
||||
**Why bumping `PI_VERSION` is the only way to get it:** pi publishes an
|
||||
`npm-shrinkwrap.json`, which pins transitive dependencies *exactly*. pi
|
||||
0.84.1's shrinkwrap pins `@earendil-works/pi-ai` to **0.84.1**, so although
|
||||
0.84.1's `package.json` range is `^0.84.1` — which would otherwise admit
|
||||
0.84.2 — rebuilding the old pin can never pick the fix up. Transitive upstream
|
||||
fixes do not leak into this image; `PI_VERSION` is the whole gate.
|
||||
|
||||
Rest of the audit, against the integration surface the pin policy names:
|
||||
|
||||
- **Session `.jsonl` format — unchanged.** Identical
|
||||
`migrateV1ToV2`/`migrateV2ToV3` ladder in both versions, so existing sessions
|
||||
on the named volume load as-is and `pi-session-repair`'s parse target is
|
||||
untouched.
|
||||
- **Node engine floor — unchanged** at `>=22.19.0` (image ships 22.23.2).
|
||||
- **pi-atelier — no change needed.** The pin stays `v0.8.0`: the hard floor is
|
||||
"never pair < 0.7.1 with pi >= 0.84", this bump does not leave 0.84.x, and
|
||||
atelier's `peerDependencies` (`>=0.80.7`) are satisfied. pi-atelier **0.8.1**
|
||||
is published but deliberately NOT adopted here — one variable at a time, and
|
||||
atelier is the component that has drawn blood at startup.
|
||||
- **Directly relevant to `pi --ssh` use of this image:** 0.84.2 fixes split
|
||||
`Alt+Enter` over SSH being misread as Escape, and adds `PI_TUI_ESC_TIMEOUT`
|
||||
for high-latency terminals.
|
||||
- **Keybindings — one surface worth knowing.** `pi-toolkit` ships exactly one
|
||||
override, `tui.input.newLine: [shift+enter, ctrl+j, alt+j]`. 0.84.2's new
|
||||
fullscreen transcript search (`Ctrl+Shift+F`) binds `Shift+Enter` to
|
||||
*previous match* while its overlay is focused. Different context, so no
|
||||
conflict is expected — but it is the one place the override meets a new
|
||||
default, and the first place to look if "shift+enter stopped inserting a
|
||||
newline" is ever reported.
|
||||
- **New `defaultTools` setting** (choose startup built-in tools globally or per
|
||||
project) is additive; `pi-toolkit`'s `settings.example.json` does not set it,
|
||||
so the bootstrap template needs no change.
|
||||
|
||||
### Bumped: pi-atelier v0.8.0 → v0.8.1
|
||||
|
||||
- **`ARG PI_ATELIER_REF` / `ARG PI_ATELIER_VERSION` = `v0.8.1`**
|
||||
(`Dockerfile.variant`), bumped *together* with `PI_VERSION` as that pin's
|
||||
comment requires — and this pairing is a good advert for the rule, because both
|
||||
sides touched fullscreen input handling within three days of each other.
|
||||
|
||||
atelier 0.8.1 (2026-08-12) is two changes, only one of them code: *"Preserve
|
||||
fullscreen transcript mouse-wheel scrolling after Sidebar resize and visibility
|
||||
changes by leaving Pi's persistent mouse reporting enabled"*, plus a README
|
||||
simplification. The single source file that differs from 0.8.0 is
|
||||
`src/split-pane.ts`. It extracts an `isPiFullscreenRenderer()` predicate and,
|
||||
under pi's fullscreen renderer, stops writing its own
|
||||
`\e[?1002h\e[?1006h` / `\e[?1006l\e[?1002l` pair around a sidebar resize —
|
||||
previously it enabled mouse reporting on grab and disabled it on release, which
|
||||
tore down the reporting **pi itself** had switched on and left the wheel dead
|
||||
afterwards. Outside fullscreen it manages mouse mode exactly as before. It also
|
||||
now captures the terminal it enabled mouse on and writes the disable sequence to
|
||||
*that* terminal instead of to whatever `tui` currently points at.
|
||||
|
||||
**The audit that matters is the private-internals coupling**, since that is what
|
||||
hung startup at 0.6.0/0.7.0. atelier reaches into three pi internals; all three
|
||||
are unchanged in pi 0.84.2:
|
||||
|
||||
- **`TuiAltScreen`** — detected *by constructor name*, so a rename would
|
||||
silently disable both the resize-input prioritisation and the new mouse
|
||||
behaviour, with no error. Still
|
||||
`class TuiAltScreen extends TuiBase implements ViewportTUI`.
|
||||
- **`tui.inputListeners`** — a private `Set` that atelier deletes from and
|
||||
re-adds to, to get its resize handler ahead of pi's viewport listener (which
|
||||
"consumes every mouse event for text selection"). Still `inputListeners = new
|
||||
Set()`, at the identical line 103 of `pi-tui/dist/tui.js` in both versions,
|
||||
and still a `Set` — atelier guards with `instanceof Set`.
|
||||
- **the prototype `render` descriptor** it wraps via `findPrototypeRender`.
|
||||
Still an own `render(width)` on `TuiAltScreen`.
|
||||
|
||||
pi's mouse sequences are byte-identical between 0.84.1 and 0.84.2 (same
|
||||
`1002h`/`1006h`/`1002l`/`1006l`/`1003h` occurrence counts), so atelier's
|
||||
assumption about what pi leaves enabled still holds. `pi-tui`'s base class
|
||||
changed additively only (one new `isOverlayFocused()`), and `TuiAltScreen`'s own
|
||||
changes are the new search feature (`activeSearch`, `openSearch`/`closeSearch`,
|
||||
the two search match styles, `copySelection`).
|
||||
|
||||
**Caveat, stated plainly:** pi 0.84.2 adds a focused fullscreen *search overlay*
|
||||
that participates in input handling, while atelier reorders input listeners
|
||||
around pi's viewport listener. The two look convergent — 0.84.2 separately fixes
|
||||
*"focused fullscreen overlays not receiving mouse wheel or viewport scroll
|
||||
keys"* — but this pairing is reasoned from the diffs, **not proven by
|
||||
execution**: the CI smoke test does not drive the TUI, so a fullscreen
|
||||
interaction regression would not be caught before pull. Worth an `alt+a` plus a
|
||||
sidebar resize and a wheel scroll in fullscreen on first use of this image.
|
||||
|
||||
Version metadata is unchanged: `engines.node >=22.19.0`, `peerDependencies`
|
||||
still the uninformative `>=0.80.7` on both pi packages (so still nothing in npm
|
||||
metadata encodes the real floor), and still zero runtime dependencies — so the
|
||||
"no `npm install` step" note above stays true. The GitHub tag `v0.8.1` exists
|
||||
(commit `c31d7439`), which is what CI resolves to a SHA.
|
||||
|
||||
### Notes
|
||||
|
||||
- The `Dockerfile.base` change moves the base hash, so this needs a base
|
||||
rebuild; the `~/.local/bin` self-heal exists so the feature does not have to
|
||||
wait for one. The skip-notice change is in `entrypoint-user.sh`, which is
|
||||
`COPY`d in `Dockerfile.base` too, so it rides the same rebuild — until then,
|
||||
older images keep skipping silently and the two commands in the toolkit's
|
||||
`phase-1-exposure-runbook.md` §3.7 are the way to tell.
|
||||
- Requires the matching `mempalace-toolkit` change (`--prepare` two-phase
|
||||
split, remote transport, and the auto-feed triggers in
|
||||
`extensions/pi/mempalace.ts`). The split exists because the palace is
|
||||
single-writer: a live pi session holds it through the extension's own
|
||||
`mempalace-mcp`, so a CLI `mempalace mine` during a session fails with
|
||||
"palace ... is held by PID". Staging is therefore done by the CLI and the
|
||||
mine itself by whichever process already holds the palace.
|
||||
|
||||
---
|
||||
|
||||
## v1.7.0 — 2026-08-07
|
||||
|
||||
Minor release. Headline: **pi-atelier is now part of the image** — the TUI
|
||||
|
||||
+8
-1
@@ -65,12 +65,19 @@ The entrypoint deploys/registers all of these on first container start. Re-runni
|
||||
### Document and image tooling
|
||||
|
||||
- **pandoc** — universal Markdown↔HTML/Org/RST/etc. conversion. Useful well beyond pi: agent-driven doc exports, format conversion, etc.
|
||||
- **Typst** — markup-based typesetting, used as pandoc's `--pdf-engine`
|
||||
- **graphviz** (`dot`) — diagram rendering pipelines
|
||||
- **imagemagick** (`magick`) — image conversion / resizing
|
||||
|
||||
### Browser automation
|
||||
|
||||
- **agent-browser** — CLI for driving a real browser (open pages, click/fill/`eval`, snapshot the DOM, screenshots) so agents can verify front-end work instead of guessing
|
||||
- **Playwright** + a headless **Chromium** are pre-installed and pinned together; `AGENT_BROWSER_EXECUTABLE_PATH` is preset to the baked browser, so `agent-browser open <url>` works out of the box with no setup
|
||||
- **socat** — TCP bridge used to expose the pi-studio server outside the container's loopback
|
||||
|
||||
### Modern CLI tooling
|
||||
|
||||
- **Editor**: neovim (LazyVim defaults), tmux (configured for 0-indexed sessions)
|
||||
- **Editor**: neovim (system-wide `termguicolors` default; bring your own config/plugins), tmux (configured for 0-indexed sessions)
|
||||
- **Search/nav**: ripgrep, fd, fzf, zoxide
|
||||
- **Display**: bat, eza, htop, tree
|
||||
- **Data**: jq, yq
|
||||
|
||||
+59
-2
@@ -384,7 +384,56 @@ ARG INSTALL_MEMPALACE=true
|
||||
# mempalace_checkpoint (#2023/#2034).
|
||||
#
|
||||
# Keep in lockstep with opencode-devbox when bumping.
|
||||
ARG MEMPALACE_VERSION=3.6.0
|
||||
#
|
||||
# 3.7.1 (from 3.6.0) is safe for anyone with an EXISTING LOCAL palace: verified
|
||||
# against the 3.7.1 source, not the changelog. Legacy drawers lack the new
|
||||
# `chunk_total` marker and both decision sites trust them ("trust the match as
|
||||
# before"), NORMALIZE_VERSION is 2 in both, chromadb stays <2 (no index-format
|
||||
# migration), there is no auto-migration ("We do NOT auto-migrate"), and the one
|
||||
# new palace file (logstream.sqlite3) is created lazily on first logstream use.
|
||||
# Two behaviour changes to know: MEMPALACE_MCP_ALLOW_PEER_WRITER no longer works
|
||||
# on local/chroma palaces, and writer-lock setup failures now fail CLOSED
|
||||
# (refuse the write) rather than fail open. Neither affects the container's
|
||||
# normal MCP-server-plus-CLI-feeder pattern, which already serialised on the
|
||||
# same lock under 3.6.0.
|
||||
#
|
||||
# 3.8.0 (2026-08-23, PyPI, released hours after this project's own v1.8.5 tag
|
||||
# the same day) is additive/reliability only — reviewed for MCP tool-schema
|
||||
# changes before bumping, as always: there are NONE. Two PRs matter:
|
||||
# - PR #2320/#2322: `sync --apply` no longer deletes a drawer solely because
|
||||
# its source_file was unreachable AT THAT MOMENT — it now asks for
|
||||
# corroboration first. This fixes losing a whole mined project to one
|
||||
# `sync --apply` while its volume happened to be unmounted.
|
||||
# IMPORTANT — do not over-read this fix: it addresses TRANSIENT
|
||||
# unreachability, not the standing landmine (documented in the operator's
|
||||
# global AGENTS.md) against running `mempalace_sync` / `mempalace_delete_by_source`
|
||||
# beyond dry-run on the SHARED central palace. On that palace most
|
||||
# source_file paths are PERMANENTLY absent from whichever host runs the
|
||||
# sync — a different machine's paths simply do not exist here, ever, not
|
||||
# merely "right now". That is a different failure shape than #2320/#2322
|
||||
# fixes. The landmine still stands; this bump does not relax it.
|
||||
# - PR #2307: long-running Chroma servers no longer invalidate their own
|
||||
# HNSW cache on their own writes (server-side perf fix). This does NOT
|
||||
# make `mempalace_reconnect` unnecessary — that tool exists for EXTERNAL
|
||||
# writes bypassing the in-process client (e.g. direct sqlite backfills,
|
||||
# CLI commands against a running server), a different scenario #2307
|
||||
# does not touch.
|
||||
#
|
||||
# Known gap, carried forward (flagged in prior release notes, not fixed here):
|
||||
# unlike PI_VERSION, which CI's resolve-versions job verifies is published and
|
||||
# warns — never silently adopts — on npm drift, MEMPALACE_VERSION has NO
|
||||
# equivalent CI-side audit (confirmed: zero references to MEMPALACE_VERSION in
|
||||
# .gitea/workflows/docker-publish.yml). This is a literal Dockerfile string
|
||||
# with no automated freshness or publish check.
|
||||
#
|
||||
# Deployment sequencing note for whoever ships this bump: synlig (the shared
|
||||
# central palace host) currently serves mempalace 3.7.1 SERVER-SIDE via
|
||||
# docker-compose.mempalace.yml, which reuses this same devbox image. Bumping
|
||||
# this ARG changes only the CLIENT version baked into pi-devbox images: it
|
||||
# introduces client/server skew until synlig's compose stack is separately
|
||||
# rebuilt/redeployed with the new pin. Not something to code around here —
|
||||
# just sequence the redeploy.
|
||||
ARG MEMPALACE_VERSION=3.8.0
|
||||
ENV UV_TOOL_DIR=/opt/uv-tools
|
||||
ENV UV_TOOL_BIN_DIR=/usr/local/bin
|
||||
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
||||
@@ -424,9 +473,15 @@ RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" =
|
||||
[ "$ok" = "1" ] && \
|
||||
ln -sf /opt/mempalace-toolkit/bin/mempalace-session /usr/local/bin/mempalace-session && \
|
||||
ln -sf /opt/mempalace-toolkit/bin/mempalace-docs /usr/local/bin/mempalace-docs && \
|
||||
chmod +x /opt/mempalace-toolkit/bin/mempalace-session /opt/mempalace-toolkit/bin/mempalace-docs && \
|
||||
ln -sf /opt/mempalace-toolkit/bin/mempalace-pi-session /usr/local/bin/mempalace-pi-session && \
|
||||
ln -sf /opt/mempalace-toolkit/bin/mempalace-census /usr/local/bin/mempalace-census && \
|
||||
chmod +x /opt/mempalace-toolkit/bin/mempalace-session /opt/mempalace-toolkit/bin/mempalace-docs \
|
||||
/opt/mempalace-toolkit/bin/mempalace-pi-session \
|
||||
/opt/mempalace-toolkit/bin/mempalace-census && \
|
||||
mempalace-session --help >/dev/null && \
|
||||
mempalace-docs --help >/dev/null && \
|
||||
mempalace-pi-session --help >/dev/null && \
|
||||
mempalace-census --help >/dev/null && \
|
||||
echo "mempalace-toolkit installed at $(cd /opt/mempalace-toolkit && git rev-parse --short HEAD)" ; \
|
||||
fi
|
||||
|
||||
@@ -668,12 +723,14 @@ COPY rootfs/usr/local/share/pi-devbox/ /usr/local/share/pi-devbox/
|
||||
COPY rootfs/usr/local/bin/studio-expose /usr/local/bin/studio-expose
|
||||
COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch
|
||||
COPY rootfs/usr/local/bin/pi-devbox-version /usr/local/bin/pi-devbox-version
|
||||
COPY rootfs/usr/local/bin/devbox-skill-reconcile /usr/local/bin/devbox-skill-reconcile
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
||||
/usr/local/bin/studio-expose \
|
||||
/usr/local/bin/dot-watch \
|
||||
/usr/local/bin/pi-devbox-version \
|
||||
/usr/local/bin/devbox-skill-reconcile \
|
||||
/usr/local/lib/pi-devbox/*.sh 2>/dev/null || true
|
||||
|
||||
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
||||
|
||||
+35
-3
@@ -56,7 +56,22 @@ ARG USER_NAME=developer
|
||||
# current when it was first populated (shipped the same bytes for pi-devbox
|
||||
# v0.74.0..v0.75.5; discovered + fixed in v0.75.5b, 2026-05-23). The `latest`
|
||||
# branch below is kept only for a deliberate local `docker build` override.
|
||||
ARG PI_VERSION=0.84.1
|
||||
#
|
||||
# AUDITED AT 0.84.3 (2026-08-25, was 0.84.2): upstream's notes carry a
|
||||
# "Breaking Changes" heading — `GoogleThinkingLevel` renamed to
|
||||
# `GoogleApiThinkingLevel`. INERT FOR THIS IMAGE: all four vendored companions
|
||||
# (/opt/pi-fork, /opt/pi-observational-memory, /opt/pi-atelier, /opt/pi-studio)
|
||||
# were grepped for that symbol and reference it ZERO times, so nothing here
|
||||
# couples to the renamed type. Recorded because the heading will look alarming
|
||||
# to the next reader doing step 1 above — the audit is done, don't redo it.
|
||||
# Adopted for two fixes that land squarely on this repo's own vendored-skill
|
||||
# wiring (see devbox-skill-reconcile, v1.8.5): nested Markdown skills inside
|
||||
# `.agents/skills/<group>/` directories were not discovered, and root Markdown
|
||||
# files such as README.md / AGENTS.md inside a skill dir were reported as
|
||||
# broken skills unless they declared valid skill frontmatter.
|
||||
# pi-atelier needs no companion bump: v0.8.2 clears the >=0.7.1 floor that
|
||||
# pi >= 0.84 requires (see PI_ATELIER_REF below).
|
||||
ARG PI_VERSION=0.84.3
|
||||
ARG PI_TOOLKIT_REF=main
|
||||
ARG PI_EXTENSIONS_REF=main
|
||||
# Repo URLs default to the canonical gitea origin but are overridable so a
|
||||
@@ -92,9 +107,9 @@ ARG PI_OBSMEM_REF=master
|
||||
# the /opt checkout. Adding an install here would be a no-op that only costs
|
||||
# build time.
|
||||
ARG PI_ATELIER_REPO=https://github.com/michaelmjhhhh/pi-atelier.git
|
||||
ARG PI_ATELIER_REF=v0.8.0
|
||||
ARG PI_ATELIER_REF=v0.8.2
|
||||
# Human-readable tag PI_ATELIER_REF was resolved from; recorded as a label.
|
||||
ARG PI_ATELIER_VERSION=v0.8.0
|
||||
ARG PI_ATELIER_VERSION=v0.8.2
|
||||
|
||||
RUN set -e && \
|
||||
# git_fetch_ref: clone-equivalent helper that accepts EITHER a branch name
|
||||
@@ -297,6 +312,19 @@ RUN set -e; \
|
||||
mkdir -p /etc/pi-devbox; \
|
||||
rev() { git -C "$1" rev-parse HEAD 2>/dev/null || echo "unknown"; }; \
|
||||
PI_V="$(pi --version 2>/dev/null | head -n1 | tr -d '\r\n')"; \
|
||||
# mempalace CORE (the PyPI package behind the MCP tools) is installed in
|
||||
# Dockerfile.base via `uv tool install`, so no /opt clone reveals it and
|
||||
# until v1.8.6 the manifest could not answer "which palace shipped here?" —
|
||||
# a palace bug could not be correlated to an image, which is precisely the
|
||||
# correlation this file exists to provide. Read from the INSTALLED BINARY,
|
||||
# not from ARG MEMPALACE_VERSION, per the ground-truth rule above: that is
|
||||
# what catches an install which resolved to something other than the pin.
|
||||
# `mempalace --version` prints "MemPalace 3.7.1" — NAME-PREFIXED, unlike
|
||||
# pi's bare "0.84.2" — hence the $NF pick rather than a straight read. The
|
||||
# leading-digit test then rejects usage/error text (a renamed flag prints a
|
||||
# usage block) and degrades to JSON null, so this can never fail the build.
|
||||
MP_V="$(mempalace --version 2>/dev/null | head -n1 | tr -d '\r' | awk '{print $NF}')"; \
|
||||
case "$MP_V" in [0-9]*) MP_CORE="\"${MP_V}\"" ;; *) MP_CORE='null' ;; esac; \
|
||||
STUDIO_REV='null'; \
|
||||
if [ -d /opt/pi-studio/.git ]; then STUDIO_REV="\"$(rev /opt/pi-studio)\""; fi; \
|
||||
{ \
|
||||
@@ -305,6 +333,10 @@ RUN set -e; \
|
||||
echo " \"build_date\": \"${BUILD_DATE}\","; \
|
||||
echo " \"source_revision\": \"${SOURCE_REVISION}\","; \
|
||||
echo " \"pi_version\": \"${PI_V}\","; \
|
||||
# Sibling of pi_version, NOT a member of components{}: that map holds git
|
||||
# SHAs and `pi-devbox-version` renders it with .value[0:12], which would
|
||||
# silently truncate a longer version string.
|
||||
echo " \"mempalace_version\": ${MP_CORE},"; \
|
||||
echo " \"components\": {"; \
|
||||
echo " \"pi-toolkit\": \"$(rev /opt/pi-toolkit)\","; \
|
||||
echo " \"pi-extensions\": \"$(rev /opt/pi-extensions)\","; \
|
||||
|
||||
@@ -70,9 +70,27 @@ so `TERM=xterm-kitty` is understood. Override either in your own
|
||||
### Document and image tooling
|
||||
|
||||
- `pandoc` — universal Markdown↔HTML/Org/RST/etc. converter
|
||||
- `typst` — markup-based typesetting, wired up as pandoc's `--pdf-engine` (see
|
||||
[Generating a PDF with pandoc + typst](#generating-a-pdf-with-pandoc--typst))
|
||||
- `graphviz` — `dot` rendering for diagram pipelines
|
||||
- `imagemagick` — image conversion / resizing (invoked as `magick`)
|
||||
|
||||
### Browser automation
|
||||
|
||||
- `agent-browser` — CLI for driving a real headless browser: open pages,
|
||||
click/fill/`eval`, snapshot the DOM, take screenshots. Useful whenever a task
|
||||
involves a web UI or verifying how a page actually renders (live DOM, WebGL,
|
||||
layout, popup positioning) instead of guessing from source.
|
||||
- `playwright` + a pre-installed headless **Chromium** back it.
|
||||
`AGENT_BROWSER_EXECUTABLE_PATH` is preset to the baked browser via a stable
|
||||
`/usr/local/bin/agent-chrome` symlink (insulated from Playwright's
|
||||
per-version/arch install directory), so `agent-browser open <url>` works
|
||||
out of the box with no setup. Run `agent-browser skills get core --full`
|
||||
for the command set and workflow patterns.
|
||||
- `socat` — TCP bridge used by `studio-expose` to reach pi-studio's
|
||||
loopback-bound server from outside the container (see
|
||||
[Using pi-studio](#using-pi-studio--studio-variant))
|
||||
|
||||
### Language toolchains
|
||||
|
||||
- `python3` + `python3-venv` + `python3-pip` (system Python)
|
||||
@@ -547,7 +565,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 +577,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 +595,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
|
||||
@@ -774,8 +805,9 @@ docker inspect --format '{{json .Config.Labels}}' joakimp/pi-devbox:latest | jq
|
||||
`org.opencontainers.image.{version,revision,created}` plus
|
||||
`se.jordbo.pi-devbox.*-ref` record the intended pi version and companion
|
||||
refs. The on-disk `/etc/pi-devbox/build-manifest.json` records **ground
|
||||
truth** — the actual checked-out commit of each `/opt` clone and the live
|
||||
`pi --version` — so a tag is reconstructable after CI logs rotate:
|
||||
truth** — the actual checked-out commit of each `/opt` clone, the live
|
||||
`pi --version`, and (from v1.8.6) the live `mempalace --version` of the
|
||||
installed palace core — so a tag is reconstructable after CI logs rotate:
|
||||
|
||||
```bash
|
||||
docker run --rm --entrypoint= joakimp/pi-devbox:latest cat /etc/pi-devbox/build-manifest.json
|
||||
@@ -882,12 +914,31 @@ cat ~/.ssh-local/mypeer_devbox_ed25519.pub
|
||||
# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEXAMPLE0000EXAMPLE0000EXAMPLE0000ex devbox-0d11ec7731c7
|
||||
```
|
||||
|
||||
**2. On the peer** — authorize it narrowly rather than bare:
|
||||
**2. On the peer** — append that public key to `~/.ssh/authorized_keys` **of
|
||||
the account you will log in as** (the `User` from step 3), narrowly authorized
|
||||
rather than bare:
|
||||
|
||||
```
|
||||
```bash
|
||||
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||
cat >> ~/.ssh/authorized_keys <<'KEY'
|
||||
from="192.168.1.0/24,192.168.4.0/24,10.8.0.7",restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEXAMPLE0000EXAMPLE0000EXAMPLE0000ex devbox-mymachine
|
||||
KEY
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
```
|
||||
|
||||
Both lines are safe on a peer that is already set up: `mkdir -p` is a no-op
|
||||
when the directory exists, the `chmod`s only tighten, and appending never
|
||||
touches keys already listed. Use `>>`, never `>` — one stray truncation
|
||||
revokes every other key on that account. The options prefix must sit on the
|
||||
**same physical line** as the key, comma-separated with no spaces: a paste
|
||||
that wrapped is the likeliest reason a key that looks right is refused.
|
||||
`ssh-copy-id` cannot add that prefix, so append by hand (or let it copy the
|
||||
bare key and edit the line afterwards). If authentication still fails with no
|
||||
clear reason, suspect permissions — sshd's `StrictModes` silently ignores
|
||||
`authorized_keys` when the home directory, `~/.ssh` or the file itself is
|
||||
group- or world-writable, and says why only in the peer's own log
|
||||
(`journalctl -u ssh`, `/var/log/auth.log`).
|
||||
|
||||
`restrict` disables pty, agent/X11 and port forwarding; append
|
||||
`port-forwarding` and `permitopen="127.0.0.1:<port>"` after it if you need one
|
||||
specific tunnel. `from=` must list the **host's** addresses, not the
|
||||
@@ -970,9 +1021,9 @@ resolved to `latest` at build time:
|
||||
|
||||
| Component | Pin | Where |
|
||||
|---|---|---|
|
||||
| pi | `0.84.1` | `ARG PI_VERSION` — `Dockerfile.variant` |
|
||||
| pi-atelier | `v0.8.0` | `ARG PI_ATELIER_REF` — `Dockerfile.variant` |
|
||||
| mempalace | `3.6.0` | `ARG MEMPALACE_VERSION` — `Dockerfile.base` |
|
||||
| pi | `0.84.2` | `ARG PI_VERSION` — `Dockerfile.variant` |
|
||||
| pi-atelier | `v0.8.2` | `ARG PI_ATELIER_REF` — `Dockerfile.variant` |
|
||||
| mempalace | `3.7.1` | `ARG MEMPALACE_VERSION` — `Dockerfile.base` |
|
||||
|
||||
The objective is **not** to freeze versions. Bumping is routine — usually one
|
||||
line plus a changelog note. The objective is that adopting a new upstream
|
||||
|
||||
@@ -18,8 +18,23 @@ for OS packages, the per-package copyright files inside the image at
|
||||
| pi-fork | github.com/elpapi42/pi-fork | MIT |
|
||||
| pi-observational-memory | github.com/elpapi42/pi-observational-memory | MIT |
|
||||
| pi-studio *(`-studio` variant only)* | github.com/omaclaren/pi-studio | MIT |
|
||||
| pi-atelier | github.com/michaelmjhhhh/pi-atelier | MIT |
|
||||
| pi-toolkit, pi-extensions, mempalace-toolkit | authored by the maintainer (Joakim Persson) | MIT |
|
||||
|
||||
## MemPalace (AI memory)
|
||||
|
||||
| Component | Upstream | License |
|
||||
| --- | --- | --- |
|
||||
| mempalace (core, MCP server) | github.com/MemPalace/mempalace (PyPI: `mempalace`) | MIT — the GitHub repo declares MIT; the PyPI package's own metadata omits a license classifier, so if you need clearance from the package artifact alone, verify against the repo's `LICENSE` file rather than the sdist/wheel metadata |
|
||||
|
||||
## Browser automation
|
||||
|
||||
| Component | Upstream | License |
|
||||
| --- | --- | --- |
|
||||
| agent-browser | github.com/vercel-labs/agent-browser (npm: `agent-browser`) | Apache-2.0 |
|
||||
| Playwright | github.com/microsoft/playwright (npm: `playwright`) | Apache-2.0 |
|
||||
| Chromium | chromium.googlesource.com/chromium/src | BSD-3-Clause for Chromium's own code, plus a large set of bundled third-party components each under their own license (see Chromium's own `LICENSE`/`about:credits`). The binary in this image is **not compiled here** — it is the build Playwright downloads for its pinned version ("Chrome for Testing"), installed via `playwright install --with-deps chromium` at `/usr/local/share/ms-playwright/`. Treat Playwright's own distribution terms for that build as authoritative over any summary here. |
|
||||
|
||||
## Tooling baked into the base image
|
||||
|
||||
| Component | Upstream | License (best effort) |
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# Point every client at it by setting, in that client's .env:
|
||||
#
|
||||
# MEMPALACE_REMOTE_URL=http://<reachable-host>:8765/mcp
|
||||
# MEMPALACE_REMOTE_TOKEN=<the shared bearer token>
|
||||
#
|
||||
# (see .env.example). When set, the client connects over HTTP and does NOT
|
||||
# spawn its own local mempalace-mcp.
|
||||
@@ -18,12 +19,21 @@
|
||||
# (both are pinned by the same image build). Override with a slimmer image via
|
||||
# MEMPALACE_SERVER_IMAGE if you prefer (it must provide `mempalace-mcp`).
|
||||
#
|
||||
# ⚠ SECURITY: mempalace-mcp's HTTP transport has NO authentication of its own.
|
||||
# Do NOT expose port 8765 to an untrusted network. The default below binds to
|
||||
# 127.0.0.1 (host loopback) only. To let sibling containers reach it, either
|
||||
# attach them to the shared `mempalace-net` network (container-to-container, no
|
||||
# host port needed — use http://mempalace-server:8765/mcp), or front it with a
|
||||
# reverse proxy that enforces MEMPALACE_REMOTE_TOKEN as `Authorization: Bearer`.
|
||||
# ⚠ SECURITY: the HTTP transport IS authenticated as of mempalace 3.6.0 — an
|
||||
# earlier version of this comment said otherwise and was wrong. The server
|
||||
# compares `Authorization: Bearer <token>` with hmac.compare_digest and
|
||||
# **refuses to start on a non-loopback bind without a token**, so
|
||||
# MEMPALACE_REMOTE_TOKEN below is required, not optional: without it this
|
||||
# service crash-loops. It also pins `Host` and allowlists `Origin`.
|
||||
#
|
||||
# Still do not publish port 8765 to an untrusted network. The default binds to
|
||||
# 127.0.0.1 (host loopback) only. To let sibling containers reach it, attach
|
||||
# them to the shared `mempalace-net` network (container-to-container, no host
|
||||
# port needed — use http://mempalace-server:8765/mcp). To reach it from
|
||||
# elsewhere, terminate TLS in a tunnel/reverse proxy and let the bearer token be
|
||||
# the authentication — do NOT add browser-shaped auth (SSO/PIN/password) in
|
||||
# front, because every MCP client here is a headless JSON-RPC POST and would
|
||||
# receive a login page where JSON should be.
|
||||
|
||||
name: mempalace-server
|
||||
|
||||
@@ -40,6 +50,11 @@ services:
|
||||
user: "0:0"
|
||||
environment:
|
||||
- HOME=/data
|
||||
# Required: mempalace refuses a non-loopback bind without a token (it
|
||||
# would exit at startup and, with restart:unless-stopped, crash-loop).
|
||||
# `:?` fails fast at `docker compose up` with a readable message instead.
|
||||
# Clients send the same value as MEMPALACE_REMOTE_TOKEN.
|
||||
- MEMPALACE_MCP_HTTP_TOKEN=${MEMPALACE_REMOTE_TOKEN:?set MEMPALACE_REMOTE_TOKEN in .env — the shared palace requires a bearer token}
|
||||
command:
|
||||
- mempalace-mcp
|
||||
- --transport
|
||||
@@ -60,16 +75,28 @@ services:
|
||||
- mempalace-shared:/data/.mempalace
|
||||
# Embedding-model cache (~79 MB, disposable) so search does not re-download.
|
||||
- mempalace-shared-chroma:/data/.cache/chroma
|
||||
# Transcript inbox. Clients cannot mine into a remote palace directly:
|
||||
# `mempalace_mine` expands its source path in THIS process, so it can only
|
||||
# see paths inside this container. Each client rsyncs its staged session
|
||||
# exports to a per-device subdirectory on the host (see
|
||||
# MEMPALACE_PI_SSH_TARGET in .env.example) and then calls mempalace_mine
|
||||
# with the container-side path below (MEMPALACE_PI_REMOTE_PATH=/data/feed).
|
||||
# Read-only: mining only reads sources, and all locks live palace-side.
|
||||
- ${MEMPALACE_FEED_DIR:-./feed}:/data/feed:ro
|
||||
networks:
|
||||
- mempalace-net
|
||||
healthcheck:
|
||||
# A tools/list round-trip proves the server is answering MCP (python3 is
|
||||
# always present — mempalace itself is a python tool in the image).
|
||||
# GET /healthz, which is Host/Origin-gated but deliberately token-free —
|
||||
# so this probe needs no credentials. Do NOT go back to POSTing
|
||||
# `tools/list` here: that carries no Authorization header and now 401s,
|
||||
# marking a perfectly healthy server unhealthy forever. The Host pin is
|
||||
# only enforced on loopback *binds* (this one is 0.0.0.0), so a request to
|
||||
# 127.0.0.1 inside the container passes.
|
||||
test:
|
||||
- CMD
|
||||
- python3
|
||||
- -c
|
||||
- "import urllib.request,json; d=json.dumps({'jsonrpc':'2.0','id':1,'method':'tools/list','params':{}}).encode(); r=urllib.request.Request('http://127.0.0.1:8765/mcp',data=d,headers={'Content-Type':'application/json','Accept':'application/json'}); urllib.request.urlopen(r,timeout=5).read()"
|
||||
- "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8765/healthz',timeout=5).status==200 else 1)"
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
+104
-5
@@ -58,10 +58,17 @@ fi
|
||||
# the runtime skill-link assertion. Pointing at the image path (/usr/local/...)
|
||||
# keeps the skill fresh from the image and surviving volume recreate (unlike
|
||||
# anything baked under a home dir, which a named volume would shadow). Created
|
||||
# only when absent, so a same-named skillset skill (deployed later, at the end
|
||||
# of this script) or a user override is never clobbered; the skillset deploy
|
||||
# classifies these as foreign-links and its --prune-stale pass leaves them
|
||||
# alone (only dangling symlinks are pruned).
|
||||
# only when absent, so a user override is never clobbered.
|
||||
#
|
||||
# NB: "created only when absent" does NOT hand a same-named skillset skill
|
||||
# priority — the opposite. The skillset deploy runs at the end of this script
|
||||
# and classifies these links as foreign, so through v1.8.4 the BAKED copy
|
||||
# always won and an edit pushed to a skillset-owned skill was invisible until
|
||||
# the next image build. The links below are therefore the FALLBACK only;
|
||||
# devbox-skill-reconcile (invoked right after the skillset deploy) hands the
|
||||
# skillset-OWNED skills back to the live clone. Ownership is per-skill, listed
|
||||
# in skills/skillset-owned.txt — see VENDORED.md for why pi-extensions must
|
||||
# keep losing to the baked copy.
|
||||
DEVBOX_SKILLS_SRC=/usr/local/share/pi-devbox/skills
|
||||
if [ -d "$DEVBOX_SKILLS_SRC" ]; then
|
||||
mkdir -p "$HOME/.agents/skills"
|
||||
@@ -69,7 +76,16 @@ if [ -d "$DEVBOX_SKILLS_SRC" ]; then
|
||||
[ -d "$_sk" ] || continue
|
||||
_skname=$(basename "$_sk")
|
||||
if [ ! -e "$HOME/.agents/skills/$_skname" ]; then
|
||||
ln -s "${_sk%/}" "$HOME/.agents/skills/$_skname"
|
||||
# -sfn, not -s: `[ ! -e ]` is TRUE for a DANGLING symlink (-e follows the
|
||||
# link), and since v1.8.5 these links can point into /workspace/skillset
|
||||
# (see devbox-skill-reconcile, invoked after the skillset deploy). If that
|
||||
# mount vanishes while the writable layer survives — a `docker restart` or
|
||||
# a host reboot under restart: unless-stopped, as opposed to a recreate —
|
||||
# plain `ln -s` fails with "File exists" and, under `set -e`, aborts
|
||||
# container start before `exec "$@"`. With -f the broken link heals back to
|
||||
# the baked fallback, and the reconciler re-points it in the same boot if
|
||||
# the clone is back.
|
||||
ln -sfn "${_sk%/}" "$HOME/.agents/skills/$_skname"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
@@ -92,6 +108,82 @@ if command -v mempalace &>/dev/null && [ -d /workspace ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── MemPalace: pi transcript feeder ─────────────────────────────────
|
||||
# mempalace-toolkit ships `mempalace-pi-session`, which mines pi's own JSONL
|
||||
# session transcripts into the palace. pi's mempalace extension drives it on
|
||||
# session_shutdown and on a debounced agent_settled; this is the catch-up for
|
||||
# the one case no handler can cover — a hard kill (docker kill, OOM, host
|
||||
# reboot) runs nothing at all, so without this the previous life's transcripts
|
||||
# are never mined.
|
||||
#
|
||||
# No MEMPALACE_PI_STAGE override here on purpose: the feeder stages next to the
|
||||
# palace it feeds (<palace-root>/pi-stage), so the stage and the dedup keys
|
||||
# referencing it share one lifetime — whatever persistence the palace has, the
|
||||
# stage inherits. Pinning it elsewhere (e.g. into the ~/.pi volume) would
|
||||
# re-introduce the very split that design prevents: palace volume kept, stage
|
||||
# volume dropped, and `mempalace sync` then prunes every conversation drawer.
|
||||
#
|
||||
# Backgrounded: a cold mine can take tens of seconds and must never delay the
|
||||
# shell. Contention with a live session is handled by the tool itself (it exits
|
||||
# 0 and lets the palace holder do the mine).
|
||||
|
||||
# Self-heal onto PATH for images whose base predates the toolkit symlink.
|
||||
# ~/.local/bin is already ahead of /usr/local/bin on PATH (Dockerfile.base sets
|
||||
# it in ENV PATH) and is writable by this (non-root) user, unlike /usr/local/bin.
|
||||
if [ -x /opt/mempalace-toolkit/bin/mempalace-pi-session ] && \
|
||||
! command -v mempalace-pi-session >/dev/null 2>&1; then
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
ln -sf /opt/mempalace-toolkit/bin/mempalace-pi-session "$HOME/.local/bin/mempalace-pi-session"
|
||||
fi
|
||||
|
||||
# Resolve the feeder explicitly rather than trusting PATH: this runs before any
|
||||
# login shell, and a silently-skipped catch-up is exactly the failure we are
|
||||
# here to prevent.
|
||||
MEMPALACE_FEEDER=""
|
||||
if command -v mempalace-pi-session >/dev/null 2>&1; then
|
||||
MEMPALACE_FEEDER="mempalace-pi-session"
|
||||
elif [ -x /opt/mempalace-toolkit/bin/mempalace-pi-session ]; then
|
||||
MEMPALACE_FEEDER="/opt/mempalace-toolkit/bin/mempalace-pi-session"
|
||||
fi
|
||||
|
||||
if [ "${MEMPALACE_FEED:-1}" != "0" ] && [ -n "$MEMPALACE_FEEDER" ]; then
|
||||
if [ -n "${MEMPALACE_REMOTE_URL:-}" ] && [ -z "${MEMPALACE_PI_SSH_TARGET:-}" ]; then
|
||||
# Remote palace, but no inbox to ship transcripts to — the feeder genuinely
|
||||
# cannot do anything here, so skipping is right. Saying so is the point:
|
||||
# this branch used to be a bare `:`, and the skip happens *before* the
|
||||
# subshell below that writes mempalace-catchup.log, so a container in this
|
||||
# state contributed nothing to the palace and left no artifact at all — not
|
||||
# even an empty log — to explain why. That is indistinguishable from a
|
||||
# healthy run that simply had nothing to file. `tee` puts the notice both in
|
||||
# the container's start output (docker logs) and at the path anyone
|
||||
# debugging "why is nothing from this container in the palace?" looks first.
|
||||
# This is an entrypoint: a notice must never be able to stop a container
|
||||
# from starting. An unwritable ~/.pi (root-owned volume — a classic Docker
|
||||
# permission accident) makes `mkdir -p` fail, and under `set -e` that would
|
||||
# abort startup entirely: a brand-new failure mode in precisely the branch
|
||||
# that used to do nothing at all. Degrade to stdout-only instead.
|
||||
_mp_log="$HOME/.pi/agent/mempalace-catchup.log"
|
||||
mkdir -p "$HOME/.pi/agent" 2>/dev/null || _mp_log=/dev/null
|
||||
{
|
||||
echo "MemPalace catch-up skipped: remote palace with no transcript inbox."
|
||||
echo " MEMPALACE_REMOTE_URL is set (${MEMPALACE_REMOTE_URL})"
|
||||
echo " but MEMPALACE_PI_SSH_TARGET is not, so there is nowhere to ship this"
|
||||
echo " container's staged sessions. MCP tools still read and write the shared"
|
||||
echo " palace — but this container's own conversations are mined nowhere."
|
||||
echo " Fix: set MEMPALACE_PI_SSH_TARGET (and MEMPALACE_PI_DEVICE) in .env,"
|
||||
echo " or unset MEMPALACE_REMOTE_URL to keep the palace local."
|
||||
echo " Deliberate? MEMPALACE_FEED=0 turns the feed off and silences this."
|
||||
} | tee "$_mp_log" 2>/dev/null || true
|
||||
unset _mp_log
|
||||
else
|
||||
mkdir -p "$HOME/.pi/agent"
|
||||
(
|
||||
"$MEMPALACE_FEEDER" --reason container-start \
|
||||
>"$HOME/.pi/agent/mempalace-catchup.log" 2>&1 || true
|
||||
) &
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Git config defaults ──────────────────────────────────────────────
|
||||
if [ -n "${GIT_USER_NAME:-}" ] && ! git config --global user.name &>/dev/null; then
|
||||
git config --global user.name "$GIT_USER_NAME"
|
||||
@@ -309,6 +401,13 @@ elif [ -x /workspace/skillset/deploy-skills.sh ]; then
|
||||
fi
|
||||
if [ -n "$SKILLSET_DEPLOY" ]; then
|
||||
"$SKILLSET_DEPLOY" --bootstrap --prune-stale >/dev/null 2>&1 || true
|
||||
# The deploy leaves the early baked links (above) in place as foreign links,
|
||||
# which silently shadows the live clone for skills the skillset OWNS. Repoint
|
||||
# just those; baked stays the fallback, user overrides still win. `|| true`:
|
||||
# a skill-link refinement must never break container start.
|
||||
if command -v devbox-skill-reconcile >/dev/null 2>&1; then
|
||||
devbox-skill-reconcile "$(dirname "$SKILLSET_DEPLOY")" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Execute command ──────────────────────────────────────────────────
|
||||
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/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 <skillset-root> [skills-dir] [baked-src]
|
||||
# skillset-root the mounted skillset repo (contains skills/<name>/)
|
||||
# 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"
|
||||
@@ -55,6 +55,10 @@ release_tag=$(jq -r '.release_tag' "$MANIFEST")
|
||||
build_date=$(jq -r '.build_date' "$MANIFEST")
|
||||
source_rev=$(jq -r '.source_revision' "$MANIFEST")
|
||||
pi_version_baked=$(jq -r '.pi_version' "$MANIFEST")
|
||||
# `// empty` matters: images built before v1.8.6 have no such field, and
|
||||
# `jq -r` renders a JSON null as the 4-char string "null" — which would
|
||||
# print as a bogus version rather than being treated as absent.
|
||||
mp_version_baked=$(jq -r '.mempalace_version // empty' "$MANIFEST")
|
||||
|
||||
if [ "$MODE" = "quiet" ]; then
|
||||
printf '%s (%s)\n' "$release_tag" "${source_rev:0:7}"
|
||||
@@ -71,6 +75,16 @@ if command -v pi >/dev/null 2>&1; then
|
||||
pi_version_live=$(pi --version 2>/dev/null | head -n1 | tr -d '\r\n')
|
||||
fi
|
||||
|
||||
# Same check for the palace, which matters more than it looks: mempalace is
|
||||
# the one component that is BOTH client (here) and server (synlig runs this
|
||||
# same image), so a skew between the two is a real failure mode rather than
|
||||
# cosmetic. `mempalace --version` prints "MemPalace 3.8.0" — name-prefixed,
|
||||
# unlike pi's bare "0.84.3" — hence $NF rather than reading the whole line.
|
||||
mp_version_live=""
|
||||
if command -v mempalace >/dev/null 2>&1; then
|
||||
mp_version_live=$(mempalace --version 2>/dev/null | head -n1 | awk '{print $NF}' | tr -d '\r\n')
|
||||
fi
|
||||
|
||||
printf 'pi-devbox %s\n' "$release_tag"
|
||||
printf ' built: %s (source %s)\n' "$build_date" "${source_rev:0:12}"
|
||||
if [ -n "$pi_version_live" ] && [ "$pi_version_live" != "$pi_version_baked" ]; then
|
||||
@@ -79,5 +93,15 @@ else
|
||||
printf ' pi: %s\n' "${pi_version_live:-$pi_version_baked}"
|
||||
fi
|
||||
|
||||
# Printed only when known, so this degrades quietly on pre-v1.8.6 images
|
||||
# instead of showing an empty or "null" palace line.
|
||||
if [ -n "$mp_version_live" ] || [ -n "$mp_version_baked" ]; then
|
||||
if [ -n "$mp_version_live" ] && [ -n "$mp_version_baked" ] && [ "$mp_version_live" != "$mp_version_baked" ]; then
|
||||
printf ' palace: %s \033[33m(baked as %s — drift detected)\033[0m\n' "$mp_version_live" "$mp_version_baked"
|
||||
else
|
||||
printf ' palace: %s\n' "${mp_version_live:-$mp_version_baked}"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf ' components:\n'
|
||||
jq -r '.components | to_entries[] | select(.value != null) | " \(.key): \(.value[0:12])"' "$MANIFEST"
|
||||
|
||||
@@ -41,3 +41,32 @@ especially load-bearing here — a pi-devbox container is frequently recreated,
|
||||
the palace is your only memory across recreates. Without the habit it is just
|
||||
storage, not memory. (The skill is the consumer side; feeding the palace is the
|
||||
separate `opencode-mempalace-bridge` skill, if present.)
|
||||
|
||||
### If the palace is central, it is shared — three rules
|
||||
|
||||
If `MEMPALACE_REMOTE_URL` is set, the MCP tools write to a **central palace
|
||||
shared with other machines**, not to a local one. Your drawers are not the only
|
||||
ones in there, and most drawers' `source_file` paths do not exist on this host.
|
||||
The skill covers the orientation side (provenance, chronology, whose diary is
|
||||
whose); these three are here instead because getting them wrong does *damage*
|
||||
rather than merely confusing you:
|
||||
|
||||
- **Never run `mempalace sync` / `mempalace_sync` against a shared palace.** It
|
||||
prunes drawers whose source files look gitignored, deleted, or moved — and on
|
||||
a shared palace that describes most of the content, including every other
|
||||
machine's. Compounding it (RFC-001 §7.2): feeders now stage *inside* the
|
||||
palace root, so a scoped sync can delete the very drawers it just filed.
|
||||
`mempalace_delete_by_source` is exact-match rather than existence-based, but
|
||||
its blast radius is now the whole fleet's palace — leave it on its default
|
||||
`dry_run=true` and confirm the match count before committing.
|
||||
- **A timeout is not a failure.** The palace is single-writer, and one large
|
||||
mine can block every client for minutes, so a write or mine that exceeds the
|
||||
client's deadline has usually *completed* server-side. Verify with
|
||||
`mempalace_get_drawer` or `mempalace_search` before retrying — a blind retry
|
||||
files a duplicate. `[mempalace ext] feed (tick) failed: mine timed out after
|
||||
30000ms` is the common benign instance: the transcript is already in the
|
||||
server's inbox and the mine is idempotent, so nothing is lost either way.
|
||||
- **The `mempalace` CLI is not remote-aware.** It always opens a palace on
|
||||
local disk, so `mempalace search` can return older and different results than
|
||||
the MCP tools while both look correct. Use the MCP tools for the central
|
||||
palace; the CLI only for a local one.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Vendored fallback skills
|
||||
|
||||
Most directories here are **image-baked skills** that `entrypoint-user.sh`
|
||||
symlinks into `~/.agents/skills/` on container start (only when a skill of the
|
||||
same name is not already present, so a mounted `skillset` repo or a user
|
||||
override always wins).
|
||||
symlinks into `~/.agents/skills/` on container start. They are the **fallback**
|
||||
layer: see *Runtime precedence* below for which copy actually wins when a
|
||||
`skillset` repo is mounted (through v1.8.4 the answer was "always the baked
|
||||
one", which was a bug).
|
||||
|
||||
| skill | owner | how it gets here |
|
||||
|-------|-------|------------------|
|
||||
@@ -38,6 +39,35 @@ its skill file needed baking.
|
||||
*different* skill, `opencode-mempalace-bridge`), so there is no public
|
||||
package source to copy from. This snapshot is refreshed manually per release.
|
||||
|
||||
## Runtime precedence (v1.8.5+)
|
||||
|
||||
The baked links are created **early** in `entrypoint-user.sh` (before pi-deploy,
|
||||
to close a smoke readiness race) with a create-only-when-absent guard, and the
|
||||
skillset deploy runs **last** and treats them as foreign links. Through v1.8.4
|
||||
that combination meant the baked snapshot always won: an edit pushed to
|
||||
`skillset/skills/mempalace/SKILL.md` was invisible in every container until the
|
||||
next image build (measured on two hosts — live `md5 129bcc4752` vs baked
|
||||
`5236024fef`, new section absent). Editing those skills *appeared* to work.
|
||||
|
||||
`devbox-skill-reconcile` now runs immediately after the skillset deploy and
|
||||
repoints the links for skills the **skillset owns**, listed one per line in
|
||||
`skillset-owned.txt`. Precedence, highest first:
|
||||
|
||||
1. **user override** — a real directory, or a symlink pointing outside the baked
|
||||
tree; never touched by anything
|
||||
2. **live skillset clone** — but only for names in `skillset-owned.txt`
|
||||
3. **baked snapshot** — everything else, and every skill when no skillset is
|
||||
mounted
|
||||
|
||||
Ownership is per-skill on purpose: `pi-extensions`' authoritative source is the
|
||||
package repo (copied over the snapshot at build), and `skillset` carries a
|
||||
downstream copy that can lag, so handing it to the clone would *regress* the
|
||||
skill. Only `mempalace` is skillset-owned today.
|
||||
|
||||
Verify with `readlink -f ~/.agents/skills/<skill>` — not by reading the
|
||||
entrypoint. Smoke covers both directions (baked resolution with no skillset
|
||||
mounted, plus a fabricated-skillset run of the reconciler).
|
||||
|
||||
## Refreshing the snapshots
|
||||
|
||||
cp <pi-extensions-pkg>/skill/SKILL.md pi-extensions/SKILL.md
|
||||
@@ -50,4 +80,9 @@ also carries a copy, but it is a downstream duplicate and can lag), and
|
||||
`mempalace` from `skillset`. Copying `pi-extensions` from `skillset` would
|
||||
regress the snapshot to whatever that repo last mirrored.
|
||||
|
||||
Snapshot provenance at last refresh: skillset `63f3bf5`, pi-extensions pkg `e73cb9f`.
|
||||
Snapshot provenance at last refresh: skillset `670f7f1`, pi-extensions pkg `e73cb9f`.
|
||||
|
||||
When you refresh the `mempalace` snapshot, also update the phrase asserted by
|
||||
the "mempalace skill snapshot is current" smoke test — it deliberately pins the
|
||||
**newest** section, because the previous canary grepped a phrase that survived
|
||||
the very edit that made the snapshot stale, and so passed on stale content.
|
||||
|
||||
@@ -275,18 +275,30 @@ Wings are top-level categories, typically one per project or domain:
|
||||
- Named after the project directory (e.g., `cli_utils`, `opencode_devbox`)
|
||||
- Agent diaries live in `wing_<agent_name>` (e.g., `wing_orchestrator`, `wing_pi`)
|
||||
|
||||
#### Multi-harness palace
|
||||
#### Shared palace: multiple harnesses, and possibly multiple machines
|
||||
|
||||
A single palace can be fed by multiple coding-agent harnesses. On this machine the palace is shared between **opencode** and **pi** (Mario Zechner's pi-coding-agent). Implications:
|
||||
A single palace can be fed by multiple coding-agent harnesses, and — when
|
||||
`MEMPALACE_REMOTE_URL` points at a central palace — by multiple *machines*. On
|
||||
this machine the palace is shared between **opencode** and **pi** (Mario
|
||||
Zechner's pi-coding-agent). Implications:
|
||||
|
||||
- **`wing_conversations` mixes sources.** Both harnesses' session feeders write into the same wing. To tell them apart, look at the `source_file` metadata on each drawer:
|
||||
- `pi_<uuid>.jsonl` → pi session
|
||||
- `<slug>_ses_<id>.jsonl` → opencode session
|
||||
- The first chunk of each session also carries a `| source: opencode` or `| source: pi` marker in the synthetic header line.
|
||||
- **Other wings may belong to other harnesses.** For example `wing_pi` is pi's diary, not opencode's. Don't assume every diary entry was written by you — check `agent_name` on the entry.
|
||||
- **Session feeders run on different schedules.** Pi sessions are fed Tue 03:00, opencode sessions Mon 03:00. Recent sessions from either harness can lag the palace by up to a week, so absence-of-evidence in `wing_conversations` is not evidence-of-absence for recent work.
|
||||
- **Session feeders run on different schedules.** Pi sessions are fed Tue 03:00, opencode sessions Mon 03:00 (launchd `Weekday`: `0`/`7`=Sunday, `1`=Monday, `2`=Tuesday — misreading this by one day is easy). Recent sessions from either harness can lag the palace by up to a week, so absence-of-evidence in `wing_conversations` is not evidence-of-absence for recent work.
|
||||
- **Reading another harness's diary is useful.** When orienting after a gap, `mempalace_diary_read agent_name=pi` (or whichever sibling agent has been active) often gives a fresher picture than waiting for the conversations feeder to catch up.
|
||||
|
||||
When the palace is **central** (shared across machines), five more things apply:
|
||||
|
||||
- **Check which machine a conversation came from.** Transcripts are fed per device, so `source_path` reads `…/mempalace-feed/<device>/pi_<uuid>.jsonl` while the displayed `source_file` is only the basename. One search can legitimately return hits from several machines at once — look at the device segment before attributing a decision to *this* project.
|
||||
- **Attribute what you file yourself.** Drawers now carry `device` and `agent_kind` metadata (plus `device_source`/`agent_kind_source` recording *how* each was determined, so an inference is never mistaken for a fact). Mined content gets these for free — the inbox path gives the device, the filename shape gives the harness — and a timer on the palace host re-stamps hourly, because live re-mining replaces metadata rows and silently drops earlier stamps. But for anything **you** file by hand, the only signal is what you pass: set `added_by="<harness>@<device>"` (e.g. `pi@emb-7kj4vr4g`, from `$MEMPALACE_PI_DEVICE`) on `add_drawer`/`checkpoint`/`mine`. Skip it and your drawer joins the ~16k historic `/workspace` project mines that are permanently unattributable, because `/workspace` exists identically on every devbox. Note the palace preserves `source_file` in full (see `source_path`) but *displays* only the basename — so a device prefix there survives storage even though it looks stripped.
|
||||
- **Mined drawers carry the MINE date, not the session date.** When history is imported, or re-mined on the palace host, `filed_at`/`created_at` is the *import* time — so sorting by them does not give chronological order. Real session time is recoverable from the UUIDv7 in `pi_<uuid>.jsonl`: the first 12 hex digits are milliseconds since the epoch (and UUIDv7 sorts lexicographically in time order, so a plain filename sort is already chronological). Agent-authored drawers and diaries have no such backdoor — for those `filed_at` is the only chronology, which is why it must never be restamped.
|
||||
- **Beware the timezone mismatch when you combine those.** Palace `filed_at`/`created_at` are naive timestamps in the palace host's local time, while a UUIDv7 decodes to UTC. Comparing them directly introduces a silent offset (2 h for a CEST host). Normalise before drawing conclusions about ordering.
|
||||
- **`agent_name` is not device-scoped.** `mempalace_diary_read(agent_name="pi")` returns *every* machine's `pi` diary, interleaved. Read the entry before assuming it is your own history.
|
||||
- **One writer, no queue.** A concurrent mine returns a structured `already-running` error rather than waiting its turn, and one large mine can make the palace unresponsive to every client for minutes. After another client's mine, call `mempalace_reconnect` to see the new drawers. A client-side timeout is not evidence of failure — verify before retrying, or you file a duplicate.
|
||||
|
||||
### Rooms
|
||||
|
||||
Rooms are aspects within a wing:
|
||||
@@ -324,3 +336,4 @@ Entity-relationship triples with temporal validity. Query with `mempalace_kg_que
|
||||
- **Don't mine .git directories or node_modules.** The CLI miner respects .gitignore by default.
|
||||
- **Don't create duplicate drawers.** Use `mempalace_check_duplicate` before adding manually.
|
||||
- **Don't treat the palace as a task list.** It's for knowledge and context, not todos.
|
||||
- **Don't hand-craft provenance.** Leave `added_by` alone (and never put a machine name in a diary's `agent_name` — it becomes the wing name and hides your entries from `diary_read`). Recording *which device* wrote a record is client/server infrastructure, not your job: a hostname or container ID is not a stable identity, and an invented value is worse than none because it silently corrupts any future palace merge. If you find notes in the palace describing an `origin_device` scheme, that is a design for the client to implement — not an instruction for you to start stamping.
|
||||
|
||||
@@ -130,6 +130,36 @@ are "command not found" there — you must spell out the underlying command.
|
||||
If a command "works in my terminal but not when the agent runs it," this alias
|
||||
gap is the first thing to suspect.
|
||||
|
||||
### A negative result is usually your own filter
|
||||
|
||||
**When you are about to report that something is absent, unreachable, or not
|
||||
running, the filter you wrote is the prime suspect — not the thing.** This
|
||||
environment produces false negatives cheaply, and they are convincing because
|
||||
the command "succeeded". Three real instances from one session, all wrong, all
|
||||
mine:
|
||||
|
||||
| Claim I made | Why it was false |
|
||||
|---|---|
|
||||
| "`tor-ms22` is not in the SSH config" | `grep … \| head -20` — the entry was at **line 454**. `~/.ssh/config` here is ~500 lines. |
|
||||
| "the Docker host has no `docker`" | non-interactive SSH `PATH` lacks `/usr/local/bin` (§2, §3). It was at `/usr/local/bin/docker`. |
|
||||
| "no ControlMaster is running" | pattern `ssh ` (trailing space) cannot match a master: those processes **rename themselves** to `ssh: <controlpath> [mux]`. |
|
||||
|
||||
Habits that would have caught all three:
|
||||
|
||||
```sh
|
||||
# don't cap the output of a search whose answer you don't already know
|
||||
grep -n -i -A6 'tor-ms22' ~/.ssh/config # not | head -20
|
||||
|
||||
# on the host, resolve the binary instead of trusting PATH
|
||||
ssh -F "$HOME/.ssh-local/config" mac 'command -v docker || ls /usr/local/bin/docker'
|
||||
|
||||
# match a process's ACTUAL argv, not the name you imagine
|
||||
ps -eo pid,etime,args | grep -Ei 'mux|mosh|ssh'
|
||||
```
|
||||
|
||||
A positive result needs no such scepticism — it carries its own evidence. Only
|
||||
absence has to be *earned*, so spend the extra command there.
|
||||
|
||||
**`dscp`/`scp` with accented filenames on a macOS host.** macOS stores filenames
|
||||
in Unicode **NFD** (decomposed — e.g. `ä` is `a` + combining U+0308), while the
|
||||
string you type or paste is usually **NFC** (precomposed `ä`, U+00E4). The bytes
|
||||
@@ -175,6 +205,23 @@ Two related mechanisms (don't reinvent them):
|
||||
CGNAT per-destination flow caps on residential ISPs. If `~/.ssh/config` pins
|
||||
a `ControlPath` under the read-only `~/.ssh`, override with
|
||||
`-o ControlPath=none` (or use the sidecar, which already redirects it).
|
||||
- **A live master socket MASKS auth and config changes on the far end.** Once
|
||||
`~/.ssh-local/cm/<user>@<host>:22` exists, later commands ride it and
|
||||
authenticate **not at all** — so after editing remote `authorized_keys`,
|
||||
`sshd_config`, host keys, or firewall rules, "it still works" proves nothing.
|
||||
A corrupted `authorized_keys` then bites on the next *cold* connect, likely in
|
||||
a future session with no memory of the edit. Prove it immediately instead:
|
||||
|
||||
```sh
|
||||
ssh -F "$HOME/.ssh-local/config" -O check <host> # 'Master running (pid=N)'
|
||||
ssh -F "$HOME/.ssh-local/config" -o ControlPath=none -o ControlMaster=no \
|
||||
-o BatchMode=yes <host> 'echo COLD AUTH OK'
|
||||
```
|
||||
|
||||
To attribute a socket rather than guess whose it is: `ps -p <pid> -o
|
||||
pid,ppid,lstart,etime,args`. A `mosh` the *user* started on the host
|
||||
bootstraps with the **host's** `~/.ssh/cm/` and is invisible from in here;
|
||||
only a mosh started *inside* the container shares `~/.ssh-local/cm/`.
|
||||
- **`pi --ssh <host>`** rewires pi's own read/write/edit/bash tools to run on a
|
||||
remote host; it has its own writable-socket fallback. See the `pi-extensions`
|
||||
skill for that path.
|
||||
@@ -257,6 +304,10 @@ hardcode. Details are in the `mempalace` skill.
|
||||
- [ ] Writing durable output? → `/workspace`, not the ephemeral layer.
|
||||
- [ ] Using `dssh`/`dscp`/`ll` in the bash tool? → spell out the real command.
|
||||
- [ ] Assuming a hostname / domain / nameserver / host OS? → stop, detect it.
|
||||
- [ ] About to report something **absent / unreachable / not running**? → re-run
|
||||
without your own `head`/pattern/`PATH` assumptions first (§2).
|
||||
- [ ] Changed remote `authorized_keys` / `sshd_config`? → prove it with a **cold**
|
||||
connect; a live master socket hides breakage (§3).
|
||||
- [ ] "Resolves but won't connect"? → check route *and* DNS (§3 + §4).
|
||||
- [ ] `apt`/toolchain install? → tell the user it's ephemeral unless imaged.
|
||||
- [ ] Editing a skill? → `readlink -f ~/.agents/skills/<name>` first (§1).
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Skills in this directory whose OWNER is the skillset repo.
|
||||
#
|
||||
# Read by devbox-skill-reconcile, which runs after the skillset deploy in
|
||||
# entrypoint-user.sh: for each name below, if the mounted skillset ships a
|
||||
# skill of that name, the baked link in ~/.agents/skills/ is repointed at the
|
||||
# live clone. The baked copy remains the fallback for containers started
|
||||
# WITHOUT a skillset mount, and a user override always wins over both.
|
||||
#
|
||||
# Add a name here ONLY if the skillset repo is the authoritative source (see
|
||||
# the ownership table in VENDORED.md). Do NOT add:
|
||||
# pi-devbox-environment — authored in this repo; baked IS canonical
|
||||
# pi-extensions — owned by the pi-extensions package repo and copied
|
||||
# over the snapshot at build time; the skillset copy
|
||||
# is a downstream duplicate that can lag, so letting
|
||||
# it win would regress the skill.
|
||||
mempalace
|
||||
+234
-1
@@ -43,12 +43,23 @@ PASS=0; FAIL=0
|
||||
# catching an unexpected +GB regression.
|
||||
SIZE_THRESHOLD_MB=3800
|
||||
|
||||
# On failure, surface the last few lines the command produced. This used to
|
||||
# discard output entirely (`>/dev/null 2>&1`), which made a red ❌ carry zero
|
||||
# diagnostic weight: explaining the single v1.8.0 stage-default failure took a
|
||||
# full CI-log dig plus a registry-config inspection, when the container had
|
||||
# already printed the answer and thrown it away. Assertions that want a
|
||||
# diagnostic just echo it to stderr — it stays hidden while they pass.
|
||||
run() {
|
||||
local label="$1"; local cmd="$2"
|
||||
if docker run --rm --entrypoint="" "$IMAGE" sh -c "$cmd" >/dev/null 2>&1; then
|
||||
local out
|
||||
if out=$(docker run --rm --entrypoint="" "$IMAGE" sh -c "$cmd" 2>&1); then
|
||||
printf " ✅ %s\n" "$label"; PASS=$((PASS+1))
|
||||
else
|
||||
printf " ❌ %s\n" "$label"; FAIL=$((FAIL+1))
|
||||
# `if`, not `&&` — a trailing false under `set -e` would abort the script.
|
||||
if [ -n "$out" ]; then
|
||||
printf " └─ %s\n" "$(printf '%s' "$out" | tail -3 | tr '\n' ' ' | cut -c1-300)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -91,6 +102,136 @@ run "terminfo: modern emulators (ncurses-term)" 'for t in wezterm alacritty foot
|
||||
run "terminfo: xterm-ghostty alias (tic)" "infocmp -x xterm-ghostty >/dev/null 2>&1"
|
||||
run "nvim true-colour default (sysinit.vim)" "nvim --headless -c 'lua os.exit(vim.o.termguicolors and 0 or 1)'"
|
||||
run "mempalace-mcp" "mempalace-mcp --help"
|
||||
run "mempalace-pi-session on PATH" "mempalace-pi-session --help"
|
||||
# The staging dir must sit next to the palace, not in a disposable cache: the
|
||||
# palace keys per-source dedup on the STAGED path, so a stage that can be wiped
|
||||
# while the palace survives lets `mempalace sync` prune every drawer mined from
|
||||
# it. Assert the resolved default, not an env var — the guarantee is "stage
|
||||
# shares the palace's lifetime", which an ENV pin would quietly break.
|
||||
# NOTE: --sessions-dir gets an EMPTY temp dir, never /tmp. The stage banner is
|
||||
# printed before any export, so nothing needs to be found — and pointing a
|
||||
# default-staged run at a populated dir would export whatever transcripts it
|
||||
# finds into the real stage, which is how a synthetic test session ends up
|
||||
# staged for mining as if it were a real conversation.
|
||||
#
|
||||
# Asserted $HOME-RELATIVE, not against a literal /home/developer. `run` invokes
|
||||
# `docker run --entrypoint=""`, and neither Dockerfile sets USER or ENV HOME
|
||||
# (HOME is set by entrypoint-user.sh, which --entrypoint="" deliberately skips),
|
||||
# so these assertions execute as root with HOME=/root. The original literal
|
||||
# /home/developer form could therefore never match and failed the v1.8.0
|
||||
# release — a test bug, not a product one: the stage resolution was correct all
|
||||
# along, it just follows $HOME. The invariant under test ("the stage sits beside
|
||||
# the palace, sharing its lifetime") is user-independent, so pinning the user
|
||||
# was never part of it. A cache-dir default still fails the pattern below, which
|
||||
# is the regression this guards.
|
||||
#
|
||||
# It went unnoticed for three days because this workflow only triggers on
|
||||
# `push: tags: v*` — the assertion was added on a main push, so v1.8.0 was its
|
||||
# first execution ever. Use the `smoke_only` workflow_dispatch input to run
|
||||
# smoke against HEAD without cutting a tag.
|
||||
run "pi stage defaults next to the palace (not a cache dir)" '
|
||||
out=$(mempalace-pi-session --dry-run --reason smoke --sessions-dir "$(mktemp -d)" 2>&1) || true
|
||||
stage=$(echo "$out" | grep -oE "stage=[^ ]+" | head -1)
|
||||
echo "resolved ${stage:-<no stage= line>} with HOME=$HOME" >&2
|
||||
case "$stage" in
|
||||
"stage=$HOME/.mempalace/pi-stage/"*) exit 0 ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
'
|
||||
# Companion to the above: the deployment-specific case the literal assertion was
|
||||
# reaching for, done properly by supplying the HOME the container actually runs
|
||||
# with instead of assuming it.
|
||||
run "pi stage is palace-adjacent for the developer user" '
|
||||
out=$(HOME=/home/developer mempalace-pi-session --dry-run --reason smoke --sessions-dir "$(mktemp -d)" 2>&1) || true
|
||||
echo "$out" | grep -oE "stage=[^ ]+" | head -1 >&2
|
||||
echo "$out" | grep -q "stage=/home/developer/.mempalace/pi-stage/"
|
||||
'
|
||||
run "pi stage follows MEMPALACE_PALACE_PATH" '
|
||||
out=$(MEMPALACE_PALACE_PATH=/tmp/alt/.mempalace/palace \
|
||||
mempalace-pi-session --dry-run --reason smoke --sessions-dir "$(mktemp -d)" 2>&1) || true
|
||||
echo "$out" | grep -q "stage=/tmp/alt/.mempalace/pi-stage/"
|
||||
'
|
||||
# The feeder's --agent default is WHO a drawer is attributed to. mempalace core
|
||||
# records neither the machine nor the harness on a write, and one shared bearer
|
||||
# token means the server cannot tell clients apart, so toolkit c64ffa1 changed
|
||||
# this default from $USER to pi@$MEMPALACE_PI_DEVICE — the one string that makes
|
||||
# a write attributable to both. Nothing ever PRINTED the resolved value (the
|
||||
# banner shows mode= and stage= only), so an image built from a pre-c64ffa1
|
||||
# toolkit ref would ship unattributed writes with every check still green.
|
||||
#
|
||||
# `--help` assigns AGENT (script top) before it parses args, then exits 0 with
|
||||
# no side effects — so `bash -x` observes the REAL resolution, env interpolation
|
||||
# and fallback included, rather than grepping the source for a literal line that
|
||||
# any reformat would break. Two-sided on purpose: device set => pi@<device>;
|
||||
# device UNSET => must not be pi@anything. The second half is what fails against
|
||||
# the old unconditional $USER default, which ignored the device entirely.
|
||||
#
|
||||
# Probes the PATH entry (a symlink into the /opt clone) rather than that clone
|
||||
# path directly: this is the invocation the systemd/launchd timers and
|
||||
# entrypoint-user.sh actually use, so it is the default that reaches the palace.
|
||||
run "feeder resolves --agent to pi@<device> (drawer attribution)" '
|
||||
f=$(command -v mempalace-pi-session) || { echo "feeder not on PATH" >&2; exit 1; }
|
||||
with=$(MEMPALACE_PI_DEVICE=smoke-device bash -x $f --help 2>&1 | sed -n "s/^+* *AGENT=//p" | tail -n1)
|
||||
without=$(env -u MEMPALACE_PI_DEVICE bash -x $f --help 2>&1 | sed -n "s/^+* *AGENT=//p" | tail -n1)
|
||||
echo "resolved with-device=[$with] without-device=[$without]" >&2
|
||||
[ "$with" = "pi@smoke-device" ] || exit 1
|
||||
case "$without" in pi@*) exit 1 ;; esac
|
||||
echo ok
|
||||
'
|
||||
# Regression guard for the pi transcript exporter. If pi ever changes its
|
||||
# session JSONL shape, the exporter stops recognising sessions and the palace
|
||||
# silently gets nothing (or, worse, raw JSON chunked as prose). Feed it a
|
||||
# synthetic session and assert it is actually exported. Uses --dry-run so no
|
||||
# palace is touched, and a temp stage so nothing real is written.
|
||||
run "pi transcript exporter recognises a pi session" '
|
||||
set -e
|
||||
d=$(mktemp -d); s="$d/sessions/--workspace--"; mkdir -p "$s"
|
||||
{
|
||||
printf "%s\n" "{\"type\":\"session\",\"version\":1,\"id\":\"smoke\",\"cwd\":\"/workspace\",\"timestamp\":\"2026-01-01T00:00:00Z\"}"
|
||||
printf "%s\n" "{\"type\":\"message\",\"message\":{\"role\":\"user\",\"content\":\"question one\"}}"
|
||||
a=$(printf "a%.0s" $(seq 1 1200))
|
||||
printf "%s\n" "{\"type\":\"message\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"$a\"}]}}"
|
||||
printf "%s\n" "{\"type\":\"message\",\"message\":{\"role\":\"user\",\"content\":\"question two\"}}"
|
||||
printf "%s\n" "{\"type\":\"message\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"short reply\"}]}}"
|
||||
} > "$s/2026-01-01T00-00-00-000Z_smoke.jsonl"
|
||||
out=$(mempalace-pi-session --dry-run --sessions-dir "$d/sessions" --stage "$d/stage" 2>&1)
|
||||
echo "$out" | grep -q "Exported 1 session"
|
||||
'
|
||||
# The same guard from the other side: a session with no real assistant output
|
||||
# (an abandoned prompt, whose bulk is injected skill text) must NOT be filed.
|
||||
run "pi transcript exporter rejects an abandoned session" '
|
||||
set -e
|
||||
d=$(mktemp -d); s="$d/sessions/--workspace--"; mkdir -p "$s"
|
||||
{
|
||||
printf "%s\n" "{\"type\":\"session\",\"version\":1,\"id\":\"smoke2\",\"cwd\":\"/workspace\",\"timestamp\":\"2026-01-01T00:00:00Z\"}"
|
||||
u=$(printf "u%.0s" $(seq 1 13000))
|
||||
printf "%s\n" "{\"type\":\"message\",\"message\":{\"role\":\"user\",\"content\":\"$u\"}}"
|
||||
printf "%s\n" "{\"type\":\"message\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Ready. What would you like to work on?\"}]}}"
|
||||
} > "$s/2026-01-01T00-00-00-000Z_smoke2.jsonl"
|
||||
out=$(mempalace-pi-session --dry-run --sessions-dir "$d/sessions" --stage "$d/stage" 2>&1)
|
||||
echo "$out" | grep -q "no sessions qualified"
|
||||
'
|
||||
# The remote-palace-without-inbox skip must ANNOUNCE itself, not vanish. This
|
||||
# branch of entrypoint-user.sh runs at container start (not reachable from a
|
||||
# `docker run` one-shot), so assert against the entrypoint that actually shipped
|
||||
# in the image. Guards a silent regression back to the bare `:` no-op, which
|
||||
# left a container contributing nothing to the palace with no artifact saying
|
||||
# why — the log it would normally leave is written by the other branch.
|
||||
run_expect "remote-palace-without-inbox skip is announced, not silent" \
|
||||
"grep -o 'MemPalace catch-up skipped' /usr/local/bin/entrypoint-user.sh | head -1" \
|
||||
"MemPalace catch-up skipped"
|
||||
run "...and the skip notice names the variable that fixes it" \
|
||||
"grep -A6 'MemPalace catch-up skipped' /usr/local/bin/entrypoint-user.sh | grep -q 'MEMPALACE_PI_SSH_TARGET'"
|
||||
# A remote mine that FAILS must not report success. MCP answers a hard tool
|
||||
# failure with HTTP 200 and the tool's own JSON escaped inside
|
||||
# result.content[].text, so the feeder's old `'\"error\"' in body` check could
|
||||
# never see it: on 2026-08-15 a mine that died with "source directory not found:
|
||||
# '/data/feed/...'" logged "Done. Wing updated." and exited 0, and this
|
||||
# container's transcripts were filed nowhere for a whole session. The feeder
|
||||
# carries fixtures for that exact body; run them against the baked toolkit so a
|
||||
# stale/reverted toolkit ref can't reintroduce a silent feed.
|
||||
run "baked feeder detects a failed remote mine (no silent false success)" \
|
||||
"mempalace-pi-session --self-test"
|
||||
# v1.0.0 base additions — verify presence and basic functionality.
|
||||
run "pandoc" "pandoc --version"
|
||||
run "typst" "typst --version"
|
||||
@@ -121,6 +262,16 @@ run "image-baked mempalace fallback skill" \
|
||||
# baked copy must be the fresh package copy (Option 1), not the stale snapshot.
|
||||
run "pi-extensions skill refreshed from package when present" \
|
||||
"if [ -f /opt/pi-extensions/skill/SKILL.md ]; then cmp -s /opt/pi-extensions/skill/SKILL.md /usr/local/share/pi-devbox/skills/pi-extensions/SKILL.md; else true; fi"
|
||||
# Runtime ownership handover (v1.8.5): the baked links are a FALLBACK, and
|
||||
# skillset-OWNED skills must be repointed at the live clone when one is mounted.
|
||||
# The list is data, so assert its content, not just its presence: mempalace in,
|
||||
# pi-extensions deliberately out (its skillset copy is a lagging duplicate).
|
||||
run "devbox-skill-reconcile helper present + executable" \
|
||||
"test -x /usr/local/bin/devbox-skill-reconcile"
|
||||
run "skillset-owned list ships and names mempalace" \
|
||||
"grep -qx 'mempalace' /usr/local/share/pi-devbox/skills/skillset-owned.txt"
|
||||
run "skillset-owned list excludes pi-extensions (ownership)" \
|
||||
"! grep -qx 'pi-extensions' /usr/local/share/pi-devbox/skills/skillset-owned.txt"
|
||||
|
||||
# ── tmux 0-indexing (required for pi-studio variants) ─────────────────
|
||||
echo ""
|
||||
@@ -139,6 +290,26 @@ run "pi-fork clone + node_modules" \
|
||||
"test -f /opt/pi-fork/package.json && test -d /opt/pi-fork/node_modules"
|
||||
run "pi-observational-memory clone + node_modules" \
|
||||
"test -f /opt/pi-observational-memory/package.json && test -d /opt/pi-observational-memory/node_modules"
|
||||
# ...and that the clone carries the AUTH FIX, not merely that it exists. om's
|
||||
# pre-flight hasUsableAuth() check silently disabled `recall` for ~8 weeks once
|
||||
# pi moved to request-time SigV4 signing and stopped exposing a static Bedrock
|
||||
# key; upstream fixed it in ce9fc98, adopted in v1.8.4. PI_OBSMEM_REF tracks
|
||||
# master, so an upstream revert or force-push would ship a dead `recall` with
|
||||
# the clone assertion above still green — the exact gap flagged as open in the
|
||||
# v1.8.5 changelog.
|
||||
#
|
||||
# Pin the markers to src/runtime.ts, the fix SITE, rather than grepping the
|
||||
# repo: two of these three strings also appear under tests/, so a repo-wide
|
||||
# grep stays green with runtime.ts itself reverted. That is a false green of the
|
||||
# same family as the old skill-snapshot canary.
|
||||
run "pi-observational-memory carries the ce9fc98 auth fix (recall stays alive)" '
|
||||
f=/opt/pi-observational-memory/src/runtime.ts
|
||||
test -f "$f" || { echo "fix site missing: $f" >&2; exit 1; }
|
||||
for m in availability_recheck providerCredentialConfigured hasConfiguredAuth; do
|
||||
grep -q "$m" "$f" || { echo "marker absent from runtime.ts: $m" >&2; exit 1; }
|
||||
done
|
||||
echo ok
|
||||
'
|
||||
# pi-atelier: deliberately NO node_modules assertion, unlike its siblings —
|
||||
# it declares zero runtime dependencies (only peerDeps, satisfied by the baked
|
||||
# pi) and has no build step, so Dockerfile.variant skips `npm install` for it.
|
||||
@@ -183,6 +354,20 @@ run_expect "manifest records pi-atelier" \
|
||||
"cat /etc/pi-devbox/build-manifest.json" '"pi-atelier"'
|
||||
run_expect "manifest records pi_version" \
|
||||
"cat /etc/pi-devbox/build-manifest.json" '"pi_version"'
|
||||
# mempalace CORE was absent from the manifest through v1.8.5: the toolkit SHA
|
||||
# was recorded but the palace version behind the MCP tools was not, so a palace
|
||||
# bug could not be correlated to an image version. Assert the field exists AND
|
||||
# equals the installed binary — recording it from ARG MEMPALACE_VERSION instead
|
||||
# would look identical here yet drift silently the first time an install
|
||||
# resolved to something other than the pin, which is the whole reason this file
|
||||
# is built from ground truth. `// empty` matters: jq -r prints the 4-char
|
||||
# string "null" for a JSON null, which would satisfy a naive -n test.
|
||||
run "manifest mempalace_version matches the installed core" '
|
||||
m=$(jq -r ".mempalace_version // empty" /etc/pi-devbox/build-manifest.json)
|
||||
b=$(mempalace --version 2>/dev/null | head -n1 | tr -d "\r"); b=${b##* }
|
||||
echo "manifest=[$m] installed=[$b]" >&2
|
||||
[ -n "$m" ] && [ "$m" = "$b" ]
|
||||
'
|
||||
# Every component must be a resolved commit (or null for pi-studio in the
|
||||
# non-studio variant) — 'unknown' means a clone silently failed to resolve.
|
||||
run "manifest has no unresolved ('unknown') components" \
|
||||
@@ -254,6 +439,54 @@ exec_test "settings.json bootstrapped" 'test -f $HOME/.pi/agent/sett
|
||||
exec_test "pi-devbox-environment skill linked" 'test -L $HOME/.agents/skills/pi-devbox-environment && test -f $HOME/.agents/skills/pi-devbox-environment/SKILL.md && echo ok'
|
||||
exec_test "pi-extensions skill linked (fallback)" 'test -L $HOME/.agents/skills/pi-extensions && test -f $HOME/.agents/skills/pi-extensions/SKILL.md && echo ok'
|
||||
exec_test "mempalace skill linked (fallback)" 'test -L $HOME/.agents/skills/mempalace && test -f $HOME/.agents/skills/mempalace/SKILL.md && echo ok'
|
||||
# The vendored mempalace snapshot is refreshed MANUALLY per release (see
|
||||
# rootfs/usr/local/share/pi-devbox/skills/VENDORED.md). Through v1.8.4 it also
|
||||
# silently SHADOWED the live skillset copy, so staleness was invisible — and the
|
||||
# canary that was supposed to catch it could not: it grepped "Shared palace:
|
||||
# multiple harnesses", a phrase present in BOTH the stale and the fresh copy.
|
||||
# A snapshot canary must pin the NEWEST section, so update this string whenever
|
||||
# the snapshot is refreshed — that is the point of it.
|
||||
exec_test "mempalace skill snapshot is current" 'grep -q "Attribute what you file yourself" $HOME/.agents/skills/mempalace/SKILL.md && echo ok'
|
||||
# Link TARGETS, not just link existence: with no skillset mounted (as here) the
|
||||
# baked tree must be what resolves, for all three vendored skills.
|
||||
exec_test "vendored skills resolve to the baked tree (no skillset mounted)" \
|
||||
'for s in mempalace pi-extensions pi-devbox-environment; do
|
||||
case "$(readlink -f $HOME/.agents/skills/$s)" in
|
||||
/usr/local/share/pi-devbox/skills/$s) ;;
|
||||
*) echo "$s resolves to $(readlink -f $HOME/.agents/skills/$s)" >&2; exit 1 ;;
|
||||
esac
|
||||
done; echo ok'
|
||||
# The handover path itself. CI never mounts a skillset, so without this the
|
||||
# v1.8.5 fix would ship untested: fabricate a skillset + a skills dir holding
|
||||
# baked-style links, run the reconciler, and assert all three outcomes —
|
||||
# owned skill repointed, unowned skill left baked, user override untouched.
|
||||
exec_test "reconciler: owned skill handed to live clone, others untouched" \
|
||||
'set -e; t=$(mktemp -d); mkdir -p $t/ss/skills/mempalace $t/ss/skills/pi-extensions $t/skills
|
||||
echo LIVE > $t/ss/skills/mempalace/SKILL.md; echo LIVE > $t/ss/skills/pi-extensions/SKILL.md
|
||||
ln -s /usr/local/share/pi-devbox/skills/mempalace $t/skills/mempalace
|
||||
ln -s /usr/local/share/pi-devbox/skills/pi-extensions $t/skills/pi-extensions
|
||||
mkdir -p $t/skills/mine; echo MINE > $t/skills/mine/SKILL.md
|
||||
devbox-skill-reconcile $t/ss $t/skills >/dev/null
|
||||
devbox-skill-reconcile $t/ss $t/skills >/dev/null # idempotent
|
||||
[ "$(readlink $t/skills/mempalace)" = "$t/ss/skills/mempalace" ] || { echo "owned skill NOT repointed" >&2; exit 1; }
|
||||
[ "$(readlink $t/skills/pi-extensions)" = /usr/local/share/pi-devbox/skills/pi-extensions ] || { echo "unowned skill was repointed" >&2; exit 1; }
|
||||
[ "$(cat $t/skills/mine/SKILL.md)" = MINE ] || { echo "user override clobbered" >&2; exit 1; }
|
||||
rm -rf $t; echo ok'
|
||||
# The case above cannot fail if the reconciler stops checking WHERE a link
|
||||
# points — a mutation test showed all three of its assertions still passing with
|
||||
# that guard deleted, which is the same false-green shape as the old snapshot
|
||||
# canary. This one discriminates: an OWNED name (so it is considered) whose link
|
||||
# is a user override pointing outside the baked tree (so it must be left alone).
|
||||
exec_test "reconciler: user override on an owned name is left alone" \
|
||||
'set -e; t=$(mktemp -d); mkdir -p $t/ss/skills/mempalace $t/skills $t/mine-skill
|
||||
echo LIVE > $t/ss/skills/mempalace/SKILL.md; echo USERLINK > $t/mine-skill/SKILL.md
|
||||
ln -sfn $t/mine-skill $t/skills/mempalace
|
||||
devbox-skill-reconcile $t/ss $t/skills >/dev/null
|
||||
[ "$(cat $t/skills/mempalace/SKILL.md)" = USERLINK ] || { echo "user symlink override clobbered" >&2; exit 1; }
|
||||
rm -rf $t; echo ok'
|
||||
# mempalace-census gained a /usr/local/bin symlink in v1.8.3; its three siblings
|
||||
# had one since they were added, so this asserts the set stays complete.
|
||||
exec_test "mempalace-census on PATH" 'command -v mempalace-census >/dev/null && mempalace-census --help >/dev/null && echo ok'
|
||||
|
||||
# pi-fork + pi-observational-memory are registered by entrypoint-user.sh via
|
||||
# `pi install /opt/<pkg>`, which runs slightly after the keybindings marker.
|
||||
|
||||
Reference in New Issue
Block a user