Files
opencode-devbox/AGENTS.md
T
joakimp 896380bb9c
Validate / docs-check (push) Successful in 16s
Validate / validate-base (push) Successful in 12m25s
Validate / validate-omos (push) Successful in 16m40s
Validate / validate-with-pi (push) Successful in 14m0s
Validate / validate-omos-with-pi (push) Successful in 18m18s
Rename @mariozechner/pi-coding-agent to @earendil-works/pi-coding-agent
Pi moved to its new home at earendil-works on 2026-05-07
(https://pi.dev/news/2026/5/7/pi-has-a-new-home).

The old @mariozechner/pi-coding-agent npm package is deprecated with
the explicit message 'please use @earendil-works/pi-coding-agent
instead going forward', and the version stream has moved on (old
top-out 0.73.1; new currently 0.74.0). Anyone npm-installing the old
name today gets a deprecation warning + a stale binary, so this is
a non-optional migration before the next tagged release.

Sweep:
- Dockerfile (production single-Dockerfile path) and Dockerfile.variant
  (split-base path on main): npm install -g target updated.
- README, AGENTS, HUB_TEMPLATE: github.com/mariozechner/pi-coding-agent
  URL refs (which now 404) -> github.com/earendil-works/pi.
- DOCKER_HUB.md regenerated (5529 bytes, ~78% headroom).
- CHANGELOG Unreleased: rename entry added with migration context.

Brew install references (`brew install pi-coding-agent`) left as-is:
formula still works at 0.73.1 and a homebrew tap update is tracked
upstream at earendil-works/pi#2755.

Historical CHANGELOG entries: only github URL refs updated (the
package name was never spelled out in those entries; we're correcting
dead hyperlinks, not rewriting feature descriptions).
2026-05-09 17:58:07 +02:00

16 KiB

AGENTS.md

Project overview

Docker image packaging opencode into a production-ready dev container. Two image variants (base and omos) are published to Docker Hub via Gitea Actions CI. Not a library or application — this is infrastructure (Dockerfile, entrypoint scripts, docker-compose, documentation).

File roles

  • Dockerfile — production single-Dockerfile build. Variants are gated by build args: INSTALL_OMOS (Bun + multi-agent layer), INSTALL_OPENCODE (default true), INSTALL_PI (default false), INSTALL_MEMPALACE (default true). All GitHub-sourced binaries are pinned with version ARGs.
  • Dockerfile.base and Dockerfile.variantWIP, branch feat/split-build only. Two-Dockerfile split-base build: base contains all variant-independent layers; variant FROMs the base and adds only opencode/omos/pi installs. Used by docker-publish-split.yml (workflow_dispatch only) for parallel testing alongside the production pipeline. See CHANGELOG Unreleased for the migration plan and trade-offs.
  • entrypoint.sh — runs as root: UID/GID adjustment, SSH permissions, volume ownership fixes (skipped via .devbox-owner sentinel when ownership is already correct). Then drops to developer via gosu. Volume ownership loop covers ~/.pi/ when INSTALL_PI=true.
  • entrypoint-user.sh — runs as developer: git config, opencode.jsonc generation (delegated to generate-config.py), pi-toolkit + pi-extensions deploy (when pi installed), pi settings.json bootstrap, mempalace pi-bridge symlink, skillset auto-deploy from mounted skillset repo, OMOS setup.
  • rootfs/usr/local/lib/opencode-devbox/generate-config.py — generates ~/.config/opencode/opencode.jsonc from env vars. Never overwrites an existing config (checks both .json and .jsonc). Auto-registers MCP servers for detected tools (mempalace via mempalace-mcp, gitea-mcp, context7 remote endpoint).
  • scripts/smoke-test.sh — post-build image verification. Asserts binary presence, opencode startup, entrypoint correctness, config generation idempotency, and image size thresholds. Used by both CI workflows.
  • scripts/generate-dockerhub-md.py — generates DOCKER_HUB.md from a hand-maintained HUB_TEMPLATE constant. --check fails if the committed file is out of sync (enforced by the validate workflow).
  • DOCKER_HUB.mdauto-generated from HUB_TEMPLATE in scripts/generate-dockerhub-md.py. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes.
  • README.md — authoritative source documentation for everything in this repo. Independent of DOCKER_HUB.md: the Hub doc is hand-maintained in the generator's HUB_TEMPLATE and intentionally slim, linking back to the gitea README for depth.
  • .gitea/workflows/validate.yml — lightweight amd64 build + smoke test on push to main and PRs. Also runs the DOCKER_HUB.md sync check.
  • .gitea/workflows/docker-publish.yml — production CI pipeline on tag push: smoke-test each variant on amd64, then full multi-arch (amd64 + arm64) build-and-push, then update Docker Hub description.
  • .gitea/workflows/docker-publish-split.ymlWIP, branch feat/split-build only. Two-phase split-base pipeline. Triggers on workflow_dispatch only so it runs alongside the production pipeline without conflict. Pushes to user-supplied release_tag input (e.g. v0.0.0-split-test); latest* aliases only updated when promote_latest: true. Compute base hash, conditionally build base, then 4 variant deltas in parallel.

Versioning scheme

Tags follow v{opencode_version}[letter] — e.g. v1.14.20 for the first build on a new opencode release, and v1.14.20b, v1.14.20c, … for subsequent rebuilds on the same opencode version.

  • The number tracks the opencode npm version (see OPENCODE_VERSION ARG in Dockerfile).
  • No letter suffix on the first build of a new opencode version — the bare v{opencode_version} tag is the canonical release.
  • Letter suffix is the build ordinal, starting at b for the second build. The letter a is never used — think of the suffix as counting rebuilds: b = 2nd, c = 3rd, d = 4th, …. For opencode version 1.14.20: first build v1.14.20, second v1.14.20b, third v1.14.20c, and so on.
  • A letter suffix is only used for container-level rebuilds — tooling changes, CVE fixes, doc-driven rebuilds, entrypoint bugfixes — that don't change the underlying opencode version.

CI produces eight Docker Hub tags per release: vX.Y.Z[n], latest, vX.Y.Z[n]-omos, latest-omos, vX.Y.Z[n]-with-pi, latest-with-pi, vX.Y.Z[n]-omos-with-pi, latest-omos-with-pi — one tag pair (versioned + floating alias) per build variant.

When bumping the opencode version, also bump OPENCODE_VERSION in Dockerfile and update the comment in .env.example if it names a specific model/version for context.

Critical conventions

  • entrypoint.sh volume ownership loop — when adding a new named volume mount point, add it to the for dir in ... loop in entrypoint.sh so root-owned volumes get chowned on startup. The loop writes a .devbox-owner sentinel after a successful chown so subsequent starts skip the recursive walk. Users should not touch these files.

  • Documentation coupling on release — four docs co-vary and drift in lockstep when not updated together:

    • README.md is the source of truth for user-facing build/run/config detail.
    • DOCKER_HUB.md is auto-generated from HUB_TEMPLATE in scripts/generate-dockerhub-md.py. CI's --check run fails if it's stale. Hub-facing copy is intentionally slim (~5.5 kB, ~78% headroom against the 25 kB Hub limit) — update the template here when image variants, quick-start flow, or the elevator pitch change. README.md no longer feeds into Hub, so README edits do NOT require regenerating DOCKER_HUB.md.
    • CHANGELOG.md records every release. When cutting a tag, promote ## Unreleased to ## vX.Y.Z[n] — YYYY-MM-DD BEFORE pushing the tag so the tag points at a CHANGELOG that names itself. Keep entries reverse-chronological (newest at top, after the Unreleased block). Doc-only updates that happen post-tag (Hub description live-patches, README clarifications) get a fresh ## Unreleased block with a note that they don't trigger a new image build.
    • AGENTS.md (this file) carries domain facts that change on structural releases — tag-count statements, CI job lists, install contracts. After any change to .gitea/workflows/*.yml or the variant matrix, grep this file for stale numbers (grep -nE "four|eight|all [0-9]").
    • .env.example must be hand-updated to match Dockerfile/entrypoint behavior — it is not auto-generated.

    Release-day checklist: README → (regenerate DOCKER_HUB.md only if HUB_TEMPLATE changed) → promote CHANGELOG Unreleased → grep AGENTS.md for stale counts → commit → tag → push tag.

  • GitHub/Gitea-sourced binaries float by default — gosu, fzf, git-lfs, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to latest. Each build-time install step reads the /releases/latest Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same ARCH case-switch pattern for multi-arch support (amd64/arm64). Intentional pins: OPENCODE_VERSION (drives the image tag), NODE_VERSION=22 (major pin), DEBIAN_VERSION=trixie-slim (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag.

  • Resolved versions are logged by the smoke testscripts/smoke-test.sh prints a "Resolved component versions" table as its first step. CI logs always capture what got baked into a given image even when ARGs default to latest.

  • Shell scripts use set -euo pipefail — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with || true.

  • MemPalace install path — installed via uv tool install into /opt/uv-tools/mempalace/. Both the mempalace CLI and the mempalace-mcp MCP server binary are shipped as entry points by the mempalace package itself and placed on PATH by uv as shims whose shebangs point at the venv's Python. No hand-rolled wrapper is needed. Do not use pip install --break-system-packages — that was the previous approach and has been removed. Do not use ["python3", "-m", "mempalace.mcp_server"] in opencode.jsonc — system Python can't import from the uv venv.

  • generate-config.py idempotency — the script MUST never overwrite an existing opencode.jsonc or legacy opencode.json. Config persists in the devbox-opencode-config named volume; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.

  • Skillset auto-deploy — on every container start, entrypoint-user.sh looks for a skillset repo (detection order: $SKILLSET_CONTAINER_PATH$HOME/skillset/workspace/skillset) and runs deploy-skills.sh --bootstrap --prune-stale. This creates relative symlinks in ~/.agents/skills/ and ~/.config/opencode/instructions/. Do NOT bind-mount ~/.agents/skills/ from the host — the container manages its own skills with relative symlinks that differ from the host's. The named volume devbox-opencode-config persists the deployed config across restarts.

  • Config persistence via named volumedevbox-opencode-config is a Docker named volume mounted at ~/.config/opencode/. It is NOT a host bind mount by default. This separation allows both native and containerized opencode to coexist on the same machine without symlink conflicts. Users who need to override can replace the named volume with a host bind mount in their compose file. Same pattern for pi: devbox-pi-config is mounted at ~/.pi/ and persists user toggles (/ext-disabled extensions), ~/.pi/agent/settings.json edits, and — because NPM_CONFIG_PREFIX is set to ~/.pi/npm-global — anything installed via pi install npm:... or npm install -g as the developer user, across container recreate AND image rebuild.

  • pi install contractINSTALL_PI=true (default false) opt-in build arg. The baked pi binary is npm-installed globally to /usr at build time (system prefix). At runtime, NPM_CONFIG_PREFIX=/home/developer/.pi/npm-global is set in the image ENV with that prefix's bin/ prepended to PATH — so any pi install npm:... or npm install -g invoked by the developer user lands on the named volume and survives everything except docker compose down -v. The new ENVs are declared after all build-time npm install -g calls in the Dockerfile so they don't redirect the baked installs into a path that the volume mount would later shadow. If the user runs npm install -g @earendil-works/pi-coding-agent themselves, the user-installed copy on the volume wins via PATH order; otherwise image rebuild is the upgrade path for the baked pi (same contract as OPENCODE_VERSION). The pi-toolkit and pi-extensions repos are git-cloned into /opt/ at build time, then their install.sh runs from entrypoint-user.sh on each container start to symlink into ~/.pi/agent/ (which lives on the named volume). The mempalace pi-bridge is symlinked manually from /opt/mempalace-toolkit/extensions/pi/mempalace.ts — we do NOT call mempalace-toolkit's full install.sh because its install_skill step would race with skillset auto-deploy --prune-stale.

  • Pi deploy ordering matters in entrypoint-user.shpi-toolkit runs first (creates keybindings.json symlink and writes pi-env.zsh), then pi-extensions, then settings.json template bootstrap, then mempalace bridge symlink. mempalace-toolkit's check_pi_toolkit probe (when called from the host install path) expects keybindings to already be present — not currently called from container, but ordering matches host convention.

  • Default CMD is bash -l — not a harness. docker compose run --rm devbox drops the user into a login shell to choose: aws sso login, then opencode or pi (or any tool). Pass the harness explicitly to launch directly: docker compose run --rm devbox opencode / docker compose run --rm devbox pi. docker compose exec bypasses entrypoint+CMD entirely (existing user workflow unchanged).

  • Docker Hub description update — uses /v2/auth/token endpoint (not the deprecated /v2/users/login). Auth uses identifier/secret fields, returns access_token, sent as Bearer. Short description must be ≤100 bytes.

CI quirks

  • Both build jobs include an IPv4 preference step (gai.conf + driver-opts: network=host for buildx) to work around intermittent IPv6 failures on the Gitea runners.
  • update-description job runs only when both builds succeed (needs: [build-base, build-omos]).
  • Tags must be pushed to trigger the publish workflow. The validate workflow runs on push to main and PRs.
  • Smoke tests run on amd64 only (single-arch load into the local daemon). The multi-arch push happens after smoke passes.
  • Gitea Actions runner has ~40 GB disk, often 70%+ used at job start. All eight load: true jobs (validate-base, validate-omos, validate-with-pi, validate-omos-with-pi, smoke-base, smoke-omos, smoke-with-pi, smoke-omos-with-pi) include a Reclaim runner disk step that strips catthehacker-resident toolchains and prunes stale docker state before setup-buildx-action. Build jobs use a lighter version (push-by-digest doesn't need docker system prune). Don't remove these steps without testing on a fresh runner.
  • docker/build-push-action@v7 with platforms: linux/amd64,linux/arm64 handles multi-arch push natively in a single job — produces a proper manifest list, no matrix or merge step needed. An earlier revision split into per-arch matrix jobs with digest artifacts, but that pattern requires actions/{upload,download}-artifact@v4+ which Gitea Actions doesn't support (see below).
  • actions/upload-artifact and actions/download-artifact must stay at @v3 on Gitea. v4+ uses a GitHub-Enterprise-specific Artifact API; runs fail with GHESNotSupportedError. If you need artifacts for a new reason (build logs, SBOMs, etc.), pin @v3 explicitly.
  • Step scripts run under /bin/sh (dash), not bash. Avoid bash-isms like ${VAR//a/b} parameter-pattern substitution; use POSIX alternatives (tr, sed) or declare shell: bash on the step.
  • BUILDKIT_PROGRESS=plain is set at workflow level on docker-publish.yml so arm64-under-QEMU builds log each layer line-by-line. The default collapsed progress UI hides which step is stalled, which made diagnosing earlier hangs expensive.

Testing changes

The smoke test (scripts/smoke-test.sh) is the canonical check and runs automatically in CI. To run locally:

# Base image
docker compose build
bash scripts/smoke-test.sh opencode-devbox --variant base

# OMOS image
docker build --build-arg INSTALL_OMOS=true -t opencode-devbox:omos .
bash scripts/smoke-test.sh opencode-devbox:omos --variant omos

For manual/exploratory testing:

  1. docker compose run --rm devbox bash
  2. Check specific tools inside: nvim --version, bat --version, uv --version, mempalace --help, etc.
  3. For entrypoint changes: test with a non-1000 UID workspace to verify UID adjustment, volume ownership fixes, and the .devbox-owner sentinel behavior.
  4. For generate-config.py changes: run standalone with HOME=/tmp/fake OPENCODE_PROVIDER=anthropic python3 rootfs/usr/local/lib/opencode-devbox/generate-config.py.

Commit style

Imperative mood, first line summarizes the change. Multi-line body explains "why" when non-obvious. Examples from history:

  • Fix ownership of named volume mount points in entrypoint
  • Add uv package manager to base image for on-demand Python support
  • Upgrade base image from Debian bookworm to trixie (current stable)