1baba79c96
v1.9.1's @esbuild prune fixed the 431 MB size-gate failure but still shipped
+131 MB compressed over v1.8.14, nearly all of it in the pi/extensions install
layer (87 -> 206 MB). That leftover was filed as an open item with an explicit
hypothesis — the @mariozechner/clipboard-* family, same npm 11 behaviour, a
different package — and an explicit warning that the hypothesis was not a
measured cause. Measured now, after recreating onto v1.9.1, and the hypothesis
accounted for one sixth of it:
+110 MB /root/.npm/_cacache (35.2 -> 145.3 MB) the build's npm cache
+ 21 MB clipboard foreign platform packages, both install sites
= 131 MB i.e. the whole delta, no unexplained remainder
Method, since there is no docker CLI inside the container: pulled both variant
layer blobs straight from the registry with a token + manifest + blob fetch and
listed the tarballs (29 789 vs 29 889 entries, 270.8 vs 401.9 MB uncompressed),
then aggregated per package. The file COUNT barely moved, which is what said
"few large files", not "npm installed more packages".
- purge_build_caches: npm cache clean --force + rm -rf /root/.npm, in the SAME
layer as the installs, in both the main RUN and the studio RUN. npm 11
caches every platform tarball it downloads, including the ones the prune
then deletes, so the cache grew faster than the tree. Nothing at runtime
reads it: build is root, container is developer with its own cache in $HOME.
- prune_foreign_esbuild -> prune_foreign_natives: now covers both MEASURED
families. Clipboard keeps linux-$arch-gnu AND -musl because its napi-rs
loader picks between them at runtime via its own isMusl() probe; the musl
package is a 420-byte stub. The bare @mariozechner/clipboard wrapper has no
hyphen suffix and cannot match the pattern.
Verified on arm64 before writing the glob — a widened rm -rf against a tree you
cannot inspect is the one change shape not to write blind, which is why the
order was update-then-patch. Exercised against a copy of the real trees with
foreign dirs fabricated back in (aix-ppc64, android-arm64, darwin-arm64,
win32-x64, linux-x64): all removed, host linux-arm64 kept at both sites, 21 MB
freed, require('@mariozechner/clipboard') still loads and exports all 18
functions, esbuild.transformSync still compiles TS at both sites. v1.9.1's own
arm64 validation also passed here; CI could only smoke amd64.
Two sentinel assertions, because the size gate did not catch this: it has
~225 MB of margin, so 131 MB of residue stayed green. Both were verified RED
against the running v1.9.1 image and GREEN against a pruned tree. The cache one
refuses to run as non-root: test ! -d /root/.npm on mode-700 /root would
otherwise pass for the wrong reason. Size-failure diagnostics now list cache
paths too — they previously enumerated only node_modules and /opt, where these
bytes were not.
Also fixed: -printf '%f\\n' reaches the shell with both backslashes (confirmed
from the published image's recorded created_by), so v1.9.1's progress line
printed a mangled "li ux-arm64" — find emitted a literal backslash and tr ate
the n out of the name. Single backslash now.
Deliberately not purged: /tmp/node-compile-cache (1.3 MB). The manifest RUN
calls pi --version again, so deleting it earlier only relocates those bytes into
that layer — today's manifest layer is 128 kB precisely because it finds the
cache warm.
Dockerfile.base untouched, so no base rebuild: this rides the next release.
365 lines
21 KiB
Markdown
365 lines
21 KiB
Markdown
# AGENTS.md — pi-devbox
|
||
|
||
Self-contained Docker image for the **pi coding-agent**. Decoupled from
|
||
opencode-devbox at v1.0.0 (2026-06-09); previously pi-devbox was a thin
|
||
re-brand of opencode-devbox's `pi-only` variant.
|
||
|
||
## Repository layout
|
||
|
||
- `Dockerfile.base` — multi-arch base layer with system packages,
|
||
GitHub-binary tools (fzf, eza, zoxide, neovim, bat, gosu, gitleaks,
|
||
git-lfs, uv, gitea-mcp, tealdeer), AWS CLI v2, mempalace + toolkit,
|
||
Node.js, Python toolchain, locales, ssh ControlMaster defaults, and
|
||
`/etc/tmux.conf` with 0-indexed sessions.
|
||
- `Dockerfile.variant` — `FROM base-<hash>`, adds pi + companions
|
||
(`pi-toolkit`, `pi-extensions`, `pi-fork`, `pi-observational-memory`)
|
||
and, when `INSTALL_STUDIO=true`, vendors `pi-studio` to `/opt/pi-studio`
|
||
(`-studio` variant). Also appends the pi-devbox managed block from
|
||
`pi-global-AGENTS.append.md` onto pi-toolkit's `pi-global-AGENTS.md` (the
|
||
single global instruction slot pi loads) so containers proactively load the
|
||
baked `pi-devbox-environment` skill. Idempotent via a marker grep. After the
|
||
pinned clones it also refreshes the vendored `pi-extensions` fallback skill
|
||
by copying `/opt/pi-extensions/skill/` over the committed `rootfs/` snapshot
|
||
(Option 1 over Option 2 — see `skills/VENDORED.md`).
|
||
- `entrypoint.sh` — UID/GID alignment as root, then drops to `developer`.
|
||
- `entrypoint-user.sh` — per-container start: prints the `pi-devbox-version`
|
||
banner first (which build/commit is running, from the manifest below),
|
||
then SSH ControlMaster socket dir, LAN-access setup, MemPalace init,
|
||
pi-toolkit + pi-extensions deploy, mempalace-bridge symlink, fork/recall +
|
||
pi-studio pi-install, optional `studio-expose` bridge (when
|
||
`STUDIO_EXPOSE=1`), image-baked skills symlink-in, skillset deploy.
|
||
- `rootfs/` — files baked into the image (bash aliases, inputrc,
|
||
setup-lan-access.sh, `studio-expose` helper, `pi-devbox-version` — wraps
|
||
`/etc/pi-devbox/build-manifest.json` into a human-readable summary + live
|
||
drift check, see README “Build provenance”). Also
|
||
`usr/local/share/pi-devbox/skills/<name>/SKILL.md` — image-baked agent
|
||
skills (the repo-authored `pi-devbox-environment`, plus vendored fallback
|
||
copies of `pi-extensions` and `mempalace` — see `skills/VENDORED.md`)
|
||
symlinked into `~/.agents/skills/` by the entrypoint, available with or
|
||
without a mounted skillset — plus
|
||
`usr/local/share/pi-devbox/pi-global-AGENTS.append.md` (the global-AGENTS
|
||
pointer concatenated in `Dockerfile.variant`).
|
||
- `scripts/smoke-test.sh` — sanity checks run by CI before pushing to Hub.
|
||
- `.gitea/workflows/docker-publish.yml` — two-phase CI (base-decide →
|
||
build-base → smoke → build-variant → promote-base-latest →
|
||
update-description). The `-studio` variant adds independent
|
||
`smoke-studio` + `build-variant-studio` jobs that gate only the
|
||
`-studio` tags (never the core `:latest` release).
|
||
|
||
## Versioning scheme
|
||
|
||
- Tags follow semver. **v1.0.0** is the first decoupled release; future
|
||
minor bumps add variants (`-studio`, `-studio-tex`) or significant base
|
||
additions (e.g. v1.2.0 image-baked agent skills); patch bumps follow
|
||
pi npm version updates and small fixes.
|
||
- Docker Hub tags: `joakimp/pi-devbox:vX.Y.Z` + `joakimp/pi-devbox:latest`
|
||
+ (since v1.1.0) `joakimp/pi-devbox:vX.Y.Z-studio` +
|
||
`joakimp/pi-devbox:latest-studio`.
|
||
Internal tags: `joakimp/pi-devbox:base-<hash>` (content-addressed) +
|
||
`joakimp/pi-devbox:base-latest` (alias of most recent base).
|
||
|
||
## Release-day checklist
|
||
|
||
1. Confirm `pi --version` resolves from npm to the expected version
|
||
(`curl -sf 'https://registry.npmjs.org/@earendil-works%2Fpi-coding-agent/latest' | jq -r .version`).
|
||
Check release notes at https://github.com/earendil-works/pi/releases for
|
||
the upstream changelog to include in `CHANGELOG.md`.
|
||
2. **Refresh the vendored mempalace skill snapshot if the skillset moved:**
|
||
`scripts/vendor-mempalace-skill.sh --check` (reads a real skillset clone,
|
||
writes nothing). Three exit codes, not two — a stale-but-truthful record is
|
||
**not** a release blocker, so don't treat any non-zero exit as "must
|
||
refresh" without reading which one it was:
|
||
- **0** — the record is truthful. This includes stale-but-truthful
|
||
(upstream has moved past the recorded ref, or the local clone has
|
||
uncommitted changes) — a `NOTICE` is printed, but nothing is lying.
|
||
**Skipping the refresh in this case is the legitimate, sanctioned
|
||
outcome** — every enrolled host reads its own live skillset clone, so
|
||
the baked copy is only a no-mount fallback. What is not legitimate is
|
||
skipping it *silently*: the drift is visible here, in
|
||
`pi-devbox-version`, and in the manifest, so decide rather than forget.
|
||
- **1** — a confirmed problem: the vendored bytes provably do NOT match
|
||
the file at the recorded ref (a lying record), or the recorded ref
|
||
doesn't even resolve to that path in this clone. Refresh.
|
||
- **2** — cannot determine (the recorded ref itself isn't resolvable in
|
||
this clone — commonly a shallow checkout missing history). Fetch full
|
||
history and re-check before deciding; don't refresh blind.
|
||
Refresh with `scripts/vendor-mempalace-skill.sh`, which rewrites the file
|
||
**and** the ARG together so they cannot drift apart, and refuses (exit 1)
|
||
rather than silently rewinding provenance if the skillset clone's HEAD is
|
||
behind the already-recorded ref (detached HEAD, older checkout) — pass
|
||
`--force` only if that rewind is genuinely intended.
|
||
Two consequences to accept deliberately on an actual refresh: the snapshot
|
||
is hashed into `base_tag`, so it costs a base rebuild (~67 min); and if the
|
||
section the phrase canary names has changed, re-pin it in
|
||
`scripts/smoke-test.sh`.
|
||
3. **Update the docs this release makes stale — BEFORE you tag.** Rename
|
||
`CHANGELOG.md`'s `## Unreleased` to `## vX.Y.Z — YYYY-MM-DD` (em dash, as
|
||
every prior release heading uses), then run the gate:
|
||
|
||
```bash
|
||
bash scripts/check-doc-drift.sh # 0 in sync / 1 drift / 2 cannot run
|
||
```
|
||
|
||
It compares README.md's version-pin table against the ARGs it names, and
|
||
DOCKER_HUB.md's Node claim against `ARG NODE_VERSION`, plus Hub's
|
||
25 000-char limit, unsubstituted `{{PLACEHOLDERS}}`, and stale `Unreleased`
|
||
pointers in user-facing docs.
|
||
|
||
**Why before and not after:** `docker-publish.yml` runs `actions/checkout@v4`
|
||
with no `ref:`, so every job reads `github.ref` — the **tag**. A doc fix
|
||
pushed to `main` after tagging does not reach the release, and for
|
||
`DOCKER_HUB.md` it does not reach the published Hub page either, because
|
||
`update-description` POSTs that file as Docker Hub's `full_description` from
|
||
the tag's tree. Getting it in afterwards means re-pointing the tag, which is
|
||
its own hazard (v1.8.14 went `601fc98` → `361babd` and broke deploy
|
||
verification until `git fetch --tags --force`).
|
||
|
||
The gate is deliberately narrow — it only checks claims verifiable from files
|
||
in this repo. Still eyeball, because these are NOT gated:
|
||
- counts and sizes (`~1.1 GB`, "N `mempalace_*` tools", "7 extensions") —
|
||
they need a running image; assert them in `scripts/smoke-test.sh` instead
|
||
- feature prose that quietly became false, e.g. a "Planned for an upcoming
|
||
release" section describing something that already shipped
|
||
- `Dockerfile.base`'s `# BASE_REBUILD_DATE:` marker. Ungated on purpose:
|
||
`base_tag` hashes Dockerfile.base's content, comments included, so
|
||
demanding it be current would force a ~60 min base rebuild on a release
|
||
that touched no base files. **Fix it when the base is already rebuilding —
|
||
then it is free.**
|
||
|
||
Measured cost of skipping this, 2026-09-10 (v1.9.0): five stale claims, one
|
||
of them published. README's pin table was wrong on all three rows, and
|
||
DOCKER_HUB.md — untouched for eight releases — still said Node v22 while the
|
||
image shipped Node 24.
|
||
4. Verify `docker compose up` works locally with the current `latest` image
|
||
if you're upgrading users from a previous version. Then run the
|
||
**post-recreate sanity check** inside the running container to confirm
|
||
persisted volumes survived and the pi runtime wiring re-deployed (not just
|
||
that the container booted):
|
||
`docker compose exec devbox bash scripts/recreate-sanity-check.sh --expected-image-version X.Y.Z`
|
||
(or just `pi-devbox-sanity --expected-image-version X.Y.Z` if
|
||
`cli_utils/bin` is on PATH). This is the runtime peer of the build-time
|
||
`smoke-test.sh` gate.
|
||
**`X.Y.Z` here is the pi-devbox release tag** you are shipping (e.g.
|
||
`1.8.9`), which is what the rest of this checklist means by `vX.Y.Z`.
|
||
`--expected-image-version` is the flag that asserts it. There is also an
|
||
`--expected-version`, and it means something else — the **pi coding agent**
|
||
version (e.g. `0.84.3`, the `ARG PI_VERSION` pin). Handing the release tag
|
||
to that one used to report *"pi version mismatch: expected 1.8.8, got
|
||
0.84.3"*, i.e. a red on the final gate of the release accusing the wrong
|
||
component; it now tells you to use `--expected-image-version` instead, and
|
||
the reverse mix-up is caught too. Both flags are optional — with neither,
|
||
the live pi version is asserted against the version recorded in the image's
|
||
own build manifest (which catches a stale `pi` in the `~/.pi/npm-global`
|
||
volume shadowing the baked one) and the image tag is reported
|
||
informationally.
|
||
5. Push tag: `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
||
6. Watch CI: smoke job builds amd64 only and asserts size + extensions +
|
||
pi version + new-base-tooling presence. Variant build is multi-arch
|
||
(amd64 + arm64) only after smoke passes. A tag push fires **only**
|
||
`docker-publish.yml` — `lint.yml` is scoped to `branches: ['**']`, which
|
||
excludes tag refs on purpose (the tagged tree was already linted when the
|
||
commit hit `main`, and a fast lint run sorting above the slow publish run
|
||
made releases look finished before anything shipped). Verified on v1.8.4:
|
||
`refs/tags/v1.8.4` produced run 571 (publish) and nothing else. Still filter
|
||
discovery on `head_sha` **and** the workflow `path` — see *Gitea API access*
|
||
below — because that guard costs nothing and a future workflow added on `v*`
|
||
would silently reintroduce the ambiguity.
|
||
7. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus
|
||
base-latest if the base was rebuilt this run).
|
||
8. **Revoke any short-lived Gitea PAT** used during the release at
|
||
`gitea.jordbo.se/user/settings/applications`. N/A if you used the
|
||
`GITEA_ACCESS_TOKEN` env var instead (see *Gitea API access* below) —
|
||
its lifecycle is managed host-side, nothing to revoke.
|
||
|
||
## Verifying this repo's reality from inside a container
|
||
|
||
Most work on this repo happens **inside** a pi-devbox container, inspecting a
|
||
host or a peer over SSH. That setup manufactures convincing false negatives, so
|
||
when you are about to report that something is **absent, unreachable, or not
|
||
running**, suspect your own command first. Recurring instances:
|
||
|
||
- **`docker` is not on the host's non-interactive SSH `PATH`.** `ssh mac 'docker
|
||
ps'` says *command not found* on a host that plainly runs Docker; use
|
||
`/usr/local/bin/docker` (or `command -v docker` first). Every step in the
|
||
*Release-day checklist* that inspects a running container hits this.
|
||
- **Don't `| head -N` a search whose answer you don't already know.** The host's
|
||
`~/.ssh/config` is ~500 lines; a `head -20` "proved" a peer absent that was
|
||
defined at line 454.
|
||
- **The deployment compose file is not this repo's.** `docker-compose.yml` here
|
||
is a template pinning `:latest`; a real host runs its own per-machine file
|
||
(find it with `docker inspect <container> --format '{{ index .Config.Labels
|
||
"com.docker.compose.project.config_files" }}'`). Recreating from the repo copy
|
||
can silently move a host off `:latest-studio` onto `:latest`.
|
||
- **A live SSH ControlMaster hides remote auth changes** — after editing a
|
||
peer's `authorized_keys`, prove access with `-o ControlPath=none -o
|
||
ControlMaster=no`, or the breakage surfaces in a later session instead.
|
||
|
||
Depth and further mechanisms: the repo-authored `pi-devbox-environment` skill
|
||
(`rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/SKILL.md`) §2
|
||
and §3 — that file is the one an agent actually loads mid-session, whereas this
|
||
`AGENTS.md` is only auto-read when the cwd *is* this repo.
|
||
|
||
## Gitea API access (env token)
|
||
|
||
`GITEA_ACCESS_TOKEN` + `GITEA_HOST` are passed into the container from the
|
||
host `.env` via `docker-compose.yml` (`${GITEA_ACCESS_TOKEN:-}` /
|
||
`${GITEA_HOST:-}`), primarily to enable the `gitea-mcp` server. They are
|
||
**not** baked into the image. When configured, they are also available for
|
||
**any** direct Gitea API interaction from inside the container — inspecting
|
||
CI runs, checking published tags, listing commits — e.g.
|
||
`curl -H "Authorization: token $GITEA_ACCESS_TOKEN" "$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs?limit=20"`.
|
||
Prefer this over a short-lived PAT file when the env token is present (the
|
||
`ci-release-watcher` skill auto-detects it). Public-repo GET listings work
|
||
unauthenticated too, so the token matters mainly for private repos or
|
||
rate-limit headroom; its lifecycle is host-managed, so there is nothing to
|
||
revoke after use. Never echo the token value (including into logs).
|
||
|
||
**Gotcha — a tag push fires EVERY workflow whose triggers match the tag ref.**
|
||
`lint.yml` uses a bare `push:` trigger, so a release tag yields *both* a lint run
|
||
and the publish run. The listing is newest-first and lint sorts **above** the
|
||
publish run, so "take the first run whose `path` contains `refs/tags/<tag>`"
|
||
picks the wrong one **reliably, not occasionally**. Real listing for v1.6.4:
|
||
|
||
```
|
||
id=531 #104 lint.yml@refs/tags/v1.6.4 <- wrong; sorts first
|
||
id=530 #103 docker-publish.yml@refs/tags/v1.6.4 <- the release build
|
||
id=529 #102 lint.yml@refs/heads/main <- same commit, linted on push
|
||
```
|
||
|
||
Lint goes green in minutes while the image is still building, so watching it
|
||
makes a release look finished when nothing has been published yet.
|
||
|
||
**Gotcha — the jobs endpoint takes the internal `id`, NOT the `run_number` the
|
||
UI shows as `#104`.** The two diverge widely, and `GET
|
||
.../actions/runs/<run_number>/jobs` does **not** error — it silently returns a
|
||
*different* run's jobs. Always read `id` from the run listing:
|
||
|
||
```bash
|
||
# Which runs did this tag/commit trigger? Filter on head_sha; never trust
|
||
# ordering or run numbering. limit=20, not 5 — with two runs per push the
|
||
# publish run falls off a 5-item window fast.
|
||
curl -sS -H "Authorization: token $GITEA_ACCESS_TOKEN" \
|
||
"$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs?limit=20" \
|
||
| jq --arg sha "$(git rev-list -n1 vX.Y.Z)" \
|
||
'.workflow_runs[] | select(.head_sha==$sha) | {id, run_number, path, status, conclusion}'
|
||
# pick the id whose .path starts with docker-publish.yml, then:
|
||
curl -sS -H "Authorization: token $GITEA_ACCESS_TOKEN" \
|
||
"$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs/<id>/jobs" \
|
||
| jq '.jobs[] | {name, status, conclusion}'
|
||
```
|
||
|
||
**Watcher config for this repo** (`ci-release-watcher` skill, hub-only shape —
|
||
pi-devbox has no downstream host to deploy to):
|
||
|
||
- `EXPECT_WORKFLOW=docker-publish.yml` — the skill's `preflight_run()` aborts at
|
||
startup if the run id belongs to lint instead.
|
||
- `EXPECTED_FRESH_TAGS='vX.Y.Z latest vX.Y.Z-studio latest-studio'`
|
||
- `EXPECTED_EXISTS_TAGS='base-latest'` — existence only: it is content-addressed
|
||
and legitimately keeps its old timestamp when the base is a cache hit.
|
||
- `CRITICAL_JOBS='build-variant build-variant-studio'` — job names are matched
|
||
**exactly** (`critical.issubset(succeeded)`), so the studio variant must be
|
||
listed explicitly; the skill's default omits it. Leave `promote-base-latest`
|
||
out: it legitimately skips on a base cache hit, which would misclassify a good
|
||
run. `update-description` is the cosmetic post-publish job.
|
||
|
||
## Cache-hit footgun (must-know)
|
||
|
||
`PI_VERSION` defaults to `latest` in `Dockerfile.variant` but **CI must
|
||
resolve it to a concrete version string** before passing as a build-arg.
|
||
Otherwise the build-arg string is byte-identical across releases →
|
||
identical layer hash → registry buildcache silently reuses the old
|
||
layer. `resolve-versions` job in the workflow handles this.
|
||
|
||
Discovered in pi-devbox 2026-05-23 (every release v0.74.0..v0.75.5
|
||
shipped the same image bytes); preventatively fixed for `PI_VERSION` +
|
||
`PI_FORK_REF` + `PI_OBSMEM_REF`.
|
||
|
||
## Smoke-test gate
|
||
|
||
`scripts/smoke-test.sh` runs amd64-only against a freshly-built variant
|
||
image. Verifies binaries, repo clones, runtime deployment (waits for
|
||
keybindings + mempalace bridge + ≥4 extensions before sampling — fixes
|
||
the parallel-build-load race documented in opencode-devbox c6f9d11
|
||
2026-06-08), build-time leftovers (see below), and image size threshold
|
||
(3800 MB in `SIZE_THRESHOLD_MB`; revisit after a few releases as actuals
|
||
settle — this doc said 3500 until 2026-09-11, after the bar had already
|
||
moved twice).
|
||
|
||
If smoke fails on size threshold but build is otherwise fine: bump
|
||
`SIZE_THRESHOLD_MB` in scripts/smoke-test.sh in a follow-up commit and
|
||
re-run. The threshold exists to catch *runaway* growth (an accidental
|
||
texlive bake-in, a forgotten chrome dependency), not to block ordinary
|
||
upstream bumps.
|
||
|
||
**The size gate is not a substitute for naming the residue.** It carries
|
||
~225 MB of deliberate margin, so v1.9.1 shipped +131 MB of pure build
|
||
residue — 110 MB of it npm's own download cache under `/root/.npm`, the
|
||
rest foreign platform packages — and stayed green. Two named assertions
|
||
now cover exactly that: no foreign npm-11 platform packages beyond the
|
||
host arch (`@esbuild/*`, `@mariozechner/clipboard-*`), and no
|
||
`/root/.npm` in the image. Note the second one refuses to run as
|
||
non-root: `test ! -d /root/.npm` on mode-700 `/root` would otherwise
|
||
pass for the wrong reason, which is the failure shape to watch for in
|
||
any assertion about a path you may not be able to read.
|
||
|
||
## Build pipeline notes
|
||
|
||
- **Two-phase**: base + variant. Base is rebuilt only when
|
||
`Dockerfile.base`, `rootfs/`, or `entrypoint*.sh` change (CI computes
|
||
a content hash and probes Hub for an existing `base-<hash>` tag).
|
||
- **`base-latest` alias** is promoted from `base-<hash>` via `crane copy`
|
||
(manifest copy, no rebuild) only when the base actually changed.
|
||
- **`docker buildx build --push` retry**: 3 attempts with backoff for
|
||
transient Hub blips. Deterministic failures fail all 3 and the job
|
||
fails as expected.
|
||
- **Registry buildcache disabled**: buildkit's cache-export hits HTTP 400
|
||
on Hub CDN since ~2026-05-23. Image push works fine; we pay the full
|
||
base build on Dockerfile.base change, but base tags are content-
|
||
addressed so unchanged bases short-circuit at the probe step.
|
||
|
||
## Decoupling history (briefly)
|
||
|
||
Pre-v1.0.0 pi-devbox was `FROM joakimp/pi-devbox:base-pi-only`, where
|
||
`base-pi-only` was a tag built by **opencode-devbox CI** (with
|
||
`INSTALL_OPENCODE=false` in their variant Dockerfile) and pushed under
|
||
the pi-devbox repo as an internal building-block tag. This setup
|
||
required rebuilding opencode-devbox before pi-devbox could be tagged
|
||
and meant pi-devbox docs needed cross-referencing into opencode-devbox.
|
||
|
||
v1.0.0 brings pi install logic into this repo, drops the cross-repo
|
||
dependency, and the `base-pi-only*` tags from opencode-devbox become
|
||
deprecated artifacts (to be removed in opencode-devbox v2.0.0).
|
||
|
||
## What we DON'T install (and why)
|
||
|
||
- **No texlive** (~600 MB–1 GB). PDF export from pandoc / pi-studio works
|
||
out of the box via **`typst`** (~30 MB static binary), which the base ships
|
||
as the pandoc PDF engine (`pandoc --pdf-engine=typst`) — small enough to live
|
||
in base rather than a dedicated `:latest-studio-tex` variant. We don't bake in
|
||
a full TeX Live: it's heavy and typst covers the common Markdown→PDF case.
|
||
Users needing LaTeX-exact output can install the higher-fidelity fallback on
|
||
demand: `sudo apt-get install texlive-xetex texlive-latex-recommended` (then
|
||
`pandoc --pdf-engine=xelatex`).
|
||
- **pi-studio** ships in the `:latest-studio` variant (since v1.1.0),
|
||
vendored to `/opt/pi-studio` and registered at container start via
|
||
`pi install /opt/pi-studio` (see Dockerfile.variant `INSTALL_STUDIO`).
|
||
The default `:latest` image stays studio-free. Note: pi-studio binds
|
||
`127.0.0.1` inside the container, so browser access needs host
|
||
networking or the bundled `studio-expose` bridge (socat; auto-starts
|
||
when `STUDIO_EXPOSE=1`) — see README "Using pi-studio".
|
||
- **No Julia/R/GHCi/Clojure runtimes**. Use `uv run --with X` for
|
||
Python REPLs; `apt install` other-language runtimes ad-hoc per
|
||
container if needed.
|
||
|
||
## Backward compatibility
|
||
|
||
- The host `~/.mempalace` bind-mount path is unchanged.
|
||
- Volume names (`devbox-pi-config`, `devbox-ssh-local`,
|
||
`devbox-shell-history`, `devbox-zoxide`, `devbox-nvim-data`,
|
||
`devbox-uv`; optional `devbox-palace`, `devbox-chroma-cache`) are
|
||
unchanged.
|
||
- `~/.pi/agent/` layout inside the container is unchanged; existing
|
||
named volumes work without recreation.
|
||
- The `:latest` and `vX.Y.Z` Hub tags continue to point at a "base + pi"
|
||
image. Same tag, same shape, just built differently.
|