Compare commits

..

11 Commits

Author SHA1 Message Date
pi fa6e9dc9d6 release: v1.4.0 — typst PDF engine + host SSH startup check
Publish Docker Image / resolve-versions (push) Successful in 6s
Publish Docker Image / base-decide (push) Successful in 9s
Lint workflows / actionlint (push) Successful in 1m9s
Publish Docker Image / build-base (push) Successful in 34m32s
Publish Docker Image / smoke-studio (push) Successful in 4m16s
Publish Docker Image / smoke (push) Successful in 10m52s
Publish Docker Image / build-variant-studio (push) Successful in 18m1s
Publish Docker Image / build-variant (push) Successful in 18m51s
Publish Docker Image / promote-base-latest (push) Successful in 11s
Publish Docker Image / update-description (push) Successful in 18s
Finalize the Unreleased batch as v1.4.0 (minor — significant base
additions). Base rebuilds (Dockerfile.base for typst/xz-utils;
.bash_aliases for the SSH check). pi auto-resolves latest (0.80.3 ->
0.80.6); mempalace stays 3.5.0 (current).
2026-07-11 17:15:11 +02:00
joakimp bd0627a557 docs: note host SSH startup check in CHANGELOG Unreleased
Lint workflows / actionlint (push) Successful in 1m18s
2026-07-11 17:07:55 +02:00
pi 67da05b99b feat(base): ship typst as lightweight pandoc PDF engine
Lint workflows / actionlint (push) Successful in 15s
pandoc has been in the base since v1.0.0 but only as a front-end; PDF
export (studio_export_pdf / pandoc -o out.pdf) failed with 'xelatex not
found' because no back-end engine was installed. Ship typst (~30 MB
static Rust binary, no LaTeX) as the default engine via
`pandoc --pdf-engine=typst`, chosen over a ~600 MB TeX Live install.
texlive-xetex remains the higher-fidelity install-on-demand fallback.

- Dockerfile.base: install typst (latest GitHub-release idiom, pin via
  TYPST_VERSION); add xz-utils (typst ships .tar.xz); bump
  BASE_REBUILD_DATE. Lands in base-<hash>.
- smoke-test.sh: verify typst + a real pandoc --pdf-engine=typst render.
- README/AGENTS/CHANGELOG: typst now shipped (supersedes the planned
  :latest-studio-tex variant).

No tag pushed — CI build intentionally deferred.
2026-07-11 17:04:19 +02:00
joakimp 4563b4d76d feat: warn at shell startup if Mac host SSH is not reachable
Lint workflows / actionlint (push) Successful in 14s
Adds a _devbox_check_host_ssh() check to ~/.bash_aliases (baked into
the image). On first bash session of each container it tries a quick
SSH probe to the Mac host; if it fails it prints a clear one-time
warning with the exact two steps needed to fix it:

  1. Enable Remote Login in macOS System Settings
  2. echo '<public key>' >> ~/.ssh/authorized_keys

The check is guarded:
  - only runs inside a container (/.dockerenv)
  - only when the jump key exists (~/.ssh-local/devbox_jump_ed25519.pub)
  - only once per container lifetime (/tmp flag, cleared on recreate)

After --force-recreate the key changes, the flag is gone, and the
check runs again on the first bash window. Subsequent windows are
silent.
2026-07-11 15:26:23 +02:00
pi f19c35da32 docs: note lightweight PDF engine (typst) as preferred over full texlive
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.
2026-07-09 16:25:54 +02:00
pi 6002c6299d release: v1.3.0 — shared/external MemPalace + nano/micro editors + CI lint
Publish Docker Image / resolve-versions (push) Successful in 5s
Publish Docker Image / base-decide (push) Successful in 8s
Lint workflows / actionlint (push) Successful in 19s
Publish Docker Image / build-base (push) Successful in 33m17s
Publish Docker Image / smoke (push) Successful in 3m55s
Publish Docker Image / smoke-studio (push) Successful in 6m50s
Publish Docker Image / build-variant (push) Successful in 16m12s
Publish Docker Image / promote-base-latest (push) Successful in 9s
Publish Docker Image / update-description (push) Successful in 6s
Publish Docker Image / build-variant-studio (push) Successful in 20m50s
Promotes the Unreleased block to v1.3.0. Bundled contents:
- feat: shared/external MemPalace — mempalace.ts bridge honours MEMPALACE_REMOTE_URL;
  adds docker-compose.mempalace.yml.
- feat: nano + micro non-modal editors (Dockerfile.base → base rebuild).
- ci: lint.yml (sh-vs-bash guard + actionlint/shellcheck), docker-publish.yml bash
  defaults, promote-base-latest shell fix.
pi stays 0.80.3 (== npm latest). Base rebuilds (mempalace-toolkit ref advanced +
Dockerfile.base nano/micro), so the new bridge + editors land in base-<hash>.
2026-07-02 14:50:17 +02:00
pi d73bf2e9d3 feat: optional shared/external MemPalace via MEMPALACE_REMOTE_URL
Lint workflows / actionlint (push) Successful in 21s
Wire the shared-palace option (implemented in mempalace-toolkit's mempalace.ts)
into the container:
- .env.example: document MEMPALACE_REMOTE_URL / MEMPALACE_REMOTE_TOKEN (env_file-only,
  per this repo's convention).
- docker-compose.mempalace.yml: optional shared server (mempalace-mcp --transport http),
  loopback-bound by default.
- docker-compose.yml: local-vs-external note on the palace-volume comment.
- README + CHANGELOG (Unreleased).
2026-07-02 13:09:29 +02:00
pi 3a59e15563 feat: ship nano + micro (non-modal editors) alongside nvim
Lint workflows / actionlint (push) Successful in 13s
The image shipped only nvim (EDITOR=nvim), a modal vi-style editor. Not
everyone is comfortable with vi keybindings, so add both a classic and a
modern non-modal option:

- nano (apt): ~2.8 MB installed; deps (libc6, libncursesw6, libtinfo6)
  already present via nvim/less/htop/tmux, so no extra packages pulled in.
- micro: ~12 MB single static Go binary from GitHub releases (same pattern
  as bat/eza/zoxide). Desktop-style keys (Ctrl+S/Ctrl+Q), mouse, syntax
  highlighting. ARG MICRO_VERSION pins; defaults to latest.

Combined ~15 MB (<0.5% of the ~3.2 GB image). EDITOR stays nvim; both new
editors are opt-in (export EDITOR=micro | nano). Uses the canonical
micro-editor/micro URL because the old zyedidia/micro org rename makes
/releases/latest redirect to another /latest, defeating the tag-parsing
latest-resolution idiom.

Base-image change, so it lands on the next base-<hash> rebuild. Updates
README tool table + EDITOR note, CHANGELOG (Unreleased/Added), and
smoke-test.sh (nano + micro presence checks).
2026-07-01 23:11:25 +02:00
pi d1db595f17 ci(lint): pass explicit workflow paths to actionlint
Lint workflows / actionlint (push) Successful in 15s
actionlint's no-arg project auto-detection looks for .github/workflows
and hard-fails (exit 3, 'no project was found') on this .gitea/workflows
layout — observed on run 420. Glob the workflow files explicitly. The
Gitea shell guard step already passed in that run; only the actionlint
invocation needed the path fix.
2026-07-01 22:06:40 +02:00
pi 26384fe9f1 ci: eliminate the sh-vs-bash footgun class (defaults + lint guard)
Lint workflows / actionlint (push) Failing after 34s
Root cause of the recurring 'Illegal option -o pipefail' failures
(ed49b8d resolve-versions; b7197e8 promote-base-latest, run 418):
docker-publish.yml had no workflow-level default shell, so Gitea's
sh/dash default applied and every bash-syntax step had to individually
remember 'shell: bash'.

- docker-publish.yml: add 'defaults: run: shell: bash' — fixes the whole
  class; all pre-existing dash steps are POSIX so bash runs them unchanged.
- lint.yml: new workflow, runs on every push/PR (not just release tags):
    * scripts/check-workflow-shell.sh — Gitea-accurate guard: fails if any
      run: step doesn't resolve to bash. Catches the omit-shell+bash-syntax
      case that actionlint MISSES (actionlint models GitHub, where the
      default shell is bash, so a shell-less step is assumed bash).
    * actionlint + shellcheck — catches explicit 'shell: sh' + bash syntax
      (SC3040) and general workflow errors.
  Verified locally: guard + actionlint pass current workflows; guard fails
  a synthetic omit-shell+pipefail workflow; shellcheck clean.
2026-07-01 22:05:04 +02:00
pi b33e9dc592 fix(ci): promote-base-latest re-tag step needs shell: bash (set -o pipefail)
b7197e8 moved the digest-compare into the re-tag step with 'set -euo
pipefail' but no 'shell: bash'; Gitea's default sh (dash) aborts on
-o pipefail, leaving base-latest un-promoted on the v1.2.4 release
(run 418). Same footgun as ed49b8d. Consumer tags unaffected (they
FROM base-<hash>, not base-latest).
2026-07-01 17:57:13 +02:00
12 changed files with 518 additions and 15 deletions
+13
View File
@@ -9,6 +9,19 @@ WORKSPACE_PATH=~/projects
# Path to SSH keys on host # Path to SSH keys on host
SSH_KEY_PATH=~/.ssh SSH_KEY_PATH=~/.ssh
# ── MemPalace memory (local by default) ───────────────────────────
# By default the mempalace.ts extension spawns a LOCAL mempalace-mcp stdio
# server (palace at ~/.mempalace). Uncomment the devbox-palace volume in
# docker-compose.yml to persist it across container recreation.
#
# To instead share ONE MemPalace across containers/harnesses (pi + opencode
# + native), set the URL below. When set, the extension connects over HTTP
# and NO local mempalace-mcp is spawned; the devbox-palace volume is then
# irrelevant. MEMPALACE_REMOTE_TOKEN, if set, is sent as a bearer token.
# Serve it with: mempalace-mcp --transport http --host 0.0.0.0 --port 8765
# MEMPALACE_REMOTE_URL=http://mempalace.lan:8765/mcp
# MEMPALACE_REMOTE_TOKEN=
# ── LAN access from the container (host-OS-agnostic) ───────────────── # ── LAN access from the container (host-OS-agnostic) ─────────────────
# On VM-backed hosts (macOS OrbStack / Docker Desktop) the container can't # On VM-backed hosts (macOS OrbStack / Docker Desktop) the container can't
# reach the host's directly-attached LAN peers by default. The entrypoint # reach the host's directly-attached LAN peers by default. The entrypoint
+17
View File
@@ -38,6 +38,16 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false cancel-in-progress: false
# Gitea Actions' default step shell is `sh -e {0}` (dash), which rejects
# bash-only syntax like `set -o pipefail`, `[[ ]]`, and arrays. Setting the
# default to bash workflow-wide eliminates the whole class of "forgot
# `shell: bash` on this step" bugs (hit twice: ed49b8d resolve-versions,
# b7197e8/b33e9dc promote-base-latest). All existing dash steps use only
# POSIX syntax, so bash (a superset) runs them unchanged.
defaults:
run:
shell: bash
env: env:
BUILDKIT_PROGRESS: plain BUILDKIT_PROGRESS: plain
IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/pi-devbox IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/pi-devbox
@@ -600,6 +610,13 @@ jobs:
-u ${{ vars.DOCKERHUB_USERNAME }} \ -u ${{ vars.DOCKERHUB_USERNAME }} \
-p "${{ secrets.DOCKERHUB_TOKEN }}" -p "${{ secrets.DOCKERHUB_TOKEN }}"
- name: Re-tag base-<hash> as base-latest (only if stale) - name: Re-tag base-<hash> as base-latest (only if stale)
# shell: bash is REQUIRED — Gitea Actions' default step shell is
# `sh -e {0}` (dash), which rejects `set -o pipefail` with
# "Illegal option -o pipefail" and aborts the step before the
# crane digest-compare runs, leaving base-latest un-promoted.
# Same footgun as ed49b8d (resolve-versions). Regression shipped
# in b7197e8, caught on the v1.2.4 release (run 418).
shell: bash
env: env:
BASE_HASH_REF: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }} BASE_HASH_REF: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
BASE_LATEST_REF: ${{ env.IMAGE }}:base-latest BASE_LATEST_REF: ${{ env.IMAGE }}:base-latest
+69
View File
@@ -0,0 +1,69 @@
name: Lint workflows
# Durable guard against CI-workflow bugs — most importantly the recurring
# "bash-only syntax under the default `sh`/dash shell" footgun that broke
# resolve-versions (ed49b8d) and promote-base-latest (b7197e8 → run 418).
# actionlint runs shellcheck against each `run:` step using its *effective*
# shell, so `set -o pipefail` under dash is flagged as SC3040 before any
# expensive build runs. This is cheap (~10s) and independent of the build
# pipeline, so it fires on every push/PR — not just on release tags, which
# is where the build workflow (docker-publish.yml) is otherwise only
# triggered.
on:
push:
pull_request:
workflow_dispatch:
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
actionlint:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- uses: actions/checkout@v4
- name: Install shellcheck
run: |
apt-get update
apt-get install -y --no-install-recommends shellcheck python3-yaml
- name: Gitea shell guard (catches the actionlint blind spot)
# actionlint models GitHub Actions, where the default run shell is
# bash, so it does NOT flag bash syntax in a step that merely OMITS
# `shell:` — which is exactly how ed49b8d and b7197e8 manifested on
# Gitea (default sh/dash). This guard enforces that every run: step
# resolves to bash under Gitea's real defaults. Run it BEFORE
# actionlint so the more precise diagnostic surfaces first.
run: bash scripts/check-workflow-shell.sh .gitea/workflows
- name: Install actionlint (pinned)
env:
ACTIONLINT_VERSION: 1.7.7
run: |
curl -fsSL \
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \
| tar -xz -C /usr/local/bin actionlint
actionlint --version
- name: Run actionlint
# SHELLCHECK_OPTS excludes pure-style codes (quoting/style opinions)
# so the guard stays focused on correctness bugs — crucially the
# SC3xxx "not POSIX / wrong shell" family that catches the pipefail
# footgun. Do NOT exclude SC3040 (set -o pipefail under sh) or any
# other SC3xxx code.
env:
SHELLCHECK_OPTS: "-e SC2086 -e SC2016 -e SC2129 -e SC2001 -e SC2312"
# Pass explicit paths: actionlint's no-arg mode auto-detects a
# project by looking for `.github/workflows`, which doesn't exist in
# this `.gitea/workflows` repo and hard-fails with exit 3
# ("no project was found"). Globbing the workflow files is the
# supported way to lint a non-GitHub layout.
run: actionlint -color .gitea/workflows/*.yml
+8 -4
View File
@@ -153,10 +153,14 @@ deprecated artifacts (to be removed in opencode-devbox v2.0.0).
## What we DON'T install (and why) ## What we DON'T install (and why)
- **No texlive** (~600 MB1 GB). Users who need PDF export from pandoc - **No texlive** (~600 MB1 GB). PDF export from pandoc / pi-studio works
or pi-studio can install on demand: `sudo apt-get install texlive-xetex out of the box via **`typst`** (~30 MB static binary), which the base ships
texlive-latex-recommended`. The planned `:latest-studio-tex` variant as the pandoc PDF engine (`pandoc --pdf-engine=typst`) — small enough to live
will bake this in. in base rather than a dedicated `:latest-studio-tex` variant. We don't bake in
a full TeX Live: it's heavy and typst covers the common Markdown→PDF case.
Users needing LaTeX-exact output can install the higher-fidelity fallback on
demand: `sudo apt-get install texlive-xetex texlive-latex-recommended` (then
`pandoc --pdf-engine=xelatex`).
- **pi-studio** ships in the `:latest-studio` variant (since v1.1.0), - **pi-studio** ships in the `:latest-studio` variant (since v1.1.0),
vendored to `/opt/pi-studio` and registered at container start via vendored to `/opt/pi-studio` and registered at container start via
`pi install /opt/pi-studio` (see Dockerfile.variant `INSTALL_STUDIO`). `pi install /opt/pi-studio` (see Dockerfile.variant `INSTALL_STUDIO`).
+128
View File
@@ -11,6 +11,134 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
--- ---
## v1.4.0 — 2026-07-11
Minor release. Headline: **PDF export works out of the box** — the base now
ships **`typst`** as the pandoc PDF engine (`pandoc --pdf-engine=typst`), so
`studio_export_pdf` / `pandoc -o out.pdf` no longer fail with "xelatex not
found". Also adds a **host SSH reachability check at shell startup**. Both are
base-affecting (`Dockerfile.base` apt+RUN for typst/xz-utils; `.bash_aliases`
for the SSH check is COPYd into the base), so the base rebuilds and both land in
`base-<hash>`. pi auto-resolves `latest` at build time (0.80.3 → 0.80.6);
mempalace stays pinned at 3.5.0 (current PyPI latest).
### Added
- **Host SSH reachability check at shell startup.** `~/.bash_aliases` (baked
into the image) now runs a one-time SSH probe on the first bash session of
each container. If the Mac host is not reachable (Remote Login disabled or
the `devbox_jump` key not yet authorized) it prints a clear warning with the
exact two steps to fix it, including the container's public key inline.
Subsequent shells in the same container skip the check (flag in `/tmp`,
cleared on recreate). Silent when SSH is working. Complements the existing
key-generation message in `setup-lan-access.sh` which only fires once at key
creation time and can easily be missed. Commit `4563b4d`.
- **`typst` — lightweight PDF engine for pandoc (Markdown→PDF).** `pandoc` has
shipped in the base since v1.0.0 but as a front-end only — with no PDF
back-end installed, `studio_export_pdf` / `pandoc -o out.pdf` failed with
"xelatex not found". The base now installs `typst`, a single ~30 MB static
Rust binary (no LaTeX), used via `pandoc --pdf-engine=typst`. Chosen over a
~600 MB TeX Live install; a fuller TeX Live remains the higher-fidelity
fallback for anyone needing LaTeX-exact output (install on demand). Also adds
`xz-utils` to the apt layer (typst ships a `.tar.xz` asset that `tar` needs
`xz` to extract). Installed with the standard `latest` GitHub-release idiom;
pin with `--build-arg TYPST_VERSION=vX.Y.Z`. This lands in `base-<hash>`
(Dockerfile.base changed). Supersedes the previously-planned
`:latest-studio-tex` variant — typst is small enough to ship in BASE, so no
separate TeX variant is needed. See `pi-devbox-roadmap`.
---
## v1.3.0 — 2026-07-02
Minor release. Headline: **shared/external MemPalace** — the `mempalace.ts`
bridge can now point at one MemPalace HTTP server (`MEMPALACE_REMOTE_URL`,
optional `MEMPALACE_REMOTE_TOKEN`) shared across containers/harnesses instead of
a per-container local palace; ships `docker-compose.mempalace.yml` for the
server. Also ships the **`nano` + `micro`** non-modal editors and a **CI
workflow-lint layer** (Gitea-accurate sh-vs-bash guard + actionlint/shellcheck),
with the `docker-publish.yml` bash-defaults and `promote-base-latest` shell
fixes. pi stays `0.80.3`; the base image rebuilds (the mempalace-toolkit ref
advanced and `Dockerfile.base` gained nano/micro), so the new bridge and editors
land in `base-<hash>`.
### Added
- **Share one MemPalace across containers via `MEMPALACE_REMOTE_URL`.** The
`mempalace.ts` bridge (from `mempalace-toolkit`) can now connect to a shared
MemPalace over HTTP instead of spawning a per-container local server: set
`MEMPALACE_REMOTE_URL=http://<host>:8765/mcp` (optionally
`MEMPALACE_REMOTE_TOKEN`) in `.env` and no local `mempalace-mcp` is spawned.
A new `docker-compose.mempalace.yml` stands up such a shared server
(`mempalace-mcp --transport http`). Leaving the URL unset keeps the default
local-per-container palace. See `.env.example`. (The HTTP transport is
unauthenticated — keep it on a trusted network or behind a reverse proxy.)
- **Two non-modal terminal editors alongside `nvim`: `nano` and `micro`.**
The image previously shipped only `nvim` (with `EDITOR=nvim`), a modal
vi-style editor. Not everyone is comfortable with vi keybindings, so both
a classic and a modern non-modal option now ship:
- **`nano`** (apt) — ~2.8 MB installed. Its dependencies (`libc6`,
`libncursesw6`, `libtinfo6`) are already present via `nvim`/`less`/`htop`/
`tmux`, so it pulls in **no extra packages**. On-screen shortcut hints
(`^O` write, `^X` exit) make it the lowest-friction fallback.
- **`micro`** — ~12 MB, a single static Go binary installed from GitHub
releases (same pattern as `bat`/`eza`/`zoxide`). Desktop-style keybindings
(`Ctrl+S` save, `Ctrl+Q` quit, `Ctrl+C/V/X`, `Ctrl+Z` undo), mouse
support, and syntax highlighting out of the box. Pin with
`--build-arg MICRO_VERSION=vX.Y.Z`; defaults to `latest`.
Combined footprint is ~15 MB (<0.5% of the ~3.2 GB image). **`EDITOR`
stays `nvim`** — the new editors are opt-in via `export EDITOR=micro`
(or `nano`) and/or `git config --global core.editor micro`.
Note: micro's upstream repo moved `zyedidia/micro``micro-editor/micro`;
the Dockerfile uses the canonical URL because the old org's
`/releases/latest` redirect lands on another `/latest` URL (the org
rename), which would defeat the tag-parsing `latest`-resolution idiom.
These are base-image additions, so they only land once the `base-<hash>`
rebuilds (this file changed, so the next build picks them up).
### Added (CI)
- **Workflow lint (`.gitea/workflows/lint.yml`) running on every push and PR.**
Two complementary checks, so CI-workflow bugs are caught before an expensive
build runs:
- **`scripts/check-workflow-shell.sh`** — a Gitea-accurate guard that fails
if any `run:` step doesn't resolve to `bash` under Gitea's real defaults.
This catches the exact recurrence class (omit `shell:`, use bash syntax),
which **actionlint alone does not** — actionlint models GitHub Actions
(default shell = bash) and so assumes a shell-less step is bash, whereas
Gitea's default is `sh`/dash.
- **`actionlint` + `shellcheck`** — catches explicit `shell: sh` + bash
syntax (SC3040 etc.), expression errors, and general workflow mistakes.
Style-only shellcheck codes are excluded; the SC3xxx "wrong shell" family
is kept.
### Changed (CI)
- **Workflow-level `defaults: run: shell: bash` in `docker-publish.yml`.**
Gitea Actions defaults each `run:` step to `sh` (dash), so every bash-syntax
step had to individually remember `shell: bash` — a discipline requirement
that failed twice (ed49b8d, b7197e8). Setting the default workflow-wide
eliminates the whole class. All pre-existing dash steps use only POSIX
syntax, so bash (a superset) runs them unchanged.
### Fixed (CI)
- **`promote-base-latest` now sets `shell: bash` on the base-latest re-tag
step.** The `b7197e8` fix (v1.2.4) moved the digest-compare into that step
with `set -euo pipefail`, but Gitea Actions' default step shell is `sh`
(dash), which rejects `-o pipefail` (`Illegal option -o pipefail`) and aborts
the step before the `crane copy` runs. On the v1.2.4 release (run 418) this
left `base-latest` un-promoted, still pointing at the v1.2.3 base — the four
consumer tags (`v1.2.4`, `latest`, `v1.2.4-studio`, `latest-studio`) were
unaffected because they `FROM` the exact `base-<hash>`, not `base-latest`.
Same footgun as `ed49b8d` (`resolve-versions needs shell: bash`).
---
## v1.2.4 — 2026-06-29 ## v1.2.4 — 2026-06-29
Patch release. Headline: **pi `0.80.2` → `0.80.3`** (npm `latest`). Also ships a Patch release. Headline: **pi `0.80.2` → `0.80.3`** (npm `latest`). Also ships a
+70 -2
View File
@@ -14,7 +14,7 @@
# content-addressed over this file, so any byte change invalidates the # content-addressed over this file, so any byte change invalidates the
# cache. Recommended cadence: once per release for security updates. # cache. Recommended cadence: once per release for security updates.
# #
# BASE_REBUILD_DATE: 2026-06-09 (v1.0.0 — decoupled from opencode-devbox) # BASE_REBUILD_DATE: 2026-07-11 (Unreleased — typst PDF engine + xz-utils)
# #
# ── Lineage note ───────────────────────────────────────────────────── # ── Lineage note ─────────────────────────────────────────────────────
# Adapted from opencode-devbox/Dockerfile.base (commit before v1.16.2). # Adapted from opencode-devbox/Dockerfile.base (commit before v1.16.2).
@@ -46,7 +46,14 @@ ENV DEBIAN_FRONTEND=noninteractive
# Additions vs the upstream opencode-devbox base (2026-06-09): # Additions vs the upstream opencode-devbox base (2026-06-09):
# pandoc — Markdown↔HTML/PDF/etc. conversion. Required by pi-studio # pandoc — Markdown↔HTML/PDF/etc. conversion. Required by pi-studio
# preview/export pipelines and broadly useful for any # preview/export pipelines and broadly useful for any
# agent-driven document workflow. ~200 MB. # agent-driven document workflow. ~200 MB. NOTE: pandoc is
# only the front-end — PDF output needs a back-end engine.
# We ship `typst` (installed further down) as the
# lightweight default engine (`pandoc --pdf-engine=typst`)
# instead of a ~600 MB TeX Live install.
# xz-utils — `xz` decompressor. tar shells out to it for `.tar.xz`
# assets (typst ships .tar.xz). ~0.5 MB. Also generally
# useful for extracting xz-compressed archives.
# graphviz — `dot` rendering for many diagram tools. ~10 MB. # graphviz — `dot` rendering for many diagram tools. ~10 MB.
# See the bundled `dot-watch` helper for live .dot -> PNG # See the bundled `dot-watch` helper for live .dot -> PNG
# re-render (handy with pi-studio's image preview). # re-render (handy with pi-studio's image preview).
@@ -57,6 +64,12 @@ ENV DEBIAN_FRONTEND=noninteractive
# pi-studio's container-loopback server to the container's # pi-studio's container-loopback server to the container's
# external interface so a published port can reach it. # external interface so a published port can reach it.
# ~1 MB; generally useful for any port-forwarding need. # ~1 MB; generally useful for any port-forwarding need.
# nano — small, non-modal terminal editor for users who don't want
# a vi-based editor. ~2.8 MB installed; its deps (libc6,
# libncursesw6, libtinfo6) are already pulled in by nvim/less/
# htop/tmux, so it adds no extra packages. Companion to nvim
# and the `micro` binary installed further down. EDITOR stays
# nvim; users opt in via `export EDITOR=nano`.
RUN apt-get update && \ RUN apt-get update && \
apt-get upgrade -y --no-install-recommends && \ apt-get upgrade -y --no-install-recommends && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
@@ -89,9 +102,11 @@ RUN apt-get update && \
python3-pip \ python3-pip \
python3-venv \ python3-venv \
pandoc \ pandoc \
xz-utils \
graphviz \ graphviz \
imagemagick \ imagemagick \
socat \ socat \
nano \
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \ && ln -s /usr/bin/fdfind /usr/local/bin/fd \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@@ -231,6 +246,33 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;
ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \ ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \
nvim --version | head -1 nvim --version | head -1
# micro — modern, non-modal terminal editor. Ships alongside nvim so users
# who aren't comfortable with vi-style modal editing have a friendly option:
# desktop-style keybindings (Ctrl+S save, Ctrl+Q quit, Ctrl+C/V/X, Ctrl+Z
# undo), mouse support, and syntax highlighting out of the box. A single
# static Go binary (~12 MB) installed from GitHub releases, exactly like
# bat/eza/zoxide below. EDITOR stays nvim (see below); users opt in with
# `export EDITOR=micro` or `git config --global core.editor micro`.
#
# NOTE: upstream moved zyedidia/micro -> micro-editor/micro. The old org URL
# still 302s, but its /releases/latest redirect lands on ANOTHER /latest URL
# (the org rename), so the tag-parsing idiom below would resolve "latest"
# instead of a version. Use the canonical micro-editor/micro URL.
# Arch asset naming differs from the others: amd64 -> linux64, arm64 ->
# linux-arm64. The tarball extracts to micro-<version>/micro.
ARG MICRO_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "linux64" ;; arm64) echo "linux-arm64" ;; *) echo "linux64" ;; esac) && \
V="${MICRO_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/micro-editor/micro/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && [ -n "$V" ] && \
echo "Installing micro ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/micro-editor/micro/releases/download/v${V}/micro-${V}-${ARCH}.tar.gz" | tar -xz -C /tmp && \
install /tmp/micro-${V}/micro /usr/local/bin/micro && \
rm -rf /tmp/micro-${V} && \
micro --version
# bat # bat
ARG BAT_VERSION=latest ARG BAT_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
@@ -410,6 +452,32 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
chmod +x /usr/local/bin/tldr && \ chmod +x /usr/local/bin/tldr && \
tldr --version tldr --version
# ── typst — lightweight PDF engine for pandoc (Markdown→PDF) ─────────
# pandoc (apt-installed above) is only a front-end; rendering PDF needs a
# back-end engine. Rather than a ~600 MB TeX Live install, we ship typst:
# a single ~30 MB static Rust binary with no LaTeX dependency. pi-studio's
# PDF export (studio_export_pdf) and pandoc invocations use it via
# `pandoc --pdf-engine=typst`. A fuller TeX Live remains the higher-
# fidelity fallback for anyone who needs LaTeX-exact output (not shipped
# here — install on demand or in a future variant).
#
# Follows the `latest` GitHub-release convention (like tealdeer/uv/bat).
# typst ships a `.tar.xz` asset (hence xz-utils in the apt layer above)
# that extracts to typst-<arch>-unknown-linux-musl/typst. Pin a specific
# tag with --build-arg TYPST_VERSION=vX.Y.Z.
ARG TYPST_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
V="${TYPST_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/typst/typst/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && [ -n "$V" ] && \
echo "Installing typst ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/typst/typst/releases/download/v${V}/typst-${ARCH}-unknown-linux-musl.tar.xz" | tar -xJ -C /tmp && \
install /tmp/typst-${ARCH}-unknown-linux-musl/typst /usr/local/bin/typst && \
rm -rf /tmp/typst-${ARCH}-unknown-linux-musl && \
typst --version
# ── yq (mikefarah) — YAML processor, jq's companion for YAML ───────── # ── yq (mikefarah) — YAML processor, jq's companion for YAML ─────────
# Installed as the mikefarah Go binary — NOT Debian's `yq` apt package, which # Installed as the mikefarah Go binary — NOT Debian's `yq` apt package, which
# is the unrelated Python kislyuk/yq (a jq wrapper with different syntax and # is the unrelated Python kislyuk/yq (a jq wrapper with different syntax and
+20 -7
View File
@@ -29,13 +29,18 @@ on the host.
- ChromaDB embedding model pre-warmed at build time (`all-MiniLM-L6-v2`) - ChromaDB embedding model pre-warmed at build time (`all-MiniLM-L6-v2`)
The host-mounted palace at `~/.mempalace` is shared across the host and The host-mounted palace at `~/.mempalace` is shared across the host and
this container so all your agents share one brain. this container so all your agents share one brain. To instead share a palace
across *several* containers/harnesses, set `MEMPALACE_REMOTE_URL` to a shared
MemPalace HTTP endpoint (see `.env.example` and `docker-compose.mempalace.yml`);
the bridge then connects over HTTP and spawns no local server.
### Modern CLI tooling ### Modern CLI tooling
| Tool | Purpose | | Tool | Purpose |
|---|---| |---|---|
| `nvim` | Neovim text editor | | `nvim` | Neovim text editor (modal / vi-style) |
| `nano` | Small non-modal editor (on-screen shortcut hints) |
| `micro` | Modern non-modal editor (Ctrl+S/Ctrl+Q keys, mouse, syntax highlighting) |
| `tmux` | Terminal multiplexer (configured for 0-indexed sessions) | | `tmux` | Terminal multiplexer (configured for 0-indexed sessions) |
| `ripgrep`, `fd` | Fast file content / filename search | | `ripgrep`, `fd` | Fast file content / filename search |
| `fzf` | Fuzzy finder | | `fzf` | Fuzzy finder |
@@ -49,6 +54,11 @@ this container so all your agents share one brain.
| `gosu` | Privilege de-escalation in entrypoint | | `gosu` | Privilege de-escalation in entrypoint |
| `htop`, `tree`, `less` | Inspection utilities | | `htop`, `tree`, `less` | Inspection utilities |
The default `$EDITOR` is `nvim`. Three editors ship so you can pick your
comfort level — if you'd rather not use a vi-style editor, `nano` and `micro`
are both non-modal. Set your preference with `export EDITOR=micro` (or `nano`)
in your shell profile, and/or `git config --global core.editor micro`.
### Document and image tooling ### Document and image tooling
- `pandoc` — universal Markdown↔HTML/Org/RST/etc. converter - `pandoc` — universal Markdown↔HTML/Org/RST/etc. converter
@@ -139,8 +149,10 @@ Currently published:
Planned for an upcoming minor release: Planned for an upcoming minor release:
- `joakimp/pi-devbox:latest-studio-tex``-studio` plus `texlive-xetex` - *(shipped in Unreleased/base)* **PDF export from Studio/pandoc** now works:
for PDF export from Studio. Adds ~600 MB on top of `-studio`. the base image ships **`typst`** as the PDF engine (`pandoc --pdf-engine=typst`),
a single ~30 MB static binary — no separate `-tex` variant needed.
`texlive-xetex` stays the higher-fidelity fallback (install on demand).
## Using pi-studio (`-studio` variant) ## Using pi-studio (`-studio` variant)
@@ -275,9 +287,10 @@ Assuming the compose file publishes `127.0.0.1:8765:8765` (see method B):
> until step 2 runs. If the browser can't connect, verify Studio is up > until step 2 runs. If the browser can't connect, verify Studio is up
> (`/studio --status`) and the bridge is running (`ps aux | grep socat`). > (`/studio --status`) and the bridge is running (`ps aux | grep socat`).
> PDF export (`/studio-pdf`, `studio_export_pdf`) needs a LaTeX engine, > PDF export (`/studio-pdf`, `studio_export_pdf`) uses **`typst`**, shipped in
> which is **not** in `-studio` (only the planned `-studio-tex`). HTML > the base image as the pandoc PDF engine (`pandoc --pdf-engine=typst`). For
> export, KaTeX, Mermaid, and all REPL features work without it. > LaTeX-exact output you can install `texlive-xetex` on demand as a heavier
> fallback. HTML export, KaTeX, Mermaid, and all REPL features work regardless.
### Graphviz diagrams in Studio: `dot-watch` ### Graphviz diagrams in Studio: `dot-watch`
+84
View File
@@ -0,0 +1,84 @@
# Shared MemPalace server (optional) — one palace for many clients.
#
# Runs `mempalace-mcp` over HTTP so several containers/harnesses (pi +
# opencode + native) can share ONE palace instead of each keeping its own.
# Point every client at it by setting, in that client's .env:
#
# MEMPALACE_REMOTE_URL=http://<reachable-host>:8765/mcp
#
# (see .env.example). When set, the client connects over HTTP and does NOT
# spawn its own local mempalace-mcp.
#
# Start: docker compose -f docker-compose.mempalace.yml up -d
# Stop: docker compose -f docker-compose.mempalace.yml down
# Logs: docker compose -f docker-compose.mempalace.yml logs -f
#
# Why reuse the devbox image? mempalace-mcp is already installed in it, and
# reusing it GUARANTEES the server's mempalace version matches the clients'
# (both are pinned by the same image build). Override with a slimmer image via
# MEMPALACE_SERVER_IMAGE if you prefer (it must provide `mempalace-mcp`).
#
# ⚠ SECURITY: mempalace-mcp's HTTP transport has NO authentication of its own.
# Do NOT expose port 8765 to an untrusted network. The default below binds to
# 127.0.0.1 (host loopback) only. To let sibling containers reach it, either
# attach them to the shared `mempalace-net` network (container-to-container, no
# host port needed — use http://mempalace-server:8765/mcp), or front it with a
# reverse proxy that enforces MEMPALACE_REMOTE_TOKEN as `Authorization: Bearer`.
name: mempalace-server
services:
mempalace:
image: ${MEMPALACE_SERVER_IMAGE:-joakimp/pi-devbox:latest}
container_name: mempalace-server
# Bypass the devbox entrypoint (dev-shell/LAN/config setup) and run the
# HTTP MCP server directly. HOME + explicit --palace pin the data path so
# it does not depend on the image's default user/HOME. Runs as root so it
# can initialise the fresh named volume; the volume is dedicated to this
# server (clients reach it over HTTP, never by mounting it).
entrypoint: []
user: "0:0"
environment:
- HOME=/data
command:
- mempalace-mcp
- --transport
- http
- --host
- "0.0.0.0"
- --port
- "8765"
- --palace
- /data/.mempalace
restart: unless-stopped
# Loopback-only by default (see SECURITY note). Use "8765:8765" to expose on
# all host interfaces, or drop `ports:` entirely and rely on mempalace-net.
ports:
- "127.0.0.1:8765:8765"
volumes:
# The shared palace data — precious; back this up.
- mempalace-shared:/data/.mempalace
# Embedding-model cache (~79 MB, disposable) so search does not re-download.
- mempalace-shared-chroma:/data/.cache/chroma
networks:
- mempalace-net
healthcheck:
# A tools/list round-trip proves the server is answering MCP (python3 is
# always present — mempalace itself is a python tool in the image).
test:
- CMD
- python3
- -c
- "import urllib.request,json; d=json.dumps({'jsonrpc':'2.0','id':1,'method':'tools/list','params':{}}).encode(); r=urllib.request.Request('http://127.0.0.1:8765/mcp',data=d,headers={'Content-Type':'application/json','Accept':'application/json'}); urllib.request.urlopen(r,timeout=5).read()"
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
volumes:
mempalace-shared:
mempalace-shared-chroma:
networks:
mempalace-net:
name: mempalace-net
+4 -1
View File
@@ -76,7 +76,10 @@ services:
# Persist uv data (Python installs, tool installs) # Persist uv data (Python installs, tool installs)
- devbox-uv:/home/developer/.local/share/uv - devbox-uv:/home/developer/.local/share/uv
# Optional: persist MemPalace data (conversation memory, knowledge graph) # Optional: persist MemPalace data (conversation memory, knowledge graph).
# Applies to the LOCAL palace only (the default). In EXTERNAL mode
# (MEMPALACE_REMOTE_URL set in .env) the shared server owns the data, so
# this volume is irrelevant.
# - devbox-palace:/home/developer/.mempalace # - devbox-palace:/home/developer/.mempalace
# Optional: persist ChromaDB embedding model cache (~79 MB) # Optional: persist ChromaDB embedding model cache (~79 MB)
+32
View File
@@ -54,6 +54,38 @@ alias gs='git status'
alias gd='git diff' alias gd='git diff'
alias gl='git log --oneline --graph --decorate -20' alias gl='git log --oneline --graph --decorate -20'
# ── Host SSH reachability check (once per container lifetime) ───────────────
# Warns at first shell startup if the Mac host is not reachable via SSH.
# Only runs inside a container, only if the jump key exists, and only once
# per container lifetime (/tmp flag is cleared on recreate).
_devbox_check_host_ssh() {
[ -f "/.dockerenv" ] || return 0
local ssh_cfg="$HOME/.ssh-local/config"
[ -f "$ssh_cfg" ] || return 0
local key_pub="$HOME/.ssh-local/devbox_jump_ed25519.pub"
[ -f "$key_pub" ] || return 0
local flag="/tmp/.devbox_host_ssh_ok"
[ -f "$flag" ] && return 0
if ssh -F "$ssh_cfg" \
-o BatchMode=yes \
-o ConnectTimeout=2 \
-o StrictHostKeyChecking=accept-new \
mac true 2>/dev/null; then
touch "$flag"
return 0
fi
local pub_key
pub_key=$(cat "$key_pub")
printf '\n\033[1;33m⚠ devbox: Mac host not reachable via SSH\033[0m\n'
printf ' Some tools use SSH to run commands on the Mac host.\n'
printf ' Fix (run both on the Mac):\n\n'
printf ' \033[1mStep 1\033[0m System Settings → General → Sharing → Remote Login → ON\n\n'
printf ' \033[1mStep 2\033[0m echo '"'"'%s'"'"' >> ~/.ssh/authorized_keys\n' "$pub_key"
printf '\n Then open a new shell in the container to verify.\n\n'
}
_devbox_check_host_ssh
unset -f _devbox_check_host_ssh
# ── LAN access via the host (dssh) ─────────────────────────────────── # ── LAN access via the host (dssh) ───────────────────────────────────
# When running on a VM-backed host (macOS OrbStack / Docker Desktop), the # When running on a VM-backed host (macOS OrbStack / Docker Desktop), the
# entrypoint's setup-lan-access.sh generates ~/.ssh-local/config so the host # entrypoint's setup-lan-access.sh generates ~/.ssh-local/config so the host
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Gitea-accurate guard against the recurring "bash syntax under the default
# sh/dash shell" footgun (ed49b8d resolve-versions; b7197e8 promote-base-latest,
# run 418).
#
# WHY A CUSTOM CHECK AND NOT JUST actionlint:
# actionlint models *GitHub* Actions, whose default `run` shell is bash. It
# therefore assumes a step that omits `shell:` runs under bash, and does NOT
# flag `set -o pipefail` there. Gitea Actions' default is `sh` (dash), so the
# exact bug we hit (omit `shell:`, use bash syntax) is invisible to actionlint.
# actionlint only fires when a step *explicitly* declares `shell: sh`.
#
# THE INVARIANT THIS ENFORCES:
# Every `run:` step in every .gitea/workflows/*.yml must resolve to an
# effective shell of `bash` — via the step's own `shell:`, a job-level
# `defaults.run.shell`, or a workflow-level `defaults.run.shell`. Any step
# that would fall through to Gitea's `sh` default is a FAILURE, because a
# future author adding bash syntax to it fails silently in CI.
#
# Pair this with actionlint (which catches explicit `shell: sh` + bash syntax,
# expression errors, and much else). Together they cover the class on Gitea.
set -euo pipefail
WF_DIR="${1:-.gitea/workflows}"
python3 - "$WF_DIR" <<'PY'
import sys, glob, os
try:
import yaml
except ImportError:
sys.stderr.write("ERROR: python3 yaml module missing (apt install python3-yaml)\n")
sys.exit(2)
wf_dir = sys.argv[1]
files = sorted(glob.glob(os.path.join(wf_dir, "*.yml")) + glob.glob(os.path.join(wf_dir, "*.yaml")))
if not files:
sys.stderr.write(f"ERROR: no workflow files under {wf_dir}\n")
sys.exit(2)
problems = []
for f in files:
with open(f) as fh:
doc = yaml.safe_load(fh) or {}
wf_shell = (((doc.get("defaults") or {}).get("run") or {}).get("shell"))
jobs = doc.get("jobs") or {}
for jname, job in jobs.items():
job = job or {}
job_shell = (((job.get("defaults") or {}).get("run") or {}).get("shell"))
steps = job.get("steps") or []
for i, step in enumerate(steps):
step = step or {}
if "run" not in step:
continue # `uses:` steps have no shell
eff = step.get("shell") or job_shell or wf_shell or "sh" # Gitea default = sh
if eff != "bash":
name = step.get("name") or f"step[{i}]"
problems.append(f"{f}: job '{jname}' / '{name}': effective shell = '{eff}' (Gitea default is sh; declare shell: bash or a bash default)")
if problems:
sys.stderr.write("Workflow shell guard FAILED — bash default not guaranteed:\n")
for p in problems:
sys.stderr.write(f" - {p}\n")
sys.exit(1)
print(f"Workflow shell guard OK — all run: steps in {len(files)} workflow file(s) resolve to bash.")
PY
+8 -1
View File
@@ -6,6 +6,8 @@
# Verifies: # Verifies:
# - pi binary present and (if EXPECTED_PI_VERSION set) matches CI's resolved version # - pi binary present and (if EXPECTED_PI_VERSION set) matches CI's resolved version
# - new v1.0.0 base additions (pandoc, graphviz, imagemagick, yq, tealdeer) # - new v1.0.0 base additions (pandoc, graphviz, imagemagick, yq, tealdeer)
# - typst PDF engine for pandoc (Unreleased) — `pandoc --pdf-engine=typst`
# - non-modal editors nano + micro (alongside nvim)
# - tmux 0-indexing baked in /etc/tmux.conf (required for pi-studio variants) # - tmux 0-indexing baked in /etc/tmux.conf (required for pi-studio variants)
# - pi-toolkit cloned at /opt/pi-toolkit # - pi-toolkit cloned at /opt/pi-toolkit
# - pi-extensions cloned at /opt/pi-extensions # - pi-extensions cloned at /opt/pi-extensions
@@ -24,7 +26,8 @@ set -euo pipefail
IMAGE="${1:?usage: $0 <image>}" IMAGE="${1:?usage: $0 <image>}"
PASS=0; FAIL=0 PASS=0; FAIL=0
# pi-devbox v1.0.0 (decoupled from opencode-devbox) added pandoc, graphviz, # pi-devbox v1.0.0 (decoupled from opencode-devbox) added pandoc, graphviz,
# imagemagick, yq, tealdeer, and a baked /etc/tmux.conf. Local arm64 build # imagemagick, yq, tealdeer, a baked /etc/tmux.conf, and the non-modal
# editors nano + micro (~15 MB combined). Local arm64 build
# observed 3.20 GB. CI amd64 builds may differ slightly; threshold below # observed 3.20 GB. CI amd64 builds may differ slightly; threshold below
# carries +300 MB margin to absorb arch differences without false reds. # carries +300 MB margin to absorb arch differences without false reds.
# Tighten in a follow-up release once amd64 actuals are observed in CI logs. # Tighten in a follow-up release once amd64 actuals are observed in CI logs.
@@ -71,9 +74,13 @@ run "git" "git --version"
run "aws" "aws --version" run "aws" "aws --version"
run "uv" "uv --version" run "uv" "uv --version"
run "nvim" "nvim --version" run "nvim" "nvim --version"
run "nano" "nano --version"
run "micro" "micro --version"
run "mempalace-mcp" "mempalace-mcp --help" run "mempalace-mcp" "mempalace-mcp --help"
# v1.0.0 base additions — verify presence and basic functionality. # v1.0.0 base additions — verify presence and basic functionality.
run "pandoc" "pandoc --version" run "pandoc" "pandoc --version"
run "typst" "typst --version"
run "pandoc+typst PDF engine" "printf '# hi\n' | pandoc --pdf-engine=typst -o /tmp/_smoke.pdf - && test -s /tmp/_smoke.pdf; rm -f /tmp/_smoke.pdf"
run "graphviz (dot)" "dot -V" run "graphviz (dot)" "dot -V"
run "imagemagick" "magick --version" run "imagemagick" "magick --version"
run "yq (mikefarah v4)" "yq --version | grep -qE 'mikefarah.*version v4'" run "yq (mikefarah v4)" "yq --version | grep -qE 'mikefarah.*version v4'"