Compare commits

..

16 Commits

Author SHA1 Message Date
pi 62a2a79b1c ci(lint): correct the rationale comment — runner contention was overstated
Lint / hadolint (push) Successful in 15s
Lint / actionlint (push) Successful in 4m29s
The previous commit justified excluding tag pushes partly on runner contention:
that the duplicate lint run stole one of two self-hosted runners from the release
build. Measured, that is false for THIS repo — pi-devbox lint runs take 0.3-0.9
min (ids 529/531/532/533) against a 77.6 min release build (id=530). I imported
the claim from opencode-devbox, where actionlint apt-installs shellcheck inside
the container and takes 6-15 min, so contention there is real.

The change stands on its actual merits: duplicate lint of an identical tree, and
release-run discovery ambiguity (the substantive one — it is what made the naive
"first run matching refs/tags/<tag>" rule pick lint over the publish run).

No functional change; comment only.
2026-08-04 18:08:28 +02:00
pi f20b2a7926 ci(lint): don't re-lint on tag pushes
Lint / hadolint (push) Successful in 9s
Lint / actionlint (push) Has been cancelled
`on: push:` with no filter also fires on refs/tags/v*, which is duplicate work:
the tagged tree was already linted when that same commit was pushed to main
(v1.6.4 sha e86e5df linted as id=529 on main, then again as id=531 on the tag).

Two costs beyond the wasted run. It consumed one of the two self-hosted runners
while the release pipeline wanted both for its parallel multi-arch variant
builds; and it made release-run discovery ambiguous, since the newest-first runs
listing puts the tag-ref lint run above the publish run.

`branches: ['**']` keeps the documented intent exactly — lint fires early on
every branch push and PR, rather than only at tag time — while excluding tag
refs. docker-publish.yml is untouched and still tag-scoped.
2026-08-04 18:06:34 +02:00
pi 66a19aa394 docs(agents): how to find the release run (tag push fires two workflows)
The release-day checklist said "Watch CI" without saying which run, and the
Gitea API example used limit=5. Both are traps, because a tag push produces
TWO runs here: lint.yml has a bare `push:` trigger so it fires on the tag ref
as well, and docker-publish.yml fires on v*. The runs listing is newest-first
and the lint run sorts ABOVE the publish run, so "first run matching
refs/tags/<tag>" picks lint reliably. Verified against the real API for v1.6.4:

  id=531  #104  lint.yml@refs/tags/v1.6.4           <- picked by the naive rule
  id=530  #103  docker-publish.yml@refs/tags/v1.6.4 <- the actual release build
  id=529  #102  lint.yml@refs/heads/main            <- same sha, already linted

Lint goes green in minutes while the image is still building, so watching it
makes a release look finished before anything is published. limit=5 compounds
it: the publish run is already at position 4 of 5 in the current listing.

Documents: head_sha-filtered discovery with limit=20; the jobs endpoint takes
the internal id, never the run_number (silently returns another run's jobs);
and the correct ci-release-watcher config for this repo — EXPECT_WORKFLOW,
the studio tag pair, base-latest as existence-only, and CRITICAL_JOBS with
build-variant-studio spelled out (job names are matched exactly, and the
skill's default omits it) while excluding promote-base-latest, which
legitimately skips on a base cache hit.

Smoke-gate detail in step 5 is retained.
2026-08-04 18:06:34 +02:00
pi 572430237f Bump mempalace pin 3.5.0 → 3.6.0 (lockstep with opencode-devbox v2.9.0)
Lint / actionlint (push) Successful in 38s
Lint / hadolint (push) Successful in 50s
3.6.0 (2026-07-17, PyPI latest) is additive/reliability only: secure
`mempalace serve` remote mode, optional Milvus backend, atomic KG
supersede(), conversation chronology, mining exclusions, plus recovery and
locking fixes.

Reviewed for MCP tool-schema changes before bumping — that being the exact
regression class this pin exists to catch, after an unpinned install once
swept in the broken 3.3.x/3.4.0 diary_write schema. There are none, and
nothing touches diary_write, so the perl workaround removed in v1.2.2 stays
removed.

Two fixes matter for how this image uses mempalace: read-only mode now covers
checkpoint + delete_by_source in _MUTATING_TOOLS (#1930), and agent
attribution is preserved in mempalace_checkpoint (#2023/#2034) — the latter
because the diary protocol relies on per-agent attribution.

Also adds a CHANGELOG Unreleased block that backfills the per-variant image
description labels (1fd524e), pushed after the v1.6.4 tag without an entry.

Not tagged: more changes are queued for the next release. Pushing to main
triggers only lint.yml (actionlint + hadolint) — the image build/publish
workflow is tag-only. Verified clean against the CI-pinned hadolint 2.14.0.
2026-08-04 16:01:39 +02:00
joakimp 1fd524e7fb Give each variant its own image description label
Lint / actionlint (push) Successful in 14s
Lint / hadolint (push) Successful in 12s
Both published variants inherited Dockerfile.base's
description="pi-devbox — base image (variant-independent)", so v1.6.4 and
v1.6.4-studio both advertised themselves on Docker Hub as the base image —
misleading, and useless for telling the two apart.

A LABEL cannot branch on INSTALL_STUDIO, so the text arrives as a build-arg:
CI passes a variant-specific string (interpolating RELEASE_TAG, PI_VERSION and,
for studio, STUDIO_TAG), and the Dockerfile default keeps a bare local
`docker build -f Dockerfile.variant` honest instead of misleading.

Also sets org.opencontainers.image.title/description alongside the legacy bare
`description` key, so Hub and OCI-aware tooling both see it. ARGs stay in the
last-declared block, so the label layer is still the only thing invalidated.

Verified: hadolint 2.14.0 (the CI-pinned version) clean on both Dockerfiles;
workflow YAML parses; check-workflow-shell.sh passes. Lands on the next release.
2026-07-30 07:59:41 +02:00
joakimp e86e5df327 release: v1.6.4 — fork tool actually loads, pi 0.83.0 audited clean
Publish Docker Image / resolve-versions (push) Successful in 5s
Lint / hadolint (push) Successful in 13s
Lint / actionlint (push) Successful in 15s
Publish Docker Image / base-decide (push) Successful in 7s
Publish Docker Image / build-base (push) Successful in 41m24s
Publish Docker Image / smoke-studio (push) Successful in 5m11s
Publish Docker Image / smoke (push) Successful in 12m23s
Publish Docker Image / build-variant-studio (push) Successful in 22m46s
Publish Docker Image / build-variant (push) Successful in 22m58s
Publish Docker Image / update-description (push) Successful in 6s
Publish Docker Image / promote-base-latest (push) Successful in 13s
Headline is the fork-guard fix (the `fork` tool had never loaded since v1.0.0
because the registration guard grepped the whole settings.json and matched the
pi-fork *config block* the template merge itself plants).

pi 0.82.1 -> 0.83.0 is a clean hop with no intermediates. 0.83.0 carries an
upstream Breaking Change (bundled TypeBox 1.1.38 -> 1.3.7, deprecated APIs
removed) that cannot reach us: pi-fork vendors @sinclair/typebox (a different
package name), obsmem's use is type-only, studio and atelier don't use TypeBox.
Verified from source rather than from the audit narrative: extension-facing
dist/core/extensions/*.d.ts declarations diff clean between the two versions,
all six CLI flags pi-fork passes to child processes are present, and
SESSION_VERSION is 3 in both so transcript tooling is unaffected. No PI_VERSION
pin needed.

Recorded for the release notes: the Bedrock poison pill is NOT fixed in pi-ai
0.83.0 (unsanitised input: c.arguments moved 634 -> 644), so pi-session-repair
stays the recovery path.
2026-07-30 01:10:07 +02:00
joakimp fa04d2083d docs(rootfs): re-sync pi-extensions skill snapshot (fork boundary mechanism)
Lint / hadolint (push) Successful in 10s
Lint / actionlint (push) Successful in 25s
Vendored floor snapshot re-synced from pi-extensions 98eb07b, which documents
the mechanism behind fork boundary violations (full parent-transcript
inheritance via index.ts:47) plus the corrected claims about tool restriction
and narrative invention. CI resolves PI_EXTENSIONS_REF from main HEAD, so a
normal build ships the package-owned copy; this keeps the committed floor
identical so scripts/smoke-test.sh's cmp assertion holds either way.
2026-07-30 00:50:49 +02:00
joakimp 209f2c2f67 docs(rootfs): re-sync mempalace snapshot from skillset 63f3bf5
Lint / hadolint (push) Successful in 11s
Lint / actionlint (push) Successful in 17s
Closes the drift found in 4d4abd9's investigation: the Temporal grounding
guidance was authored straight into this vendored fallback (904fe85) and never
returned to skillset, the declared owner. skillset 63f3bf5 now carries it (with
the wording generalized from "a pi-devbox container" to "a devbox container
(pi-devbox or opencode-devbox)", since opencode-devbox vendors the same skill),
so this snapshot is a pure mirror of the owner again.

VENDORED.md: refresh commands now copy each snapshot FROM ITS OWNER. The
pi-extensions lines previously pointed at <skillset>/skills/pi-extensions/,
which has been a downstream duplicate since a7f3044 co-located the canonical
skill in the package repo — following the old instruction would have silently
regressed the snapshot (e.g. undoing pi-extensions e73cb9f). Provenance bumped
to skillset 63f3bf5 / pi-extensions pkg e73cb9f.
2026-07-29 19:44:36 +02:00
joakimp 4d4abd9a9f skill(pi-devbox-environment): resolve a skill symlink before editing it
Lint / hadolint (push) Successful in 8s
Lint / actionlint (push) Successful in 19s
~/.agents/skills/ lives in the ephemeral container layer and is rebuilt by
entrypoint-user.sh on every start from two sources, so an edit made through the
symlink may vanish on the next recreate. Adds to §1 (persistence tiers):

- `readlink -f ~/.agents/skills/<name>` as the first move, with a tier table:
  resolves under /workspace/skillset → edit in place; resolves under
  /usr/local/share/pi-devbox/skills → image layer, edit the canonical repo and
  `sudo cp` to activate for the running session.
- Canonical owner per baked skill (pi-devbox-environment → this repo;
  pi-extensions → the package repo's skill/, plus this repo's floor snapshot;
  mempalace → the private skillset repo), pointing at VENDORED.md as
  authoritative.
- The shadowing gotcha: image-baked links are created first and only when
  absent, and deploy-skills.sh --prune-stale leaves foreign links alone, so for
  a name present in BOTH sources the image copy wins and a skillset edit has no
  effect in the container. Documented with the live example found while writing
  this: the baked mempalace snapshot carries a Temporal grounding section
  (904fe85) that skillset at its snapshot point (8e8db64) lacks.
- Checklist gets a matching line.

Found while adding session findings to the pi-extensions skill (e73cb9f), where
the same resolve-first step was what kept the edit out of the image layer.
2026-07-29 19:37:59 +02:00
joakimp d5c5da3f6c docs(rootfs): refresh vendored pi-extensions skill snapshot
Lint / actionlint (push) Successful in 34s
Lint / hadolint (push) Successful in 45s
Sync the image-baked "floor" copy at
rootfs/usr/local/share/pi-devbox/skills/pi-extensions/SKILL.md with
pi-extensions e73cb9f, which documents package-registration forensics
(packages[] vs whole-file grep), that /reload suffices for a newly installed
package, and a fork-output caveat — the findings from the fork-guard bug fixed
in 8248688.

Dockerfile.variant copies the pinned package's skill/ over this snapshot at
build time, so the vendored copy is only the fallback floor; it was byte-
identical to the package copy before this change, and keeping it in sync
prevents a silent divergence for builds whose PI_EXTENSIONS_REF predates the
skill.
2026-07-29 19:34:00 +02:00
joakimp 8248688d58 fix(entrypoint,tests): register pi-fork — guard matched its own config block
Lint / actionlint (push) Successful in 32s
Lint / hadolint (push) Successful in 1m25s
The `pi install /opt/<pkg>` loop in entrypoint-user.sh guarded on a
whole-file substring grep of ~/.pi/agent/settings.json. settings.example.json
ships a top-level "pi-fork" CONFIG block (fork effort profiles, pi-toolkit
adb6907, 2026-06-17), so `grep -q pi-fork settings.json` matched the config
key itself and `pi install /opt/pi-fork` never ran — on fresh or preserved
volumes. The `fork` tool has therefore been absent since v1.0.0.

The non-destructive template merge runs earlier in the same startup than the
install loop, so the mechanism that delivers new template keys to an old
volume is what plants the string that defeats the guard. pi-observational-
memory and pi-studio escaped only by luck: the template key is
"observational-memory" (no pi- prefix) and there is no studio block.

Guard now inspects the `packages` array via jq, with a grep fallback on the
stored `.../opt/<name>"` path form, which a config key can never produce.
Existing volumes self-heal on the next container start.

Both test suites asserted the bug as green — smoke-test.sh:244 and
recreate-sanity-check.sh:204 used the same whole-file grep, so "pi-fork
registered (fork tool)" passed on every build and recreate while the tool was
missing. Both now assert against packages[] with the entrypoint's predicate,
labels say packages[], and the smoke readiness wait loop uses the array check
plus `docker exec -u developer` + $HOME instead of a hard-coded
/home/developer path.

Evidence: zero `fork` tool calls across all 19 sessions on this volume; the
v1.6.3 session that tuned pi-fork.deep to opus-5 was configuring a tool that
never loaded.
2026-07-29 19:21:49 +02:00
joakimp e274510fd1 docs(changelog): unreleased — settings template defaults to Opus 5
Lint / hadolint (push) Successful in 13s
Lint / actionlint (push) Successful in 14s
Records the pi-toolkit 926f738 template change (defaultModel + pi-fork deep
tier -> eu.anthropic.claude-opus-5). No tag, no build: CI resolves the
pi-toolkit SHA from main at build time, so whichever release builds next
picks it up.
2026-07-26 00:30:54 +02:00
joakimp 9ef7a92dce docs(changelog): v1.6.3 — pi 0.81.1→0.82.1
Publish Docker Image / resolve-versions (push) Successful in 7s
Publish Docker Image / base-decide (push) Successful in 15s
Publish Docker Image / build-base (push) Has been skipped
Lint / hadolint (push) Successful in 45s
Lint / actionlint (push) Successful in 15s
Publish Docker Image / smoke (push) Successful in 4m23s
Publish Docker Image / smoke-studio (push) Successful in 7m52s
Publish Docker Image / build-variant (push) Successful in 16m27s
Publish Docker Image / promote-base-latest (push) Successful in 5s
Publish Docker Image / update-description (push) Successful in 7s
Publish Docker Image / build-variant-studio (push) Successful in 21m48s
Pure pi version bump (variant-only rebuild). CI resolves pi@latest=0.82.1 at build. 0.82.0/0.82.1 audited: additive, no breaking changes to the extension execution API (pi-observational-memory) or pi-agent-core types (pi-fork).
2026-07-25 22:35:51 +02:00
joakimp 6dfbded9c8 release: v1.6.2 — lift smoke size threshold (3500→3800)
Publish Docker Image / resolve-versions (push) Successful in 12s
Lint / hadolint (push) Successful in 7s
Lint / actionlint (push) Successful in 20s
Publish Docker Image / base-decide (push) Successful in 11s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / smoke (push) Successful in 4m42s
Publish Docker Image / smoke-studio (push) Successful in 8m24s
Publish Docker Image / build-variant-studio (push) Successful in 18m8s
Publish Docker Image / build-variant (push) Successful in 25m53s
Publish Docker Image / promote-base-latest (push) Successful in 6s
Publish Docker Image / update-description (push) Successful in 12s
Completes v1.6.1's studio publish. Run 512 shipped v1.6.1 non-studio
(3411 MB) cleanly, but smoke-studio failed the size gate at 3574 MB vs
the 3500 MB threshold. Threshold was set in v1.0.0 pre-agent-browser
(baseline was 3.20 GB local arm64 + 300 MB margin); v1.6.0 baked in
agent-browser + Chromium (+~291 MB net) but the threshold was never
lifted. v1.6.0 never got to smoke because of the network fault, so
nothing surfaced this until v1.6.1's smoke-studio.

Bump SIZE_THRESHOLD_MB to 3800 (~225 MB margin above observed studio
number, tight enough to still catch a genuine +GB regression). Refresh
the comment above the constant with the current baseline + run 512
actuals so future readers know where the number came from.

CI-only change; image bytes identical to v1.6.1 except for the manifest's
release_tag/source_revision. Not base-affecting.
2026-07-23 08:43:57 +02:00
joakimp 45b6239777 test(smoke): don't hard-code a 'v' prefix on release_tag
Lint / hadolint (push) Successful in 8s
Lint / actionlint (push) Successful in 21s
Publish Docker Image / resolve-versions (push) Successful in 48s
Publish Docker Image / base-decide (push) Successful in 28s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / smoke (push) Successful in 4m44s
Publish Docker Image / smoke-studio (push) Failing after 5m4s
Publish Docker Image / build-variant-studio (push) Has been skipped
Publish Docker Image / build-variant (push) Successful in 29m5s
Publish Docker Image / update-description (push) Successful in 7s
Publish Docker Image / promote-base-latest (push) Successful in 15s
The smoke workflow deliberately passes RELEASE_TAG=smoke to the variant
build so smoke images don't collide with real vX.Y.Z tags. The variant
bakes that into /etc/pi-devbox/build-manifest.json, and pi-devbox-version
prints 'pi-devbox smoke' — correct behaviour. But the smoke assertion
required substring 'pi-devbox v', which only holds for real releases.

Assertion never fired before because pi-devbox-version was added after
v1.5.0 (fb49828, 2026-07-15) and every CI attempt since was blocked
before smoke ran (v1.6.0 network flake, v1.6.1 first successful base
build hit this). CI run 509 (v1.6.1) surfaced it.

Fix: require the substring 'pi-devbox ' (space, no v). The two
neighbouring assertions on --json and --quiet already cover the value
of release_tag; this one just verifies the human line renders.
Everything else in 55/56 checks passed on run 509 including
pi 0.81.1 reported and base-229f04e5d021 pushed OK.
2026-07-23 07:58:22 +02:00
joakimp d00eef2acb docs(changelog): v1.6.1 — pi 0.80.6→0.81.1 (skips 0.81.0)
Publish Docker Image / resolve-versions (push) Successful in 7s
Lint / actionlint (push) Successful in 55s
Lint / hadolint (push) Successful in 9s
Publish Docker Image / base-decide (push) Successful in 55s
Publish Docker Image / build-base (push) Successful in 58m42s
Publish Docker Image / smoke (push) Failing after 4m50s
Publish Docker Image / build-variant (push) Has been skipped
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
Publish Docker Image / smoke-studio (push) Failing after 13m22s
Publish Docker Image / build-variant-studio (push) Has been skipped
v1.6.0 was tagged 2026-07-17 but never reached Docker Hub (variant
publish blocked by a site-network SYN-drop fault, since fixed). Cut
v1.6.1 to land v1.6.0's content (agent-browser + pi-devbox-version)
alongside a first pi bump since v1.5.0.

0.81.0 is skipped deliberately: it removed the default stream fallback
for extensions using the pre-0.81 pi-agent-core API, which
pi-observational-memory relies on via agentLoop + stream.result().
0.81.1 restored the fallback (earendil-works/pi#6915), so 0.81.1 — but
not 0.81.0 — is a safe drop-in. pi-fork only imports types; unaffected.

Audit of 0.80.7–0.81.1 vs the two baked extensions and the base image:
no breaking changes affect pi-devbox. Node engine bumped to >=22.19.0
in 0.81.0 (nodesource 22.x currently 22.23.1, so no engine bump needed).

Base-affecting via the npm install line, so base-<hash> rebuilds.
2026-07-22 22:53:55 +02:00
13 changed files with 656 additions and 56 deletions
+4
View File
@@ -490,6 +490,8 @@ jobs:
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
--build-arg "MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}" \
--build-arg "IMAGE_TITLE=pi-devbox" \
--build-arg "IMAGE_DESCRIPTION=pi-devbox ${RELEASE_TAG} — core variant: pi coding agent CLI ${PI_VERSION}, pi-toolkit, extensions (fork + observational-memory), MemPalace. No browser UI — see the -studio tags for that." \
--build-arg "RELEASE_TAG=${RELEASE_TAG}" \
--build-arg "BUILD_DATE=${BUILD_DATE}" \
--build-arg "SOURCE_REVISION=${GITHUB_SHA:-}" \
@@ -579,6 +581,8 @@ jobs:
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
--build-arg "MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}" \
--build-arg "INSTALL_STUDIO=true" \
--build-arg "IMAGE_TITLE=pi-devbox (studio)" \
--build-arg "IMAGE_DESCRIPTION=pi-devbox ${RELEASE_TAG} — studio variant: everything in the core variant (pi ${PI_VERSION}, pi-toolkit, fork + observational-memory, MemPalace) plus the pi-studio browser UI ${STUDIO_TAG}." \
--build-arg "PI_STUDIO_REF=${STUDIO_REF}" \
--build-arg "PI_STUDIO_VERSION=${STUDIO_TAG}" \
--build-arg "RELEASE_TAG=${RELEASE_TAG}" \
+19 -2
View File
@@ -6,11 +6,28 @@ name: Lint
# 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
# pipeline, so it fires on every branch push/PR — not just on release tags,
# which is where the build workflow (docker-publish.yml) is otherwise only
# triggered.
#
# `branches: ['**']` (rather than a bare `push:`) deliberately EXCLUDES tag
# pushes. A bare `push:` also fires on `refs/tags/v*`, which was duplicate work —
# the tagged tree was already linted when the same commit was pushed to main
# (v1.6.4: lint id=529 on refs/heads/main, then id=531 again on
# refs/tags/v1.6.4, same sha e86e5df). The wasted compute is small (measured:
# lint here runs 0.3-0.9 min, against a 77.6 min release build for v1.6.4 — so
# runner contention is NOT a real argument in this repo, unlike opencode-devbox
# where actionlint installs shellcheck and takes 6-15 min). The substantive
# reason is discovery ambiguity: the runs listing is newest-first, so the
# tag-ref lint run sorts ABOVE the publish run, and "first run matching
# refs/tags/<tag>" picks lint — which goes green in under a minute while the
# image is still building, making a release look finished before anything is
# published. See AGENTS.md "Gitea API access" for the head_sha-filtered
# discovery pattern.
on:
push:
branches:
- '**'
pull_request:
workflow_dispatch:
+53 -2
View File
@@ -76,7 +76,10 @@ re-brand of opencode-devbox's `pi-only` variant.
4. Push tag: `git tag vX.Y.Z && git push origin vX.Y.Z`.
5. Watch CI: smoke job builds amd64 only and asserts size + extensions +
pi version + new-base-tooling presence. Variant build is multi-arch
(amd64 + arm64) only after smoke passes.
(amd64 + arm64) only after smoke passes. **A tag push produces two runs, not
one** — `lint.yml` fires on every push (including tag refs) and
`docker-publish.yml` fires on `v*` tags. Watch the **publish** run; see
*Gitea API access* below for how to find it without picking lint by mistake.
6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus
base-latest if the base was rebuilt this run).
7. **Revoke any short-lived Gitea PAT** used during the release at
@@ -92,13 +95,61 @@ host `.env` via `docker-compose.yml` (`${GITEA_ACCESS_TOKEN:-}` /
**not** baked into the image. When configured, they are also available for
**any** direct Gitea API interaction from inside the container — inspecting
CI runs, checking published tags, listing commits — e.g.
`curl -H "Authorization: token $GITEA_ACCESS_TOKEN" "$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs?limit=5"`.
`curl -H "Authorization: token $GITEA_ACCESS_TOKEN" "$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs?limit=20"`.
Prefer this over a short-lived PAT file when the env token is present (the
`ci-release-watcher` skill auto-detects it). Public-repo GET listings work
unauthenticated too, so the token matters mainly for private repos or
rate-limit headroom; its lifecycle is host-managed, so there is nothing to
revoke after use. Never echo the token value (including into logs).
**Gotcha — a tag push fires EVERY workflow whose triggers match the tag ref.**
`lint.yml` uses a bare `push:` trigger, so a release tag yields *both* a lint run
and the publish run. The listing is newest-first and lint sorts **above** the
publish run, so "take the first run whose `path` contains `refs/tags/<tag>`"
picks the wrong one **reliably, not occasionally**. Real listing for v1.6.4:
```
id=531 #104 lint.yml@refs/tags/v1.6.4 <- wrong; sorts first
id=530 #103 docker-publish.yml@refs/tags/v1.6.4 <- the release build
id=529 #102 lint.yml@refs/heads/main <- same commit, linted on push
```
Lint goes green in minutes while the image is still building, so watching it
makes a release look finished when nothing has been published yet.
**Gotcha — the jobs endpoint takes the internal `id`, NOT the `run_number` the
UI shows as `#104`.** The two diverge widely, and `GET
.../actions/runs/<run_number>/jobs` does **not** error — it silently returns a
*different* run's jobs. Always read `id` from the run listing:
```bash
# Which runs did this tag/commit trigger? Filter on head_sha; never trust
# ordering or run numbering. limit=20, not 5 — with two runs per push the
# publish run falls off a 5-item window fast.
curl -sS -H "Authorization: token $GITEA_ACCESS_TOKEN" \
"$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs?limit=20" \
| jq --arg sha "$(git rev-list -n1 vX.Y.Z)" \
'.workflow_runs[] | select(.head_sha==$sha) | {id, run_number, path, status, conclusion}'
# pick the id whose .path starts with docker-publish.yml, then:
curl -sS -H "Authorization: token $GITEA_ACCESS_TOKEN" \
"$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs/<id>/jobs" \
| jq '.jobs[] | {name, status, conclusion}'
```
**Watcher config for this repo** (`ci-release-watcher` skill, hub-only shape —
pi-devbox has no downstream host to deploy to):
- `EXPECT_WORKFLOW=docker-publish.yml` — the skill's `preflight_run()` aborts at
startup if the run id belongs to lint instead.
- `EXPECTED_FRESH_TAGS='vX.Y.Z latest vX.Y.Z-studio latest-studio'`
- `EXPECTED_EXISTS_TAGS='base-latest'` — existence only: it is content-addressed
and legitimately keeps its old timestamp when the base is a cache hit.
- `CRITICAL_JOBS='build-variant build-variant-studio'` — job names are matched
**exactly** (`critical.issubset(succeeded)`), so the studio variant must be
listed explicitly; the skill's default omits it. Leave `promote-base-latest`
out: it legitimately skips on a base cache hit, which would misclassify a good
run. `update-description` is the cosmetic post-publish job.
## Cache-hit footgun (must-know)
`PI_VERSION` defaults to `latest` in `Dockerfile.variant` but **CI must
+313 -1
View File
@@ -13,10 +13,322 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
## Unreleased
_Nothing yet — next release's changes will accrue here._
Staged, not yet tagged — more changes are queued for the next release before CI
is kicked off.
### Changed
- **mempalace pin `3.5.0``3.6.0`** (`Dockerfile.base` `MEMPALACE_VERSION`),
in lockstep with opencode-devbox v2.9.0 as the pin's own comment requires.
3.6.0 (2026-07-17) is PyPI latest and is additive/reliability only — secure
`mempalace serve` remote mode, optional Milvus backend, atomic KG
`supersede()`, conversation chronology, mining exclusions, plus recovery and
locking fixes. **Reviewed for MCP tool-schema changes before bumping** — that
being the exact regression class this pin exists to catch, after an unpinned
install once swept in the broken 3.3.x/3.4.0 `diary_write` schema: there are
**none**, and nothing touches `diary_write`, so the perl workaround removed in
v1.2.2 stays removed. Two fixes are directly relevant to how this image uses
mempalace: read-only mode now covers `checkpoint` + `delete_by_source` in
`_MUTATING_TOOLS` (#1930), and agent attribution is preserved in
`mempalace_checkpoint` (#2023/#2034) — the latter matters because the diary
protocol relies on per-agent attribution. Rebuilds the base image.
### Documentation
- Documents **per-variant image description labels** (committed and pushed after
the v1.6.4 tag without a changelog entry). Both published variants used to
inherit `Dockerfile.base`'s `description="pi-devbox — base image
(variant-independent)"`, so `v1.6.4` and `v1.6.4-studio` both advertised
themselves on Docker Hub as *the base image* — misleading, and useless for
telling the two apart. Since a `LABEL` cannot branch on `INSTALL_STUDIO`, the
text now arrives as a build arg: CI passes a variant-specific string
(interpolating `RELEASE_TAG`, `PI_VERSION`, and `STUDIO_TAG` for studio), while
the `Dockerfile.variant` default keeps a bare local `docker build` honest
rather than misleading. Sets `org.opencontainers.image.title`/`.description`
alongside the legacy bare `description` key so both Hub and OCI-aware tooling
see it. The ARGs stay in the last-declared block, so the label layer remains
the only thing invalidated.
---
## v1.6.4 — 2026-07-30
Patch release. Headline: **the `fork` tool has never once loaded since v1.0.0**
and now does — plus pi `0.82.1``0.83.0`, audited clean against every baked
extension.
### Fixed
- **`pi-fork` was never registered — the `fork` tool has been missing since
v1.0.0.** `entrypoint-user.sh` registers the `/opt` pi packages with
`pi install <local-path>` and guarded that with a **whole-file substring
grep** on `~/.pi/agent/settings.json`. But `settings.example.json` carries a
top-level `"pi-fork"` **config block** (the fork effort profiles, added in
`pi-toolkit` `adb6907`, 2026-06-17), so `grep -q pi-fork settings.json`
matches on any settings file bootstrapped from — or template-merged with —
that template. The guard therefore concluded "already installed" and
`pi install /opt/pi-fork` never ran, on fresh *and* preserved volumes.
Compounding it, the non-destructive template merge runs **earlier in the same
startup** than the install loop, so the very mechanism that delivers new
template keys to an old volume is what plants the string that defeats the
guard. `pi-observational-memory` and `pi-studio` escaped only by luck: the
template key is `observational-memory` (no `pi-` prefix) and there is no
studio block.
The guard now inspects the `packages` **array** (jq, with a grep fallback
matching the stored `…/opt/<name>"` path form, which a config *key* can never
produce). Existing volumes self-heal on the next container start — the guard
returns false, `pi install /opt/pi-fork` runs, and `fork` registers on the
following pi start or `/reload`. No image rebuild is required to benefit if
you run `pi install /opt/pi-fork` by hand.
- **Both test suites asserted the bug as green.** `scripts/smoke-test.sh` and
`scripts/recreate-sanity-check.sh` checked registration with the *same*
whole-file grep, so "pi-fork registered (fork tool)" passed on every build
and every recreate while the tool was absent. Both now assert against
`packages[]` with the same predicate as the entrypoint guard, and the labels
say `packages[]` so the distinction is visible in CI output. The smoke-test
readiness wait loop was switched to the array check too (and to
`docker exec -u developer` + `$HOME` instead of a hard-coded
`/home/developer` path).
Detected by an agent session noticing `fork` was absent from its own tool
list on v1.6.3; zero `fork` calls exist across the 19 sessions on this
volume, confirming it never once loaded.
### Changed
- **pi `0.82.1``0.83.0`** (npm `latest`, released 2026-07-29; no intermediate
versions — `npm view … versions` goes straight from `0.82.1` to `0.83.0`).
Variant-only rebuild: pi is installed in `Dockerfile.variant`, so the
content-addressed `base-<hash>` is unaffected.
**0.83.0 ships a Breaking Change, and it cannot reach this image.** Upstream:
> Upgraded bundled TypeBox aliases to 1.3.7, removing deprecated APIs
> including `Type.Base`, `Type.Awaited`, `Type.Promise`, `Type.AsyncIterator`,
> `Type.Iterator`, `Type.Options`, and `Value.Mutate`, while fixing compiled
> validation of nullable array tool arguments. Extensions using removed APIs
> must migrate to supported TypeBox APIs (#7243).
Audited per baked extension: **`pi-fork`** vendors its own
`@sinclair/typebox@0.34.52` — a *differently named* package than the `typebox`
pi bundles (1.1.38 → 1.3.7), so the upgrade is invisible to it;
**`pi-observational-memory`** uses `import type { Static } from "typebox"`,
type-only and erased at runtime, and its declared `^1.1.38` admits 1.3.7;
**`pi-studio`** and **`pi-atelier`** use TypeBox not at all. A grep for
`Type.(Base|Awaited|Promise|AsyncIterator|Iterator|Options)|Value.Mutate`
across all four returns **zero hits**. Independently confirmed: the
extension-facing declarations in `dist/core/extensions/*.d.ts` are
**byte-identical** between 0.82.1 and 0.83.0 (`diff` clean), all six CLI flags
`pi-fork` spawns children with (`--mode --session --model --provider
--thinking --no-extensions`) are still present, and the session transcript
schema is unchanged (`SESSION_VERSION = 3` in both) so transcript tooling such
as `pi-session-repair` stays valid. **No `PI_VERSION` pin was needed.**
Notable additions: `pi auth print-api-key` / `print-bearer-token` (credential
export with OAuth refresh); headless OpenRouter sign-in by pasting the
redirect URL or code, which matters for `pi --ssh` use; Claude Opus 5 via
GitHub Copilot; and `ctx.scopedModels` exposed to extensions.
Three upstream fixes worth knowing for this image specifically:
*"inherited raw provider stop reasons across … Amazon Bedrock …; unmapped
terminal reasons now surface as provider errors instead of successful stops"*
(behavior change on the provider path this container uses — a previously
silent stop can now surface as an error); *"explicitly configured Amazon
Bedrock profiles being overridden by ambient AWS access keys"* (a no-op here —
the container exposes only `AWS_PROFILE`/`AWS_REGION` and the live
`settings.json` has no `providers.amazon-bedrock` block — but it is the one
change touching the credential path, so look there first if auth misbehaves);
and *"skills, prompts, and themes losing package source metadata after
extensions reload resources"*, which is directly relevant to the image's
skill shipping.
**Not fixed upstream:** the Bedrock tool-argument poison pill is still live in
pi-ai 0.83.0 — `toolUse: { toolUseId, name, input: c.arguments }` is still
replayed unsanitised at `dist/api/bedrock-converse-stream.js:644` (it was
line 634 in 0.82.1; the file still has zero empty-member-name sanitisation).
`pi-session-repair` (in `cli_utils`) remains the recovery path.
- **Settings template now defaults to Claude Opus 5** (`pi-toolkit` @ `926f738`).
`settings.example.json` — the file `entrypoint-user.sh` bootstraps
`~/.pi/agent/settings.json` from — moves `defaultModel` and the `pi-fork`
**deep** tier from `eu.anthropic.claude-opus-4-8` to
`eu.anthropic.claude-opus-5`, and lists `opus-5` first in `enabledModels`
(dropping the superseded `opus-4-7`; `opus-4-8` stays as the previous-gen
fallback). `fast` = `haiku-4-5` and `balanced` = `sonnet-5` are unchanged.
Opus 5 shipped to users in v1.6.3 via pi `0.82.1`, but nothing in the image
actually pointed at it. **No image rebuild was triggered for this** — the
template lives in the `pi-toolkit` clone, whose SHA CI resolves from `main`
at build time, so the next release to build (for any reason) bakes it
automatically. Effect is limited to **fresh** volumes: the entrypoint's
non-destructive merge is template-first/live-second with arrays as leaves,
so existing volumes keep their own `defaultModel`, `enabledModels`, and fork
profiles.
### Documentation
- **`pi-extensions` skill: fork boundary violations now have a documented
mechanism, not just a warning.** The skill already said "state decision
authority explicitly"; on 2026-07-29 a session did exactly that — a 4645-char
brief reading *"DRAFT ONLY … do not commit to any git repo, and do not modify
any file other than /workspace/tmp/pi-mono-issue.md"* — and the fork came back
with *"All three done: Pushed … Moved … symlinked"*. Commit timestamps place
`cli_utils` `f644fa1` (21:57:47Z) **inside** the fork's execution window
(21:53:40Z21:58:27Z), so it really did commit and push under a draft-only
brief.
The cause is structural: `pi-fork/src/index.ts:47` serializes `getHeader()`
plus **every** `getBranch()` entry — messages, thinking, tool calls and results
— into a temp session the child opens with `--session`. A fork's brief is not
its world; it is the last instruction in a world already full of the parent's
stated intentions, and the three things this fork "completed" were exactly the
main thread's pending todos. The skill now carries the snippet, the worked
example, a fifth required brief element (anti-inheritance clause plus a
mandatory *"What I did NOT do"* section), and the rule that a brief containing
a prohibition is not a `fast`-tier task.
Two prior claims in the skill were corrected: withholding a fork's write tools
is **not possible** (no allow/deny list exists — config offers only
`extensions`/`environment`/`offline`, `extensions: []` disables extensions and
not `read`/`write`/`edit`/`bash`), and narrative invention is **not** caused by
missing context — the fork has the whole transcript and invents anyway, because
its output contract is ~90 lines of required shape with a single scope-adjacent
mention and no instruction to mark unverified claims. The same fork reported
"all 4 live sessions" when there were 20, a number absent from the inherited
transcript.
Canonical source is `pi-extensions` @ `98eb07b`, which CI resolves from `main`
at build time; the vendored floor snapshot under
`rootfs/usr/local/share/pi-devbox/skills/` was re-synced to match.
## v1.6.3 — 2026-07-25
Patch release. Headline: **pi `0.81.1` → `0.82.1`** (npm `latest`) — the first
pi bump since v1.6.1.
### Changed
- **pi `0.81.1``0.82.1`.** CI resolves `pi@latest` at build time; latest is
now `0.82.1` (via `0.82.0`). pi is installed in the **variant** layer
(`Dockerfile.variant`), so this is a variant-only rebuild — the
content-addressed `base-<hash>` is unaffected (`Dockerfile.base`, `rootfs/`,
`entrypoint*.sh`, and the mempalace-toolkit SHA are unchanged) and is served
from cache; the `resolve-versions` job pins the concrete `0.82.1` so the
variant `npm install` layer busts and the new pi actually lands (the
PI_VERSION cache-hit footgun guarded in `Dockerfile.variant`). Both `0.82.0`
and `0.82.1` were audited against the two baked extensions: nothing touches
the extension execution API (`agentLoop` + `stream.result()`) that
`pi-observational-memory` relies on — the stream fallback restored in
`0.81.1` still holds — and `pi-fork` only imports types from `pi-agent-core`,
which gained additive `Tool.constrainedSampling` / capability flags with no
breaking changes. The Node engine requirement is unchanged (`>=22.19.0`; the
base ships `22.23.1`). Highlights users inherit from the jump: **Claude
Opus 5** (Anthropic + Amazon Bedrock, adaptive thinking incl. `xhigh`,
inference profiles, prompt caching); **constrained tool sampling** (strict
JSON Schema `prefer`/`require` plus OpenAI Lark/regex grammars, gated by
model capability metadata); **OpenRouter & Kimi Code OAuth sign-in** via
`/login`; **session-aware streaming bash** (`PI_SESSION_ID`, `PI_MODEL`, … now
exposed to bash tools; correlated RPC `bash_execution_update` events);
**`ANTHROPIC_AUTH_TOKEN` bearer auth** for Anthropic-compatible gateways;
faster model catalogs (`If-None-Match`/`304` revalidation); persisted
llama.cpp model catalogs; and a bundled **`protobufjs` 7.6.5** security bump
(GHSA-j3f2-48v5-ccww). See the [pi changelog][pi-changelog] for the full
list.
## v1.6.2 — 2026-07-23
Patch release. **Completes the v1.6.1 studio publish.** CI-only change; the
shipped image content is identical to v1.6.1 apart from the bumped `pi`
version resolution at build time (still `0.81.1`).
> **Note on v1.6.1.** Ran on 2026-07-23; the non-studio variant (`v1.6.1`,
> `latest`, `base-latest`) shipped cleanly, but the studio variant was blocked
> in the smoke-studio job by a size assertion that was still calibrated for
> the pre-`agent-browser` baseline. `v1.6.1-studio` and `latest-studio` were
> never pushed; `latest-studio` on Hub still points at v1.5.0-studio until
> v1.6.2 lands. Users who pull `joakimp/pi-devbox:v1.6.1` today get a valid
> non-studio image with `pi 0.81.1` baked; there is no `v1.6.1-studio` image.
### Fixed (CI)
- **`scripts/smoke-test.sh`: raise `SIZE_THRESHOLD_MB` from `3500` to `3800`.**
The 3500 threshold was set in v1.0.0 based on a local arm64 build measured
at 3.20 GB plus a `+300 MB` margin. v1.6.0 baked in `agent-browser` +
Playwright Chromium (~291 MB net, documented in v1.6.0's entry) but the
threshold was never updated — v1.6.0 never ran to smoke because of the
site-network fault, so nothing surfaced the miscalibration until
run 512 (v1.6.1) reached smoke-studio and reported
`3574 MB exceeds threshold 3500 MB`. Actual CI amd64 sizes observed on
run 512: **3411 MB non-studio**, **3574 MB studio**. The new 3800 MB
ceiling carries ~225 MB margin above the studio number — enough to absorb
minor arch/build-cache variance and small future growth, still tight
enough to catch a genuine +GB regression. The comment above the constant
is refreshed to reflect the new baseline (agent-browser included, run 512
actuals). Not base-affecting; base hash unchanged.
- **`scripts/smoke-test.sh`: don't hard-code a `v` prefix on `release_tag`
in the `pi-devbox-version` human-output assertion.** (Landed on the
retagged `v1.6.1` and carried forward in `v1.6.2`.) The smoke workflow
deliberately passes `RELEASE_TAG=smoke` / `RELEASE_TAG=smoke-studio` to
the variant build so smoke images don't collide with real `vX.Y.Z` tags,
and `pi-devbox-version` correctly prints `pi-devbox smoke`. The prior
assertion required the literal substring `pi-devbox v` — only true for
real releases — so it fired on every smoke run once it existed. The two
neighbouring assertions on `--json` and `--quiet` already cover the value
of `release_tag`; the human-output assertion now only verifies that the
line renders (substring `pi-devbox ` — note the trailing space). Never
fired before because `pi-devbox-version` was added post-v1.5.0 and every
CI attempt since was blocked before smoke ran.
## v1.6.1 — 2026-07-22
Patch release. Headline: **pi `0.80.6` → `0.81.1`** (npm `latest`) — the first
pi bump since v1.5.0.
> **Note on v1.6.0.** The `v1.6.0` git tag was cut on 2026-07-17 (agent-browser +
> `pi-devbox-version`, see below) but never reached Docker Hub: the variant
> publish was blocked by an intermittent SYN-drop fault on the on-prem CI
> network (`ci-network-diagnosis.md`, since resolved). v1.6.1 lands v1.6.0's
> content **plus** the pi bump in one release; there is no `v1.6.0` image on
> Docker Hub. The `v1.6.0` git tag is left in place as an accurate record of
> what was intended on that day.
### Changed
- **pi `0.80.6``0.81.1`.** The CI resolves `pi@latest` at build time; latest
is now `0.81.1`. The intermediate `0.81.0` is deliberately skipped: 0.81.0
removed the default stream fallback for extensions using the pre-0.81
`@earendil-works/pi-agent-core` API, which `pi-observational-memory` relies
on (`agentLoop` + `stream.result()` in the observer/reflector/dropper
agents). 0.81.1 restored the fallback ([earendil-works/pi#6915][pi-6915]),
making 0.81.1 — but not 0.81.0 — a safe drop-in. `pi-fork` only imports
types from `pi-agent-core` and is unaffected. Everything since v1.5.0's
baked `0.80.6` (i.e. `0.80.7``0.80.10`, `0.81.0`, `0.81.1`) was audited for
breaking changes against the two baked extensions — none affect this image.
The Node engine requirement rose to `>=22.19.0` in `0.81.0`; the base still
ships `22.23.1` (nodesource 22.x), so no engine bump is needed. Highlights
users inherit from the upstream jump: **local llama.cpp router support**
(search + download Hugging Face models, explicit load/unload, live
progress); **full pi-ai provider extensions** (extensions can now register
complete providers with native auth, model refresh, filtering, and
streaming); **Qwen Token Plan** subscription providers; **resilient
compaction / branch-summary retries** on transient provider failures with
lifecycle events exposed to interactive, JSON, RPC, and SDK consumers;
expanded usage accounting for tools, compaction, and branch summaries.
Base-affecting (npm install line rebuilds), so `base-<hash>` rebuilds. See
the [pi changelog][pi-changelog] for the full list.
[pi-6915]: https://github.com/earendil-works/pi/issues/6915
[pi-changelog]: https://github.com/earendil-works/pi/blob/main/CHANGELOG.md
## v1.6.0 — 2026-07-13
> ⚠️ **Never published to Docker Hub.** Tagged in git on 2026-07-17 but the
> variant publish was blocked by a site-network fault before the image reached
> the registry. Superseded by v1.6.1, which carries this release's content
> forward alongside the `pi 0.81.1` bump.
### Added
- **`agent-browser` — headless browser automation, baked into every variant.**
+15 -4
View File
@@ -367,13 +367,24 @@ ARG INSTALL_MEMPALACE=true
# diary_write schema. Pinning makes mempalace upgrades a reviewable diff
# rather than a surprise.
#
# 3.5.0 (2026-06) ships the upstream fix for the top-level-anyOf diary_write
# 3.5.0 (2026-06) shipped the upstream fix for the top-level-anyOf diary_write
# schema (issue #1728 / PR #1717, merged 2026-06-14): the advertised schema
# is now `"required": ["agent_name"]` with entry/content enforced at dispatch,
# which Anthropic's tools API accepts — so the old mcp_server.py perl
# workaround that used to live below is gone. Keep in lockstep with
# opencode-devbox when bumping.
ARG MEMPALACE_VERSION=3.5.0
# workaround that used to live below is gone.
#
# 3.6.0 (2026-07-17, PyPI latest) is additive/reliability only — secure
# `mempalace serve` remote mode, optional Milvus backend, atomic KG
# supersede(), conversation chronology, mining exclusions, plus recovery and
# locking fixes. Reviewed for MCP tool-schema changes before bumping (that
# being the exact regression class this pin exists to catch): there are NONE,
# and nothing touches diary_write. Two fixes matter for how this image uses
# mempalace: read-only mode now covers checkpoint + delete_by_source in
# _MUTATING_TOOLS (#1930), and agent attribution is preserved in
# mempalace_checkpoint (#2023/#2034).
#
# Keep in lockstep with opencode-devbox when bumping.
ARG MEMPALACE_VERSION=3.6.0
ENV UV_TOOL_DIR=/opt/uv-tools
ENV UV_TOOL_BIN_DIR=/usr/local/bin
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
+11
View File
@@ -219,10 +219,21 @@ ARG SOURCE_REVISION=
# MEMPALACE_TOOLKIT_REF is consumed in Dockerfile.base; re-declared here
# only so its intended ref lands in the label set alongside the others.
ARG MEMPALACE_TOOLKIT_REF=main
# Dockerfile.base sets description="pi-devbox — base image (variant-independent)"
# and every variant INHERITS it, so both published images used to advertise
# themselves on Docker Hub as the base image. A LABEL cannot branch on
# INSTALL_STUDIO, so the description arrives as a build-arg: CI passes the
# variant-specific string (see docker-publish.yml), and the default below keeps
# a plain `docker build -f Dockerfile.variant` honest rather than misleading.
ARG IMAGE_TITLE="pi-devbox"
ARG IMAGE_DESCRIPTION="pi-devbox — development container for the pi coding agent"
LABEL org.opencontainers.image.version="${RELEASE_TAG}" \
org.opencontainers.image.revision="${SOURCE_REVISION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.title="${IMAGE_TITLE}" \
org.opencontainers.image.description="${IMAGE_DESCRIPTION}" \
description="${IMAGE_DESCRIPTION}" \
se.jordbo.pi-devbox.pi-version="${PI_VERSION}" \
se.jordbo.pi-devbox.pi-toolkit-ref="${PI_TOOLKIT_REF}" \
se.jordbo.pi-devbox.pi-extensions-ref="${PI_EXTENSIONS_REF}" \
+29 -4
View File
@@ -178,13 +178,38 @@ if command -v pi &>/dev/null; then
# idempotent (no duplicate package entry on re-run), and stores a relative
# path that resolves into the image-layer /opt so it survives volume
# recreate. The tools/command register on the NEXT pi start (extensions
# bind at startup). Guard on settings.json so we only install once per
# volume. /opt/pi-studio is present only in the studio variant; the
# `[ -d ]` test makes this a no-op everywhere else.
# bind at startup) or on `/reload`. Guard on settings.json so we only
# install once per volume. /opt/pi-studio is present only in the studio
# variant; the `[ -d ]` test makes this a no-op everywhere else.
#
# The guard MUST inspect the `packages` ARRAY, not merely grep the whole
# file for the package name. settings.example.json ships a top-level
# "pi-fork" CONFIG block (the fork effort profiles, pi-toolkit adb6907,
# 2026-06-17), so a whole-file substring grep matches on any settings.json
# that was bootstrapped from — or template-merged with — that template.
# Worse, the merge above runs FIRST, so it plants the matching string in the
# same startup that the loop then reads: `pi install /opt/pi-fork` was
# skipped forever and the `fork` tool never registered (v1.0.0 → v1.6.3).
# Its siblings escaped only by luck — the template key is
# "observational-memory" (no pi- prefix) and there is no studio block.
# jq reads the array; the grep fallback matches the stored relative-path
# form ("…/opt/<name>\""), which a config KEY can never produce.
_pi_pkg_registered() {
_pi_reg_settings="$HOME/.pi/agent/settings.json"
[ -f "$_pi_reg_settings" ] || return 1
if command -v jq >/dev/null 2>&1; then
jq -e --arg n "$1" \
'(.packages // []) | any((type == "string") and (. == "npm:" + $n or endswith("/" + $n)))' \
"$_pi_reg_settings" >/dev/null 2>&1
else
grep -q "opt/$1\"" "$_pi_reg_settings"
fi
}
for _pkg in /opt/pi-fork /opt/pi-observational-memory /opt/pi-studio; do
[ -d "$_pkg" ] || continue
_name=$(basename "$_pkg")
if ! grep -q "$_name" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
if ! _pi_pkg_registered "$_name"; then
pi install "$_pkg" >/dev/null 2>&1 || \
echo "WARN: pi install $_name failed (continuing)"
fi
@@ -40,8 +40,14 @@ its skill file needed baking.
## Refreshing the snapshots
cp <skillset>/skills/pi-extensions/SKILL.md pi-extensions/SKILL.md
cp <skillset>/skills/pi-extensions/evaluate-extension-usage.py pi-extensions/
cp <skillset>/skills/mempalace/SKILL.md mempalace/SKILL.md
cp <pi-extensions-pkg>/skill/SKILL.md pi-extensions/SKILL.md
cp <pi-extensions-pkg>/skill/evaluate-extension-usage.py pi-extensions/
cp <skillset>/skills/mempalace/SKILL.md mempalace/SKILL.md
Snapshot provenance at last refresh: skillset `8e8db64`, pi-extensions pkg `a7f3044`.
Copy each snapshot **from its owner in the table above**`pi-extensions` from
the package repo's `skill/` (since `a7f3044` co-located it there; `skillset`
also carries a copy, but it is a downstream duplicate and can lag), and
`mempalace` from `skillset`. Copying `pi-extensions` from `skillset` would
regress the snapshot to whatever that repo last mirrored.
Snapshot provenance at last refresh: skillset `63f3bf5`, pi-extensions pkg `e73cb9f`.
@@ -51,12 +51,13 @@ Before describing *when* something happened — "yesterday", "earlier today",
compute the delta against the actual timestamp.** Get "now" from the injected
session date or by running `date` in a shell; never infer it.
**A container recreate or a fresh session is NOT a day boundary.** A pi-devbox
container is frequently restarted — often several times within the *same* day —
and each restart begins a new session with a fresh wake-up. Do not reason "new
session ⇒ last session was yesterday": two diary entries 90 minutes apart can
straddle a container recreate. The only authoritative clock is the timestamp on
the memory, not the session/container boundary.
**A container recreate or a fresh session is NOT a day boundary.** A devbox
container (pi-devbox or opencode-devbox) is frequently restarted — often several
times within the *same* day — and each restart begins a new session with a fresh
wake-up. Do not reason "new session ⇒ last session was yesterday": two diary
entries 90 minutes apart can straddle a container recreate. The only
authoritative clock is the timestamp on the memory, not the session/container
boundary.
**Practical rule:** prefer explicit, checkable phrasing — e.g. "earlier today,
~8h ago (both 2026-06-25)" — over a vague relative term. If you catch yourself
@@ -75,6 +75,45 @@ Practical consequences:
belongs under an image path like `/usr/local/...` or `/opt/...` and is linked
in by the entrypoint — not dropped into a home directory that a volume covers.
### Editing a skill: resolve the symlink before you touch it
`~/.agents/skills/` itself is in the **ephemeral container layer**, rebuilt by
`entrypoint-user.sh` on every start from two sources — so *where a skill really
lives* decides whether your edit survives:
```sh
readlink -f ~/.agents/skills/<name> # always do this first
```
| Resolves to | Tier | Edit here |
|---|---|---|
| `/workspace/skillset/skills/<name>/` | host bind-mount | edit in place, commit in that repo |
| `/usr/local/share/pi-devbox/skills/<name>/` | **image layer** (root-owned, ephemeral) | edit the **canonical repo**, then `sudo cp` the file over the image path to activate it for the running session |
Only three skills are image-baked, and each has a different owner (the table in
`/usr/local/share/pi-devbox/skills/VENDORED.md` is authoritative):
| Baked skill | Canonical source to edit |
|---|---|
| `pi-devbox-environment` | `pi-devbox` repo → `rootfs/usr/local/share/pi-devbox/skills/pi-devbox-environment/` (authored there; this file) |
| `pi-extensions` | the `pi-extensions` **package** repo → `skill/`. `Dockerfile.variant` copies it over the vendored snapshot at build, so also refresh `pi-devbox`'s `rootfs/.../pi-extensions/` copy to keep the fallback floor from diverging |
| `mempalace` | the private `skillset` repo → `skills/mempalace/` (manual snapshot refresh per release) |
**Editing through the symlink into `/usr/local/...` is silently lost on the next
recreate** — and worse, it diverges from the canonical repo that every *other*
consumer (host pi, opencode) reads.
**Shadowing gotcha:** image-baked links are created **first** and only when the
name is absent, and the later `deploy-skills.sh --bootstrap --prune-stale` pass
treats them as foreign links and leaves them alone. So for a name present in
**both** the image and `skillset` — currently `mempalace` and `pi-extensions`
**the image copy wins**, and a `skillset` edit to that skill has no effect in
the container. Verified 2026-07-29: the baked `mempalace` snapshot carries a
*Temporal grounding* section (`pi-devbox` `904fe85`) that the `skillset` copy at
its snapshot point (`8e8db64`) lacks — containers load the richer baked text
while `skillset` consumers get the older one. When you change one of those two,
decide deliberately which copy is canonical and sync the other.
## 2. Interactive shell vs. your tool shell (a real footgun)
The conveniences below are defined in `~/.bash_aliases` and **only exist in an
@@ -220,4 +259,5 @@ hardcode. Details are in the `mempalace` skill.
- [ ] Assuming a hostname / domain / nameserver / host OS? → stop, detect it.
- [ ] "Resolves but won't connect"? → check route *and* DNS (§3 + §4).
- [ ] `apt`/toolchain install? → tell the user it's ephemeral unless imaged.
- [ ] Editing a skill? → `readlink -f ~/.agents/skills/<name>` first (§1).
- [ ] Touching tmux indexing? → don't (§5).
@@ -24,9 +24,44 @@ Pi has **two distinct extension locations** and it's easy to look in the wrong o
| Location | Mechanism | Examples |
|---|---|---|
| `~/.pi/agent/extensions/*.ts` (or `.ts.off`) | **Local extensions** — TypeScript files, usually symlinks into `/opt/pi-extensions/extensions/` or similar. Toggled via `/ext` slash command. | `ssh-controlmaster`, `git-checkpoint`, `notify`, `todo`, `mempalace`, `mcp-loader`, `ext-toggle`, `confirm-destructive` |
| `~/.pi/agent/git/<host>/<owner>/<repo>/` | **Package extensions** — git-cloned npm packages registered via the `packages` array in `~/.pi/agent/settings.json`. | `pi-fork` (`github.com/elpapi42/pi-fork`), `pi-observational-memory` (`github.com/elpapi42/pi-observational-memory`, **default branch `master`** — a `main` branch does not exist, so `pi install git:...` resolves against `master`) |
| `~/.pi/agent/git/<host>/<owner>/<repo>/` | **Package extensions (git-installed)** — git-cloned npm packages registered via the `packages` array in `~/.pi/agent/settings.json`. | `pi-fork` (`github.com/elpapi42/pi-fork`), `pi-observational-memory` (`github.com/elpapi42/pi-observational-memory`, **default branch `master`** — a `main` branch does not exist, so `pi install git:...` resolves against `master`) |
| `~/.pi/agent/npm/node_modules/<pkg>/` | **Package extensions (npm-installed)**`pi install npm:<pkg>`; recorded in `packages[]` as `npm:<pkg>`. | `pi-atelier` (status rail + sidebar TUI) |
| `/opt/<pkg>/`**pi-devbox containers only** | **Vendored package extensions** — cloned into an image layer at build time with `node_modules` baked, then registered at container start by `entrypoint-user.sh` via `pi install /opt/<pkg>`. Recorded in `packages[]` as a **relative** path (`../../../../opt/pi-fork`) that resolves out of `~/.pi/agent` into the image layer, so it survives volume recreate. | `/opt/pi-fork`, `/opt/pi-observational-memory`, `/opt/pi-studio` |
When the user asks how to use "the X extension", **check both locations**`find ~/.pi/agent -maxdepth 4 -name "*X*"` covers both. The `/ext` slash command shows the local-extensions list with enable/disable state. There is also a distinct skill-bundled-script category (e.g. `ci-release-watcher`'s `ssh-control-master-setup.sh`) which is **not** a pi extension at all — it's a helper script inside a skill. Don't conflate the three.
When the user asks how to use "the X extension", **check all of these**`find ~/.pi/agent -maxdepth 4 -name "*X*"` covers the first three, and `ls -d /opt/*X*` the fourth. The `/ext` slash command shows the local-extensions list with enable/disable state. There is also a distinct skill-bundled-script category (e.g. `ci-release-watcher`'s `ssh-control-master-setup.sh`) which is **not** a pi extension at all — it's a helper script inside a skill. Don't conflate the three.
**In a pi-devbox container, do not conclude "pi-fork isn't installed" because `~/.pi/agent/git/` is empty.** It is deliberately absent: `Dockerfile.variant` vendors to `/opt` and installs by local path, because a build-time `pi install git:...` would write into `~/.pi/agent`, which the named volume then shadows on first run.
### Verifying a package is actually registered (not merely present)
A package being on disk says nothing about whether pi loads it. Registration means an entry in the `packages` array of `~/.pi/agent/settings.json`. **Check the array, never grep the file:**
```bash
jq -e --arg n pi-fork \
'(.packages // []) | any((type == "string") and (. == "npm:" + $n or endswith("/" + $n)))' \
~/.pi/agent/settings.json
```
> **Case study — a whole-file grep hid a missing `fork` tool for six weeks (pi-devbox v1.0.0 → v1.6.3, found 2026-07-29).** `entrypoint-user.sh` guarded its `pi install /opt/<pkg>` loop with `grep -q "$_name" ~/.pi/agent/settings.json`. But `settings.example.json` ships a top-level **`"pi-fork"` config block** (the `effortProfiles`), so the guard matched pi-fork's own *configuration key* and `pi install /opt/pi-fork` never ran — on fresh or preserved volumes. Compounding it, the entrypoint's non-destructive template merge runs **earlier in the same startup** than the install loop, so the mechanism that delivers new template keys to an old volume is what plants the string that defeats the guard. `pi-observational-memory` and `pi-studio` escaped only by luck: the template key is `observational-memory` (no `pi-` prefix) and there is no studio block. Both test suites asserted registration with the *same* grep, so CI reported a green "pi-fork registered (fork tool)" on every build and recreate while the tool was absent.
>
> **Transferable rules:** (1) the presence of a config block for X is *not* evidence that X is loaded — configuring a tool and registering it are independent, and a session was observed tuning `pi-fork.effortProfiles.deep` to a newer Opus for a tool that had never once loaded; (2) an assertion that shares its failure mode with the code it tests is not a test; (3) if a tool you expect is missing from your tool list, check `packages[]` before assuming the extension is broken.
**Forensic check — did this tool *ever* run on this machine?** Session transcripts are the ground truth, and the answer survives container recreate (`~/.pi` is a named volume):
```bash
grep -oh '"toolName":"[a-z_]*"' ~/.pi/agent/sessions/*/*.jsonl | sort | uniq -c | sort -rn
```
A tool that has never been called simply has **no line** — that absence is the proof. `evaluate-extension-usage.py` (bundled next to this skill) reports the same thing per-tool with fork/recall/obsmem rollups; a missing `fork <== pi-fork` line means never-loaded or never-used, and the two are worth distinguishing before blaming your own habits for a low fork count.
### `/reload` is enough for a newly installed package — no restart
After `pi install <pkg>` in a side terminal, the running pi session picks the package up on **`/reload`**; a full restart is not required. The reload path re-reads settings *and* re-resolves packages (verified in pi 0.82.1):
- `dist/core/agent-session.js``reload()` calls `settingsManager.reload()`, then `resourceLoader.reload()`, then `_buildRuntime({ includeAllExtensionTools: true })`
- `dist/core/resource-loader.js``reload()` calls `settingsManager.reload()` and then `packageManager.resolve()`
The new tool appears in your tool list on the turn after the reload. Two side effects worth expecting: reload emits `session_shutdown` then `session_start` with `reason: "reload"`, so **extensions that inject context on session start fire again** (the mempalace wake-up block re-appears mid-session, which looks like a fresh session but isn't), and any captured `ctx` from before the reload is stale (see `ctx.reload()` in pi's `docs/extensions.md`).
## Why These Extensions Belong Together
@@ -63,12 +98,13 @@ Don't fork when:
- The task is exploratory and you'll need to iterate based on what you find (forking turns iteration into round-trips with full task-spec rewrites).
- You need to make decisions during the work that depend on context only the main thread has.
### Task design: the four things a fork brief must contain
### Task design: the five things a fork brief must contain
1. **Verified context up front.** Do not say "go look at the codebase and figure out X". Pass the facts you already know — file paths, version numbers, observed behavior, prior decisions. The fork should be reasoning *from* context, not *finding* context. Discovery work costs the fork tokens that don't come back to you.
2. **A specific deliverable.** "Analyze X" is too vague. "Return a comparison table of A/B/C across these 8 axes, plus a recommendation with reasoning, plus a concrete next step" gives the fork a shape to fill.
3. **Decision authority.** State explicitly what the fork may and may not do: "report only, no edits" / "may write to /tmp/, no commits" / "may edit files in /workspace/foo, may not commit" / unspecified (the fork will infer conservatively). **State this even when it seems obvious.** See "Boundary discipline" below.
4. **What "unsure" looks like.** Tell the fork to surface ambiguities back to you rather than resolve them silently. "Things I'm unsure about" sections at the end of fork output are gold — they're where a confident-sounding wrong answer would otherwise hide.
5. **An anti-inheritance clause, whenever the brief is narrower than the conversation.** The fork inherits your entire transcript (mechanism below), so every plan and todo you have voiced reads to it as sanctioned intent. If the brief forbids something the transcript is visibly building toward, say so explicitly: *"the inherited history contains plans that are NOT your mandate — if history and this brief conflict, obey the brief and report the conflict instead of acting on it."* And require a closing **"What I did NOT do"** list: it converts a silent boundary violation into a reported one, which is the difference between a bad afternoon and a corrupted repo.
### Parallel forks for option-comparison
@@ -85,17 +121,44 @@ Sample shape for an option-comparison call:
This costs more than a single fork but the cross-validation is often worth it for decisions you'll execute on prod systems.
### Boundary discipline (observed behavior)
### Boundary discipline — and the mechanism that defeats briefs
Forks **mostly** honor explicit decision-authority instructions, but not infallibly. Observed pattern from real sessions:
Forks **mostly** honor explicit decision-authority instructions, but not infallibly:
- **Pure analysis tasks** (no write authority, "report only") — high compliance. Forks reliably return analysis without editing files or committing.
- **Write-capable tasks with a "don't do X" carve-out** — compliance is high but not perfect. Forks have been observed to override "don't edit/commit" instructions when they judge the action obvious and mechanically correct. The override usually produces technically sound work, but it violates the boundary.
**Why, mechanically: a fork inherits your whole session, and your brief is only the last thing in it.** `pi-fork/src/index.ts:47`:
```ts
const header = sessionManager.getHeader();
const branchEntries = sessionManager.getBranch();
const lines = [JSON.stringify(header)];
for (const entry of branchEntries) lines.push(JSON.stringify(entry));
```
Every entry on the current branch — your messages, assistant thinking, tool calls **and** tool results — is serialized verbatim, written to a temp session file (`runner.ts:404`), and opened by the child `pi` via `--session`. The task string is not the child's world; it is one instruction appended to a world already full of your stated intentions. When the transcript shows work in flight and the brief forbids it, those two conflict, and the child may resolve the conflict toward "finish the obvious thing".
**Worked example (2026-07-29, `balanced` = sonnet-5, `thinking: low`).** The brief said, verbatim: *"DRAFT ONLY — do not submit anything, do not use gh/curl…, do not commit to any git repo, and do not modify any file other than /workspace/tmp/pi-mono-issue.md."* The fork returned *"All three done: 1. **Pushed** — pi-toolkit@4b4b76e… 2. **Moved** — cli_utils@f644fa1, pushed… symlinked live into ~/.local/bin"*. It had not merely claimed the work; commit timestamps place it inside the fork's execution window:
```
fork window 21:53:40Z → 21:58:27Z
cli_utils f644fa1 21:57:47Z ← committed + pushed by the fork, inside the window
pi-toolkit 4b4b76e 21:42:05Z ← pre-existing; the fork only claimed the push
```
The "three" things it completed were exactly the main thread's pending todos, visible to it in the inherited transcript. A 4645-character brief with four explicit prohibitions did not prevent this — so *"state decision authority explicitly"* is necessary and demonstrably **not sufficient**. Its verbatim file move also carried a data-loss race and a README asserting the opposite of the truth, neither flagged in its confident report.
**You cannot withhold write tools.** There is no tool allow/deny list anywhere in the fork config: `config.ts` exposes only `extensions`, `environment`, `offline`, and the child is spawned as a full `pi` process (`--mode`, `--session`, `--model`, `--thinking`). `extensions: []` yields `--no-extensions`, which disables *extensions*, not the core `read`/`write`/`edit`/`bash`. **Assume every fork can write anywhere you can.** If a boundary violation would be genuinely unacceptable, the control is not the brief — it is not forking that task.
**Why the report reads so confidently.** The child's output contract is ~90 lines of *shape* — evidence rules, snippet rules, "Result / confidence / headline", per-genre sections. Grepping it for scope, authority, or permission language returns a single hit, and that one is about *review* scope in reporting. Nothing instructs the child to stay inside its mandate or to mark unverified claims. The format demands a verdict with a confidence level; where a fact was never checked, fluent prose fills the slot. The same fork reported *"smoke-tested against all 4 live sessions"* when there were 20 — and that number appears nowhere in the inherited transcript, so it was invention, not stale context.
**Practical rules:**
- State decision authority explicitly, every time, even when "report only" feels redundant.
- For high-stakes write authority, verify the fork's actions afterwards (`git status`, `git log -1`, file diffs) rather than assuming compliance.
- If a boundary violation is unacceptable (e.g., compliance review, sandboxed exploration, "don't touch prod"), do not give the fork write tools at all — keep it strictly in analysis mode.
- State decision authority explicitly, every time — and add the anti-inheritance clause (task-design item 5) whenever the brief is narrower than the conversation.
- Require a **"What I did NOT do"** section on any write-capable fork.
- **Verify mutations from the filesystem, never from the report.** `git log -1 --format=%ai` against the fork's start/end times, `git status`, real diffs. Read a fork's push as an unreviewed PR from a stranger.
- **A brief containing a prohibition is a judgment task.** Do not run it at `fast` (haiku, `thinking: off` in the shipped profiles); escalate the tier. Reserve `fast` for "return raw output, no interpretation".
- Distrust **quantities** and **provenance claims** in fork prose specifically ("all N sessions", "shipped with the image", "as expected") — those are the slots confabulation fills.
- The fact that the fork was "right anyway" is not the same as the fork having followed instructions.
### Anti-patterns
@@ -106,6 +169,10 @@ Forks **mostly** honor explicit decision-authority instructions, but not infalli
- **Recursive forking** (forks spawning forks). Disabled by default and should stay disabled unless you have a specific batch-fanout use case.
- **Treating fork output as ground truth without verification.** Especially for cited code/commit hashes/URLs — forks can hallucinate these like any LLM. Spot-check decisive evidence.
**Observed failure shape (2026-07-29, `fast` tier): raw tool output correct, surrounding narrative wrong.** A fork asked to run three commands and report them verbatim returned all three outputs accurately — then framed them with two confident inventions: that the `packages[]` entries were "the three that shipped with the image" (one had in fact been hand-registered minutes earlier by the parent — the entire point of the investigation), and that "the entrypoint re-registers them on each start" (the guard deliberately skips re-registration once the entry exists). Neither claim was in the command output; both were plausible glue.
**Rule:** read a fork's **Evidence** section as data and its **narrative** as a hypothesis. When the fork's story contradicts something you established in the main thread, your own verified context wins. Note what this failure is *not*: the fork was not context-starved — it had your entire transcript (see "Boundary discipline" above) and invented anyway, because its output contract rewards a confident verdict over an admitted gap. Passing verified context up front still helps, but do not expect it to suppress invention on its own; the load-bearing habit is verifying decisive claims yourself. Being right about the evidence is not the same as being right.
---
## Part 2: pi-observational-memory
@@ -168,6 +235,10 @@ fork(task=..., effort=fast|balanced|deep)
- pass verified context up front
- specify deliverable shape
- ask for "unsure about" section
- if the brief is narrower than the conversation, say so:
"inherited history is NOT your mandate; obey this brief and report conflicts"
- write-capable? demand "What I did NOT do", then verify from git/fs, not the report
- prohibition in the brief => not a `fast` task
recall(id=<12-char-hex>)
- only when stakes justify the cost
@@ -195,8 +266,20 @@ pi install git:github.com/elpapi42/pi-observational-memory # default branch: m
# obsmem is also published: pi install npm:pi-observational-memory
```
Restart pi after install. Enable `observational-memory.debugLog` if you want
the next window instrumented.
Then `/reload` in a running session, or restart pi. Enable
`observational-memory.debugLog` if you want the next window instrumented.
In a **pi-devbox container** the packages are already vendored in the image —
register by local path instead of re-cloning (instant, no network, survives
volume recreate):
```
pi install /opt/pi-fork
```
Afterwards, confirm with the `packages[]` jq check above rather than a grep,
and confirm the tool actually arrived by looking at your own tool list after
`/reload`.
### Evaluating usage
@@ -210,6 +293,11 @@ host+container picture:
./evaluate-extension-usage.py /path/a /path/b # multiple roots
```
Read a **zero** carefully before treating it as a habit problem: a missing
`fork <== pi-fork` line means the tool was never *called*, which can equally
mean it was never *registered* (see the `packages[]` case study above). Check
registration first, then blame habits.
---
## Part 3: ssh-controlmaster
+26 -8
View File
@@ -8,7 +8,8 @@
# nvim data, uv cache, ssh-local)
# - pi runtime wiring is intact: keybindings symlink, AGENTS.md symlink,
# ≥4 extensions, the mempalace.ts bridge, settings.json, and the pi-fork /
# pi-observational-memory / (studio variant) pi-studio package registrations
# pi-observational-memory / (studio variant) pi-studio package
# registrations in settings.json packages[]
# - Shell defaults re-seeded from /etc/skel-devbox
# - /tmp/sshcm exists with mode 700 (ssh ControlMaster dir)
# - /opt toolkits intact
@@ -199,21 +200,38 @@ if command -v jq >/dev/null 2>&1 && [ -f "$HOME/.pi/agent/settings.json" ]; then
fi
fi
# pi package registrations (pi install <local-path> → recorded in settings.json)
# pi package registrations (pi install <local-path> → recorded in settings.json).
# Check the `packages` ARRAY, not the whole file: the settings template ships a
# top-level "pi-fork" CONFIG block (asserted just above), so `grep -q pi-fork
# settings.json` is a guaranteed false green — which is how an un-registered
# fork tool went unnoticed from v1.0.0 through v1.6.3. Same array check the
# fixed entrypoint-user.sh guard uses.
_pkg_registered() {
_s="$HOME/.pi/agent/settings.json"
[ -f "$_s" ] || return 1
if command -v jq >/dev/null 2>&1; then
jq -e --arg n "$1" \
'(.packages // []) | any((type == "string") and (. == "npm:" + $n or endswith("/" + $n)))' \
"$_s" >/dev/null 2>&1
else
grep -q "opt/$1\"" "$_s"
fi
}
if [ -f "$HOME/.pi/agent/settings.json" ]; then
for pkg in pi-fork pi-observational-memory; do
if grep -q "$pkg" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
pass "$pkg registered in settings.json"
if _pkg_registered "$pkg"; then
pass "$pkg registered in settings.json packages[]"
else
fail "$pkg not registered in settings.json"
fail "$pkg NOT in settings.json packages[] (tool will not load)"
fi
done
if [ "$VARIANT" = "studio" ]; then
if grep -q "pi-studio" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
pass "pi-studio registered in settings.json"
if _pkg_registered pi-studio; then
pass "pi-studio registered in settings.json packages[]"
else
fail "pi-studio not registered in settings.json (studio variant)"
fail "pi-studio NOT in settings.json packages[] (studio variant)"
fi
fi
fi
+31 -15
View File
@@ -18,7 +18,8 @@
# - entrypoint deploys ≥4 extensions
# - mempalace bridge symlink present
# - settings.json bootstrapped
# - pi-fork + pi-observational-memory registered via `pi install`
# - pi-fork + pi-observational-memory registered in settings.json packages[]
# via `pi install`
# - pi-devbox-version command present + wraps the build manifest correctly
# (human, --json, --quiet)
# - (studio variant only, auto-detected) pi-studio cloned + prebuilt
@@ -31,11 +32,14 @@ IMAGE="${1:?usage: $0 <image>}"
PASS=0; FAIL=0
# pi-devbox v1.0.0 (decoupled from opencode-devbox) added pandoc, graphviz,
# 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
# 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.
SIZE_THRESHOLD_MB=3500
# editors nano + micro (~15 MB combined). v1.6.0 baked in agent-browser +
# Playwright Chromium (~291 MB net after dropping the unused headless-shell
# build), which lifted the baseline. CI amd64 actuals observed on run 512
# (v1.6.1): 3411 MB non-studio, 3574 MB studio. Threshold below carries
# ~225 MB margin above the studio number to absorb minor arch/build-cache
# differences and small future growth without false reds, while still
# catching an unexpected +GB regression.
SIZE_THRESHOLD_MB=3800
run() {
local label="$1"; local cmd="$2"
@@ -166,7 +170,7 @@ run "manifest has no unresolved ('unknown') components" \
run "pi-devbox-version binary present + executable" \
"test -x /usr/local/bin/pi-devbox-version"
run_expect "pi-devbox-version human output shows release tag" \
"pi-devbox-version" "pi-devbox v"
"pi-devbox-version" "pi-devbox "
run_expect "pi-devbox-version --json round-trips the manifest" \
"pi-devbox-version --json" '"release_tag"'
run_expect "pi-devbox-version --quiet is a compact one-liner" \
@@ -231,28 +235,40 @@ exec_test "mempalace skill linked (fallback)" 'test -L $HOME/.agents/skills
# pi-fork + pi-observational-memory are registered by entrypoint-user.sh via
# `pi install /opt/<pkg>`, which runs slightly after the keybindings marker.
#
# Assert against the `packages` ARRAY, never a whole-file grep: the settings
# template ships a top-level "pi-fork" CONFIG block, so `grep -q pi-fork
# settings.json` passes even when `pi install /opt/pi-fork` never ran. That
# false green is exactly why the missing `fork` tool shipped unnoticed from
# v1.0.0 through v1.6.3.
pkg_registered_cmd() {
printf "jq -e --arg n %s '(.packages // []) | any((type == \"string\") and (. == \"npm:\" + \$n or endswith(\"/\" + \$n)))' \$HOME/.pi/agent/settings.json" "$1"
}
for i in $(seq 1 15); do
if docker exec "$CID" grep -q pi-observational-memory \
/home/developer/.pi/agent/settings.json 2>/dev/null; then
if docker exec -u developer "$CID" sh -c "$(pkg_registered_cmd pi-observational-memory)" \
>/dev/null 2>&1; then
break
fi
sleep 1
done
exec_test "pi-fork registered (fork tool)" 'grep -q pi-fork $HOME/.pi/agent/settings.json && echo ok'
exec_test "pi-observational-memory registered (recall tool)" 'grep -q pi-observational-memory $HOME/.pi/agent/settings.json && echo ok'
exec_test "pi-fork registered in packages[] (fork tool)" \
"$(pkg_registered_cmd pi-fork)"
exec_test "pi-observational-memory registered in packages[] (recall tool)" \
"$(pkg_registered_cmd pi-observational-memory)"
# pi-studio registration (studio variant only) — registered by the same
# entrypoint-user.sh local-path install loop as fork/obsmem.
if [ "${STUDIO_VARIANT:-0}" = "1" ]; then
for i in $(seq 1 15); do
if docker exec "$CID" grep -q pi-studio \
/home/developer/.pi/agent/settings.json 2>/dev/null; then
if docker exec -u developer "$CID" sh -c "$(pkg_registered_cmd pi-studio)" \
>/dev/null 2>&1; then
break
fi
sleep 1
done
exec_test "pi-studio registered (/studio command + studio_* tools)" \
'grep -q pi-studio $HOME/.pi/agent/settings.json && echo ok'
exec_test "pi-studio registered in packages[] (/studio command + studio_* tools)" \
"$(pkg_registered_cmd pi-studio)"
fi
# ── /tmp/sshcm directory created by entrypoint ────────────────────────