v2.1.0: symlink OMOS bundled skills from image, bump opencode 1.17.4->1.17.5
Validate / base-change-warning (push) Successful in 7s
Validate / docs-check (push) Successful in 13s
Publish Docker Image / base-decide (push) Successful in 8s
Publish Docker Image / resolve-versions (push) Successful in 10s
Validate / validate-omos (push) Successful in 4m15s
Validate / validate-base (push) Successful in 5m2s
Publish Docker Image / build-base (push) Successful in 30m33s
Publish Docker Image / smoke-base (push) Successful in 3m20s
Publish Docker Image / smoke-omos (push) Successful in 4m24s
Publish Docker Image / build-variant-base (push) Successful in 13m37s
Publish Docker Image / build-variant-omos (push) Successful in 30m18s
Publish Docker Image / update-description (push) Successful in 6s
Publish Docker Image / promote-base-latest (push) Successful in 14s

Deploy the five oh-my-opencode-slim bundled skills (clonedeps, codemap,
deepwork, oh-my-opencode-slim, simplify) by symlinking them from the image
path into ~/.agents/skills/ on every container start, instead of the
installer copying them into the persistent config volume on first run only.
Image-sourced links mean 'docker compose pull' + recreate refreshes the
skills with no installer run and no config reset; the old copy-on-first-run
froze them in the volume forever.

- entrypoint-user.sh: new non-fatal OMOS bundled-skills reconcile block
  (runs after skillset deploy so OMOS wins the simplify collision; absolute
  symlinks; gated by OMOS_SKILLS, now independent of ENABLE_OMOS). Both
  installer calls now pass --skills=no. One-time migration backs up (never
  deletes) frozen real copies in ~/.config/opencode/skills/ to .bak.<epoch>.
- scripts/smoke-test.sh: assert the bundled-skills source path on omos.
- Bump OPENCODE_VERSION 1.17.4 -> 1.17.5.
- Versioning: document the move to independent image semver (v2.0.0 was the
  decouple point), mirroring pi-devbox. README/AGENTS/.env.example/CHANGELOG
  updated; new docs/omos-skills.md.
This commit is contained in:
Joakim Persson
2026-06-13 22:32:09 +02:00
parent 72298ae77e
commit ba8000732d
8 changed files with 273 additions and 30 deletions
+6 -2
View File
@@ -98,5 +98,9 @@ SSH_KEY_PATH=~/.ssh
# Requires image built with INSTALL_OMOS=true # Requires image built with INSTALL_OMOS=true
# ENABLE_OMOS=false # ENABLE_OMOS=false
# OMOS_TMUX=false # Enable tmux multiplexer integration # OMOS_TMUX=false # Enable tmux multiplexer integration
# OMOS_SKILLS=true # Install recommended skills (simplify, agent-browser, cartography) # OMOS_SKILLS=true # Symlink bundled OMOS skills (clonedeps, codemap,
# OMOS_RESET=false # Force regenerate oh-my-opencode-slim config on next start # # deepwork, oh-my-opencode-slim, simplify) from the
# # image into ~/.agents/skills/ each start; updates
# # on image pull. Independent of ENABLE_OMOS.
# # See docs/omos-skills.md
# OMOS_RESET=false # Force regenerate oh-my-opencode-slim config on next start (does not affect skills)
+28 -12
View File
@@ -18,7 +18,7 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
- `Dockerfile.base` — variant-independent layers (apt, locales, AWS CLI, Node.js, mempalace, gitea-mcp, user setup, chromadb prewarm, ENVs, entrypoints). Published as `joakimp/opencode-devbox:base-<sha12>`. Rebuilt only when its content hash changes. - `Dockerfile.base` — variant-independent layers (apt, locales, AWS CLI, Node.js, mempalace, gitea-mcp, user setup, chromadb prewarm, ENVs, entrypoints). Published as `joakimp/opencode-devbox:base-<sha12>`. Rebuilt only when its content hash changes.
- `Dockerfile.variant``FROM`s the base and adds only opencode/omos installs gated by build args: `INSTALL_OPENCODE` (default true), `INSTALL_OMOS`, and `INSTALL_MEMPALACE`. All GitHub-sourced binaries are pinned with version ARGs. Two variants: `base` (`INSTALL_OPENCODE=true`) and `omos` (`+INSTALL_OMOS=true`). - `Dockerfile.variant``FROM`s the base and adds only opencode/omos installs gated by build args: `INSTALL_OPENCODE` (default true), `INSTALL_OMOS`, and `INSTALL_MEMPALACE`. All GitHub-sourced binaries are pinned with version ARGs. Two variants: `base` (`INSTALL_OPENCODE=true`) and `omos` (`+INSTALL_OMOS=true`).
- `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. - `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.
- `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), LAN-access setup (delegated to `setup-lan-access.sh`), a one-time npm-global prefix migration shim (legacy `~/.pi/npm-global``~/.config/opencode/npm-global`), skillset auto-deploy from mounted skillset repo, OMOS setup. - `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), LAN-access setup (delegated to `setup-lan-access.sh`), a one-time npm-global prefix migration shim (legacy `~/.pi/npm-global``~/.config/opencode/npm-global`), skillset auto-deploy from mounted skillset repo, OMOS bundled-skills reconcile (symlinks the image's bundled skills into `~/.agents/skills/`), OMOS config 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, via `host.docker.internal` resolution) and generates a writable `~/.ssh-local/config` using the host as an SSH jump; no-op on native Linux. Controlled by `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER` / `DEVBOX_HOST_ALIAS` / `DEVBOX_LAN_AUTOJUMP_PRIVATE`. Ships the mechanism only (generic `host` jump alias); user targets stay host-side — named-peer `ProxyJump host` overrides go in a bind-mounted `~/.config/devbox-shell/ssh-lan.conf` (Included before `~/.ssh/config`), never baked into the image. **Scoping invariant:** every `Include` in the generated config MUST be preceded by a bare `Host *` reset — an `Include` is scoped to the enclosing `Host`/`Match` block, so without the reset the included config only applies when targeting `host`/`mac` and named peers fall back to SSH defaults. The top `Host *` block also overrides `UserKnownHostsFile` and `ControlPath` into the writable `~/.ssh-local` sidecar (first-value-wins), because the bind-mounted `~/.ssh` is read-only — otherwise multiplexed hosts (`ControlPath ~/.ssh/cm/...`) fail to create their master socket. Non-fatal. Counted in the base hash, so editing it advances `base-latest`. - `rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh` — host-OS-agnostic LAN reachability helper. Detects VM-backed hosts (macOS OrbStack / Docker Desktop, via `host.docker.internal` resolution) and generates a writable `~/.ssh-local/config` using the host as an SSH jump; no-op on native Linux. Controlled by `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER` / `DEVBOX_HOST_ALIAS` / `DEVBOX_LAN_AUTOJUMP_PRIVATE`. Ships the mechanism only (generic `host` jump alias); user targets stay host-side — named-peer `ProxyJump host` overrides go in a bind-mounted `~/.config/devbox-shell/ssh-lan.conf` (Included before `~/.ssh/config`), never baked into the image. **Scoping invariant:** every `Include` in the generated config MUST be preceded by a bare `Host *` reset — an `Include` is scoped to the enclosing `Host`/`Match` block, so without the reset the included config only applies when targeting `host`/`mac` and named peers fall back to SSH defaults. The top `Host *` block also overrides `UserKnownHostsFile` and `ControlPath` into the writable `~/.ssh-local` sidecar (first-value-wins), because the bind-mounted `~/.ssh` is read-only — otherwise multiplexed hosts (`ControlPath ~/.ssh/cm/...`) fail to create their master socket. Non-fatal. Counted in the base hash, so editing it advances `base-latest`.
- `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). - `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/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.
@@ -31,21 +31,36 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
## Versioning scheme ## 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. Image tags follow **independent semver** — they version *this image*, not the
bundled opencode release. **`v2.0.0` is the decoupling point** (the pi-removal
breaking release); from there the opencode npm version is tracked in
`CHANGELOG.md` and the `OPENCODE_VERSION` ARG but no longer drives the tag. This
mirrors the sibling [`joakimp/pi-devbox`](https://gitea.jordbo.se/joakimp/pi-devbox)
repo, which decoupled from the pi tool version at its own `v1.0.0`.
- The number tracks the opencode npm version (see `OPENCODE_VERSION` ARG in `Dockerfile.variant`). - **MAJOR** — breaking changes to how users run/configure the container (volume
- **No letter suffix** on the first build of a new opencode version — the bare `v{opencode_version}` tag is the canonical release. layout, removed variants/build-args, an entrypoint contract change that
- **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. requires user action). `v2.0.0` (pi removal + npm-prefix relocation) is the
- 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. reference example.
- **Pre-flight check before cutting any non-letter-suffixed tag** — verify the bump is real: - **MINOR** — backward-compatible features: new variants/tags, new opt-in
behavior, new env vars, or changed-but-compatible semantics. Example: `v2.1.0`
added the OMOS bundled-skills image-symlink mechanism.
- **PATCH** — opencode/tool version bumps and small fixes that don't change the
contract. When a release pairs a tool bump with a feature, the feature wins
and it's a minor.
- **Pre-flight check** — whenever an opencode bump is part of the release,
verify it is real before claiming it in the CHANGELOG:
```bash ```bash
npm view opencode-ai version # must equal the X.Y.Z in your tag npm view opencode-ai version # must equal the X.Y.Z you pin in Dockerfile.variant
``` ```
If 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 bare `vX.Y.Z` tag is a claim that opencode upstream just released `X.Y.Z`** — if that claim is wrong, future opencode releases will collide with your tag namespace and the version-tracking story breaks. Historical note: under the *old* `v{opencode_version}[letter]` scheme a
mismatched tag was a namespace hazard — e.g. `v1.15.12` was cut while
Cautionary example: 2026-05-28 morning, `v1.15.12` was cut while opencode-ai was still at `1.15.11`. The commit message itself acknowledged "OPENCODE_VERSION stays at 1.15.11" but tagged `v1.15.12` anyway. Re-cut as `v1.15.11c` the same afternoon (see CHANGELOG). The `v1.15.12` git 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.** opencode was still `1.15.11`, then re-cut as `v1.15.11c` (2026-05-28), costing
a CI cycle. Semver tags no longer encode the opencode version, so that
specific collision class is gone — but a CHANGELOG that names the wrong
upstream version is still wrong.
CI produces four Docker Hub tags **under `opencode-devbox`** per release: `vX.Y.Z[n]`, `latest`, `vX.Y.Z[n]-omos`, `latest-omos` — one tag pair (versioned + floating alias) per variant (two variants: `base`, `omos`). CI produces four Docker Hub tags **under `opencode-devbox`** per release: `vX.Y.Z`, `latest`, `vX.Y.Z-omos`, `latest-omos` — one tag pair (versioned + floating alias) per variant (two variants: `base`, `omos`).
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. 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.
@@ -92,6 +107,7 @@ curl -s https://api.github.com/repos/anomalyco/opencode/releases/tags/v1.15.10 |
- **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. - **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. - **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. - **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.
- **OMOS bundled-skills reconcile** — on the omos variant, `entrypoint-user.sh` symlinks the five skills bundled with `oh-my-opencode-slim` (`clonedeps`, `codemap`, `deepwork`, `oh-my-opencode-slim`, `simplify`) from the image path `/usr/lib/node_modules/oh-my-opencode-slim/src/skills/<name>` into `~/.agents/skills/`, on **every** start, *after* the skillset deploy (so OMOS wins name collisions via `ln -sfn` — the only overlap is `simplify`, which was removed from the skillset repo). These are **absolute** symlinks (target is image-internal at a fixed `/usr` path) — do NOT "fix" them to relative like skillset's. Because the target lives in the image, pulling a newer image updates the skills with no installer run and no config reset. The block is non-fatal (`{ … } || true`), gated by `OMOS_SKILLS` (default true, **independent of `ENABLE_OMOS`**) and the presence of the source dir (no-op on the base variant). The two `oh-my-opencode-slim install` calls now pass `--skills=no` unconditionally — the installer manages only `oh-my-opencode-slim.json`, never skills; do not reintroduce installer-managed skills. A one-time migration (marker: `~/.config/opencode/.omos-skills-migrated`) backs up — never deletes — any frozen real copies the old installer left in `~/.config/opencode/skills/` to `<name>.bak.<epoch>`, because those would otherwise shadow the fresh image-sourced symlinks. The build-time smoke test asserts the bundled-skills source path exists (catches an upstream package restructure loudly). Full rationale: `docs/omos-skills.md`.
- **Config persistence via named volume** — `devbox-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. Because `NPM_CONFIG_PREFIX` is set to `~/.config/opencode/npm-global` (relocated from the legacy `~/.pi/npm-global` in v2.0.0), anything installed via `npm install -g` as the developer user also lands on this volume and survives container recreate AND image rebuild. - **Config persistence via named volume** — `devbox-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. Because `NPM_CONFIG_PREFIX` is set to `~/.config/opencode/npm-global` (relocated from the legacy `~/.pi/npm-global` in v2.0.0), anything installed via `npm install -g` as the developer user also lands on this volume and survives container recreate AND image rebuild.
- **npm-global prefix relocation (v2.0.0 breaking change)** — the user-writable global npm prefix moved from `~/.pi/npm-global` to `~/.config/opencode/npm-global`. The old path lived on the `devbox-pi-config` volume (only mounted in `docker-compose.yml`); the new path is on `devbox-opencode-config`, which is a persistent named volume in BOTH `docker-compose.yml` and `docker-compose.shared.yml`. `entrypoint-user.sh` carries a one-time migration shim: if `~/.pi/npm-global` exists and the marker `~/.config/opencode/npm-global/.migrated-from-dot-pi` is absent, it `cp -an` the old `lib/`/`bin/`/`share/` into the new prefix (never overwriting fresh installs) and writes the marker. Baked binaries stay on `/usr` (the variant Dockerfile runs each `npm install -g` with `NPM_CONFIG_PREFIX=/usr`) so the volume mount doesn't shadow them. The `ENV NPM_CONFIG_PREFIX`/`PATH` lines in `Dockerfile.base` are declared *after* all build-time installs. - **npm-global prefix relocation (v2.0.0 breaking change)** — the user-writable global npm prefix moved from `~/.pi/npm-global` to `~/.config/opencode/npm-global`. The old path lived on the `devbox-pi-config` volume (only mounted in `docker-compose.yml`); the new path is on `devbox-opencode-config`, which is a persistent named volume in BOTH `docker-compose.yml` and `docker-compose.shared.yml`. `entrypoint-user.sh` carries a one-time migration shim: if `~/.pi/npm-global` exists and the marker `~/.config/opencode/npm-global/.migrated-from-dot-pi` is absent, it `cp -an` the old `lib/`/`bin/`/`share/` into the new prefix (never overwriting fresh installs) and writes the marker. Baked binaries stay on `/usr` (the variant Dockerfile runs each `npm install -g` with `NPM_CONFIG_PREFIX=/usr`) so the volume mount doesn't shadow them. The `ENV NPM_CONFIG_PREFIX`/`PATH` lines in `Dockerfile.base` are declared *after* all build-time installs.
- **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 any tool). Pass the harness explicitly to launch directly: `docker compose run --rm devbox opencode`. `docker compose exec` bypasses entrypoint+CMD entirely (existing user workflow unchanged). - **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 any tool). Pass the harness explicitly to launch directly: `docker compose run --rm devbox opencode`. `docker compose exec` bypasses entrypoint+CMD entirely (existing user workflow unchanged).
+45
View File
@@ -8,6 +8,51 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
## Unreleased ## Unreleased
## v2.1.0 — 2026-06-13
Image-semver **minor**: adds the OMOS bundled-skills image-symlink mechanism and
bumps opencode. `v2.0.0` decoupled image versioning from the opencode version
(see [AGENTS.md](AGENTS.md#versioning-scheme)); this is the first feature release
on the semver line, mirroring how the sibling `pi-devbox` repo moved to semver
after its own `v1.0.0` decouple.
### Bumped: opencode-ai 1.17.4 → 1.17.5
`OPENCODE_VERSION` ARG in `Dockerfile.variant`.
### Changed: OMOS bundled skills are now symlinked from the image, not copied
The five skills bundled with `oh-my-opencode-slim` (`clonedeps`, `codemap`,
`deepwork`, `oh-my-opencode-slim`, `simplify`) are now **symlinked from the
image** into `~/.agents/skills/` on every container start, instead of being
**copied** into `~/.config/opencode/skills/` once on first run by the OMOS
installer.
Why: the old copy landed in the persistent `devbox-opencode-config` volume and
was gated by config-existence, so it **froze** at whatever the image shipped on
first run — pulling a newer image never refreshed the skills, and the only
update path (`OMOS_RESET=true`) also clobbered the user's hand-tuned
`opencode.jsonc`. With the symlink approach the link target lives in the image,
so **`docker compose pull` + recreate updates the skills for free** — no
installer run, no config reset.
- `entrypoint-user.sh`: new OMOS bundled-skills reconcile block (runs after the
skillset deploy so OMOS wins the `simplify` name collision; absolute symlinks;
non-fatal; gated by `OMOS_SKILLS`, now independent of `ENABLE_OMOS` and the
presence of the bundled-skills source on the omos variant).
- `entrypoint-user.sh`: both `oh-my-opencode-slim install` calls now pass
`--skills=no` — the installer manages only `oh-my-opencode-slim.json`.
- One-time migration backs up (never deletes) any frozen real copies in
`~/.config/opencode/skills/` to `<name>.bak.<epoch>` (marker:
`~/.config/opencode/.omos-skills-migrated`).
- `scripts/smoke-test.sh`: asserts the bundled-skills source path exists on the
omos variant (catches an upstream package restructure).
- Docs: new `docs/omos-skills.md`; updated `README.md`, `AGENTS.md`,
`.env.example` (`OMOS_SKILLS` semantics).
**Editing `entrypoint-user.sh` advances the base hash** — this release carries a
full base rebuild and a `base-latest` re-promote.
## v2.0.0 — 2026-06-13 ## v2.0.0 — 2026-06-13
**Major release: pi is fully removed from opencode-devbox** (deprecated in **Major release: pi is fully removed from opencode-devbox** (deprecated in
+1 -1
View File
@@ -39,7 +39,7 @@ ARG USER_NAME=developer
# edit, so the cache-hit class of bug that bit pi-devbox v0.74.0.. # edit, so the cache-hit class of bug that bit pi-devbox v0.74.0..
# v0.75.5 cannot apply here. # v0.75.5 cannot apply here.
ARG INSTALL_OPENCODE=true ARG INSTALL_OPENCODE=true
ARG OPENCODE_VERSION=1.17.4 ARG OPENCODE_VERSION=1.17.5
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \ RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \ NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \
opencode --version ; \ opencode --version ; \
+8 -6
View File
@@ -146,8 +146,8 @@ docker compose exec -u developer devbox aws --version
| `EDITOR` | Default text editor | `nvim` | | `EDITOR` | Default text editor | `nvim` |
| `ENABLE_OMOS` | Enable oh-my-opencode-slim multi-agent orchestration | `false` | | `ENABLE_OMOS` | Enable oh-my-opencode-slim multi-agent orchestration | `false` |
| `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` | | `OMOS_TMUX` | Enable tmux pane integration for OMOS | `false` |
| `OMOS_SKILLS` | Install OMOS recommended skills on first run | `true` | | `OMOS_SKILLS` | Symlink bundled OMOS skills from the image into `~/.agents/skills/` each start | `true` |
| `OMOS_RESET` | Force regenerate OMOS config on next start | `false` | | `OMOS_RESET` | Force regenerate OMOS config on next start (does not affect skills) | `false` |
| `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect | | `SKILLSET_CONTAINER_PATH` | Path to skillset repo inside container (for auto-deploy when not at /workspace/skillset) | Auto-detect |
### Reaching your LAN from the container ### Reaching your LAN from the container
@@ -207,7 +207,7 @@ Host my-remote
### Custom opencode config ### Custom opencode config
Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. All changes to `opencode.jsonc`, skills, and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation. Opencode configuration is persisted automatically via the named volume `devbox-opencode-config`. This volume is mounted at `/home/developer/.config/opencode` by default — no host directory setup required. Changes to `opencode.jsonc` and (on the OMOS variant) `oh-my-opencode-slim.json` survive container recreation. Auto-deployed skills are *not* stored here — skillset and OMOS skills are symlinked into `~/.agents/skills/` and rebuilt on every start (see [Custom skills](#custom-skills) and [docs/omos-skills.md](docs/omos-skills.md)).
When an existing `opencode.jsonc` is found in the volume, the `OPENCODE_PROVIDER` auto-config is skipped. When an existing `opencode.jsonc` is found in the volume, the `OPENCODE_PROVIDER` auto-config is skipped.
@@ -232,6 +232,8 @@ When a skillset repo is detected, its skills are symlinked into `~/.agents/skill
> **Warning:** Do not bind-mount a host `~/.agents/skills` directory directly into the container. This conflicts with the symlink-based auto-deploy mechanism and causes broken skill references. > **Warning:** Do not bind-mount a host `~/.agents/skills` directory directly into the container. This conflicts with the symlink-based auto-deploy mechanism and causes broken skill references.
On the OMOS variant, the five skills bundled with oh-my-opencode-slim are also symlinked into `~/.agents/skills/` on each start — **from the image**, so pulling a newer image updates them with no installer run and no config reset. See [docs/omos-skills.md](docs/omos-skills.md).
### Neovim configuration ### Neovim configuration
The image includes neovim 0.12 with `EDITOR=nvim` set by default. To use your own neovim config (and have plugins auto-install via lazy.nvim on first start), mount it from the host: The image includes neovim 0.12 with `EDITOR=nvim` set by default. To use your own neovim config (and have plugins auto-install via lazy.nvim on first start), mount it from the host:
@@ -457,7 +459,7 @@ ENABLE_OMOS=true
docker compose run --rm devbox docker compose run --rm devbox
``` ```
On first start, the entrypoint runs the oh-my-opencode-slim installer in non-interactive mode. It generates agent configuration at `~/.config/opencode/oh-my-opencode-slim.json` inside the container. The default preset uses OpenAI models — edit the generated config or mount your own to customize. On first start, the entrypoint runs the oh-my-opencode-slim installer in non-interactive mode. It generates agent configuration at `~/.config/opencode/oh-my-opencode-slim.json` inside the container. The default preset uses OpenAI models — edit the generated config or mount your own to customize. The installer no longer manages skills (`--skills=no`); the bundled skills are symlinked from the image on every start — see [docs/omos-skills.md](docs/omos-skills.md).
### OMOS Environment Variables ### OMOS Environment Variables
@@ -465,8 +467,8 @@ On first start, the entrypoint runs the oh-my-opencode-slim installer in non-int
|---|---|---| |---|---|---|
| `ENABLE_OMOS` | `false` | Activate oh-my-opencode-slim on container start | | `ENABLE_OMOS` | `false` | Activate oh-my-opencode-slim on container start |
| `OMOS_TMUX` | `false` | Enable tmux pane integration (tmux is included in the base image) | | `OMOS_TMUX` | `false` | Enable tmux pane integration (tmux is included in the base image) |
| `OMOS_SKILLS` | `true` | Install recommended skills (simplify, agent-browser, cartography) | | `OMOS_SKILLS` | `true` | Symlink the bundled OMOS skills (`clonedeps`, `codemap`, `deepwork`, `oh-my-opencode-slim`, `simplify`) from the image into `~/.agents/skills/` on each start. Independent of `ENABLE_OMOS`. See [docs/omos-skills.md](docs/omos-skills.md) |
| `OMOS_RESET` | `false` | Force regenerate config on next start (backs up existing config) | | `OMOS_RESET` | `false` | Force regenerate config on next start (backs up existing config). Does **not** affect skills |
### Custom Configuration ### Custom Configuration
+110
View File
@@ -0,0 +1,110 @@
# OMOS bundled skills
How the five skills bundled with [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim)
(OMOS) are deployed in this image, why the mechanism changed, and how to keep
them up to date.
## TL;DR
- OMOS bundles five skills: `clonedeps`, `codemap`, `deepwork`,
`oh-my-opencode-slim`, `simplify`.
- They are **symlinked from the image** into `~/.agents/skills/` on every
container start by `entrypoint-user.sh` (gated by `OMOS_SKILLS`, default
`true`).
- **To update them, pull a newer image and recreate the container** — no
installer run, no config reset:
```bash
docker compose pull
docker compose up -d --force-recreate
```
## Why it works this way
The skills ship inside the OMOS npm package, baked into the image at
`/usr/lib/node_modules/oh-my-opencode-slim/src/skills/<name>/`. On start the
entrypoint creates one absolute symlink per skill into `~/.agents/skills/` —
the same flat directory the [skillset](#relationship-to-skillset) deploy uses
and which opencode scans (directly and via the `~/.claude/skills` pointer).
Because the symlink target lives in the **image**, not in a volume, the skill
content tracks whatever image you run. Pull a newer `*-omos` image and the
skills update on the next container start. `~/.agents/skills/` is itself an
ephemeral container-layer directory rebuilt from scratch on every start, so the
reconcile is idempotent and self-healing.
The symlinks are **absolute** (unlike skillset's relative links): the target is
always inside the container at a fixed `/usr` path, so there is no host/container
path divergence to guard against.
## The old mechanism (and the trap it created)
Previously the OMOS *installer* (`oh-my-opencode-slim install --skills=yes`)
**copied** the skills into `~/.config/opencode/skills/` — but only on the very
first container start, gated by the absence of `oh-my-opencode-slim.json`. That
directory is the persistent `devbox-opencode-config` named volume.
The consequence: once the config existed, the installer was skipped forever, so
the copied skills **froze** at whatever the image shipped on first run. Pulling
a newer image did nothing (the copies lived in the volume, not the image). The
only refresh path was `OMOS_RESET=true`, which runs `install --reset` and
**also overwrites your hand-tuned `opencode.jsonc`** (model choices, agent
config). Updating skills meant clobbering unrelated config — a bad trade.
The installer has no skills-only refresh flag (`--skills=yes|no` is all-or-
nothing within a full install; `--reset` overwrites everything), so the fix was
to stop using the installer for skills entirely. The two `install` invocations
in `entrypoint-user.sh` now pass `--skills=no`; the installer manages only the
agent config (`oh-my-opencode-slim.json`).
## One-time migration of frozen copies
Existing volumes still contain the old frozen real directories under
`~/.config/opencode/skills/`. Because opencode prefers a name found there over
the same name in `~/.agents/skills/`, those stale copies would *shadow* the
fresh image-sourced symlinks. On the first start after upgrading, the entrypoint
**backs each of them up — never deletes** — to
`~/.config/opencode/skills/<name>.bak.<epoch>` and writes a marker at
`~/.config/opencode/.omos-skills-migrated` so the migration runs exactly once.
Only real directories are touched; any symlink in that directory is left alone.
If everything looks right after a few sessions, the backups are safe to remove:
```bash
rm -rf ~/.config/opencode/skills/*.bak.*
```
## Relationship to skillset
The [skillset](https://gitea.jordbo.se/joakimp/skillset) repo deploys its own
version-controlled skills into `~/.agents/skills/` via
`deploy-skills.sh --bootstrap --prune-stale`, which the entrypoint runs on every
start — *before* the OMOS reconcile. If a skill name exists in both, **OMOS
wins**: the reconcile uses `ln -sfn`, which replaces any existing symlink. Today
the only overlap is `simplify` (the OMOS copy is richer — it bundles
`codemap.md` and a `README.md`), so `simplify` was removed from the skillset
repo and the image copy owns the name.
## Configuration
| Variable | Default | Effect |
|---|---|---|
| `OMOS_SKILLS` | `true` | Symlink the bundled skills into `~/.agents/skills/` on each start. Set `false` to deploy no skills from the image. |
`OMOS_SKILLS` is **independent of `ENABLE_OMOS`**: the skills are useful to plain
opencode even when the multi-agent orchestration config is not enabled. The
reconcile is additionally gated on the bundled-skills source being present, so
it is automatically a no-op on the non-OMOS (`base`) image variant.
## Troubleshooting
- **A skill didn't update after `docker compose pull`.** Make sure you
recreated the container (`docker compose up -d --force-recreate`); a plain
restart reuses the old container layer. Confirm the link target —
`readlink ~/.agents/skills/deepwork` should point under
`/usr/lib/node_modules/oh-my-opencode-slim/src/skills/`.
- **A skill disappeared.** If OMOS upstream restructured its package the symlink
target may no longer exist. The build-time smoke test asserts the source path,
so this should be caught in CI; if you hit it at runtime, look for a dangling
link in `ls -l ~/.agents/skills/`.
- **I want a frozen copy back.** It's at
`~/.config/opencode/skills/<name>.bak.<epoch>` until you delete it.
+66 -9
View File
@@ -118,6 +118,66 @@ if [ -n "$SKILLSET_DEPLOY" ]; then
"$SKILLSET_DEPLOY" --bootstrap --prune-stale >/dev/null 2>&1 || true "$SKILLSET_DEPLOY" --bootstrap --prune-stale >/dev/null 2>&1 || true
fi fi
# ── OMOS bundled skills: symlink from the image into the flat skills dir ──
# The oh-my-opencode-slim package bundles its skills at a fixed, image-internal
# path (npm global prefix /usr — see Dockerfile.variant). Historically the omos
# *installer* COPIED them into ~/.config/opencode/skills/ on first run only,
# freezing them in the persistent `devbox-opencode-config` named volume: pulling
# a newer image never refreshed them, and the only update path was
# `OMOS_RESET=true` (which also clobbers the user's hand-tuned opencode config).
#
# Instead we symlink them from the IMAGE into ~/.agents/skills/ — the same flat
# dir skillset uses, which opencode scans (directly and via the ~/.claude/skills
# pointer). Because the link targets live in the image, `docker compose pull` +
# recreate updates the skills for free: no installer run, no config reset.
#
# ~/.agents/skills/ is authoritative. The legacy ~/.config/opencode/skills/ real
# dir is intentionally bypassed; a one-time migration backs up (never destroys)
# the frozen copies it holds so they stop shadowing the fresh image-sourced
# skills. The migration marker lives in the parent config dir, not inside
# skills/, so it never interferes with that directory's contents.
#
# Absolute symlink (not relative like skillset): the target is always inside the
# container at a fixed /usr path, and ~/.agents/skills/ is an ephemeral
# container-layer dir rebuilt each start — there is no host/container path
# divergence to guard against. The whole block is non-fatal (`{ … } || true`):
# a transient ln/mv failure must never brick container startup, mirroring the
# skillset deploy above. Runs AFTER skillset deploy so OMOS wins any name
# collision (e.g. `simplify`) via `ln -sfn`. Gated by OMOS_SKILLS (default true)
# and the presence of the bundled skills (omos-variant images only).
if [ "${OMOS_SKILLS:-true}" = "true" ]; then
OMOS_SKILLS_SRC=""
for cand in \
/usr/lib/node_modules/oh-my-opencode-slim/src/skills \
/usr/local/lib/node_modules/oh-my-opencode-slim/src/skills; do
if [ -d "$cand" ]; then OMOS_SKILLS_SRC="$cand"; break; fi
done
if [ -n "$OMOS_SKILLS_SRC" ]; then
{
AGENTS_SKILLS_DIR="$HOME/.agents/skills"
OPENCODE_SKILLS_DIR="$HOME/.config/opencode/skills"
OMOS_SKILLS_MARKER="$HOME/.config/opencode/.omos-skills-migrated"
mkdir -p "$AGENTS_SKILLS_DIR"
for skill_path in "$OMOS_SKILLS_SRC"/*/; do
[ -d "$skill_path" ] || continue
name="$(basename "$skill_path")"
# OMOS wins collisions: -f replaces an existing symlink (e.g. skillset's).
ln -sfn "${skill_path%/}" "$AGENTS_SKILLS_DIR/$name"
# One-time unshadow: back up — never destroy — the frozen real copy the
# old installer left in the persistent config volume. `! -L` ensures we
# only ever touch a real dir, never a symlink a user/skillset created.
if [ ! -f "$OMOS_SKILLS_MARKER" ] \
&& [ -d "$OPENCODE_SKILLS_DIR/$name" ] \
&& [ ! -L "$OPENCODE_SKILLS_DIR/$name" ]; then
mv "${OPENCODE_SKILLS_DIR:?}/$name" \
"${OPENCODE_SKILLS_DIR}/${name}.bak.$(date +%s)"
fi
done
touch "$OMOS_SKILLS_MARKER" 2>/dev/null || true
} || true
fi
fi
CONFIG_DIR="$HOME/.config/opencode" CONFIG_DIR="$HOME/.config/opencode"
OMOS_CONFIG="$CONFIG_DIR/oh-my-opencode-slim.json" OMOS_CONFIG="$CONFIG_DIR/oh-my-opencode-slim.json"
@@ -139,15 +199,14 @@ if [ "${ENABLE_OMOS:-false}" = "true" ]; then
OMOS_TMUX_FLAG="yes" OMOS_TMUX_FLAG="yes"
fi fi
OMOS_SKILLS_FLAG="yes" # Skills are NOT installer-managed any more — they are symlinked from the
if [ "${OMOS_SKILLS:-true}" = "false" ]; then # image into ~/.agents/skills/ by the OMOS bundled-skills block above
OMOS_SKILLS_FLAG="no" # (gated by OMOS_SKILLS). Always pass --skills=no so the installer never
fi # writes frozen copies into the persistent config volume.
bun x oh-my-opencode-slim@latest install \ bun x oh-my-opencode-slim@latest install \
--no-tui \ --no-tui \
--tmux="${OMOS_TMUX_FLAG}" \ --tmux="${OMOS_TMUX_FLAG}" \
--skills="${OMOS_SKILLS_FLAG}" --skills=no
echo "oh-my-opencode-slim configured successfully." echo "oh-my-opencode-slim configured successfully."
else else
@@ -158,13 +217,11 @@ if [ "${ENABLE_OMOS:-false}" = "true" ]; then
echo "OMOS_RESET=true — regenerating oh-my-opencode-slim config..." echo "OMOS_RESET=true — regenerating oh-my-opencode-slim config..."
OMOS_TMUX_FLAG="no" OMOS_TMUX_FLAG="no"
[ "${OMOS_TMUX:-false}" = "true" ] && OMOS_TMUX_FLAG="yes" [ "${OMOS_TMUX:-false}" = "true" ] && OMOS_TMUX_FLAG="yes"
OMOS_SKILLS_FLAG="yes"
[ "${OMOS_SKILLS:-true}" = "false" ] && OMOS_SKILLS_FLAG="no"
bun x oh-my-opencode-slim@latest install \ bun x oh-my-opencode-slim@latest install \
--no-tui \ --no-tui \
--tmux="${OMOS_TMUX_FLAG}" \ --tmux="${OMOS_TMUX_FLAG}" \
--skills="${OMOS_SKILLS_FLAG}" \ --skills=no \
--reset --reset
fi fi
fi fi
+9
View File
@@ -180,6 +180,15 @@ if [ "$VARIANT" = "omos" ]; then
"NPM_CONFIG_PREFIX=/usr npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim" \ "NPM_CONFIG_PREFIX=/usr npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim" \
"$EXPECTED_OMOS_VERSION" "$EXPECTED_OMOS_VERSION"
fi fi
# OMOS bundled-skills SOURCE must be present at the fixed image path that
# entrypoint-user.sh symlinks into ~/.agents/skills/ on container start. If
# upstream restructures the package (moves src/skills), the runtime symlinks
# would dangle SILENTLY and the skills would just disappear — assert the
# source here so that breakage fails the build loudly instead. We check the
# source (not the runtime symlinks) because smoke tests run with
# --entrypoint="" and never execute entrypoint-user.sh.
run "omos bundled-skills source" \
"for n in clonedeps codemap deepwork oh-my-opencode-slim simplify; do test -d /usr/lib/node_modules/oh-my-opencode-slim/src/skills/\$n || exit 1; done && echo ok"
else else
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v bun" >/dev/null 2>&1; then if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v bun" >/dev/null 2>&1; then
fail "bun should NOT be in base image but was found" fail "bun should NOT be in base image but was found"