- CHANGELOG: add the missing entry for the ~/.config/devbox-shell compose-doc
commit (440218f); promote Unreleased -> v1.15.13e (2026-06-04) with a release
summary (letter-suffix rebuild on opencode 1.15.13, picks up pi 0.78.1 + LAN
key persistence + devbox-ssh-local chown fix + validate.yml false-neg fix).
- AGENTS.md: document the STRICT_REGISTRATION smoke-gate knob under CI quirks
(kept in lockstep with the validate.yml/docker-publish-split.yml change).
Docs only; no image/behavior change. Tagging v1.15.13e after this lands.
26 KiB
AGENTS.md
Project overview
Docker image packaging opencode into a production-ready dev container. Image variants are published to Docker Hub via Gitea Actions CI. Not a library or application — this is infrastructure (Dockerfiles, entrypoint scripts, docker-compose, documentation).
File roles
Dockerfile.base— variant-independent layers (apt, locales, AWS CLI, Node.js, mempalace, gitea-mcp, user setup, chromadb prewarm, ENVs, entrypoints). Published asjoakimp/opencode-devbox:base-<sha12>. Rebuilt only when its content hash changes.Dockerfile.variant—FROMs the base and adds only opencode/omos/pi installs gated by build args:INSTALL_OPENCODE(default true),INSTALL_OMOS,INSTALL_PI, andINSTALL_MEMPALACE. All GitHub-sourced binaries are pinned with version ARGs. WhenINSTALL_PI=trueit also clonespi-fork+pi-observational-memory(fromgithub.com/elpapi42, refsPI_FORK_REF/PI_OBSMEM_REF) to/optand runsnpm installthere at build time so thefork/recallextensions can load (a local-pathpi installdoes not npm-install). Thepi-onlyvariant setsINSTALL_OPENCODE=false,INSTALL_PI=true— pi without opencode, the single source of truth for the separatepi-devboximage. It is built and smoke-tested here, but published into thejoakimp/pi-devboxrepo as the internal building-block tagbase-pi-only[-vX.Y.Z](NOT underopencode-devbox), so an opencode-devbox tag never ships without opencode.entrypoint.sh— runs as root: UID/GID adjustment, SSH permissions, volume ownership fixes (skipped via.devbox-ownersentinel when ownership is already correct). Then drops to developer via gosu. Volume ownership loop covers~/.pi/whenINSTALL_PI=true.entrypoint-user.sh— runs as developer: git config, opencode.jsonc generation (delegated togenerate-config.py), LAN-access setup (delegated tosetup-lan-access.sh), pi-toolkit + pi-extensions deploy (when pi installed), pi settings.json bootstrap, mempalace pi-bridge symlink, runtimepi install /opt/{pi-fork,pi-observational-memory}registration (idempotent), skillset auto-deploy from mounted skillset repo, OMOS setup.rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh— host-OS-agnostic LAN reachability helper. Detects VM-backed hosts (macOS OrbStack / Docker Desktop, viahost.docker.internalresolution) and generates a writable~/.ssh-local/configusing the host as an SSH jump; no-op on native Linux. Controlled byDEVBOX_LAN_ACCESS/HOST_SSH_USER/DEVBOX_HOST_ALIAS/DEVBOX_LAN_AUTOJUMP_PRIVATE. Ships the mechanism only (generichostjump alias); user targets stay host-side — named-peerProxyJump hostoverrides go in a bind-mounted~/.config/devbox-shell/ssh-lan.conf(Included before~/.ssh/config), never baked into the image. Scoping invariant: everyIncludein the generated config MUST be preceded by a bareHost *reset — anIncludeis scoped to the enclosingHost/Matchblock, so without the reset the included config only applies when targetinghost/macand named peers fall back to SSH defaults. The topHost *block also overridesUserKnownHostsFileandControlPathinto the writable~/.ssh-localsidecar (first-value-wins), because the bind-mounted~/.sshis read-only — otherwise multiplexed hosts (ControlPath ~/.ssh/cm/...) fail to create their master socket. Non-fatal. Counted in the base hash, so editing it advancesbase-latest.rootfs/usr/local/lib/opencode-devbox/generate-config.py— generates~/.config/opencode/opencode.jsoncfrom env vars. Never overwrites an existing config (checks both.jsonand.jsonc). Auto-registers MCP servers for detected tools (mempalace viamempalace-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— generatesDOCKER_HUB.mdfrom a hand-maintainedHUB_TEMPLATEconstant.--checkfails if the committed file is out of sync (enforced by thevalidateworkflow).DOCKER_HUB.md— auto-generated fromHUB_TEMPLATEinscripts/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 ofDOCKER_HUB.md: the Hub doc is hand-maintained in the generator'sHUB_TEMPLATEand intentionally slim, linking back to the gitea README for depth..gitea/README.md— read this first if you're touching CI. Architectural overview of the build pipeline (production vs split-base), wall-clock estimates, NPM_CONFIG_PREFIX gotcha, runner expectations, migration plan..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-split.yml— production CI pipeline on tag push (v*). Two-phase split-base: computes base hash, conditionally builds base, runs 5 parallel smoke tests, then 5 parallel multi-arch variant builds, promotesbase-latestalias, updates Docker Hub description.
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_VERSIONARG inDockerfile.variant). -
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
bfor the second build. The letterais never used — think of the suffix as counting rebuilds:b = 2nd, c = 3rd, d = 4th, …. For opencode version1.14.20: first buildv1.14.20, secondv1.14.20b, thirdv1.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.
-
Pre-flight check before cutting any non-letter-suffixed tag — verify the bump is real:
npm view opencode-ai version # must equal the X.Y.Z in your tagIf the npm version equals the previous release's
X.Y.Z, you're cutting a letter-suffix rebuild (vX.Y.Zc,vX.Y.Zd, …), not a new minor. A barevX.Y.Ztag is a claim that opencode upstream just releasedX.Y.Z— if that claim is wrong, future opencode releases will collide with your tag namespace and the version-tracking story breaks.Cautionary example: 2026-05-28 morning,
v1.15.12was cut while opencode-ai was still at1.15.11. The commit message itself acknowledged "OPENCODE_VERSION stays at 1.15.11" but taggedv1.15.12anyway. Re-cut asv1.15.11cthe same afternoon (see CHANGELOG). Thev1.15.12git tag and Hub images stayed as historical artifacts; the slip cost a CI cycle and a CHANGELOG-rewrite. Run the npm view check at the top of every release-day cut.
CI produces eight Docker Hub tags under opencode-devbox 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 opencode-bearing variant (four variants). A fifth build, pi-only, is built+smoked here but pushed into the joakimp/pi-devbox repo as base-pi-only-vX.Y.Z (+ base-pi-only on tag builds), where it becomes the base for that image.
When bumping the opencode version, bump OPENCODE_VERSION in Dockerfile.variant and update the comment in .env.example if it names a specific model/version for context.
Upstream sources — where to look up release notes
When drafting a release CHANGELOG entry, pull notes from the canonical upstream repo for each tracked package. Getting this wrong leads to thin or wrong release notes; the image bytes are unaffected but the documentation suffers.
| Package | Canonical upstream | What you'll find there |
|---|---|---|
opencode-ai (npm) |
https://github.com/anomalyco/opencode/releases | Per-version release notes with Core / TUI / Desktop / SDK sections, contributor attributions. Some versions have empty bodies (internal/no-user-visible); most do not. |
@earendil-works/pi-coding-agent (npm) |
The CHANGELOG.md shipped inside the npm tarball: npm pack @earendil-works/pi-coding-agent@<version> then extract package/CHANGELOG.md. |
Rich changelog with New Features / Added / Changed / Fixed sections per version. |
| Other floated tools (gosu, fzf, bat, eza, zoxide, uv, nvim, gitea-mcp, Go, oh-my-opencode-slim) | Each project's own GitHub releases page | Usually less material per release; quote selectively. |
Trap to avoid: there is a github.com/sst/opencode repo that some search results surface; that's a fork (and probably the historical name people associate with opencode given the upstream lineage). It does NOT track the same release timeline. Use anomalyco/opencode for opencode release notes.
Fetch pattern (saved here for muscle memory):
# Latest stable opencode-ai versions on npm
npm view opencode-ai time --json | python3 -c 'import sys,json,re; d=json.load(sys.stdin); print(*sorted([(v,t) for v,t in d.items() if re.fullmatch(r"\d+\.\d+\.\d+",v)], key=lambda x:x[1], reverse=True)[:6], sep="\n")'
# Release notes for a specific version
curl -s https://api.github.com/repos/anomalyco/opencode/releases/tags/v1.15.10 | python3 -c 'import sys,json; print(json.load(sys.stdin).get("body","(empty)"))'
# pi changelog
cd /tmp && npm pack @earendil-works/pi-coding-agent@0.75.5 && tar -xzf earendil-works-pi-coding-agent-0.75.5.tgz package/CHANGELOG.md && head -40 package/CHANGELOG.md
Critical conventions
-
entrypoint.sh volume ownership loop — when adding a new named volume mount point, add it to the
for dir in ...loop inentrypoint.shso root-owned volumes get chowned on startup. The loop writes a.devbox-ownersentinel 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.mdis the source of truth for user-facing build/run/config detail.DOCKER_HUB.mdis auto-generated fromHUB_TEMPLATEinscripts/generate-dockerhub-md.py. CI's--checkrun 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.mdrecords every release. When cutting a tag, promote## Unreleasedto## vX.Y.Z[n] — YYYY-MM-DDBEFORE pushing the tag so the tag points at a CHANGELOG that names itself. Keep entries reverse-chronological (newest at top, after theUnreleasedblock). Doc-only updates that happen post-tag (Hub description live-patches, README clarifications) get a fresh## Unreleasedblock 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/*.ymlor the variant matrix, grep this file for stale numbers (grep -nE "four|eight|all [0-9]")..env.examplemust 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.
Between releases the same coupling applies. Doc drift is not just a release-day concern — a workflow tweak, entrypoint change, or
generate-config.pyrefactor can leave any of these four files lying. Before committing a non-release change, grep the docs for references to what you touched:git diff --name-only HEAD | xargs -I{} grep -l 'thing-you-changed' README.md AGENTS.md DOCKER_HUB.md .gitea/README.md .env.example. If a doc says "four variants" / "two phases" / "runs on amd64 only" and your change made that no longer true, fix it in the same commit. -
GitHub/Gitea-sourced binaries float by default — gosu, fzf, git-lfs, gitleaks, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to
latest. Each build-time install step reads the/releases/latestLocation redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the sameARCHcase-switch pattern for multi-arch support (amd64/arm64) — mind project-specific arch-name deviations (gitleaks usesx64, bat/eza/zoxide usex86_64/aarch64, gosu usesamd64/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 test —
scripts/smoke-test.shprints a "Resolved component versions" table as its first step. CI logs always capture what got baked into a given image even when ARGs default tolatest. -
PI_VERSIONandOMOS_VERSIONMUST be passed by CI as concrete versions, not left at thelatestdefault. The npm install steps inDockerfile.variant(npm install -g @earendil-works/pi-coding-agent/oh-my-opencode-slim@${OMOS_VERSION}) produce identical layer-hashes when the ARG values are byte-identical across builds; combined with the registry buildcache (base-buildcache) the layer gets reused even whenlatestwould have resolved to a newer upstream. This is the same class of bug that bit pi-devbox v0.74.0 → v0.75.5 (silent same-bytes-across-releases regression discovered 2026-05-23, fixed in pi-devbox v0.75.5b). It is currently masked in opencode-devbox byOPENCODE_VERSIONbeing a hard-coded ARG that bumps every release — that bump invalidates the parent-chain cache key for the downstream pi/omos layers — but the masking would fail the moment avN.N.Nbopencode-version-unchanged release ships that only bumps pi or omos. Preventative fix:.gitea/workflows/docker-publish-split.ymlhas aresolve-versionsjob that runsnpm view @earendil-works/pi-coding-agent versionandnpm view oh-my-opencode-slim version, exposing concrete values as outputs that every variant smoke + build job consumes via build-args. Smoke tests assert viaEXPECTED_PI_VERSION/EXPECTED_OMOS_VERSIONenv vars — would catch the regression on the next release rather than four releases later. If you change the variant build-args list, the resolve-versions job, or the smoke EXPECTED_*_VERSION wiring, audit all affected jobs in lockstep. -
Registry buildkit cache-export is currently disabled — do NOT re-add
cache-from/cache-toto thebuild-basestep in.gitea/workflows/docker-publish-split.ymlwithout first verifying that buildkit'smode=maxcache-export toregistry-1.docker.iono longer returns HTTP 400 from the Hub CDN edge. The regression surfaced ~2026-05-23 and broke five consecutive opencode-devbox publish attempts (runs #332/333/334/336 + a rerun); root-caused on 2026-05-28 by a manual host-side publish that reproduced the same 400 only on--cache-towhile image push worked fine. Failure shape is stable (Offset:0in the_statetoken, HTML response body = CDN-tier rejection, not registry backend), repo-specific (we're the only repo writing:base-buildcachemode=max), and explains why pinningsetup-buildx-action@v4.0.0didn't help (action pin doesn't change the bundled buildkit version on the catthehacker runner image). Trade-off: dockerfile.base changes pay a full ~3 min rebuild instead of pulling cached layers; unchanged bases short-circuit at the Hub-probe step inbase-decideand never re-build anyway. Variants don't use registry cache so they're unaffected. Re-enable condition: upstream moby/buildkit fix lands AND a low-risk test run succeeds without 400s. See CHANGELOG v1.15.12Unreleasedblock for the full diagnostic chain. Manual escape-hatch publish procedure:docs/manual-host-publish.md. -
Push steps wrap
docker buildx build --pushin a 3-attempt retry loop (15s, 30s backoff) for transientregistry-1.docker.ioblips — rate limits, brief 5xx, CDN flap. Implemented as inlineshell: bashsteps withdocker buildx buildraw rather thandocker/build-push-action@v7so the loop is visible and tweakable. Affects the 1 base + 5 variant push steps in.gitea/workflows/docker-publish-split.yml; smoke-test builds (load: true, no push) are untouched. This does NOT mask deterministic failures — a true regression (like the cache-export 400 of 2026-05-23..28) fails all 3 attempts identically and the job still fails. Orthogonal to the cache-export disablement above: cache-export was about a deterministic protocol mismatch, retry is about absorbing genuine transients. Both are belt-and-braces with theci-release-watcherskill's transient-rerun heuristic. If you change the matrix of push steps, keep the retry wrapper consistent across them — the pattern is duplicated rather than factored out because Gitea Actions doesn't support reusable composite shell steps cleanly. -
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 installinto/opt/uv-tools/mempalace/. Both themempalaceCLI and themempalace-mcpMCP 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 usepip install --break-system-packages— that was the previous approach and has been removed. Do not use["python3", "-m", "mempalace.mcp_server"]inopencode.jsonc— system Python can't import from the uv venv. -
generate-config.py idempotency — the script MUST never overwrite an existing
opencode.jsoncor legacyopencode.json. Config persists in thedevbox-opencode-confignamed volume; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this. -
Skillset auto-deploy — on every container start,
entrypoint-user.shlooks for a skillset repo (detection order:$SKILLSET_CONTAINER_PATH→$HOME/skillset→/workspace/skillset) and runsdeploy-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 volumedevbox-opencode-configpersists the deployed config across restarts. -
Config persistence via named volume —
devbox-opencode-configis 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-configis mounted at~/.pi/and persists user toggles (/ext-disabled extensions),~/.pi/agent/settings.jsonedits, and — becauseNPM_CONFIG_PREFIXis set to~/.pi/npm-global— anything installed viapi install npm:...ornpm install -gas the developer user, across container recreate AND image rebuild. -
pi install contract —
INSTALL_PI=true(default false) opt-in build arg. The bakedpibinary is npm-installed globally to/usrat build time (system prefix). At runtime,NPM_CONFIG_PREFIX=/home/developer/.pi/npm-globalis set in the image ENV with that prefix'sbin/prepended toPATH— so anypi install npm:...ornpm install -ginvoked by the developer user lands on the named volume and survives everything exceptdocker compose down -v. The new ENVs are declared after all build-timenpm install -gcalls in the Dockerfile so they don't redirect the baked installs into a path that the volume mount would later shadow. If the user runsnpm install -g @earendil-works/pi-coding-agentthemselves, the user-installed copy on the volume wins viaPATHorder; otherwise image rebuild is the upgrade path for the baked pi (same contract asOPENCODE_VERSION). The pi-toolkit and pi-extensions repos are git-cloned into/opt/at build time, then theirinstall.shruns fromentrypoint-user.shon 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 fullinstall.shbecause itsinstall_skillstep would race with skillset auto-deploy--prune-stale. -
Pi deploy ordering matters in entrypoint-user.sh —
pi-toolkitruns first (createskeybindings.jsonsymlink and writes pi-env.zsh), thenpi-extensions, thensettings.jsontemplate bootstrap, then mempalace bridge symlink. mempalace-toolkit'scheck_pi_toolkitprobe (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 devboxdrops the user into a login shell to choose:aws sso login, thenopencodeorpi(or any tool). Pass the harness explicitly to launch directly:docker compose run --rm devbox opencode/docker compose run --rm devbox pi.docker compose execbypasses entrypoint+CMD entirely (existing user workflow unchanged). -
Docker Hub description update — uses
/v2/auth/tokenendpoint (not the deprecated/v2/users/login). Auth usesidentifier/secretfields, returnsaccess_token, sent asBearer. Short description must be ≤100 bytes.
CI quirks
- Both build jobs include an IPv4 preference step (
gai.conf+driver-opts: network=hostfor buildx) to work around intermittent IPv6 failures on the Gitea runners. update-descriptionjob 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 ten
load: truejobs (validate-base,validate-omos,validate-with-pi,validate-omos-with-pi,validate-pi-only,smoke-base,smoke-omos,smoke-with-pi,smoke-omos-with-pi,smoke-pi-only) include aReclaim runner diskstep that strips catthehacker-resident toolchains and prunes stale docker state beforesetup-buildx-action. Build jobs use a lighter version (push-by-digest doesn't needdocker system prune). Don't remove these steps without testing on a fresh runner. docker/build-push-action@v7withplatforms: linux/amd64,linux/arm64handles 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 requiresactions/{upload,download}-artifact@v4+which Gitea Actions doesn't support (see below).actions/upload-artifactandactions/download-artifactmust stay at @v3 on Gitea. v4+ uses a GitHub-Enterprise-specific Artifact API; runs fail withGHESNotSupportedError. 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 declareshell: bashon the step. BUILDKIT_PROGRESS=plainis set at workflow level ondocker-publish-split.ymlso 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.STRICT_REGISTRATIONgates the fork/recall registration smoke assertions.smoke-test.sh's two pi-extension registration checks (thatpi-fork/pi-observational-memoryregistered in~/.pi/agent/settings.json) depend on the base entrypoint runningpi install /opt/<pkg>.validate.ymlbuilds variants from the publishedbase-latest, which lags the in-repo entrypoint until a release rebuilds the base — so those checks would false-negative there. They are therefore warn-only unlessSTRICT_REGISTRATION=1:validate.ymlleaves it unset (warn), anddocker-publish-split.yml(which builds the base fresh in the same run) setsSTRICT_REGISTRATION: "1"on the three pi-bearing smoke jobs to enforce them. Build-time/opt+node_moduleschecks stay hard in both paths. If you touch the registration checks or the base-freshness model, keep this flag wiring in lockstep across both workflows.
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:
docker compose run --rm devbox bash- Check specific tools inside:
nvim --version,bat --version,uv --version,mempalace --help, etc. - For entrypoint changes: test with a non-1000 UID workspace to verify UID adjustment, volume ownership fixes, and the
.devbox-ownersentinel behavior. - For
generate-config.pychanges: run standalone withHOME=/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 entrypointAdd uv package manager to base image for on-demand Python supportUpgrade base image from Debian bookworm to trixie (current stable)