f19c35da32
Lint workflows / actionlint (push) Successful in 36s
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.
186 lines
10 KiB
Markdown
186 lines
10 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: 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). 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. Update `CHANGELOG.md` Unreleased → vX.Y.Z section.
|
||
3. 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-version X.Y.Z`
|
||
(or just `pi-devbox-sanity --expected-version X.Y.Z` if `cli_utils/bin` is
|
||
on PATH). This is the runtime peer of the build-time `smoke-test.sh` gate.
|
||
4. Push tag: `git tag vX.Y.Z && git push origin vX.Y.Z`.
|
||
5. Watch CI: smoke job builds amd64 only and asserts size + extensions +
|
||
pi version + new-base-tooling presence. Variant build is multi-arch
|
||
(amd64 + arm64) only after smoke passes.
|
||
6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus
|
||
base-latest if the base was rebuilt this run).
|
||
7. **Revoke any short-lived Gitea PAT** used during the release at
|
||
`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.
|
||
|
||
## 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=5"`.
|
||
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).
|
||
|
||
## 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), and image size threshold (3500 MB; revisit after a few
|
||
releases as actuals settle).
|
||
|
||
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.
|
||
|
||
## 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). Users who need PDF export from pandoc
|
||
or pi-studio can install on demand: `sudo apt-get install texlive-xetex
|
||
texlive-latex-recommended`. PDF export is planned for a future release — but
|
||
note pandoc alone can't render PDF (it needs a back-end engine), and a full TeX
|
||
install is heavy. A lightweight engine like **`typst`** (~30 MB static
|
||
binary, `pandoc --pdf-engine=typst`) is the preferred candidate and, being
|
||
small, could land in base rather than a dedicated `:latest-studio-tex`
|
||
variant. `texlive-xetex` stays the higher-fidelity fallback.
|
||
- **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.
|