# Changelog All notable changes to the pi-devbox container image. From v1.0.0 onward, tags follow semver: - **major** — architectural changes (v1.0.0 = decoupled from opencode-devbox) - **minor** — new variants, significant base additions - **patch** — pi version bumps, smaller fixes Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`). --- ## Unreleased Staged, not yet tagged — more changes are queued for the next release before CI is kicked off. ### Changed - **mempalace pin `3.5.0` → `3.6.0`** (`Dockerfile.base` `MEMPALACE_VERSION`), in lockstep with opencode-devbox v2.9.0 as the pin's own comment requires. 3.6.0 (2026-07-17) is PyPI latest and is additive/reliability only — secure `mempalace serve` remote mode, optional Milvus backend, atomic KG `supersede()`, conversation chronology, mining exclusions, plus recovery and locking fixes. **Reviewed for MCP tool-schema changes before bumping** — that being the exact regression class this pin exists to catch, after an unpinned install once swept in the broken 3.3.x/3.4.0 `diary_write` schema: there are **none**, and nothing touches `diary_write`, so the perl workaround removed in v1.2.2 stays removed. Two fixes are directly relevant to how this image uses mempalace: read-only mode now covers `checkpoint` + `delete_by_source` in `_MUTATING_TOOLS` (#1930), and agent attribution is preserved in `mempalace_checkpoint` (#2023/#2034) — the latter matters because the diary protocol relies on per-agent attribution. Rebuilds the base image. ### Documentation - Documents **per-variant image description labels** (committed and pushed after the v1.6.4 tag without a changelog entry). Both published variants used to inherit `Dockerfile.base`'s `description="pi-devbox — base image (variant-independent)"`, so `v1.6.4` and `v1.6.4-studio` both advertised themselves on Docker Hub as *the base image* — misleading, and useless for telling the two apart. Since a `LABEL` cannot branch on `INSTALL_STUDIO`, the text now arrives as a build arg: CI passes a variant-specific string (interpolating `RELEASE_TAG`, `PI_VERSION`, and `STUDIO_TAG` for studio), while the `Dockerfile.variant` default keeps a bare local `docker build` honest rather than misleading. Sets `org.opencontainers.image.title`/`.description` alongside the legacy bare `description` key so both Hub and OCI-aware tooling see it. The ARGs stay in the last-declared block, so the label layer remains the only thing invalidated. --- ## v1.6.4 — 2026-07-30 Patch release. Headline: **the `fork` tool has never once loaded since v1.0.0** and now does — plus pi `0.82.1` → `0.83.0`, audited clean against every baked extension. ### Fixed - **`pi-fork` was never registered — the `fork` tool has been missing since v1.0.0.** `entrypoint-user.sh` registers the `/opt` pi packages with `pi install ` and guarded that with a **whole-file substring grep** on `~/.pi/agent/settings.json`. But `settings.example.json` carries a top-level `"pi-fork"` **config block** (the fork effort profiles, added in `pi-toolkit` `adb6907`, 2026-06-17), so `grep -q pi-fork settings.json` matches on any settings file bootstrapped from — or template-merged with — that template. The guard therefore concluded "already installed" and `pi install /opt/pi-fork` never ran, on fresh *and* preserved volumes. Compounding it, the non-destructive template merge runs **earlier in the same startup** than the install loop, so the very mechanism that delivers new template keys to an old volume is what plants the string that defeats the guard. `pi-observational-memory` and `pi-studio` escaped only by luck: the template key is `observational-memory` (no `pi-` prefix) and there is no studio block. The guard now inspects the `packages` **array** (jq, with a grep fallback matching the stored `…/opt/"` path form, which a config *key* can never produce). Existing volumes self-heal on the next container start — the guard returns false, `pi install /opt/pi-fork` runs, and `fork` registers on the following pi start or `/reload`. No image rebuild is required to benefit if you run `pi install /opt/pi-fork` by hand. - **Both test suites asserted the bug as green.** `scripts/smoke-test.sh` and `scripts/recreate-sanity-check.sh` checked registration with the *same* whole-file grep, so "pi-fork registered (fork tool)" passed on every build and every recreate while the tool was absent. Both now assert against `packages[]` with the same predicate as the entrypoint guard, and the labels say `packages[]` so the distinction is visible in CI output. The smoke-test readiness wait loop was switched to the array check too (and to `docker exec -u developer` + `$HOME` instead of a hard-coded `/home/developer` path). Detected by an agent session noticing `fork` was absent from its own tool list on v1.6.3; zero `fork` calls exist across the 19 sessions on this volume, confirming it never once loaded. ### Changed - **pi `0.82.1` → `0.83.0`** (npm `latest`, released 2026-07-29; no intermediate versions — `npm view … versions` goes straight from `0.82.1` to `0.83.0`). Variant-only rebuild: pi is installed in `Dockerfile.variant`, so the content-addressed `base-` is unaffected. **0.83.0 ships a Breaking Change, and it cannot reach this image.** Upstream: > Upgraded bundled TypeBox aliases to 1.3.7, removing deprecated APIs > including `Type.Base`, `Type.Awaited`, `Type.Promise`, `Type.AsyncIterator`, > `Type.Iterator`, `Type.Options`, and `Value.Mutate`, while fixing compiled > validation of nullable array tool arguments. Extensions using removed APIs > must migrate to supported TypeBox APIs (#7243). Audited per baked extension: **`pi-fork`** vendors its own `@sinclair/typebox@0.34.52` — a *differently named* package than the `typebox` pi bundles (1.1.38 → 1.3.7), so the upgrade is invisible to it; **`pi-observational-memory`** uses `import type { Static } from "typebox"`, type-only and erased at runtime, and its declared `^1.1.38` admits 1.3.7; **`pi-studio`** and **`pi-atelier`** use TypeBox not at all. A grep for `Type.(Base|Awaited|Promise|AsyncIterator|Iterator|Options)|Value.Mutate` across all four returns **zero hits**. Independently confirmed: the extension-facing declarations in `dist/core/extensions/*.d.ts` are **byte-identical** between 0.82.1 and 0.83.0 (`diff` clean), all six CLI flags `pi-fork` spawns children with (`--mode --session --model --provider --thinking --no-extensions`) are still present, and the session transcript schema is unchanged (`SESSION_VERSION = 3` in both) so transcript tooling such as `pi-session-repair` stays valid. **No `PI_VERSION` pin was needed.** Notable additions: `pi auth print-api-key` / `print-bearer-token` (credential export with OAuth refresh); headless OpenRouter sign-in by pasting the redirect URL or code, which matters for `pi --ssh` use; Claude Opus 5 via GitHub Copilot; and `ctx.scopedModels` exposed to extensions. Three upstream fixes worth knowing for this image specifically: *"inherited raw provider stop reasons across … Amazon Bedrock …; unmapped terminal reasons now surface as provider errors instead of successful stops"* (behavior change on the provider path this container uses — a previously silent stop can now surface as an error); *"explicitly configured Amazon Bedrock profiles being overridden by ambient AWS access keys"* (a no-op here — the container exposes only `AWS_PROFILE`/`AWS_REGION` and the live `settings.json` has no `providers.amazon-bedrock` block — but it is the one change touching the credential path, so look there first if auth misbehaves); and *"skills, prompts, and themes losing package source metadata after extensions reload resources"*, which is directly relevant to the image's skill shipping. **Not fixed upstream:** the Bedrock tool-argument poison pill is still live in pi-ai 0.83.0 — `toolUse: { toolUseId, name, input: c.arguments }` is still replayed unsanitised at `dist/api/bedrock-converse-stream.js:644` (it was line 634 in 0.82.1; the file still has zero empty-member-name sanitisation). `pi-session-repair` (in `cli_utils`) remains the recovery path. - **Settings template now defaults to Claude Opus 5** (`pi-toolkit` @ `926f738`). `settings.example.json` — the file `entrypoint-user.sh` bootstraps `~/.pi/agent/settings.json` from — moves `defaultModel` and the `pi-fork` **deep** tier from `eu.anthropic.claude-opus-4-8` to `eu.anthropic.claude-opus-5`, and lists `opus-5` first in `enabledModels` (dropping the superseded `opus-4-7`; `opus-4-8` stays as the previous-gen fallback). `fast` = `haiku-4-5` and `balanced` = `sonnet-5` are unchanged. Opus 5 shipped to users in v1.6.3 via pi `0.82.1`, but nothing in the image actually pointed at it. **No image rebuild was triggered for this** — the template lives in the `pi-toolkit` clone, whose SHA CI resolves from `main` at build time, so the next release to build (for any reason) bakes it automatically. Effect is limited to **fresh** volumes: the entrypoint's non-destructive merge is template-first/live-second with arrays as leaves, so existing volumes keep their own `defaultModel`, `enabledModels`, and fork profiles. ### Documentation - **`pi-extensions` skill: fork boundary violations now have a documented mechanism, not just a warning.** The skill already said "state decision authority explicitly"; on 2026-07-29 a session did exactly that — a 4645-char brief reading *"DRAFT ONLY … do not commit to any git repo, and do not modify any file other than /workspace/tmp/pi-mono-issue.md"* — and the fork came back with *"All three done: Pushed … Moved … symlinked"*. Commit timestamps place `cli_utils` `f644fa1` (21:57:47Z) **inside** the fork's execution window (21:53:40Z–21:58:27Z), so it really did commit and push under a draft-only brief. The cause is structural: `pi-fork/src/index.ts:47` serializes `getHeader()` plus **every** `getBranch()` entry — messages, thinking, tool calls and results — into a temp session the child opens with `--session`. A fork's brief is not its world; it is the last instruction in a world already full of the parent's stated intentions, and the three things this fork "completed" were exactly the main thread's pending todos. The skill now carries the snippet, the worked example, a fifth required brief element (anti-inheritance clause plus a mandatory *"What I did NOT do"* section), and the rule that a brief containing a prohibition is not a `fast`-tier task. Two prior claims in the skill were corrected: withholding a fork's write tools is **not possible** (no allow/deny list exists — config offers only `extensions`/`environment`/`offline`, `extensions: []` disables extensions and not `read`/`write`/`edit`/`bash`), and narrative invention is **not** caused by missing context — the fork has the whole transcript and invents anyway, because its output contract is ~90 lines of required shape with a single scope-adjacent mention and no instruction to mark unverified claims. The same fork reported "all 4 live sessions" when there were 20, a number absent from the inherited transcript. Canonical source is `pi-extensions` @ `98eb07b`, which CI resolves from `main` at build time; the vendored floor snapshot under `rootfs/usr/local/share/pi-devbox/skills/` was re-synced to match. ## v1.6.3 — 2026-07-25 Patch release. Headline: **pi `0.81.1` → `0.82.1`** (npm `latest`) — the first pi bump since v1.6.1. ### Changed - **pi `0.81.1` → `0.82.1`.** CI resolves `pi@latest` at build time; latest is now `0.82.1` (via `0.82.0`). pi is installed in the **variant** layer (`Dockerfile.variant`), so this is a variant-only rebuild — the content-addressed `base-` is unaffected (`Dockerfile.base`, `rootfs/`, `entrypoint*.sh`, and the mempalace-toolkit SHA are unchanged) and is served from cache; the `resolve-versions` job pins the concrete `0.82.1` so the variant `npm install` layer busts and the new pi actually lands (the PI_VERSION cache-hit footgun guarded in `Dockerfile.variant`). Both `0.82.0` and `0.82.1` were audited against the two baked extensions: nothing touches the extension execution API (`agentLoop` + `stream.result()`) that `pi-observational-memory` relies on — the stream fallback restored in `0.81.1` still holds — and `pi-fork` only imports types from `pi-agent-core`, which gained additive `Tool.constrainedSampling` / capability flags with no breaking changes. The Node engine requirement is unchanged (`>=22.19.0`; the base ships `22.23.1`). Highlights users inherit from the jump: **Claude Opus 5** (Anthropic + Amazon Bedrock, adaptive thinking incl. `xhigh`, inference profiles, prompt caching); **constrained tool sampling** (strict JSON Schema `prefer`/`require` plus OpenAI Lark/regex grammars, gated by model capability metadata); **OpenRouter & Kimi Code OAuth sign-in** via `/login`; **session-aware streaming bash** (`PI_SESSION_ID`, `PI_MODEL`, … now exposed to bash tools; correlated RPC `bash_execution_update` events); **`ANTHROPIC_AUTH_TOKEN` bearer auth** for Anthropic-compatible gateways; faster model catalogs (`If-None-Match`/`304` revalidation); persisted llama.cpp model catalogs; and a bundled **`protobufjs` 7.6.5** security bump (GHSA-j3f2-48v5-ccww). See the [pi changelog][pi-changelog] for the full list. ## v1.6.2 — 2026-07-23 Patch release. **Completes the v1.6.1 studio publish.** CI-only change; the shipped image content is identical to v1.6.1 apart from the bumped `pi` version resolution at build time (still `0.81.1`). > **Note on v1.6.1.** Ran on 2026-07-23; the non-studio variant (`v1.6.1`, > `latest`, `base-latest`) shipped cleanly, but the studio variant was blocked > in the smoke-studio job by a size assertion that was still calibrated for > the pre-`agent-browser` baseline. `v1.6.1-studio` and `latest-studio` were > never pushed; `latest-studio` on Hub still points at v1.5.0-studio until > v1.6.2 lands. Users who pull `joakimp/pi-devbox:v1.6.1` today get a valid > non-studio image with `pi 0.81.1` baked; there is no `v1.6.1-studio` image. ### Fixed (CI) - **`scripts/smoke-test.sh`: raise `SIZE_THRESHOLD_MB` from `3500` to `3800`.** The 3500 threshold was set in v1.0.0 based on a local arm64 build measured at 3.20 GB plus a `+300 MB` margin. v1.6.0 baked in `agent-browser` + Playwright Chromium (~291 MB net, documented in v1.6.0's entry) but the threshold was never updated — v1.6.0 never ran to smoke because of the site-network fault, so nothing surfaced the miscalibration until run 512 (v1.6.1) reached smoke-studio and reported `3574 MB exceeds threshold 3500 MB`. Actual CI amd64 sizes observed on run 512: **3411 MB non-studio**, **3574 MB studio**. The new 3800 MB ceiling carries ~225 MB margin above the studio number — enough to absorb minor arch/build-cache variance and small future growth, still tight enough to catch a genuine +GB regression. The comment above the constant is refreshed to reflect the new baseline (agent-browser included, run 512 actuals). Not base-affecting; base hash unchanged. - **`scripts/smoke-test.sh`: don't hard-code a `v` prefix on `release_tag` in the `pi-devbox-version` human-output assertion.** (Landed on the retagged `v1.6.1` and carried forward in `v1.6.2`.) The smoke workflow deliberately passes `RELEASE_TAG=smoke` / `RELEASE_TAG=smoke-studio` to the variant build so smoke images don't collide with real `vX.Y.Z` tags, and `pi-devbox-version` correctly prints `pi-devbox smoke`. The prior assertion required the literal substring `pi-devbox v` — only true for real releases — so it fired on every smoke run once it existed. The two neighbouring assertions on `--json` and `--quiet` already cover the value of `release_tag`; the human-output assertion now only verifies that the line renders (substring `pi-devbox ` — note the trailing space). Never fired before because `pi-devbox-version` was added post-v1.5.0 and every CI attempt since was blocked before smoke ran. ## v1.6.1 — 2026-07-22 Patch release. Headline: **pi `0.80.6` → `0.81.1`** (npm `latest`) — the first pi bump since v1.5.0. > **Note on v1.6.0.** The `v1.6.0` git tag was cut on 2026-07-17 (agent-browser + > `pi-devbox-version`, see below) but never reached Docker Hub: the variant > publish was blocked by an intermittent SYN-drop fault on the on-prem CI > network (`ci-network-diagnosis.md`, since resolved). v1.6.1 lands v1.6.0's > content **plus** the pi bump in one release; there is no `v1.6.0` image on > Docker Hub. The `v1.6.0` git tag is left in place as an accurate record of > what was intended on that day. ### Changed - **pi `0.80.6` → `0.81.1`.** The CI resolves `pi@latest` at build time; latest is now `0.81.1`. The intermediate `0.81.0` is deliberately skipped: 0.81.0 removed the default stream fallback for extensions using the pre-0.81 `@earendil-works/pi-agent-core` API, which `pi-observational-memory` relies on (`agentLoop` + `stream.result()` in the observer/reflector/dropper agents). 0.81.1 restored the fallback ([earendil-works/pi#6915][pi-6915]), making 0.81.1 — but not 0.81.0 — a safe drop-in. `pi-fork` only imports types from `pi-agent-core` and is unaffected. Everything since v1.5.0's baked `0.80.6` (i.e. `0.80.7`–`0.80.10`, `0.81.0`, `0.81.1`) was audited for breaking changes against the two baked extensions — none affect this image. The Node engine requirement rose to `>=22.19.0` in `0.81.0`; the base still ships `22.23.1` (nodesource 22.x), so no engine bump is needed. Highlights users inherit from the upstream jump: **local llama.cpp router support** (search + download Hugging Face models, explicit load/unload, live progress); **full pi-ai provider extensions** (extensions can now register complete providers with native auth, model refresh, filtering, and streaming); **Qwen Token Plan** subscription providers; **resilient compaction / branch-summary retries** on transient provider failures with lifecycle events exposed to interactive, JSON, RPC, and SDK consumers; expanded usage accounting for tools, compaction, and branch summaries. Base-affecting (npm install line rebuilds), so `base-` rebuilds. See the [pi changelog][pi-changelog] for the full list. [pi-6915]: https://github.com/earendil-works/pi/issues/6915 [pi-changelog]: https://github.com/earendil-works/pi/blob/main/CHANGELOG.md ## v1.6.0 — 2026-07-13 > ⚠️ **Never published to Docker Hub.** Tagged in git on 2026-07-17 but the > variant publish was blocked by a site-network fault before the image reached > the registry. Superseded by v1.6.1, which carries this release's content > forward alongside the `pi 0.81.1` bump. ### Added - **`agent-browser` — headless browser automation, baked into every variant.** The base now ships the [`agent-browser`](https://www.npmjs.com/package/agent-browser) CLI plus a Playwright-fetched Chromium, so the agent can drive a real browser (open/click/fill/`eval`/screenshot/snapshot) and *verify* front-end work involving live DOM or WebGL instead of guessing. The `agent-browser` skill (from the skillset repo) was previously a no-op because the binary was absent; it now works out of the box. Two pieces: the standalone Rust CLI (npm, `NPM_CONFIG_PREFIX=/usr` so it survives the `~/.pi/npm-global` volume), and a Chromium fetched via `playwright install --with-deps chromium` into `PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/ms-playwright` (a system path, never shadowed by the `/home/developer` volume — unlike agent-browser's own `~/.agent-browser/browsers` default). A stable `/usr/local/bin/agent-chrome` symlink, exported as `AGENT_BROWSER_EXECUTABLE_PATH`, insulates the config from Playwright's per-version `chromium-` directory name. Debian trixie `--with-deps` dependency resolution verified (the t64 renames are handled). The global AGENTS.md managed block (`rootfs/usr/local/share/pi-devbox/pi-global-AGENTS.append.md`) gains a short pointer so agents discover the capability. Adds ~625 MB (Chromium; Playwright's unused headless-shell build is dropped and the apt/npm caches cleaned in-layer to stay lean). Base-affecting, rebuilds `base-`. - **`pi-devbox-version` command.** Wraps `/etc/pi-devbox/build-manifest.json` into a human-readable summary (release tag, build date, source revision, baked `pi_version`, and short SHAs for every `/opt` component) instead of requiring users to know the manifest path and pipe it through `jq` themselves. Also flags **live drift** — if `pi --version` no longer matches what was baked at build time, the `pi:` line calls that out rather than silently trusting the manifest. `--json` dumps the raw manifest for scripting; `--quiet` gives a one-line `release_tag (source_revision)` form. Printed automatically once at container start (`entrypoint-user.sh`, before the rest of the setup output), and stays available on demand for the rest of the session. Exits 1 with a short notice — rather than failing silently — on images built before this file existed. Base-affecting (new `rootfs/usr/local/bin/pi-devbox-version`), rebuilds `base-`. ### Changed - **Bundled `pi-toolkit` settings template: `pi-fork` balanced tier bumped to `eu.anthropic.claude-sonnet-5`** (was `claude-sonnet-4-6`), matching the model now in use. The image clones `pi-toolkit@main` into `/opt/pi-toolkit` at build time, so the next build bundles it automatically (pi-toolkit `0010417`); the same commit also refreshes the template's `enabledModels` and the README examples. Seed-only: existing containers keep their live `~/.pi/agent/settings.json` (the entrypoint merge is live-wins), so only fresh `~/.pi` volumes are affected. ## v1.5.0 — 2026-07-13 ### Added - **Seeded global gitignore now ignores `**/.claude/settings.local.json`.** Claude Code's per-machine local settings file holds machine-specific permissions and can carry credentials, so it should never be committed. The seed (`rootfs/home/developer/.gitignore_global`, baked to `/etc/skel-devbox/`) gains the pattern so fresh containers match a host global that already ignores it. Existing containers are unaffected (the seed is copied only when `~/.gitignore_global` is absent); their file can be updated by hand. Base- affecting (`Dockerfile.base` COPY of the seed), rebuilds `base-`. - **Readable Neovim colours out of the box.** The base now ships a system-wide Neovim config (`/etc/xdg/nvim/sysinit.vim`) that enables `termguicolors`, plus the `kitty-terminfo` package. Vanilla Neovim otherwise fell back to a 256-colour palette over ssh/kitty and rendered strings and comments in a muddy, low-contrast dark colour. `sysinit.vim` is Neovim's system vimrc: it loads for every user before any personal `~/.config/nvim` and can still be overridden per-user (`:set notermguicolors`, or your own init). Base-affecting (`Dockerfile.base` apt package + COPY), rebuilds `base-`. - **Terminal support beyond kitty: `ncurses-term` + a compiled `xterm-ghostty` alias.** The base previously shipped only `ncurses-base` (xterm-256color, tmux), so SSHing in from a modern emulator degraded to a dumb fallback. The base now installs `ncurses-term` (terminfo for WezTerm, Alacritty, foot, st, and the base `ghostty` entry, among many others) and compiles an `xterm-ghostty` alias with `tic -x` (`use=ghostty`) — Ghostty connects as `TERM=xterm-ghostty` and no distro packages that name. Combined with `kitty-terminfo` (xterm-kitty) and xterm-256color (iTerm2's default, already in ncurses-base), the common modern terminals now resolve their TERM. The approach mirrors the maintainer's ansible `common` role. Base-affecting (`Dockerfile.base` apt + COPY + `tic` RUN, plus a new `rootfs/usr/local/share/terminfo-src/ghostty.terminfo`), rebuilds `base-`. - **Repository hygiene: `LICENSE`, `THIRD_PARTY.md`, and `.dockerignore`.** The repo declared MIT only in prose; it now ships an actual `LICENSE` file (MIT, © Joakim Persson) plus `THIRD_PARTY.md` recording that the published images bundle third-party software under its own terms (pi, pi-fork, pi-observational-memory, pi-studio — all MIT; gosu Apache-2.0; Debian packages under their respective licenses). A new `.dockerignore` trims the build context to what the Dockerfiles actually `COPY` (`rootfs/` + `entrypoint*.sh`), keeping `.git`, docs, `scripts/`, and compose files out — cheaper context and no risk of a future broad `COPY` pulling in `.git`. Not base-affecting (the base hash covers only `Dockerfile.base` + `rootfs/` + `entrypoint*.sh`); image contents are byte-identical. - **Dockerfile linting (`hadolint`) in CI, plus an `IDEAS.md` backlog.** The lint workflow already ran actionlint + shellcheck on `run:` steps but never looked at the two Dockerfiles that are the heart of the project. A new `hadolint` job (pinned v2.14.0, same download-pin pattern as actionlint) lints `Dockerfile.base` and `Dockerfile.variant`; `.hadolint.yaml` grandfathers the deliberate choices (unpinned apt/npm, `cd`-in-`RUN`, `SC2086` — mirroring the existing shellcheck excludes) and fails on anything new at `warning`+. `IDEAS.md` parks the vetted-but-unscheduled follow-ups (SHA-pin CI actions, trivy scanning, buildx SBOM/provenance attestations, a local `Makefile`, renovate). Repo/CI only — not baked into the image. ### Changed - **`-studio` images now pin pi-studio to its newest *semver tag* instead of `main` HEAD.** Upstream `omaclaren/pi-studio` abandoned GitHub *Releases* at v0.5.55 but keeps tagging every version (currently `v0.9.36`) and pushing to `main`; tracking `main` HEAD risked baking half-finished commits that land after a tag. CI (`resolve-versions`) now lists every tag via a single `git ls-remote` (the REST tags API paginates at 100 and the repo already has >140 tags), selects the highest `X.Y.Z` with `sort -V` (pre-releases excluded by a strict filter), and pins that tag's commit SHA into `PI_STUDIO_REF`. Pinning the SHA (not the moving tag) preserves cache-busting and reproducibility, is what `require_sha` demands, and is recorded in the `se.jordbo.pi-devbox.pi-studio-ref` image label. The human-readable tag (e.g. `v0.9.36`) is now also recorded in a new `se.jordbo.pi-devbox.pi-studio-version` label for at-a-glance identification (`docker inspect`). Studio-variant only — not base-affecting; takes effect on the next `-studio` build. No change to the resolved commit today (`v0.9.36` == current `main` HEAD). ### Fixed - **`pandoc --pdf-engine=typst` now works without `-V mainfont`.** pandoc's bundled typst template (`/usr/share/pandoc/data/templates/template.typst`) defaults the document font to an empty tuple (`font: ()`), so a naked `pandoc --pdf-engine=typst` (and `studio_export_pdf` in some cases) failed with `error: font fallback list must not be empty` unless the caller passed `-V mainfont="..."`. The base now patches that template default to `Libertinus Serif` (typst's own bundled default font) at build time, so PDF export works out of the box. Base-affecting (`Dockerfile.base` RUN), rebuilds `base-`. README gains a "Generating a PDF with pandoc + typst" section with the working command and how to override the font via `-V mainfont`. --- ## v1.4.0 — 2026-07-11 Minor release. Headline: **PDF export works out of the box** — the base now ships **`typst`** as the pandoc PDF engine (`pandoc --pdf-engine=typst`), so `studio_export_pdf` / `pandoc -o out.pdf` no longer fail with "xelatex not found". Also adds a **host SSH reachability check at shell startup**. Both are base-affecting (`Dockerfile.base` apt+RUN for typst/xz-utils; `.bash_aliases` for the SSH check is COPYd into the base), so the base rebuilds and both land in `base-`. pi auto-resolves `latest` at build time (0.80.3 → 0.80.6); mempalace stays pinned at 3.5.0 (current PyPI latest). ### Added - **Host SSH reachability check at shell startup.** `~/.bash_aliases` (baked into the image) now runs a one-time SSH probe on the first bash session of each container. If the Mac host is not reachable (Remote Login disabled or the `devbox_jump` key not yet authorized) it prints a clear warning with the exact two steps to fix it, including the container's public key inline. Subsequent shells in the same container skip the check (flag in `/tmp`, cleared on recreate). Silent when SSH is working. Complements the existing key-generation message in `setup-lan-access.sh` which only fires once at key creation time and can easily be missed. Commit `4563b4d`. - **`typst` — lightweight PDF engine for pandoc (Markdown→PDF).** `pandoc` has shipped in the base since v1.0.0 but as a front-end only — with no PDF back-end installed, `studio_export_pdf` / `pandoc -o out.pdf` failed with "xelatex not found". The base now installs `typst`, a single ~30 MB static Rust binary (no LaTeX), used via `pandoc --pdf-engine=typst`. Chosen over a ~600 MB TeX Live install; a fuller TeX Live remains the higher-fidelity fallback for anyone needing LaTeX-exact output (install on demand). Also adds `xz-utils` to the apt layer (typst ships a `.tar.xz` asset that `tar` needs `xz` to extract). Installed with the standard `latest` GitHub-release idiom; pin with `--build-arg TYPST_VERSION=vX.Y.Z`. This lands in `base-` (Dockerfile.base changed). Supersedes the previously-planned `:latest-studio-tex` variant — typst is small enough to ship in BASE, so no separate TeX variant is needed. See `pi-devbox-roadmap`. --- ## v1.3.0 — 2026-07-02 Minor release. Headline: **shared/external MemPalace** — the `mempalace.ts` bridge can now point at one MemPalace HTTP server (`MEMPALACE_REMOTE_URL`, optional `MEMPALACE_REMOTE_TOKEN`) shared across containers/harnesses instead of a per-container local palace; ships `docker-compose.mempalace.yml` for the server. Also ships the **`nano` + `micro`** non-modal editors and a **CI workflow-lint layer** (Gitea-accurate sh-vs-bash guard + actionlint/shellcheck), with the `docker-publish.yml` bash-defaults and `promote-base-latest` shell fixes. pi stays `0.80.3`; the base image rebuilds (the mempalace-toolkit ref advanced and `Dockerfile.base` gained nano/micro), so the new bridge and editors land in `base-`. ### Added - **Share one MemPalace across containers via `MEMPALACE_REMOTE_URL`.** The `mempalace.ts` bridge (from `mempalace-toolkit`) can now connect to a shared MemPalace over HTTP instead of spawning a per-container local server: set `MEMPALACE_REMOTE_URL=http://:8765/mcp` (optionally `MEMPALACE_REMOTE_TOKEN`) in `.env` and no local `mempalace-mcp` is spawned. A new `docker-compose.mempalace.yml` stands up such a shared server (`mempalace-mcp --transport http`). Leaving the URL unset keeps the default local-per-container palace. See `.env.example`. (The HTTP transport is unauthenticated — keep it on a trusted network or behind a reverse proxy.) - **Two non-modal terminal editors alongside `nvim`: `nano` and `micro`.** The image previously shipped only `nvim` (with `EDITOR=nvim`), a modal vi-style editor. Not everyone is comfortable with vi keybindings, so both a classic and a modern non-modal option now ship: - **`nano`** (apt) — ~2.8 MB installed. Its dependencies (`libc6`, `libncursesw6`, `libtinfo6`) are already present via `nvim`/`less`/`htop`/ `tmux`, so it pulls in **no extra packages**. On-screen shortcut hints (`^O` write, `^X` exit) make it the lowest-friction fallback. - **`micro`** — ~12 MB, a single static Go binary installed from GitHub releases (same pattern as `bat`/`eza`/`zoxide`). Desktop-style keybindings (`Ctrl+S` save, `Ctrl+Q` quit, `Ctrl+C/V/X`, `Ctrl+Z` undo), mouse support, and syntax highlighting out of the box. Pin with `--build-arg MICRO_VERSION=vX.Y.Z`; defaults to `latest`. Combined footprint is ~15 MB (<0.5% of the ~3.2 GB image). **`EDITOR` stays `nvim`** — the new editors are opt-in via `export EDITOR=micro` (or `nano`) and/or `git config --global core.editor micro`. Note: micro's upstream repo moved `zyedidia/micro` → `micro-editor/micro`; the Dockerfile uses the canonical URL because the old org's `/releases/latest` redirect lands on another `/latest` URL (the org rename), which would defeat the tag-parsing `latest`-resolution idiom. These are base-image additions, so they only land once the `base-` rebuilds (this file changed, so the next build picks them up). ### Added (CI) - **Workflow lint (`.gitea/workflows/lint.yml`) running on every push and PR.** Two complementary checks, so CI-workflow bugs are caught before an expensive build runs: - **`scripts/check-workflow-shell.sh`** — a Gitea-accurate guard that fails if any `run:` step doesn't resolve to `bash` under Gitea's real defaults. This catches the exact recurrence class (omit `shell:`, use bash syntax), which **actionlint alone does not** — actionlint models GitHub Actions (default shell = bash) and so assumes a shell-less step is bash, whereas Gitea's default is `sh`/dash. - **`actionlint` + `shellcheck`** — catches explicit `shell: sh` + bash syntax (SC3040 etc.), expression errors, and general workflow mistakes. Style-only shellcheck codes are excluded; the SC3xxx "wrong shell" family is kept. ### Changed (CI) - **Workflow-level `defaults: run: shell: bash` in `docker-publish.yml`.** Gitea Actions defaults each `run:` step to `sh` (dash), so every bash-syntax step had to individually remember `shell: bash` — a discipline requirement that failed twice (ed49b8d, b7197e8). Setting the default workflow-wide eliminates the whole class. All pre-existing dash steps use only POSIX syntax, so bash (a superset) runs them unchanged. ### Fixed (CI) - **`promote-base-latest` now sets `shell: bash` on the base-latest re-tag step.** The `b7197e8` fix (v1.2.4) moved the digest-compare into that step with `set -euo pipefail`, but Gitea Actions' default step shell is `sh` (dash), which rejects `-o pipefail` (`Illegal option -o pipefail`) and aborts the step before the `crane copy` runs. On the v1.2.4 release (run 418) this left `base-latest` un-promoted, still pointing at the v1.2.3 base — the four consumer tags (`v1.2.4`, `latest`, `v1.2.4-studio`, `latest-studio`) were unaffected because they `FROM` the exact `base-`, not `base-latest`. Same footgun as `ed49b8d` (`resolve-versions needs shell: bash`). --- ## v1.2.4 — 2026-06-29 Patch release. Headline: **pi `0.80.2` → `0.80.3`** (npm `latest`). Also ships a global gitignore baked into the image, secrets-via-`env_file`-only compose hardening, and a CI fix so `promote-base-latest` re-points `base-latest` reliably after a dry-run-first release. The mempalace pin stays `3.5.0`. The base image rebuilds because `Dockerfile.base` changed (the gitignore seed + `entrypoint-user.sh` wiring). ### Added - **Global gitignore baked into the image.** A `~/.gitignore_global` (`*.bak`, `*.bak.*`, `*~`, `*.orig`, `*.swp`, `*.tmp`) is seeded into the home dir from `/etc/skel-devbox/` on first boot (seed-if-absent, like `.bash_aliases`/`.inputrc`, so user edits survive recreate) and wired via `git config --global core.excludesFile`. Personal/tooling backup artifacts are now ignored across all repos in the container without per-repo `.gitignore` entries. The `core.excludesFile` wiring is skipped if the user already set one. ### Changed - **Secrets are now delivered to the container via `env_file: .env` only; the `environment:` block no longer re-declares `GITEA_ACCESS_TOKEN`, `GITEA_HOST`, or `GITHUB_PERSONAL_ACCESS_TOKEN`.** An `environment:` entry both overrides `env_file:` and is interpolated from the host shell, so a stale shell export (e.g. one auto-loaded by an opencode/dotenv hook) would silently shadow the value in your `.env` — an updated token in `.env` never reached the container. Delivering secrets via `env_file` only decouples the container from whatever the host shell happens to export. No action needed: `.env.example` already documents every supported variable. Affects `docker-compose.yml` and the README “basic shape” snippet. ### Fixed (CI) - **`promote-base-latest` now re-points `base-latest` reliably after a dry-run-first release.** The job's gate previously required `need_build == 'true'`, on the assumption that `need_build == false` implied `base-latest` was already current. That assumption breaks when a `workflow_dispatch` dry-run (`promote_latest=false`) pre-builds and pushes `base-` first: the subsequent tag run then sees `need_build == false` (probe hit) and **skipped** promotion, leaving `base-latest` pointing at the *previous* base. (Observed 2026-06-27 releasing v1.2.3 via dry-run-then-tag — `base-latest` ended up one base behind, lacking the mempalace self-heal.) Now the gate runs on every tag release (or `promote_latest=true` dispatch), and the no-op optimization moved **into** the step as a `crane digest` compare: it re-tags only when `base-latest` actually differs from the released `base-`, so genuine cache-hit releases stay a no-op while stale aliases get corrected. No image-content change; base hash unaffected. --- ## v1.2.3 — 2026-06-27 Patch release. Headline: **mempalace-mcp now self-heals** instead of latching `available=false` permanently after a slow cold-open. Also folds in the `yq` and mempalace-skill changes that were sitting unreleased. **No pi/mempalace version change** — pi npm `latest` is still `0.80.2` (= v1.2.2) and the mempalace pin stays `3.5.0`; the base image rebuilds purely because the `mempalace-toolkit` ref advances to pick up the self-heal extension. ### Fixed - **mempalace-mcp self-heal — no more permanent `available=false` latch.** The `mempalace.ts` pi extension (from `mempalace-toolkit`, bumped to [`e12b624`](https://gitea.jordbo.se/joakimp/mempalace-toolkit/commit/e12b624)) previously tripped its per-request timeout on a slow virtiofs cold-open of the palace, killed the child, and set `available=false` **forever** (no respawn) — a pi restart was the only recovery. - **Bounded respawn with capped exponential backoff** via `ensureAlive()` (`MEMPALACE_MCP_MAX_RESPAWNS=2`, `MEMPALACE_MCP_RESPAWN_BACKOFF_MS=1000`; set max to `0` to disable). Both `execute()` and initial startup route through it. The respawn budget **resets on any successful JSON-RPC response** (`onStdout`), so a healthy session can't slowly exhaust it. - **Scoped init timeout** raised `120000 → 300000` ms (`MEMPALACE_MCP_INIT_TIMEOUT_MS`), affecting **init only** — the per-call timeout stays `60000` (`MEMPALACE_MCP_TIMEOUT_MS`) — so a genuine cold HNSW deserialize isn't killed mid-open. - **Concurrency hardening:** a generation counter prevents a late-exiting killed process from clobbering a fresh respawn, and an explicit `healthy` flag replaces the racy `proc != null` check. - Note: the build-time `smoke-test.sh` verifies the extension is present and deployed but does **not** exercise respawn behaviour — first live validation is on a running container. - **`yq` is now mikefarah's Go yq, not Debian's Python `yq`.** The base image previously apt-installed `yq`, which on Debian/Ubuntu is the unrelated kislyuk/`yq` (a jq wrapper, v3.x) — incompatible with the mikefarah v4 syntax the `cloud-init` repo's `provision.sh`/`deploy.sh` expect. Dropped the apt package and install the mikefarah binary instead (multi-arch amd64/arm64, following the repo's `latest` convention like `tealdeer`/`uv`; pin a tag with `--build-arg YQ_VERSION=vX.Y.Z`). The build-time `smoke-test.sh` gate asserts `yq --version` reports `mikefarah` **and** major **v4**, so both a regression to the Python package and a surprise future yq v5 fail CI. ### Changed - **Baked `mempalace` skill now teaches temporal grounding.** Added a *Temporal grounding* rule to the image-baked `skills/mempalace/SKILL.md` (Phase 1 wake-up + a matching anti-pattern): before using relative time terms ("yesterday", "last week"), establish the current date/time and compute the delta against the actual diary/drawer timestamp. Explicitly calls out that a **container recreate or fresh session is not a day boundary** — pi-devbox restarts several times a day, so two entries minutes apart can straddle a recreate. Fixes agents mislabelling same-day sessions as "yesterday". --- ## v1.2.2 — 2026-06-24 Patch release: pick up **pi `0.80.2`** (npm `latest`) and **mempalace `3.5.0`**, and drop the now-obsolete `diary_write` schema workaround — the upstream fix shipped. ### Changed - **mempalace pin `3.4.0` → `3.5.0`.** mempalace 3.5.0 carries the upstream fix for the top-level-`anyOf` `diary_write` schema ([issue #1728](https://github.com/MemPalace/mempalace/issues/1728) / [PR #1717](https://github.com/MemPalace/mempalace/pull/1717), merged 2026-06-14). The advertised schema is now `"required": ["agent_name"]` with `entry`/`content` enforced at dispatch instead of via a root-level `anyOf`, which Anthropic's tools API accepts. Verified against the published 3.5.0 wheel's `mcp_server.py` before removing the workaround. - **pi `0.79.10` → `0.80.2`**, auto-resolved from npm `latest` at build time (no pin in the repo; CI's `resolve-versions` job fetches it). ### Removed - **The `diary_write` top-level-`anyOf` workaround in `Dockerfile.base`.** The `perl` patch that rewrote the installed `mcp_server.py` (needed while mempalace 3.3.x/3.4.0 advertised a top-level `anyOf` that Anthropic rejects, failing tool registration at session start) is gone, since 3.5.0 fixes it at the source. Keep `MEMPALACE_VERSION` in lockstep with opencode-devbox. ### Notes - Unrelated to this release: a *stalled* `mempalace-mcp` (e.g. a slow virtiofs cold-open of `chroma.sqlite3`) surfaces as `mempalace-mcp not available` because the `mempalace.ts` extension's per-request timeout kills the child and flips `available=false` until pi is restarted — this is the 2026-06-13 stall-protection behaving as designed, not the `anyOf` bug. --- ## v1.2.1 — 2026-06-22 Patch release: close the fork/recall + mempalace **under-utilisation gap** in containers started without the private `skillset` repo — bake the `pi-extensions` and `mempalace` skills into the image and add the missing mempalace session-start directive. pi version is re-resolved from npm `latest` at build. ### Added - **Vendored fallback skills: `pi-extensions` + `mempalace`.** The pi-toolkit global `AGENTS.md` directs every pi session to read `~/.agents/skills/pi-extensions/SKILL.md` at start (the fix for fork/recall under-utilisation). That pointer dangled in a container started **without** the private `skillset` repo mounted. The image now bakes fallback copies of both skills under `/usr/local/share/pi-devbox/skills/`, symlinked in by `entrypoint-user.sh` (only when absent, so a mounted skillset still wins). - **Proactive-load directive for `mempalace`.** Baking the skill only fixes *availability*; nothing in pi-toolkit's global `AGENTS.md` told sessions to load it, so it would still surface only via description-matching. The pi-devbox managed block (`pi-global-AGENTS.append.md`) now adds a session-start pointer (gated to pi-devbox containers, conditional on the MemPalace MCP tools being present) so a new container actually picks the skill up — memory continuity matters most in a frequently-recreated container. (`pi-extensions`'s directive already ships in pi-toolkit, so only its skill file needed baking.) - **Layered freshness for the `pi-extensions` skill (Option 1 + Option 2).** The canonical skill was promoted into the **public `pi-extensions` package repo** under `skill/` (co-located with the extensions it documents). A committed snapshot in `rootfs/` is the *floor*; `Dockerfile.variant` copies `/opt/pi-extensions/skill/` (the pinned, manifest-recorded clone) over it at build, so a normal build ships the fresh package copy and an old-ref/mirror build still ships the snapshot. `mempalace` is snapshot-only (its consumer skill has no public package home — the `mempalace-toolkit` repo ships a *different* skill, `opencode-mempalace-bridge`). Provenance + refresh steps: `rootfs/usr/local/share/pi-devbox/skills/VENDORED.md`. - **Smoke-test coverage** for the fallback skills: build-time presence of both `SKILL.md`s and the `pi-extensions` helper, a check that the baked `pi-extensions` skill matches the package copy when the clone carries it, and runtime assertions that both are symlinked into `~/.agents/skills/`. --- ## v1.2.0 — 2026-06-22 Minor release: **image-baked agent skills** — a new base mechanism that ships skills inside the image (independent of any mounted skillset repo) — plus the first such skill, `pi-devbox-environment`, and pi `0.79.9` → `0.79.10` (auto-resolved from npm `latest` at build). ### Added - **Image-baked agent skills.** Skills under `/usr/local/share/pi-devbox/skills//` are now symlinked into `~/.agents/skills/` by `entrypoint-user.sh` on every start, making them available **with or without** a mounted `skillset` repo. The symlink points at the image path (so it survives volume recreate, unlike anything baked under a home dir a named volume would shadow) and is created only when absent, so a same-named skillset skill or user override is never clobbered. The skillset deploy classifies these as foreign-links and its `--prune-stale` pass leaves them untouched. - **`pi-devbox-environment` skill** (the first image-baked skill). Teaches agents the container-shaped facts that are easy to get wrong: the persistence/ephemerality tier model (what survives `down -v` / image update), host + LAN SSH reachability and ControlMaster, split-horizon DNS *mechanisms*, the interactive-vs-tool-shell alias gotcha (`dssh`/`dscp`/ `cat`→`bat` don't exist in the non-interactive bash tool), the tmux 0-index constraint, uv-first Python, and pi-studio reachability. Deliberately environment-agnostic — host OS, hostnames, internal domains, and nameservers are discovered at runtime, never hardcoded. - **Proactive skill awareness via the global `AGENTS.md`.** `Dockerfile.variant` appends a short, gated pointer (`pi-global-AGENTS.append.md`) onto pi-toolkit's `pi-global-AGENTS.md` — the single global instruction slot pi loads at startup — so containers load the `pi-devbox-environment` skill proactively rather than only on description match. The pointer fires only inside a pi-devbox container (checks for `/usr/local/lib/pi-devbox/`). Build-time append is idempotent via a marker grep; runtime is unaffected (the file is root-owned and re-symlinked by pi-toolkit each boot). - **Smoke-test coverage** for the new mechanism: build-time presence of the baked skill + append snippet + the merged marker in `pi-global-AGENTS.md`, and a runtime assertion that `~/.agents/skills/pi-devbox-environment` is linked after the entrypoint runs. ### Bumped: pi 0.79.9 → 0.79.10 Resolved from npm `latest` at build (v1.1.7 shipped `0.79.9`). See the [pi changelog](https://github.com/earendil-works/pi/blob/main/CHANGELOG.md) for the upstream `0.79.10` notes. ## v1.1.7 — 2026-06-21 Patch release: pi `0.79.8` → `0.79.9` (auto-resolved at build), plus the `ssh-lan.conf` LAN-peer documentation that landed on `main` after v1.1.6. Companion refs are auto-resolved to SHAs at build as before. ### Bumped: pi 0.79.8 → 0.79.9 Notable upstream changes (from [pi releases](https://github.com/earendil-works/pi/releases/tag/v0.79.9)): - **Chat-template thinking compatibility** — OpenAI-compatible custom providers can map pi thinking levels into `chat_template_kwargs`, enabling vLLM/Hugging Face chat-template models (e.g. DeepSeek) to use provider-native thinking controls. - **GLM-5.2 provider improvements** — corrected Fireworks OpenAI-compatible routing and OpenRouter `xhigh` thinking support, improving `/model` behaviour and high-effort reasoning for GLM-5.2. - **Fixes** — same-directory session switches now reuse imported extension modules (fresh instances + lifecycle events preserved); deep session branches no longer take quadratic time to build context; Markdown streaming code-fence rendering no longer flickers on partial closing fences; fuzzy `edit` matches preserve untouched line blocks instead of rewriting the whole file; `/model` hides Copilot models unavailable to the account and ranks exact provider-prefixed matches first. ### Docs: document `~/.config/devbox-shell/ssh-lan.conf` for naming LAN peers The host-owned, bind-mounted `~/.config/devbox-shell/ssh-lan.conf` is the intended place to add `ProxyJump host` overrides for **named** LAN peers (so `pi --ssh ` / `dssh ` route through the host), but it was only mentioned in `.env.example` and the `setup-lan-access.sh` header — never in the README. Added a "Naming LAN peers" subsection to the README troubleshooting block (plus a pointer from the SSH/ControlMaster section), and corrected the stale `setup-lan-access.sh` comment that suggested editing the read-only `~/.ssh/config` instead of `ssh-lan.conf`. ## v1.1.6 — 2026-06-19 Build provenance + reproducibility hardening, plus pi `0.79.7` → `0.79.8` (auto-resolved at build). Companion refs are auto-resolved to SHAs at build as before. ### Bumped: pi 0.79.7 → 0.79.8 Notable upstream changes (from [pi releases](https://github.com/earendil-works/pi/releases/tag/v0.79.8)): - **Selective provider base entry points** — SDK users can pair `@earendil-works/pi-ai/base` and `@earendil-works/pi-agent-core/base` with explicit provider registration to keep bundled apps from including unused provider transports. - **Mistral prompt caching** — Mistral sessions use provider-side prompt caching keyed on the pi session ID, with cached-token usage/cost accounting. - **Post-compaction token estimates** — compact results and compaction events now include estimated post-compaction token counts. - **OpenRouter Fusion alias** — `openrouter/fusion` available as a built-in OpenRouter model alias. ### Added - **Self-describing images: OCI labels + on-disk build manifest.** The variant build now records exactly which pi version and companion-repo commits were baked into each image. Previously the SHAs resolved by CI only ever reached the build log (which rotates), so a published tag was not reconstructable after the fact — confirming what shipped meant triangulating from `git`, `pi --version`, and extension source. - OCI labels: `org.opencontainers.image.{version,revision,created}` plus `se.jordbo.pi-devbox.{pi,pi-toolkit,pi-extensions,pi-fork,pi-obsmem,mempalace-toolkit,pi-studio}-*ref` — inspect with `docker inspect`. - `/etc/pi-devbox/build-manifest.json` written from **ground truth** (the actual checked-out `HEAD` of each `/opt` clone + live `pi --version`), not just the intended build-args, so it also exposes a clone that silently resolved to the wrong ref. The provenance ARGs are declared last so a changing `BUILD_DATE` never invalidates the expensive install/clone layers. - **`scripts/check-base-hash.sh` — base-rebuild invariant guard.** Every floating `ARG *_REF` consumed by `Dockerfile.base` must be folded into the `base_tag` hash, or a ref-only change won't trigger a base rebuild (the v1.1.2 mempalace-toolkit staleness footgun). The guard fails CI the moment someone adds an `ARG *_REF` to `Dockerfile.base` without folding it in; it runs in the `base-decide` job and locally. Smoke-test gained assertions for the manifest (present, no `"unknown"` components) and the OCI labels. - **Overridable companion repo URLs.** The three gitea-hosted companions (`pi-toolkit`, `pi-extensions`, `mempalace-toolkit`) gained `*_REPO` build-args defaulting to their canonical `gitea.jordbo.se` origin — matching the existing `PI_FORK_REPO` / `PI_OBSMEM_REPO` / `PI_STUDIO_REPO` pattern. A relocated or forked build can now repoint a companion at a mirror, another host, or a local path (`--build-arg PI_EXTENSIONS_REPO=...`) without editing the Dockerfiles. Defaults are unchanged, so the canonical CI build is byte-identical. ### Changed - **`resolve-versions` now fails loud instead of falling back to a floating branch.** Each pi-version / companion-ref lookup previously degraded to `main`/`master` on a transient API/network failure (`|| echo "main"`), silently shipping an unpinned ref that defeats both cache-busting and reproducibility. Resolution now validates each result is a 40-hex commit SHA (and pi a real semver) and aborts the release otherwise. ## v1.1.5 — 2026-06-18 Patch release: SSH ControlMaster read-only-socket fix + pi `0.79.6` → `0.79.7` (auto-resolved at build). The `pi-extensions` ref is auto-resolved to `main` HEAD at build, so the `ssh-controlmaster` fix below lands automatically. ### Fixed - **`pi --ssh ` no longer fails with "Read-only file system" when the user's `~/.ssh/config` sets a per-host `ControlPath` under the read-only `~/.ssh` mount** (e.g. the common CGNAT idiom `ControlPath ~/.ssh/cm/%r@%h:%p`). Root cause: SSH precedence means a user's per-host `ControlPath` always wins over the baked `/etc/ssh/ssh_config.d` default, so the master socket tried to bind under the RO `~/.ssh` and `ssh … pwd` exited 255 ("Could not resolve remote pwd"). The `ssh-controlmaster` extension (pulled from `pi-extensions` `main` via `PI_EXTENSIONS_REF`) now (a) resolves the remote pwd with a direct connection (`-o ControlPath=none -o ControlMaster=no`), and (b) tests whether the system `ControlPath` dir is actually writable — falling back to its own `/tmp` master (whose command-line `-o ControlPath` overrides the user's path) when it is not. OS-agnostic and independent of whether the user uses ControlMaster, so the majority of configs (no ControlMaster at all) are unaffected. ### Changed - **`setup-lan-access.sh` now renders the writable SSH sidecar (`~/.ssh-local/config`) on every host OS, not just VM-backed ones.** Previously the whole script no-oped on native Linux, so a Linux host that also bind-mounts `~/.ssh` read-only got no `ControlPath` redirect. The `ControlPath` redirect + `Include ~/.ssh/config` (and `dssh`/`dscp` usability) now work on Linux too; only the host-jump block (`Host host mac`), its key generation, and the authorize hints remain gated on VM-backed detection (`DEVBOX_LAN_ACCESS=auto`) or `=jump`. ### Bumped: pi 0.79.6 → 0.79.7 Notable upstream changes (from [pi releases](https://github.com/earendil-works/pi/releases/tag/v0.79.7)): - **Automatic theme mode** — `/settings` can choose separate light and dark themes and follow terminal color-scheme changes (`/` is now reserved in theme names for this). - **Self-only `pi update` by default** — bare `pi update` updates pi only; `pi update --all` updates pi and packages together. - **Extension API helpers** — `CONFIG_DIR_NAME` exported so extensions resolve project config paths without hardcoding `.pi`; edit-diff helpers (`generateDiffString`, `generateUnifiedPatch`, `EditDiffResult`) exported. - **Warp inline images** via Kitty graphics capability detection. - Fixes: RPC unknown-command errors now include the request id (clients no longer hang); `/model` autocomplete matches provider/model regardless of token order; tree navigator horizontally pans deep entries. ## v1.1.4 — 2026-06-17 Patch release: config and shell-quality fixes on a preserved volume. No pi version bump (still `0.79.6`, latest). The `pi-toolkit` ref is auto-resolved to `main` HEAD at build, so the AGENTS.md change below lands automatically. ### Added - **Global `AGENTS.md` auto-loads the pi-extensions skill.** `pi-toolkit` now ships `pi-global-AGENTS.md` and symlinks it to `~/.pi/agent/AGENTS.md` (pi's global-instructions file, loaded at every start). It directs the agent to read the `pi-extensions` skill at session start and carries a core fork/recall cheat-sheet, since on-demand skill description-matching was leaving `pi-fork` / `pi-observational-memory` under-utilised. **Heads-up:** on a preserved volume any pre-existing real `~/.pi/agent/AGENTS.md` is backed up to `*.bak.` and replaced by the symlink (same behavior as `keybindings.json`). - **`settings.json` merge-on-recreate.** The bootstrap only ever copied the template when `settings.json` was *absent*, so a file on a preserved volume never picked up config added in a later image (e.g. the `observational-memory` / `pi-fork` blocks, a newly-enabled model). The entrypoint now deep-merges the template into an existing `settings.json` on start with `jq -s '.[0] * .[1]'` (template first, live second): the user's values always win and only *missing* keys are filled in. Arrays are treated as leaves (a model the user removed is not re-added); the file is only rewritten when the merge changes something, the original is backed up first, and invalid JSON on either side is skipped rather than clobbered. Opt out with `PI_SETTINGS_MERGE=0`. ### Fixed - **bash history loss in nested / tmux shells.** The `DEVBOX_HIST_SET` guard that installs the per-prompt `history -a` flush was `export`ed, so it leaked into child processes. Any nested shell — crucially each tmux pane, which inherits the tmux server's env — saw the guard already set and skipped installing `history -a`, persisting history only on a clean exit. Abrupt termination (`docker stop`, `tmux kill-server`, SIGKILL) then silently lost that shell's in-memory history. The guard is now shell-local (no `export`), so every new interactive shell re-installs its own flush. `zoxide` was less affected (its hook is unguarded and writes immediately). History and zoxide storage were never the issue — `~/.cache/bash` (`devbox-shell-history`) and `~/.local/share/zoxide` (`devbox-zoxide`) are persistent named volumes. **Note:** existing shells/panes keep the old behavior until restarted (`tmux kill-server` or open fresh shells). ### Maintainer - `scripts/recreate-sanity-check.sh` gained assertions for the new wiring: the `~/.pi/agent/AGENTS.md` symlink, a nested login shell installing `history -a`, and `settings.json` carrying the `observational-memory` + `pi-fork` blocks after recreate. --- ## v1.1.3 — 2026-06-16 Patch release: pi `0.79.4` → `0.79.5` (auto-resolved at build). ### Bumped: pi 0.79.4 → 0.79.5 Notable upstream changes (from [pi releases](https://github.com/earendil-works/pi/releases/tag/v0.79.5)): - **Provider-scoped API key environments** — `auth.json` API key entries can now include `env` overrides for provider-specific Cloudflare, Azure OpenAI, Google Vertex, Amazon Bedrock, cache retention, and proxy settings without changing the project shell. - **Global HTTP proxy setting** — configure `httpProxy` once in global settings to apply `HTTP_PROXY` / `HTTPS_PROXY` to Pi-managed HTTP clients. - **Vercel AI Gateway attribution** — requests now include Pi attribution headers by default. - **Fixes:** inherited OpenAI Responses streaming tolerates null message content before tool calls; DeepSeek V4 thinking no longer sends both `thinking` and `reasoning_effort`; device-code login no longer auto-opens the browser; various Google/Vertex Gemini model metadata corrections; session selector empty-state fix; Cursor Up history navigation fix. --- ## v1.1.2 — 2026-06-15 Patch release: pi `0.79.3` → `0.79.4` (auto-resolved at build), plus the build-plumbing fix, maintainer tooling, and docs accumulated since v1.1.1. ### Changed - **`mempalace-toolkit` is now CI-resolved to a commit SHA**, closing a silent-staleness footgun. It is the only companion cloned in `Dockerfile.base` (all others are cloned in `Dockerfile.variant`), so it was never run through the `resolve-versions` → build-arg plumbing. Its ref stayed a literal `main`, and because the base only rebuilds when the hash of `Dockerfile.base + rootfs/* + entrypoints` changes, a toolkit-only fix would *not* land in the image unless `Dockerfile.base` itself happened to change (as it did, incidentally, in v1.1.1). Now `resolve-versions` resolves `mempalace-toolkit` `main` HEAD to a SHA (new `mempalace_toolkit_ref` output), `base-decide` folds that SHA into the base-tag hash (so a moved toolkit forces a base rebuild), and `build-base` passes it as `--build-arg MEMPALACE_TOOLKIT_REF`. The base clone switched from `git clone --branch` to a SHA-capable `git fetch + checkout FETCH_HEAD` (the `--branch <40-char-SHA>` footgun previously fixed in `Dockerfile.variant`, run 374). Note: `base-decide` now depends on `resolve-versions`, so the base tag reflects a live gitea API lookup. On an API blip it falls back to `main` — which hashes differently than a SHA and triggers one *extra* rebuild, never a *missed* one (fail-toward-rebuild). ### Added (maintainer tooling, no image change) - **`scripts/recreate-sanity-check.sh`** — runtime post-recreate sanity check; the runtime peer of `smoke-test.sh`. Where `smoke-test.sh` runs at build time with `--entrypoint=""` (and so can never see persisted volumes or the entrypoint's runtime deploy), this verifies what is actually live in the container *after* `docker compose up -d --force-recreate`: persisted named volumes survived, the pi runtime wiring is intact (keybindings symlink, ≥4 extensions, `mempalace.ts` bridge, `settings.json`, and pi-fork / pi-observational-memory / pi-studio registrations), `/tmp/sshcm` is mode 700, shell defaults re-seeded, and `/opt` toolkits intact. Variant (studio/plain) auto-detected via `/opt/pi-studio`. Since pi is built from `latest` (no concrete Dockerfile pin), the version check asserts only when `--expected-version` is passed, else WARNs. Not baked into the image — repo/maintainer tooling, same category as `smoke-test.sh`. A short-name wrapper (`pi-devbox-sanity`) lives in `cli_utils/bin`, kept separate from opencode-devbox's `devbox-sanity` so hosts with only one devbox checked out stay self-contained. ### Docs (no image change) - Correct the MemPalace `diary_write` anyOf workaround watch-target in `Dockerfile.base`: upstream PR #1735 was **closed unmerged** (2026-06-11), so the old “remove once #1735 ships” TODO pointed at a dead PR. Issue #1728 is still open; PR #1717 is the current live candidate; mempalace PyPI latest is still 3.4.0 (== our pin), so the workaround stays. Removal trigger is now a PyPI release > 3.4.0 that actually strips the root-level anyOf. - Document the post-recreate sanity check: AGENTS.md release-day checklist (step 3) now runs `scripts/recreate-sanity-check.sh` inside the recreated container, and README gains a "Post-recreate sanity check" subsection alongside the build-time smoke-test note. --- ## v1.1.1 — 2026-06-13 Patch release: pi `0.79.1` → `0.79.3` (auto-resolved at build) plus the mempalace-mcp hang fix below. ### Fixed - **`mempalace-mcp` no longer hangs the pi TUI uninterruptibly.** When the palace is bind-mounted from the macOS host (OrbStack virtiofs) and the container opened a large `chroma.sqlite3` for the first time, a cold storage open / HNSW load could stall the server before it emitted its JSON-RPC response. The awaiting promise then hung forever and the TUI froze — ESC cancels the LLM stream, not a pending MCP tool call, so there was no way out short of `docker exec pkill -9 -f mempalace-mcp` and restarting pi. The fix lives in the `mempalace.ts` pi extension shipped by **mempalace-toolkit** (cloned into the base at build time via `MEMPALACE_TOOLKIT_REF`, default `main`): the JSON-RPC client now arms a **per-request** timeout. On expiry it rejects the request *and* kills the stalled child (SIGTERM→SIGKILL), so pi surfaces an error instead of hanging; the bridge then marks itself unavailable so subsequent calls fail fast (restart pi to retry). This is deliberately per-REQUEST, not a process-lifetime `timeout 60 mempalace-mcp` wrapper — the long-lived server is only killed when a request genuinely stalls. Tunables (env): `MEMPALACE_MCP_TIMEOUT_MS` (tool-call timeout, default `60000`), `MEMPALACE_MCP_INIT_TIMEOUT_MS` (initialize/tools-list handshake, default `120000`); set either to `0` to disable. Requires a base rebuild to pull the updated extension. The earlier plan of a standalone Python stdio-watchdog shim was dropped: the extension already owns request/response correlation, so a separate framing-reparsing shim is unnecessary. Still open (out of scope here): sharing one palace across harnesses ideally wants a single host-side `mempalace-mcp` daemon multiplexing stdio over a UNIX socket, so all clients share one writer on native APFS rather than each cold-opening over virtiofs. `mempalace-mcp` that applies a per-request timeout and kills the child on stall, **without** killing the long-lived server itself (a naive `timeout 60 mempalace-mcp` wrapper is wrong — it kills the server mid-session). Sharing the palace across harnesses (native pi, container pi, opencode) remains the goal — isolated palaces defeat the point. Longer term: run a single mempalace-mcp daemon on the host and multiplex stdio over a UNIX socket so all clients share one writer on native APFS. ### Added - **`dot-watch` helper** (`/usr/local/bin/dot-watch`) — auto-rerenders a Graphviz `.dot` file to PNG on every save via mtime polling (no `inotify` dependency). pi-studio renders Mermaid natively but has no DOT renderer; since its markdown preview displays local PNG/JPG/GIF/WEBP images, this closes the loop for Graphviz: edit `.dot` → `dot-watch` regenerates `.png` → Studio *refresh-from-disk* shows the update. `graphviz` was already in the base image, so no new package. Baked into `Dockerfile.base` following the `studio-expose` pattern; documented in the README Studio section. ## v1.1.0 — 2026-06-10 ### Added — `:latest-studio` variant - **New `-studio` image variant** bundling [pi-studio](https://github.com/omaclaren/pi-studio) — a two-pane browser workspace (prompt/response editor, live KaTeX/Mermaid preview, tmux-backed literate REPLs for Shell/Python/IPython/Julia/R/GHCi/Clojure) plus the `/studio` slash command and `studio_repl_send` / `studio_export_*` agent tools. Published as `:latest-studio` and `:vX.Y.Z-studio` (multi-arch). - pi-studio is **vendored to `/opt/pi-studio`** at build time (gated by `INSTALL_STUDIO=true`, ref pinned via CI-resolved `PI_STUDIO_REF`) and registered on container start by `entrypoint-user.sh` via `pi install /opt/pi-studio` — the same pattern as pi-fork / pi-observational-memory. No build step: pi-studio ships its browser bundle prebuilt in git. The non-studio `:latest` image is unchanged. - CI gains independent `smoke-studio` + `build-variant-studio` jobs that gate **only** the studio tags, so a studio build/smoke failure can never block the core `:latest` / `:vX.Y.Z` release. - `STUDIO_PORT=8765` baked as an advisory default. - **`studio-expose` helper + `socat` (base).** Because pi-studio binds the container's loopback, a published Docker port can't reach it. The new `studio-expose` helper (socat, added to the base) bridges the container's loopback to its egress interface on the same port; set `STUDIO_EXPOSE=1` in compose to auto-start it on boot (default off — Studio stays loopback-only otherwise). `socat` is in the base for all variants. - **README "Using pi-studio" section.** Documents the container access reality: pi-studio hard-binds `127.0.0.1` inside the container (`.listen(port,"127.0.0.1")`, no `--host` flag), so a plain `-p` publish does not reach it. Documents the two working paths — host networking (recommended on OrbStack) and a loopback bridge for bridge networking — plus the remote `ssh -L` forward and the **mosh caveat** (mosh cannot forward ports; run a parallel `ssh -L` alongside it). ## v1.0.1 — 2026-06-10 Patch release. Works around an upstream MemPalace bug that broke pi at first prompt against the Anthropic Claude API. ### Fixed - **`mempalace_diary_write` schema rejected by Anthropic API.** Mempalace 3.3.x and 3.4.0 advertise `diary_write`'s `input_schema` with a top-level `anyOf: [{required:[entry]}, {required:[content]}]` to express "either `entry` or `content` must be supplied". Anthropic's tools API rejects top-level `anyOf` / `oneOf` / `allOf` outright, so pi failed to register tools at session start with `tools..custom.input_schema: input_schema does not support oneOf, allOf, or anyOf at the top level`. `Dockerfile.base` now patches the installed `mcp_server.py` after `uv tool install` to drop the `anyOf` block and require `["agent_name", "entry"]` instead. The mempalace handler still accepts `content` server-side as a kwarg alias, so callers using either name keep working. Tracked upstream: [issue #1728](https://github.com/MemPalace/mempalace/issues/1728), [PR #1735](https://github.com/MemPalace/mempalace/pull/1735). The workaround is idempotent + self-deactivating and will be removed once a fixed mempalace release lands on PyPI. ### Changed - **Mempalace pinned to 3.4.0** via `MEMPALACE_VERSION` build arg. Future bumps must be a reviewable diff rather than an implicit pull of `latest` (the broken 3.3.x/3.4.0 schema slipping in unannounced is what caused this release). ## v1.0.0 — 2026-06-09 **Decoupled from opencode-devbox.** pi-devbox is now self-contained: own `Dockerfile.base` + `Dockerfile.variant`, own CI pipeline, own release cadence. Previously v0.79.0 and earlier were thin re-brands of the `pi-only` variant built by opencode-devbox CI. ### Architectural - **Self-contained build chain.** `Dockerfile.base` produces `joakimp/pi-devbox:base-` (content-addressed); `Dockerfile.variant` FROMs the base and adds the pi install. Replaces the prior 5-line `Dockerfile` shim that FROMed `joakimp/pi-devbox:base-pi-only` (an opencode-devbox CI artifact). - **No more publish-ordering coupling.** pi-devbox releases no longer require rebuilding opencode-devbox first. - **Adapted from opencode-devbox** at the time of decoupling — the apt set, ssh ControlMaster setup, MemPalace integration, entrypoint UID/GID dance, and CI pipeline shape are all derived from there. See Acknowledgements in README.md. - **CI workflow** rewritten as two-phase split-base build pipeline (mirrors opencode-devbox's `docker-publish-split.yml` shape, simplified to a single variant). Includes `crane`-based `base-latest` promotion, registry-buildcache footgun guard via concrete `PI_VERSION` resolution, and the c6f9d11 smoke-test gate (waits for keybindings + mempalace.ts + ≥4 *.ts before sampling). ### Added (base image) - **pandoc** — universal Markdown↔HTML/Org/RST/etc. conversion. ~200 MB. - **graphviz** — `dot` rendering for diagram pipelines. ~10 MB. - **imagemagick** — image conversion (invoked as `magick`, not `convert`, in v7+). ~50 MB. - **yq** — YAML-aware companion to jq. - **tldr (tealdeer)** — Rust port of tldr-pages, ~5 MB static binary. Replaced the Node `tldr` global (which was ~140 MB). - **`/etc/tmux.conf`** with `set -g base-index 0` + `set -g pane-base-index 0`. Required for the planned `:latest-studio` variant; pi-studio hard-codes its tmux send target to `:0.0`. User- level `~/.tmux.conf` overrides still win. ### Added (smoke test) - Asserts pandoc, graphviz, imagemagick, yq, and tldr are present. - Asserts `/etc/tmux.conf` has the 0-indexed config baked. - Asserts `/tmp/sshcm/` directory created mode 700 by entrypoint. - Image-size measurement now sums `docker history` layer sizes (the prior `image inspect --format='{{.Size}}'` approach returned only the variant-unique layer when the base was content-addressed and shared, understating the user-facing image size by 2+ GB). - Size threshold raised to 3500 MB (was 2850) to cover the new base additions plus +200 MB safety margin. Tighten in a follow-up release once amd64 actuals settle. ### Image size Local arm64 build of `pi-devbox-test:latest` (this branch's content): 3.20 GB. Up ~390 MB from the prior pi-only-equivalent (~2.81 GB) due to pandoc, graphviz, imagemagick, yq, and minor expansion in pi npm dependencies. ### Migration notes - Existing volumes (`devbox-pi-config`, `devbox-bash-history`, `devbox-nvim-data`, `devbox-uv-tools`, `devbox-chroma-cache`) are unchanged in name and structure. `docker compose pull && docker compose up -d --force-recreate` is a clean upgrade path. - The `:latest` and `vX.Y.Z` Hub tags continue to point at a "base + pi" image. Same shape, just built differently. - `:base-pi-only` and `:base-pi-only-vX.Y.Z` tags from prior releases remain on Hub for now; will be deprecated when opencode-devbox retires the pi paths in its next major release. ### Future work - v1.1.0: `:latest-studio` variant (adds [pi-studio](https://github.com/omaclaren/pi-studio)). - v1.3.0: `:latest-studio-tex` variant (adds texlive-xetex for PDF export). ## v0.79.0 — 2026-06-08 First build on pi **`0.79.0`** (upstream `@earendil-works/pi-coding-agent` bump from `0.78.1`). Built `FROM` the freshly republished `joakimp/pi-devbox:base-pi-only` from opencode-devbox `v1.16.2`, which carries pi `0.79.0` (and picks up opencode `1.16.2` in the sibling opencode-bearing variants, though this pi-only image has no opencode). ### Bumped: pi 0.78.1 → 0.79.0 Resolved from the tag and asserted by the smoke base-freshness guard (`EXPECTED_PI_VERSION`). Highlights from the upstream `CHANGELOG.md`: - **Project trust for local inputs** — pi now asks before loading project-local settings, resources, instructions, and packages, with saved decisions and `--approve` / `--no-approve` controls for non-interactive modes, plus a `project_trust` extension event so global/CLI extensions can decide or defer. - **Cache-hit visibility in the footer** — the interactive footer shows the latest prompt cache hit rate (`CH`). - **Richer SDK/RPC extension surfaces** — public exports now include RPC extension UI request/response types and package asset path helpers. - Plus a large batch of TUI and provider fixes (Kitty keyboard fallback, prompt-history cursor placement, large-JSONL session reads, custom-provider routing). ### Smoke size threshold 2750 → 2850 MB Tracks opencode-devbox's `pi-only` variant, which was raised to 2850 MB in `v1.16.2` for headroom against the pi `0.79.0` bump (and routine apt drift). Kept in lockstep so this image's guard matches its source-of-truth variant. ## v0.78.1 — 2026-06-04 First build on pi **`0.78.1`** (upstream `@earendil-works/pi-coding-agent` bump from `0.78.0`). Built `FROM` the freshly republished `joakimp/pi-devbox:base-pi-only` from opencode-devbox `v1.15.13e`, which carries pi `0.78.1` plus the LAN-jump key-persistence work and the `devbox-ssh-local` volume ownership fix. Adds compose/env documentation in this repo. ### Added: persist the LAN-jump key + one-line authorize hint - **compose:** persist `~/.ssh-local` via a new `devbox-ssh-local` named volume so the generated LAN-jump key survives `docker compose up --force-recreate`. You authorize the key on the host **once per machine** instead of after every container update. - **Inherited from base:** `setup-lan-access.sh` now prints a copy-paste `echo '…' >> ~/.ssh/authorized_keys` line when it generates a new key (published via opencode-devbox's `base-pi-only`). No helper file to locate. ### Docs: document optional host-owned config in the compose + env templates - **compose:** added a commented-out `~/.config/devbox-shell` bind mount with a note — the image's `~/.bash_aliases` sources `~/.config/devbox-shell/bash_aliases` if present, and `setup-lan-access.sh` reads `~/.config/devbox-shell/ssh-lan.conf` for named-peer `ProxyJump host` overrides (reach LAN peers by name via `dssh `). - **.env.example:** documented `DEVBOX_HOST_ALIAS` (host hostname to reach, default `host.docker.internal`) so getting-started is self-contained. Template/example comments only; no behavior change. ## v0.78.0c — 2026-06-04 ### Fixed / Added (inherited from the base via `FROM`) LAN-access improvements made in opencode-devbox's `setup-lan-access.sh` (baked into the `base-pi-only` image, published by opencode-devbox v1.15.13d) flow through to pi-devbox automatically — no pi-devbox source change. Built `FROM` the rebuilt `joakimp/pi-devbox:base-pi-only` (digest `83b45335…`): - **Fixed:** the generated `~/.ssh-local/config` had `Include ~/.ssh/config` scoped to the `host`/`mac` block, so `dssh ` by name was ignored. - **Fixed:** read-only `~/.ssh/cm` ControlPath broke multiplexed hosts (`pmx-jh`, `proxmox*`, …); master sockets now use the writable sidecar. - **Added:** host-owned `~/.config/devbox-shell/ssh-lan.conf` for named-peer `ProxyJump host` overrides (Included before `~/.ssh/config`). - **Added:** `DEVBOX_LAN_AUTOJUMP_PRIVATE=1` — ProxyJump any RFC1918 IP through the host for roaming laptops. ## v0.78.0b — 2026-06-03 Container-level rebuild on pi `0.78.0` (unchanged): re-brands the pi-only build as a thin `FROM joakimp/pi-devbox:base-pi-only`, inheriting fork/recall and host-OS-agnostic LAN access. Letter-suffix release (pi version unchanged). ### Changed: refactored to re-brand the opencode-devbox `pi-only` variant pi-devbox no longer installs pi itself. The `Dockerfile` is now a thin `FROM joakimp/pi-devbox:base-pi-only` (overridable via the `BASE_IMAGE` arg), inheriting pi + pi-toolkit + pi-extensions and all base tooling from the single source of truth. This eliminates the install-logic duplication that used to drift against `opencode-devbox/Dockerfile.variant`. The pi-only artifact is **built** by opencode-devbox's CI (from `opencode-devbox/Dockerfile.variant` with `INSTALL_OPENCODE=false`) but is **published into this repo** as the internal building-block tag `joakimp/pi-devbox:base-pi-only` (+ `base-pi-only-vX.Y.Z`, where `vX.Y.Z` is the opencode-devbox release version). This supersedes the brief approach of publishing it as `opencode-devbox:latest-pi-only` — an "opencode-devbox" tag with no opencode in it confused users. `base-pi-only` is internal; end users pull `joakimp/pi-devbox:latest` or a `vX.Y.Z` tag. The pi-only build uses `INSTALL_OPENCODE=false`, so this image stays lean and pi-focused — it does **not** carry opencode, and remains distinct from `opencode-devbox:latest-with-pi` (which has both). ### Added (inherited from the pi-only variant) - **`fork` tool** (pi-fork) and **`recall` tool** (pi-observational-memory), baked into `/opt` with `node_modules` and registered at runtime. - **Host-OS-agnostic LAN access**: on VM-backed hosts (macOS OrbStack / Docker Desktop) the entrypoint sets up the host as an SSH jump to reach LAN peers (`dssh` alias; `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER` env). No-op on native Linux. See the opencode-devbox README for details. ### Consequences / notes - **Publish ordering**: release opencode-devbox first so `base-pi-only` carries the target pi version, *then* tag this repo. The smoke test asserts `pi --version` matches the tag and fails loudly if the base is stale. - CI no longer passes `PI_VERSION` as a build-arg (the Dockerfile installs nothing); it still resolves the tag version to feed the smoke base-freshness guard. Smoke size threshold 2200 → 2750 MB (now tracks the pi-only variant). _pi version unchanged at `0.78.0` (still latest)._ ## v0.78.0 — 2026-05-29 pi `0.77.0` → `0.78.0` bump (first container build on the pi 0.78 line, published upstream 2026-05-29). Built against `joakimp/opencode-devbox:base-latest` (unchanged from the v0.77.0 build). ### Bumped: pi 0.77.0 → 0.78.0 **New Features** - **Named startup sessions** — `--name` / `-n` sets the session display name before startup across interactive, print, JSON, and RPC modes. - **Clickable file tool paths** — built-in file tool titles render OSC 8 `file://` hyperlinks when the terminal supports them, including supported tmux clients. **Added** - Exported `convertToPng` for extension authors. - Exported `parseArgs` and type `Args` for extension authors. - Added a resume command hint when exiting interactive sessions. - Added custom Amazon Bedrock request header support. **Fixed** - Fixed early interactive input typed before the prompt loop starts so it is buffered instead of dropped. - Fixed OpenRouter Moonshot Kimi K2.6 requests to use `system` instead of unsupported `developer` messages. - Fixed OSC 8 hyperlinks to pass through tmux when the client supports them. - Fixed ANSI text wrapping to avoid stack overflows on very long wrapped lines. - Fixed OpenAI Codex Responses SSE streams to abort response body reads after terminal events. ## v0.77.0 — 2026-05-29 pi `0.76.0` → `0.77.0` bump (first container build on the pi 0.77 line, published upstream 2026-05-28). Built against `joakimp/opencode-devbox:base-latest` (unchanged from the v0.76.0 build — same SSH-CM, gitleaks, git-crypt baked in). ### Bumped: pi 0.76.0 → 0.77.0 Notable upstream changes (from pi's CHANGELOG): - **Claude Opus 4.8 support** — Anthropic Opus 4.8 model metadata + adaptive-thinking coverage updated. - **Selective tool disablement** — `--exclude-tools` / `-xt` disables specific built-in, extension, or custom tools while leaving the rest available. - **Headless Codex subscription login** — `/login` can use device-code auth for ChatGPT Plus/Pro Codex subscriptions; browser login remains the default. - **Streaming-aware extension input** — `InputEvent.streamingBehavior` lets extensions distinguish idle prompts from mid-stream steers and queued follow-ups. - **Bugfixes** — startup timing output excludes `createAgentSessionRuntime` work; OpenRouter DeepSeek V4 `xhigh` reasoning preserves OpenRouter's native effort; SIGTERM/SIGHUP exits run extension `session_shutdown` cleanup; keyboard protocol negotiation ignores delayed terminal responses (no false Kitty detection); Windows MSYS2 ucrt64 startup crash fixed via napi-rs 3.x clipboard addon; API-key/header config resolution treats plain strings as literals with `$ENV_VAR` / `${ENV_VAR}` interpolation and `$!` escaping; session disposal aborts in-flight agent/compaction/branch-summary/retry/bash work; `pi.getAllTools()` exposes per-tool `promptGuidelines`; OpenAI Codex Responses replay after switching from Anthropic extended-thinking sessions; Anthropic-compatible replay supports `allowEmptySignature` for providers returning empty thinking signatures; OpenAI/OpenRouter GPT-5.5 Pro thinking levels limited to supported efforts; OpenCode Go Kimi K2.6 thinking-off requests; Xiaomi Token Plan model metadata cleaned of unsupported variants; follow-up messages queued by `agent_end` extension handlers drain before idle; system prompt tool-selection guidance avoids unavailable file-exploration tools; fenced `diff` highlighting restored. Workflow continues to derive `PI_VERSION` from the git tag (`v0.77.0` → `0.77.0`) and pass it as a build-arg per the v0.75.5b cache-hit fix; smoke test asserts `pi --version` matches. ### Inheritance from base No base change in `joakimp/opencode-devbox:base-latest` since v0.76.0 — the v1.15.12 opencode-devbox release also reused the unchanged base. SSH ControlMaster on a writable socket path, gitleaks, and git-crypt continue to ride along from the base. ### CI This is the second pi-devbox release exercising the cache-export-disabled workflow (after v0.76.0's clean publish on run #340) and the first to also exercise the 3-attempt retry wrapper added in 2d39766 along the publish path. ## v0.76.0 — 2026-05-28 pi `0.75.5` → `0.76.0` bump (first minor-version release on pi 0.76 line, published upstream 2026-05-27 20:03 UTC). Built against a fresh `joakimp/opencode-devbox:base-latest` which now bakes in SSH ControlMaster on a writable socket path, plus gitleaks and git-crypt — see the inherited-from-base notes below for details on each. ### Bumped: pi 0.75.5 → 0.76.0 Notable upstream changes (from pi's CHANGELOG): - **Explicit session IDs for automation** — `--session-id ` lets scripts create or resume an exact project-local session. - **RPC bash output can stay out of model context** — RPC clients can pass `excludeFromContext` to `bash` for commands whose output should not be sent with the next prompt. - **More predictable provider retries and timeouts** — Codex WebSocket/SSE waits are bounded; `retry.provider.maxRetries` controls provider retries instead of hidden SDK defaults; SDK retries default to 0; quota/billing 429s are no longer retried behind Pi's retry handling. - **Better terminal editing across environments** — Apple Terminal Shift+Enter detection on macOS, Windows Terminal OSC 8 hyperlink support, JetBrains truecolor with disabled OSC 8, Unicode-aware word navigation and deletion. - **Bugfixes** — `pi update` bypasses npm/pnpm/Bun minimum-release-age gates; user-authored ordered-list markers preserved in transcripts; image attachment token estimates aligned with tool-result images; Codex Responses cache-affinity header fixed (`session-id` not `session_id`); OpenRouter/Poolside context-overflow detection; managed npm extension updates avoid peer-dependency conflicts; RpcClient handles unexpected child exits cleanly. Workflow continues to derive `PI_VERSION` from the git tag (`v0.76.0` → `0.76.0`) and pass it as a build-arg, per the v0.75.5b cache-hit fix; smoke test asserts `pi --version` matches. ### Workflow change: registry cache-export disabled - **`.gitea/workflows/docker-publish.yml`** — `cache-from`/`cache-to` removed from the `publish` step. buildkit's `mode=max` cache-export to `registry-1.docker.io` reproducibly returns HTTP 400 on the resumable-upload PUT, surfacing ~2026-05-23. Diagnosed during opencode-devbox v1.15.12's manual host-side publish: image push works fine, only `--cache-to` fails. See opencode-devbox CHANGELOG v1.15.12 `Unreleased` for the full root-cause analysis. The pi-devbox Dockerfile is single-stage with a tiny diff (npm install pi only) on top of `base-latest`, so builds are fast even without cache (~30-60s expected). ### Inherited from opencode-devbox base: SSH ControlMaster on a writable socket path No Dockerfile change here — just a note that this release picks up the system-wide SSH ControlMaster default (`/etc/ssh/ssh_config.d/00-devbox-controlmaster.conf` → `ControlPath /tmp/sshcm/%r@%h:%p`, `ControlMaster auto`, `ControlPersist 10m`). This unblocks `ssh` and `pi --ssh user@host` from inside the container when `~/.ssh` is bind-mounted read-only from the host (the standard pi-devbox compose layout) — previously, OpenSSH's default `ControlPath` under `~/.ssh/cm/` was unwritable, so multiplexing failed with `unix_listener: cannot bind ... Read-only file system` and ssh fell back to fresh TCP connections, which on residential CGNAT manifested as banner-exchange timeouts. The fix is purely additive (per-container `/tmp/sshcm` dir, mode 700, created by entrypoint) and user `~/.ssh/config` per-host overrides still win because Debian's stock `ssh_config` sources `ssh_config.d/*.conf` before its own `Host *` block. See opencode-devbox CHANGELOG `v1.15.12` for the base-side details. ### Inherited from opencode-devbox base: gitleaks + git-crypt No Dockerfile change here — just a note that this release includes `gitleaks` (newly added to the base) and `git-crypt` (was always installed via apt; just wasn't called out). Both are useful inside the container for repos that use a gitleaks pre-commit hook or git-crypt-encrypted canonical config and don't want host-side dependencies. See opencode-devbox CHANGELOG `v1.15.12` for the base-side details. ## v0.75.5b — 2026-05-23 Recovery release fixing a **silent cache-hit regression** discovered in the v0.75.5 image. All four releases v0.74.0 through v0.75.5 had been shipping the same image bytes because the Dockerfile's `npm install -g @earendil-works/pi-coding-agent` (bare, when `PI_VERSION=latest`) produces an identical layer-hash across builds. Combined with the registry buildcache, Docker reused the layer from whatever pi version was current when the cache was first populated. Verification: `docker manifest inspect joakimp/pi-devbox:vX.Y.Z` showed identical SHA256 digests on both `linux/amd64` and `linux/arm64` for v0.74.0, v0.75.3, v0.75.4, v0.75.5. Users on `:latest` were getting whatever pi version was baked into the v0.74.0 build (probably 0.74.0 itself). - **Workflow fix:** Both `smoke` and `publish` jobs now derive `PI_VERSION` from `github.ref_name` (e.g. `v0.75.5b` → `0.75.5`) and pass it as a build-arg. The Dockerfile's existing `if PI_VERSION=latest` branch never fires in CI now — always takes the `@${PI_VERSION}` branch — so the layer-hash includes the version and cache invalidates correctly. - **Smoke test:** New `run_expect` helper asserts `pi --version` output contains `EXPECTED_PI_VERSION` (passed from the resolve step). Would have caught this regression on v0.75.3 if it had existed. - **Dockerfile:** Comment added above `ARG PI_VERSION=latest` documenting the cache-hit footgun and pointing at the workflow's resolve step + AGENTS.md gotcha. - **AGENTS.md:** New convention bullet explaining the cache-hit class of bug and noting the latent same-bug in opencode-devbox's `with-pi` variants (currently masked by OPENCODE_VERSION bumps). No image-side changes vs v0.75.5 *intent* — this build will produce the actual pi 0.75.5 image content that v0.75.5 was supposed to ship. ## v0.75.5 — 2026-05-23 pi `0.75.4` → `0.75.5` bump (one upstream patch release, two days after v0.75.4). Notable upstream changes (from pi's CHANGELOG): - Cleaner read tool output (collapsed cards show only the read line; Ctrl+O expands). - Faster file tools on Windows (async fs ops during streaming, image resize off the main TUI thread). - More reliable package updates (`pi update` reconciles git-pinned refs without losing settings). - Custom Anthropic-compatible adaptive thinking via `compat.forceAdaptiveThinking`. - Several bash/read tool card display fixes; macOS Bun clipboard sidecar resolution; per-session OpenCode-Zen routing headers; Amazon Bedrock token cap fix. Plus a new pi 0.74.2 rescue release advising Node 20 users to upgrade Node before going to newer Pi versions — the devbox base image runs newer Node so this doesn't affect us, but worth noting for users running pi outside the devbox. - **Bump:** pi `@earendil-works/pi-coding-agent@0.75.5` baked at `/usr/bin/pi` (via `PI_VERSION=latest` resolving to 0.75.5 at build time — no Dockerfile change needed). - No image-side changes from v0.75.4 beyond the pi npm version. Built on `joakimp/opencode-devbox:base-latest` which itself is unchanged (cache-hit on `base-35ee5fe7861a` since v1.14.50b). ## v0.75.4 — 2026-05-21 pi `0.75.3` → `0.75.4` bump (one upstream patch release). Plus the AGENTS.md documentation-drift sweep clause that landed on `main` between v0.75.3 and now. - **Bump:** pi `@earendil-works/pi-coding-agent@0.75.4` baked at `/usr/bin/pi` (via `PI_VERSION=latest` resolving to 0.75.4 at build time — no Dockerfile change needed). - **AGENTS.md:** documentation drift sweep as explicit pre-commit workflow step (commit `ae6253a`). Companion clause added across the wider repo set the same day. - No image-side changes beyond the pi npm version. Built on `joakimp/opencode-devbox:base-latest` which itself is unchanged (cache-hit on `base-35ee5fe7861a` since v1.14.50b). ## v0.75.3 — 2026-05-18 pi `0.74.0` → `0.75.3` bump (one upstream minor + three patch releases since the initial pi-devbox release on 2026-05-14). - **Bump:** pi `@earendil-works/pi-coding-agent@0.75.3` baked at `/usr/bin/pi` (via `PI_VERSION=latest` resolving to 0.75.3 at build time). - No image-side changes from the v0.74.0 baseline beyond the pi npm version. The pi-toolkit + pi-extensions clones, mempalace bridge symlink, and `NPM_CONFIG_PREFIX` named-volume setup all unchanged. ## v0.74.0 — 2026-05-14 Initial release. - pi `@earendil-works/pi-coding-agent@0.74.0` baked at `/usr/bin/pi` - pi-toolkit and pi-extensions cloned at build time; deployed to `~/.pi/agent/` by entrypoint on container start - mempalace bridge (`mempalace.ts`) symlinked from `/opt/mempalace-toolkit/` - Built on `joakimp/opencode-devbox:base-latest`