pandoc's bundled typst template defaults the document font to an empty
tuple (font: ()), so a naked `pandoc --pdf-engine=typst` fails with
"font fallback list must not be empty". Patch the template default to
Libertinus Serif (typst's own bundled default) at build time so PDF
export works out of the box. Document usage in README and note the fix
in CHANGELOG (Unreleased). Base-affecting.
pandoc has been in the base since v1.0.0 but only as a front-end; PDF
export (studio_export_pdf / pandoc -o out.pdf) failed with 'xelatex not
found' because no back-end engine was installed. Ship typst (~30 MB
static Rust binary, no LaTeX) as the default engine via
`pandoc --pdf-engine=typst`, chosen over a ~600 MB TeX Live install.
texlive-xetex remains the higher-fidelity install-on-demand fallback.
- Dockerfile.base: install typst (latest GitHub-release idiom, pin via
TYPST_VERSION); add xz-utils (typst ships .tar.xz); bump
BASE_REBUILD_DATE. Lands in base-<hash>.
- smoke-test.sh: verify typst + a real pandoc --pdf-engine=typst render.
- README/AGENTS/CHANGELOG: typst now shipped (supersedes the planned
:latest-studio-tex variant).
No tag pushed — CI build intentionally deferred.
PDF export from Studio/pandoc still isn't shipped. Record the engine decision
in the living docs (README + AGENTS): pandoc is in the image but has no PDF
back-end, so export fails with 'xelatex not found'. Prefer a lightweight engine
— typst (~30 MB static binary, 'pandoc --pdf-engine=typst') which is small
enough it could ship in base rather than needing a separate ':latest-studio-tex'
variant; texlive-xetex (~600 MB) kept as the higher-fidelity fallback. Also drop
stale 'v1.3.0' pins (v1.3.0 already shipped without PDF) in favour of 'a future
release'. CHANGELOG history left untouched.
Wire the shared-palace option (implemented in mempalace-toolkit's mempalace.ts)
into the container:
- .env.example: document MEMPALACE_REMOTE_URL / MEMPALACE_REMOTE_TOKEN (env_file-only,
per this repo's convention).
- docker-compose.mempalace.yml: optional shared server (mempalace-mcp --transport http),
loopback-bound by default.
- docker-compose.yml: local-vs-external note on the palace-volume comment.
- README + CHANGELOG (Unreleased).
The image shipped only nvim (EDITOR=nvim), a modal vi-style editor. Not
everyone is comfortable with vi keybindings, so add both a classic and a
modern non-modal option:
- nano (apt): ~2.8 MB installed; deps (libc6, libncursesw6, libtinfo6)
already present via nvim/less/htop/tmux, so no extra packages pulled in.
- micro: ~12 MB single static Go binary from GitHub releases (same pattern
as bat/eza/zoxide). Desktop-style keys (Ctrl+S/Ctrl+Q), mouse, syntax
highlighting. ARG MICRO_VERSION pins; defaults to latest.
Combined ~15 MB (<0.5% of the ~3.2 GB image). EDITOR stays nvim; both new
editors are opt-in (export EDITOR=micro | nano). Uses the canonical
micro-editor/micro URL because the old zyedidia/micro org rename makes
/releases/latest redirect to another /latest, defeating the tag-parsing
latest-resolution idiom.
Base-image change, so it lands on the next base-<hash> rebuild. Updates
README tool table + EDITOR note, CHANGELOG (Unreleased/Added), and
smoke-test.sh (nano + micro presence checks).
Removes GITEA_ACCESS_TOKEN / GITEA_HOST / GITHUB_PERSONAL_ACCESS_TOKEN from
the compose environment: block. 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) silently shadowed the
users .env — an updated token never reached the container. Secrets now flow
solely via env_file: .env; .env.example already documents every variable.
- docker-compose.yml: drop the 3 passthrough lines + explanatory comment
- README.md: sync the "basic shape" snippet
- CHANGELOG.md: note under Unreleased (no tag bump / unpublished)
Baking the mempalace fallback skill fixed *availability*, but mempalace had
no proactive-load directive anywhere (pi-toolkit's global AGENTS.md only
points to pi-extensions), so a new container would still surface it only via
description-matching — the same under-utilisation the pi-extensions directive
was created to fix.
Add a session-start pointer to the pi-devbox managed AGENTS.md block
(pi-global-AGENTS.append.md): gated to pi-devbox containers and conditional on
the MemPalace MCP tools being present. Memory continuity matters most in a
frequently-recreated container — the palace is its only cross-recreate memory.
- pi-global-AGENTS.append.md: '## Session start: load the mempalace skill'.
- smoke-test: assert the pointer merges into the global AGENTS.md at build.
- docs: VENDORED.md, README, CHANGELOG [Unreleased].
Now both skills are complete in pi-devbox: directive + skill file.
pi-extensions = directive (pi-toolkit) + baked skill; mempalace = directive
(this block) + baked skill.
The pi-toolkit global AGENTS.md tells every pi session to read
~/.agents/skills/pi-extensions/SKILL.md at start (the fork/recall
under-utilisation fix), but that skill lived only in the private skillset
repo — so the pointer dangled in any container started without skillset
mounted. Bake fallbacks so the pointer always resolves.
- pi-extensions (Option 1 + Option 2, layered):
* Canonical skill promoted to the public pi-extensions package repo under
skill/ (separate commit there); co-located with the code it documents.
* rootfs/ carries a committed snapshot (the floor).
* Dockerfile.variant copies /opt/pi-extensions/skill/ over the snapshot
after the pinned clone, so a normal build ships the fresh package copy
(recorded via PI_EXTENSIONS_REF) and an old-ref/mirror build still ships
the snapshot. Helper evaluate-extension-usage.py travels with it.
- mempalace (Option 2 only): snapshot in rootfs/. Its consumer skill has no
public package home (mempalace-toolkit ships a different skill,
opencode-mempalace-bridge), so no build-time refresh.
- entrypoint links both (only-when-absent; mounted skillset still wins).
- smoke-test: build-time presence + package-match check + runtime symlink
assertions; readiness gate now waits on the last-linked skill.
- docs: skills/VENDORED.md (provenance + refresh), README, AGENTS.md,
CHANGELOG [Unreleased].
Note: shipped in the NEXT release; v1.2.0 (run 409) predates this.
Ship skills inside the image (independent of any mounted skillset repo):
- rootfs/usr/local/share/pi-devbox/skills/<name>/ symlinked into
~/.agents/skills/ by entrypoint-user.sh (foreign-link, survives volume
recreate, never clobbers a skillset/user skill of the same name).
- New pi-devbox-environment skill: persistence model, host/LAN SSH
reachability, split-DNS mechanisms, interactive-vs-tool-shell alias
gotcha, tmux 0-index, uv-first Python, pi-studio reachability. Agnostic
to host OS / hostnames / domains / nameservers (discovered at runtime).
- Dockerfile.variant appends pi-global-AGENTS.append.md onto pi-toolkit's
pi-global-AGENTS.md (single global slot) so the skill is loaded
proactively; gated on /usr/local/lib/pi-devbox/. Idempotent.
- smoke-test: baked-skill + append-snippet + merged-marker presence and a
runtime symlink assertion.
- docs: README 'Agent skills' section, AGENTS.md layout, DOCKER_HUB.md;
moved studio-tex roadmap to v1.3.0.
pi 0.79.7 -> 0.79.10 (auto-resolved from npm latest at build).
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 <peer>` / `dssh <peer>` route through the host), but it was only
documented in .env.example and the setup-lan-access.sh header — never in the
README, where someone hitting "can't reach LAN peers" actually looks.
- README: add a "Naming LAN peers" subsection under the macOS LAN-peers
troubleshooting block, with a ProxyJump example and the read-only ~/.ssh
caveat; add a pointer to it from the SSH and ControlMaster section.
- setup-lan-access.sh: correct the INCLUDE_BLOCK comment that suggested adding
ProxyJump to the read-only ~/.ssh/config; point at ssh-lan.conf instead.
- CHANGELOG: note under Unreleased.
Docs/comment only — no behavior change.
Adds OCI labels + /etc/pi-devbox/build-manifest.json so a published tag is
self-describing and reconstructable after CI logs rotate (manifest is
written from the actual checked-out HEAD of each /opt clone + live
pi --version, not just the intended build-args).
Hardens the build plumbing:
- scripts/check-base-hash.sh guards the base-rebuild invariant: every
floating ARG *_REF in Dockerfile.base must be folded into the base_tag
hash, else a ref-only change silently fails to rebuild the base
(v1.1.2-class staleness footgun). Runs in base-decide and locally.
- resolve-versions now fails loud instead of falling back to a floating
main/master on a transient API failure — validates each ref is a 40-hex
SHA (and pi a real semver) and aborts the release otherwise.
- The three gitea companions (pi-toolkit, pi-extensions, mempalace-toolkit)
gained overridable *_REPO build-args (defaulting to the canonical gitea
origin) so a relocated/forked build can repoint them without editing the
Dockerfiles — matching the existing PI_FORK_REPO/PI_OBSMEM_REPO pattern.
README documents the forked/relocated build-arg trick and how to read the
labels + manifest. smoke-test asserts the manifest + labels. pi bumps
0.79.7 → 0.79.8 (auto-resolved at build).
Coordinated with the pi-extensions ssh-controlmaster fix (picked up at build via
PI_EXTENSIONS_REF=main), this makes `pi --ssh <host>` and `dssh`/`dscp` robust
to a user ~/.ssh/config whose per-host ControlPath points under the read-only
~/.ssh bind-mount (e.g. `ControlPath ~/.ssh/cm/%r@%h:%p`). A system default can
never override a user's per-host value, so the fix lives in two layers.
- setup-lan-access.sh: always render the writable ~/.ssh-local/config sidecar
(Host * ControlPath redirect into ~/.ssh-local/cm + Include ~/.ssh/config) on
EVERY host OS. Previously the script exited early (no-op) on native Linux,
leaving dssh/dscp broken when ~/.ssh was read-only there too. The host-jump
block, its key generation, and the authorize hints stay gated on VM-backed
detection / DEVBOX_LAN_ACCESS=jump (new NEED_JUMP flag).
- Dockerfile.base: document that the /etc/ssh drop-in default cannot override a
user per-host ControlPath; cross-ref the two handling layers.
- entrypoint-user.sh: correct the now-stale "no-op on native Linux" comment.
- README.md / DOCKER_HUB.md: document read-only-~/.ssh ControlPath handling.
CHANGELOG: v1.1.5 (Fixed + Changed + pi 0.79.6 -> 0.79.7 auto-resolved bump).
Patch release per AGENTS.md versioning scheme (pi version bump + smaller
fixes). Cut Unreleased → v1.1.2 (2026-06-15): pi 0.79.3→0.79.4
(CI-auto-resolved), the mempalace-toolkit SHA-resolution build fix,
recreate-sanity-check.sh maintainer tooling, and the AGENTS/README docs.
Bumped the README sanity-check version example to 0.79.4.
scripts/recreate-sanity-check.sh verifies what is actually live in a
recreated container — persisted volumes, pi runtime wiring (keybindings,
extensions, mempalace.ts bridge, settings.json, fork/obsmem/studio
registrations), /tmp/sshcm, skel defaults, /opt toolkits. smoke-test.sh
runs at build time with --entrypoint="" and cannot see any of this.
Variant (studio/plain) auto-detected via /opt/pi-studio. pi version is
asserted only with --expected-version (built from 'latest', no Dockerfile
pin to self-derive). Maintainer tooling, not baked into the image.
Documented in README and CHANGELOG.
pi-studio renders Mermaid natively but has no DOT renderer. Its markdown
preview displays local PNG/JPG/GIF/WEBP images, so dot-watch closes the
loop for Graphviz: edit .dot -> auto-render <name>.png -> Studio
refresh-from-disk shows the update. Uses mtime polling (no inotify dep).
- rootfs/usr/local/bin/dot-watch: the helper (executable)
- Dockerfile.base: COPY + chmod, following the studio-expose pattern
- README.md: 'Graphviz diagrams in Studio' subsection
- CHANGELOG.md: Unreleased entry
graphviz was already in the base image; no new package.
Audit found README/AGENTS carried a stale compose/volume set that
diverged from the shipped docker-compose.yml (DOCKER_HUB + compose +
.env.example were already consistent — README was the outlier):
- README compose block + 'Volumes and persistence' table: correct volume
names (devbox-shell-history not -bash-history; devbox-uv at
~/.local/share/uv not devbox-uv-tools at /opt/uv-tools — the latter
would SHADOW the baked mempalace install at UV_TOOL_DIR); add
devbox-ssh-local + devbox-zoxide; mark devbox-palace/-chroma-cache
optional; WORKSPACE_PATH/SSH_KEY_PATH (not HOST_WORKSPACE).
- README quickstart: 'compose exec -u developer' (no USER in image; bare
exec lands a root shell).
- README: pi-studio now 'shipped' not 'planned'; build-pipeline + tag
table cover -studio + smoke-studio/build-variant-studio.
- AGENTS: backward-compat volume names corrected; repo-layout bullets
cover pi-studio install + studio-expose + STUDIO_EXPOSE bridge.
- DOCKER_HUB: MemPalace source link -> upstream MemPalace/mempalace
(matches Dockerfile.base + CHANGELOG refs).
Note: the shipped v1.0.0 CHANGELOG migration note still lists the old
(incorrect) volume names; left as immutable released history.
pi-studio binds the container's 127.0.0.1, which a published Docker port
can't reach. Add a robust, portable bridge rather than a doc-only one-liner:
- Dockerfile.base: add socat (~1 MB, generally useful TCP relay).
- rootfs/usr/local/bin/studio-expose: socat TCP relay listening on the
container's egress IPv4 (not 0.0.0.0 — that would EADDRINUSE against
Studio's loopback listener) forwarding to 127.0.0.1:PORT on the SAME
port, so Studio's printed token URL works verbatim. Robust egress-IP
detection (hostname -I, loopback-filtered; ip route get fallback),
--help, port validation, foreground.
- entrypoint-user.sh: opt-in STUDIO_EXPOSE=1 auto-starts the bridge in the
background (studio variant only). Default OFF — Studio stays loopback-only
(its secure default) unless explicitly opted in.
- README: 'Using pi-studio' now documents host-networking (A) and the
studio-expose/STUDIO_EXPOSE bridge (B) with a security note; ssh -L for
remote, mosh caveat retained.
- smoke-test: assert socat + studio-expose present (base-level).
- CHANGELOG/AGENTS updated.
No tag — stopping for review.
Bundle pi-studio (omaclaren/pi-studio) as a new -studio image variant:
browser prompt editor, KaTeX/Mermaid preview, tmux-backed literate REPLs,
/studio command + studio_* agent tools.
- Dockerfile.variant: INSTALL_STUDIO + PI_STUDIO_REPO/REF args; vendor
pi-studio to /opt/pi-studio (no build step — prebuilt client in git;
npm install --omit=dev for 3 prod deps). STUDIO_PORT=8765 advisory.
- entrypoint-user.sh: register /opt/pi-studio via the existing pi install
local-path loop (auto-skips in non-studio variant).
- smoke-test.sh: auto-detected studio assertions (clone + prebuilt client
+ pi install registration).
- CI: resolve PI_STUDIO_REF to a SHA; independent smoke-studio +
build-variant-studio jobs that gate ONLY the -studio tags, so a studio
failure never blocks the core :latest release.
- README: 'Using pi-studio' section documenting the container access
reality — pi-studio hard-binds 127.0.0.1 (index.ts .listen(port,
'127.0.0.1'), no --host flag), so -p publish alone can't reach it.
Documents host-networking and loopback-bridge paths, the remote ssh -L
forward, and the mosh caveat (no port forwarding; run parallel ssh -L).
- CHANGELOG/AGENTS/DOCKER_HUB updated. Will tag as v1.1.0 (minor).
No tag created — stopping for review.
Self-contained build chain — own Dockerfile.base + Dockerfile.variant
+ entrypoint scripts + rootfs + CI pipeline. Previously v0.79.0 and
earlier were thin re-brands of opencode-devbox's pi-only variant
(joakimp/pi-devbox:base-pi-only built by opencode-devbox CI).
Architectural changes:
- Replace 5-line Dockerfile shim with full base+variant pair.
- Adapt CI workflow from opencode-devbox/docker-publish-split.yml,
simplified to a single variant. Includes content-addressed base hash,
PI_VERSION concrete-resolution to defeat registry-buildcache footgun,
crane-based base-latest promotion, and the c6f9d11 smoke-test gate.
- pi-devbox releases no longer require rebuilding opencode-devbox first.
Base image additions:
- pandoc, graphviz, imagemagick, yq — broadly useful, ~260 MB total.
- tldr (tealdeer) — Rust port replaces Node tldr global, saves 135 MB.
- /etc/tmux.conf with base-index 0 + pane-base-index 0 — required for
the planned :latest-studio variant; pi-studio hard-codes :0.0 target.
Smoke test:
- New checks for pandoc, graphviz, imagemagick, yq, tldr, tmux config,
/tmp/sshcm directory.
- Image-size measurement now sums docker history layers (the prior
inspect --format='{{.Size}}' returned only the variant-unique layer
with the new base/variant split, understating by 2+ GB).
- Threshold 2850 → 3500 MB to absorb base additions + arch margin.
Image size:
- Local arm64 build: 3.20 GB. ~390 MB up from prior pi-only equivalent.
- Will tighten threshold once amd64 actuals settle in CI.
Pre-1.0 history preserved at tag pre-v1.0.0-decouple-backup.
Future work:
- v1.1.0: :latest-studio variant (adds pi-studio).
- v1.2.0: :latest-studio-tex variant (adds texlive-xetex for PDF).
- opencode-devbox v2.0.0 will retire INSTALL_PI / pi-only paths.
Persist ~/.ssh-local so the generated LAN-jump key survives container
recreation; authorize it on the host once per machine. Adds the volume
to the compose template and documents it in the README volumes table.
LAN-access mechanism/script changes are inherited from base-pi-only
(opencode-devbox).
setup-lan-access.sh fixes (Include scope, ControlPath) + ssh-lan.conf and
RFC1918 autojump flow in via FROM base-pi-only. Documents the knob and new
host-owned config. Tag v0.78.0c AFTER opencode-devbox v1.15.13d publishes the
rebuilt base-pi-only, so it doesn't build on the stale base.
The pi-only building block now lives in this repo as the internal
base-pi-only tag (produced by opencode-devbox CI from Dockerfile.variant,
INSTALL_OPENCODE=false) instead of opencode-devbox:latest-pi-only — so an
'opencode-devbox' tag never ships without opencode.
- Dockerfile: BASE_IMAGE default joakimp/opencode-devbox:latest-pi-only
-> joakimp/pi-devbox:base-pi-only.
- Updated README, AGENTS, DOCKER_HUB, docker-compose, CHANGELOG.
- Single source of truth unchanged (opencode-devbox/Dockerfile.variant);
publish ordering + EXPECTED_PI_VERSION smoke guard unchanged.
Re-point the re-brand at the new pi-only variant instead of with-pi, so
pi-devbox stays a lean pi-focused image (no opencode) while the pi install
logic still lives in one place upstream. This keeps pi-devbox meaningfully
distinct from opencode-devbox:latest-with-pi.
- Dockerfile: BASE_IMAGE default -> joakimp/opencode-devbox:latest-pi-only.
- smoke-test.sh: size threshold 2900 -> 2750 MB (pi-only = with-pi minus
opencode's ~145 MB binary).
- Docs (README/AGENTS/DOCKER_HUB/CHANGELOG/docker-compose): drop the
'also contains opencode' notes; describe pi-only basis and the distinction
from with-pi.
Publish ordering unchanged: release opencode-devbox first so latest-pi-only
carries the target pi version, then tag here (smoke asserts pi --version).
pi-devbox no longer installs pi itself. The Dockerfile is now a thin
FROM joakimp/opencode-devbox:latest-with-pi (overridable via BASE_IMAGE),
inheriting pi + pi-toolkit + pi-extensions + pi-fork (fork) +
pi-observational-memory (recall) + the LAN-access helper + all base tooling
from the single source of truth. Eliminates the install-logic duplication
that drifted against opencode-devbox/Dockerfile.variant (decision #3).
Consequences (documented in CHANGELOG/AGENTS):
- The image now ALSO contains opencode (with-pi has INSTALL_OPENCODE=true).
A leaner pi-only image would need a dedicated pi-only variant upstream.
- Publish ordering: release opencode-devbox first so latest-with-pi carries
the target pi version, THEN tag this repo. The smoke test asserts
pi --version matches the tag (EXPECTED_PI_VERSION) and fails loudly if the
base is stale — turning the version coupling into an enforced ordering guard.
CI: drop PI_VERSION build-arg (Dockerfile installs nothing); keep tag->version
resolution to feed the smoke base-freshness guard. Smoke adds fork/recall
clone + node_modules + settings.json registration checks; size threshold
2200 -> 2900 MB (now tracks with-pi). Docs updated across README, AGENTS,
DOCKER_HUB, .env.example, docker-compose.
README rewrite:
- Two quick-start paths: 'no git clone' (curl docker-compose.yml +
.env.example) and 'with git clone' for hackers/forkers
- New 'Authentication' section with subsections per provider
(Anthropic, OpenAI, Gemini, AWS Bedrock static, AWS Bedrock SSO).
AWS SSO path documents the ~/.aws bind-mount.
- Persistent state expanded: 5-row volume table + optional volumes
table. Annotated what survives what.
- Configuration reference: full .env table.
- Versioning, building from source (with build args table),
troubleshooting FAQ, related projects, license.
- 11 kB total — comprehensive but readable.
DOCKER_HUB.md tweaks:
- Quick-start now has a 'no git clone' path (curl two files), pointing
users at the gitea README for the full setup guide. The git-clone
path was overkill for the 90% case (just want to docker run).
- Explicit link to gitea README at the end of the quick-start block.
pi coding-agent container built on opencode-devbox:base-latest.
Includes Dockerfile, docker-compose, CI workflow, smoke-test,
README, CHANGELOG, AGENTS.md.