Compare commits

..

6 Commits

Author SHA1 Message Date
joakimp bf265f6d54 release(v1.11.2): smoke esbuild check present-iff-declared; pi-studio 0.10.1
Lint / hadolint (push) Successful in 8s
Lint / skill-floor (push) Successful in 13s
Lint / doc-drift (push) Successful in 17s
Lint / actionlint (push) Successful in 30s
Publish Docker Image / lint-gate (push) Successful in 26s
Publish Docker Image / resolve-versions (push) Successful in 16s
Publish Docker Image / base-decide (push) Successful in 15s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / smoke (push) Successful in 5m25s
Publish Docker Image / smoke-studio (push) Successful in 8m40s
Publish Docker Image / build-variant (push) Successful in 16m48s
Publish Docker Image / update-description (push) Successful in 8s
Publish Docker Image / promote-base-latest (push) Successful in 9s
Publish Docker Image / build-variant-studio (push) Successful in 21m23s
v1.11.1 (run 724) was tagged and never published: the smoke assertion
"esbuild works at every install site" required esbuild to exist, and pi
1.2.0 dropped @earendil-works/chord, its only source. The image was right.

- smoke-test.sh: esbuild must be present iff an installed package root or
  /opt repo root declares it, and compile at every site. Body tested 8 ways
  (v1.11.0-studio ok; sites removed / natives pruned / residue / bare
  declarer fail; fixture decoy ignored; v1.11.2 candidates ok). Full smoke
  on local arm64 candidates with run 724's args: old script 102/1 (CI
  reproduced), new core 103/0, studio 106/0 (0.9.63 and 0.10.1).
- pi-studio v0.9.63 -> v0.10.1 (tagged 2026-10-14; CI takes the newest
  tag): devbox contract unchanged (manifest, client bundle, 11 tools, 6
  commands, --no-browser/--port/--listen-all, env vars); exercised under pi
  1.2.0 in headless Chromium (render, Run Prompt via ERDC, Files ->
  Document, port freed); in-image TUI starts with no extension issues.
- CHANGELOG v1.11.1 -> v1.11.2; Dockerfile.variant/workflow cycle notes.
  No base input changed: base-6f3b20266de1 (built by run 724) is reused.
2026-10-14 07:28:38 +02:00
joakimp 4efa84bb42 release(v1.11.1): pi 1.2.0; CI builds the pinned pi-obsmem SHA
Lint / hadolint (push) Successful in 9s
Lint / skill-floor (push) Successful in 10s
Lint / doc-drift (push) Successful in 13s
Lint / actionlint (push) Successful in 29s
Publish Docker Image / lint-gate (push) Successful in 25s
Publish Docker Image / resolve-versions (push) Successful in 20s
Publish Docker Image / base-decide (push) Successful in 14s
Publish Docker Image / build-base (push) Successful in 42m31s
Publish Docker Image / smoke (push) Failing after 5m30s
Publish Docker Image / build-variant (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / smoke-studio (push) Failing after 8m38s
Publish Docker Image / build-variant-studio (push) Has been skipped
- pi 1.1.0 -> 1.2.0, audited on tor-ms22 from an isolated prefix: none of
  the four breaking changes reaches the image (3 before_agent_start
  handlers checked, no triggerTurn user, pi-studio themes load); agent-core
  dist byte-identical; turn cap N=19 honoured; atelier paints in both
  renderers; obsmem /om:* registered; real-config startup clean.
- resolve-versions reads ARG PI_OBSMEM_REF from Dockerfile.variant instead
  of resolving obsmem master (which overrode the pin as --build-arg),
  requires a 40-char SHA, and warns when master moves past the pin. Block
  tested in isolation: real pin, short/branch/missing pin, older pin,
  GitHub unreachable.
- CHANGELOG: Unreleased -> v1.11.1 — 2026-10-14. README pin table,
  BASE_REBUILD_DATE marker (base rebuilds anyway), stale VENDORED.md pointer.
2026-10-14 01:17:51 +02:00
joakimp fa38e3e102 CHANGELOG: pi -p skips the palace wake-up and mailbox poll (mempalace-toolkit d5a03f4)
Lint / skill-floor (push) Successful in 8s
Lint / hadolint (push) Successful in 10s
Lint / doc-drift (push) Successful in 18s
Lint / actionlint (push) Successful in 28s
Measured on tor-ms22: 3 palace requests and 1.6 s, against 15 requests
(12 tools/call) with MEMPALACE_WAKEUP_PRINT=1. check-doc-drift.sh: DRIFT
before this entry, rc=0 after.
2026-10-13 15:32:40 +02:00
joakimp 1d53e008d4 CHANGELOG: pi -p skips the palace feed in print/JSON mode (mempalace-toolkit f590a3a)
Lint / skill-floor (push) Successful in 12s
Lint / hadolint (push) Successful in 16s
Lint / actionlint (push) Successful in 20s
Lint / doc-drift (push) Successful in 18s
Measured on tor-ms22: pi -p 2.9 s, --mode json 3.8 s (45 s with only the
timer fix 2f0edf5, 315 s before it). MEMPALACE_FEED_PRINT=1 restores the
feed. check-doc-drift.sh: DRIFT before this entry, rc=0 after.
2026-10-13 14:52:13 +02:00
joakimp 28e324d68f CHANGELOG: pi -p no longer idles 5 min after answering (mempalace-toolkit 2f0edf5)
Lint / hadolint (push) Successful in 10s
Lint / skill-floor (push) Successful in 13s
Lint / actionlint (push) Successful in 19s
Lint / doc-drift (push) Successful in 19s
The bridge's feed raced its mine against a setTimeout it never cleared, and
print mode waits for an empty event loop. Measured on tor-ms22: exit at 315 s
before the fix, 45 s after (the catch-up feed by design), 3.8 s with
MEMPALACE_FEED=0. check-doc-drift.sh: rc=0, and rc=1 with the SHA removed.
2026-10-13 13:56:50 +02:00
joakimp 3da7312166 MCP via pi's built-in support: mcp-loader retired upstream; skill floor, docs, sanity check
Lint / skill-floor (push) Successful in 11s
Lint / hadolint (push) Successful in 15s
Lint / actionlint (push) Successful in 20s
Lint / doc-drift (push) Successful in 17s
pi-extensions 750b2a8 retired mcp-loader.ts in favour of builtin:mcp (an
extension registering /mcp replaced the built-in and printed a warning on
every start). This repo has no pin to move, since PI_EXTENSIONS_REF and
MEMPALACE_TOOLKIT_REF float on main, so this commit carries what the next
build changes:

- CHANGELOG Unreleased: component table (pi-extensions a88fa99 -> 750b2a8,
  mempalace-toolkit 975ab92 -> a30af61), the switch and its measurements, the
  stale-link pruning, the new sanity check, and the floor refresh.
  check-doc-drift.sh rc=0. A two-sided check: with 750b2a8 blanked out of the
  CHANGELOG it fails rc=1, naming pi-extensions a88fa99 -> 750b2a8.
- rootfs pi-extensions skill floor refreshed to 750b2a8. check-skill-floor.sh
  was rc=1 before and is rc=0 after. Base hash changes, so the next tag
  rebuilds the base.
- recreate-sanity-check.sh: "no dangling extension links" (WARN). The
  existing count includes dangling links because ls lists them. Tested
  two-sided in a temp HOME (PASS clean; WARN naming mcp-loader.ts and x.ts.off).
  On tor-ms22 v1.11.0: rc=0, 36 pass / 3 warn / 0 fail (was 35/3/0).
- DOCKER_HUB.md: pi-extensions list was three releases stale; it now lists the
  10 that ship, plus an MCP line. Dockerfile.variant comment updated.
- lint-shell.sh: 19 files clean at severity error.
2026-10-13 07:42:23 +02:00
10 changed files with 357 additions and 25 deletions
+35 -9
View File
@@ -9,9 +9,13 @@ name: Publish Docker Image
# Pipeline shape:
# 1. base-decide compute base hash from Dockerfile.base + rootfs/
# + entrypoints; probe Docker Hub for existing tag.
# 2. resolve-versions resolve pi @ npm 'latest', pi-fork/pi-obsmem refs
# to commit SHAs (defeats registry-buildcache
# cache-hit footgun on byte-identical build args).
# 2. resolve-versions pi and mempalace from their pins (checked on npm/
# PyPI); pi-obsmem and pi-atelier from their pins in
# Dockerfile.variant; pi-fork and the Gitea repos to
# their branch heads, pi-studio to its newest tag;
# all refs to commit SHAs (defeats
# registry-buildcache cache-hit footgun on
# byte-identical build args).
# 3. build-base only if probe missed; multi-arch push of base-<hash>.
# 4. smoke amd64-only build of the variant FROMing the base
# tag; runs scripts/smoke-test.sh.
@@ -186,7 +190,7 @@ jobs:
# gap -- and for it the gap is strictly worse. Shellcheck judges the TREE:
# green on main is still green at the tag, because the bytes did not move.
# Check 9 judges the tree against UPSTREAM NOW, and the floating refs it
# watches (PI_OBSMEM_REF=master and friends, which resolve-versions below turns
# watches (PI_FORK_REF=master and friends, which resolve-versions below turns
# into SHAs) move with no commit in this repo at all -- so a green reading on
# main carries no information about tag time, and that window is exactly where
# releases live. Worked example: pi-observational-memory moved cba0334 ->
@@ -386,15 +390,37 @@ jobs:
fi
echo "mempalace_version=${MEMPALACE_VERSION}" >> "$GITHUB_OUTPUT"
# pi-fork / pi-observational-memory (GitHub) → commit SHAs.
# pi-fork (GitHub) → master's commit SHA. Floats by design.
FORK_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
"https://api.github.com/repos/elpapi42/pi-fork/commits/master" || true)
require_sha PI_FORK_REF "$FORK_REF"
OBSMEM_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
"https://api.github.com/repos/elpapi42/pi-observational-memory/commits/master" || true)
require_sha PI_OBSMEM_REF "$OBSMEM_REF"
echo "fork_ref=${FORK_REF}" >> "$GITHUB_OUTPUT"
# pi-observational-memory → the PINNED SHA in Dockerfile.variant.
# Until v1.11.2 this resolved `master` and passed it as --build-arg,
# which OVERRIDES the ARG default: the pin bound only local builds,
# and CI baked it only while master == pin (v1.9.5, v1.10.2, v1.11.0).
# The pin is a master SHA, not a tag, on purpose: obsmem tags rarely
# and lands what pi needs on master (PR #83, pi 0.87's finishTurn,
# never reached a tag). Its peerDependencies are `*`, so a mismatch
# with pi is silent at runtime. Adopting master is therefore a
# deliberate one-line bump after audit, never a side effect of the day
# CI ran. Read like atelier's tag, and require the full 40 chars: a
# short SHA would also downgrade check-doc-drift.sh's check to a SKIP.
OBSMEM_REF=$(sed -n 's/^ARG PI_OBSMEM_REF=\([^[:space:]]*\).*/\1/p' Dockerfile.variant | head -n1)
if ! printf '%s' "${OBSMEM_REF:-}" | grep -qiE '^[0-9a-f]{40}$'; then
echo "::error::ARG PI_OBSMEM_REF in Dockerfile.variant is not a full 40-char commit SHA (got '${OBSMEM_REF:-<empty>}'). pi-observational-memory must stay pinned to an audited commit — see the note above that ARG."
exit 1
fi
echo "obsmem_ref=${OBSMEM_REF}" >> "$GITHUB_OUTPUT"
# Informational only, like pi's npm drift warning: master moving past
# the pin is how a fix pi needs becomes visible. `|| true` so a GitHub
# hiccup cannot fail a release whose pin is already in the repo.
OBSMEM_MASTER=$(curl -sf -H "Accept: application/vnd.github.sha" \
"https://api.github.com/repos/elpapi42/pi-observational-memory/commits/master" || true)
if printf '%s' "${OBSMEM_MASTER:-}" | grep -qiE '^[0-9a-f]{40}$' && [ "${OBSMEM_MASTER}" != "${OBSMEM_REF}" ]; then
echo "::warning::pi-observational-memory master is ${OBSMEM_MASTER:0:7}; this build ships the audited pin ${OBSMEM_REF:0:7}. To adopt it: read https://github.com/elpapi42/pi-observational-memory/compare/${OBSMEM_REF}...${OBSMEM_MASTER} (the observer/reflector/dropper workers and their pi API use), re-run the turn-cap probe against the pinned pi, then bump ARG PI_OBSMEM_REF in Dockerfile.variant and name the SHA in CHANGELOG.md."
fi
# pi-atelier → the PINNED TAG's commit SHA. Unlike fork/obsmem
# (which track a branch head) atelier wraps pi's private TUI
@@ -483,7 +509,7 @@ jobs:
echo "Resolved PI_VERSION=${PI_VERSION} (pinned in Dockerfile.variant; npm latest is ${PI_NPM_LATEST:-unknown})"
echo "Resolved MEMPALACE_VERSION=${MEMPALACE_VERSION} (pinned in Dockerfile.base; PyPI latest is ${MEMPALACE_PYPI_LATEST:-unknown})"
echo "Resolved PI_ATELIER_REF=${ATELIER_REF} (pi-atelier ${ATELIER_TAG}, pinned)"
echo "Resolved PI_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_REF}"
echo "Resolved PI_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_REF} (pinned; master is ${OBSMEM_MASTER:-unknown})"
echo "Resolved PI_TOOLKIT_REF=${TOOLKIT_REF}, PI_EXTENSIONS_REF=${EXTENSIONS_REF}"
echo "Resolved PI_STUDIO_REF=${STUDIO_REF} (pi-studio ${STUDIO_TAG})"
echo "Resolved MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}"
+215
View File
@@ -11,6 +11,221 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
---
## v1.11.2 — 2026-10-14
**v1.11.1 was tagged and never published.** Run 724 failed one smoke assertion
in each variant (`smoke` 102 passed / **1 failed**, `smoke-studio` 105 / **1**):
*esbuild works at every install site* reported "no esbuild install found at
all". The image was right and the assertion was wrong. esbuild was never a
direct pi dependency. It came through `@earendil-works/chord` (pi 1.1.0 → chord
→ esbuild 0.28.2, at the pi and pi-fork sites), and pi 1.2.0 dropped chord, so
a correct 1.2.0 image has no esbuild. pi 1.2.0's tree has no esbuild import;
1.1.0's chord bundle had one. This is the mistake the clipboard assertion made
at v1.10.0. Every publish job skipped, so no `v1.11.1`, `latest` or Hub
description was written. The one artefact pushed is the base `base-6f3b20266de1`.
It is content-addressed and this release reuses it, which is why
`Dockerfile.base`'s `BASE_REBUILD_DATE` comment still names v1.11.1: editing it
would change the hash and rebuild an identical base. **Everything below ships
here, with the assertion fixed** (*Fixed*, first entry). The tag moves rather
than being re-pointed, as at v1.10.2. One component moved between the two tags:
pi-studio published **v0.10.0 and v0.10.1** the same morning, CI builds the
newest pi-studio tag, and this release adopts 0.10.1 after testing it (*Changed*
below) rather than pinning 0.9.63.
Patch release. Headlines: **pi 1.2.0**, **pi-studio 0.10.1** (two editors, a
Prompt and a Document, by default), **`pi -p` returns in seconds** on a
devbox instead of idling for five minutes (mempalace-toolkit), **MCP servers use
pi's built-in support** (pi-extensions' `mcp-loader.ts` is retired), and **CI
now builds the pinned pi-observational-memory commit** instead of whatever its
`master` is that day. Unchanged upstream, re-checked 2026-10-14:
pi-observational-memory `022da5e`, pi-atelier `v0.15.0`, pi-fork `e69725c`, pi-toolkit `3760d90`, mempalace `3.10.0`, Node 24 (v24.21.0
LTS). The skillset moved (`96b840e` → `1e02b44`), but the vendored mempalace
`SKILL.md` is byte-identical, so the snapshot is not refreshed. The base image is
rebuilt (the rootfs skill floor and the mempalace-toolkit ref both changed), so
the base tools pinned to `latest` re-resolve too.
### Components that move with this release
| Component | Baked in v1.11.0 | v1.11.2 | Why |
|---|---|---|---|
| **pi** | `1.1.0` | **`1.2.0`** | audited, see *Changed* below |
| **pi-studio** (studio variant) | `v0.9.63` (`fae3ad3`) | **`v0.10.1`** (`76db01f`) | tagged 2026-10-14, tested under pi 1.2.0, see *Changed* below |
| pi-extensions | `a88fa99` | **`750b2a8`** (or later: CI resolves `main` at build time) | `mcp-loader.ts` retired; `install.sh` prunes stale links (below) |
| mempalace-toolkit | `975ab92` | **`d5a03f4`** (or later) | `pi -p` returns in seconds: `2f0edf5` clears the feed's leftover 5-min timer, `f590a3a` skips the feed in print/JSON mode, and `d5a03f4` skips the wake-up and mailbox poll there too (below). Also `a30af61`: the bridge's `RemoteMcpClient` is now the only copy, and `scripts/check-mcp-client-sync.sh` is removed (comment/README only) |
### Fixed
- **The smoke test's esbuild assertion is now "present iff something declares
it"**, like clipboard's. Because esbuild arrives transitively, "declared" is
not read from pi's `package.json`. It is derived from every installed package
root and every `/opt` repo root, counting dependencies, optional dependencies
and peer dependencies. Anchoring on package roots means a test fixture's
`package.json` cannot count. Both directions fail: declared but missing (the
prune removed function), and installed with nothing declaring it (residue).
The new body was run before tagging. On published v1.11.0-studio it reports
"declared (2 package(s)) and compiles at 2 site(s)". With every esbuild dir
removed it fails, naming both chord `package.json` files. With
`@esbuild/<platform>` pruned it fails with "esbuild broken at …". On the new
images it reports that nothing declares esbuild. It also fails a fake esbuild
dir that nothing declares, and a package root that declares esbuild with no
install, but ignores a test fixture's `package.json`. The whole
`smoke-test.sh` then ran against arm64 candidates, built locally with run
724's resolved build args on the same base. The old script reproduced CI's
102/1 on core. The new one passed core 103/0 and studio 106/0, with pi-studio
0.10.1 and with 0.9.63.
- **The pi-observational-memory pin now binds CI builds** (v1.11.0's known
issue). `resolve-versions` resolved the repo's `master` and passed it as
`--build-arg PI_OBSMEM_REF`, which overrides the Dockerfile's pinned default,
so CI baked the pin only while `master` happened to equal it (v1.9.5, v1.10.2,
v1.11.0). It now reads the pin from `Dockerfile.variant`, the way pi-atelier's
tag is read. It refuses anything but a full 40-char SHA, and prints a
`::warning::` with the compare link when `master` has moved past the pin. The
pin stays an untagged `master` commit on purpose. Upstream tags rarely (the
newest tag is still 3.1.4) and lands what pi needs on `master`: #83, the
migration to pi 0.87's `finishTurn`, never reached a tag. Its
`peerDependencies` are `*`, so a mismatch with pi fails silently at runtime.
Adopting a newer `master` is now a deliberate one-line bump, after the
turn-cap probe. The new step was run on its own against six inputs: the real
pin gives `022da5e` and no warning; a short SHA, `master` or a missing ARG
fails; an older pin gives that SHA plus the warning; GitHub unreachable gives
the pin and no warning. The workflow's pipeline comment, which still said pi
resolves from npm `latest`, is corrected too.
- **`pi -p` on a devbox no longer sits idle for five minutes after
answering.** It printed its answer, then held the terminal until a timer
ran out: the feed's 300 s mine deadline (mempalace-toolkit `mempalace.ts`).
The bridge raced its transcript mine against a `setTimeout` it never cleared.
Interactive pi calls `process.exit()` and never showed it, but print mode
(`pi -p`, `--mode json`) waits for an empty event loop. Measured on tor-ms22
(pi 1.1.0): before the fix, `PONG` came at 4 s and exit at 315 s, and that
timer was the only live resource for the last 265 s. After `2f0edf5`, exit
is at 45 s, the moment the session's catch-up feed finishes (prepare 11 s,
mine 31 s). `f590a3a` then makes print and JSON mode skip that feed by
default: `pi -p` takes 2.9 s and `--mode json` 3.8 s. Nothing is lost. A
one-shot run's own transcript rarely passes the exporter's quality gate, and
the next interactive session files the device's other sessions the same
way. The mine skips an unchanged transcript and purges a grown one before
refiling, so nothing is filed twice. `MEMPALACE_FEED_PRINT=1` restores the
feed for a device that only runs `pi -p`. pi 1.0.0 behaved the same, so it
predates v1.11.0. `pi-task` and `fork` children run `--no-extensions` and
were never affected. `scripts/test-feed-exit.sh` in mempalace-toolkit pins
both changes, and each change's pre-fix source fails it. On images before
v1.11.2, scripts that call `pi -p` should keep their `timeout` or set
`MEMPALACE_FEED=0`.
- **`pi -p` no longer gets the palace wake-up.** In print and JSON mode the
mempalace bridge now skips the wake-up injection and the mid-session mailbox
poll (mempalace-toolkit `d5a03f4`). The injection is status, diary, and the
owed/closed mailbox lists. The `mempalace_*` tools are still registered.
Measured on tor-ms22: `pi -p` makes 3 palace requests (the MCP handshake)
and exits in 1.6 s. With the wake-up it made 15 requests, 12 of them
`tools/call`. The wake-up is for the person at an interactive session, who
sees it in the next one. In a script it is cost, and once it was harm: a
probe's flash model read the injected diary's open items as its task, ran
about 12 tool calls, and filed a wrong drawer in the shared palace.
`MEMPALACE_WAKEUP_PRINT=1` restores the wake-up for `pi -p`.
### Changed
- **pi `1.1.0` → `1.2.0`.** Audited 2026-10-14 on tor-ms22 from an isolated npm
prefix, by the same method as v1.11.0. 1.2.0 has four breaking changes, and
none reaches this image:
- `before_agent_start` also fires for runs started by
`pi.sendMessage(…, { triggerTurn: true })`, and its event is a union on
`messageType`. `systemPrompt` is on both members. Of the three handlers in
the image, ssh-controlmaster reads only `systemPrompt`, and aws-sso and
mempalace read no event field. Nothing in the image uses `triggerTurn`.
- The session counts as busy while a run starts, so `pi.sendUserMessage()`
then needs `deliverAs`. aws-sso already passes it. mempalace's
`/mempalace-diary` is a command, so it runs while pi is idle.
- Themes are schema-validated at startup, and the schema moved to `pi.dev`.
pi-studio's themes still name the old URL, but they load and apply on 1.2.0
with no warning.
- `pi mcp list --json` reports `resources: true` instead of counts. Nothing
here parses it.
Measured on 1.2.0. pi-agent-core's dist is byte-identical to 1.1.0's. The
turn-cap probe ran 19 turns uncapped and stopped at exactly 3 with
`maxTurns=3`. atelier paints in both renderers, with controls at 0/3.
pi-observational-memory registers `/om:status` and `/om:view`. A copy of
tor-ms22's real `~/.pi/agent` starts with the same extension list as on 1.1.0
and no extension issues. `pi -p` answers in 1.0 s through ERDC and 5.2 s
through Bedrock. One change reaches scripts: `pi -p` now exits 1 when the
reply was cut off at the model's output limit. That is text mode only;
`pi-task` and `fork` run `--mode json`. The first start after the upgrade
shows 1.2.0's What's New once. Read from the code, not reproduced: pi-studio
(0.9.63 and 0.10.1 alike) marks pi busy on `agent_start`, which 1.2.0 fires
after the session already counts as busy. A request that reaches pi in that
gap (the `before_agent_start` handlers) gets pi's "Agent is already
processing" error instead of Studio's own "pi is busy". Before 1.2.0 the same
request started a second run that failed, so retrying is the answer either
way. From the browser it is hard to reach: three **Run Prompt** clicks 30 ms
and 150 ms apart produced exactly one run in 0.10.1.
- **pi-studio `v0.9.63` → `v0.10.1`** (studio variant). 0.10.0 is the largest
Studio change in a while. By default there are two editors: a **Prompt** for
what you send to pi and a **Document** for the file you are working on. You
can switch with `/studio --single` / `--dual` or from the footer. Unsaved text
survives reloads, and `/studio` reopens your last workspace (`--blank`,
`--last`); its state lives under `~/.pi/agent/pi-studio`, on the preserved
volume. There is a new **Files** pane, a two-row toolbar, and **Add to
Prompt**. 0.10.1 fixes the Run button flickering while a run starts. Upstream
typechecks against pi 1.2.0; its peers stay `*`.
- **What pi-devbox relies on is unchanged:** the package manifest
(`./index.ts`, `./themes`), the committed `client/studio-client.js`, the 11
`studio_*` tools, the 6 commands, and the `--no-browser`, `--port` and
`--listen-all` flags that `studio-expose` and the README use. The set of
environment variables read is identical.
- **Tested under pi 1.2.0 before tagging**, in a copy of tor-ms22's real
`~/.pi/agent` with 0.10.1 swapped in:
- pi starts with no extension issues, and `/studio --no-browser --port
18765` prints its token URL.
- In headless Chromium the new UI renders with no console errors.
- A prompt typed into the Prompt and sent with **Run Prompt** came back
through ERDC in both Studio and the terminal.
- The Files pane lists the workspace, and **Open in Document** loads a file.
- Quitting pi frees the port.
- **In the image:** the 0.10.1 studio candidate passes the full smoke test,
106/0. It is 11 MB smaller than with 0.9.63, because the npm package shrank.
Inside it, pi's TUI starts with pi-studio among the extensions and no
extension issues.
- **To go back**, if 0.10 does not suit: v1.11.0-studio still has 0.9.63.
Inside a container, `/studio --single` restores the one-editor layout.
- **MCP servers now use pi's built-in MCP support; pi-extensions'
`mcp-loader.ts` is retired.** pi has shipped MCP as a built-in extension
(`builtin:mcp`) since 0.99.0: servers go in `~/.pi/agent/mcp.json` or are
added with `pi mcp add`, and `/mcp` manages them (tools, reconnect, OAuth
sign-in, enable/disable, exposure). Because an extension that registers
`/mcp` *replaces* the built-in, every start on v1.11.0 printed
`Extension … mcp-loader.ts registers command /mcp, so built-in extension mcp
was not loaded`, and `mcp.json` was never read. Measured on tor-ms22
(v1.11.0, pi 1.1.0) before the switch: time to ready, 7 interleaved pty runs
each, was 0.276 s with no MCP, 0.283 s with the loader and 0.272 s with the
built-in. A prompt cost the same tokens with either and with neither, and a
stdio server (`server-everything`) connected and answered a tool call
through the built-in. No device had servers in the loader's `mcp` block, so
nothing needs migrating. If one does, pi-extensions' README ("MCP servers")
shows how to move it to `mcp.json`. Tools from `mcp.json` default to
`codemode` exposure; set `"exposure": "direct"` to declare them to the model.
MemPalace is unaffected: `mempalace.ts` registers `mempalace_*` itself.
- **A preserved volume drops the stale link by itself.** `~/.pi/agent/` survives
a recreate, so the old `extensions/mcp-loader.ts ->
/opt/pi-extensions/extensions/mcp-loader.ts` link would outlive the file. The
entrypoint's pi-extensions `install.sh --yes` now removes links into its own
repo whose target is gone (`.ts` and `/ext`-disabled `.ts.off`). It warns once
if `settings.json` still has an `mcp` block.
- **`recreate-sanity-check.sh`: one new check, "no dangling extension links".**
The existing "N extensions deployed" line counts a dangling link as deployed
(`ls` lists it), so it cannot see this case. The new check WARNs instead of
FAILing, because whatever it still finds points outside pi-extensions, which
`install.sh` leaves alone. Expect one more ✓ than v1.11.0 printed.
- **Vendored `pi-extensions` skill floor refreshed** to pi-extensions `750b2a8`.
`check-skill-floor.sh` went red on the retirement commit and is green again.
The skill now says MCP servers are the built-in's, not an extension. Because
`rootfs/` is in the base hash, this release rebuilds the base.
- DOCKER_HUB.md's pi-extensions list was three releases stale (`mcp-loader`
listed; `task`, `fork-gate`, `secret-guard`, `aws-sso` missing). It now lists
the 10 that ship and has an MCP line. The vendored skills' `VENDORED.md`
pointed at "the Unreleased CHANGELOG entry", which had been stale since
v1.8.8; it now names v1.8.8.
## v1.11.0 — 2026-10-12
Minor release. Headlines: **pi 1.1.0**, **secret env values are kept out of
+2 -1
View File
@@ -49,7 +49,8 @@ Full setup guide — authentication for each provider (Anthropic, OpenAI, Gemini
- **pi `{{PI_VERSION}}`** ([`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)) — installed at `/usr/bin/pi`, pinned to an audited version (not npm `latest`)
- **pi-atelier** — TUI sidebar (ordered panels, split-pane, themes), vendored at `/opt/pi-atelier` and pinned to an audited tag; the exact tag is in the image labels (`se.jordbo.pi-devbox.pi-atelier-version`) and `/etc/pi-devbox/build-manifest.json`
- **[pi-toolkit](https://gitea.jordbo.se/joakimp/pi-toolkit)** — keybindings (mosh/tmux-friendly Shift+Enter, Ctrl+J, Alt+J newline bindings), AWS env loader, settings template
- **[pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions)** — 7 user-facing extensions: `ext-toggle`, `mcp-loader`, `todo`, `ssh-controlmaster`, `notify`, `git-checkpoint`, `confirm-destructive`
- **[pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions)** — 10 user-facing extensions: `ext-toggle`, `todo`, `task`, `fork-gate`, `secret-guard`, `aws-sso`, `ssh-controlmaster`, `notify`, `git-checkpoint`, `confirm-destructive`
- **MCP servers** — pi's built-in MCP support: declare servers in `~/.pi/agent/mcp.json` (or `pi mcp add`), manage them with `/mcp`
- **`fork`** ([pi-fork](https://github.com/elpapi42/pi-fork)) and **`recall`** ([pi-observational-memory](https://github.com/elpapi42/pi-observational-memory)) tools
- **mempalace bridge** — MCP extension auto-symlinked so pi reads/writes the host-mounted palace
- **image-baked agent skills** — skills under `/usr/local/share/pi-devbox/skills/` (e.g. `pi-devbox-environment`, which teaches agents the container's persistence/networking/DNS/tmux/REPL specifics) are symlinked into `~/.agents/skills/` on start, available with or without a mounted skillset repo
+1 -1
View File
@@ -14,7 +14,7 @@
# content-addressed over this file, so any byte change invalidates the
# cache. Recommended cadence: once per release for security updates.
#
# BASE_REBUILD_DATE: 2026-10-12 (v1.11.0 — rootfs gains link-pi-models.sh, entrypoint-user.sh links models.json; base tools re-resolve `latest` (upstream on 2026-10-12: uv 0.13.0, nvim 0.12.6, fzf 0.74.5); previous marker 2026-09-22 / v1.9.4)
# BASE_REBUILD_DATE: 2026-10-14 (v1.11.1 — rootfs skill floor to pi-extensions 750b2a8, mempalace-toolkit d5a03f4; apt and base tools re-resolve (upstream unchanged since 2026-10-12: uv 0.13.0, nvim 0.12.6, fzf 0.74.5); previous marker 2026-10-12 / v1.11.0)
#
# ── Lineage note ─────────────────────────────────────────────────────
# Adapted from opencode-devbox/Dockerfile.base (commit before v1.16.2).
+44 -7
View File
@@ -212,9 +212,11 @@ ARG USER_NAME=developer
# `exposure` is `codemode`, i.e. such tools are "neither declared to the model
# nor listed" and must be found with `searchTools()`. That would gut the
# MemPalace protocol if MemPalace were a builtin-MCP server. It is not: both
# mempalace.ts and mcp-loader.ts run their OWN MCP client and surface tools via
# mempalace.ts runs its OWN MCP client and surfaces tools via
# `pi.registerTool()`, which is why they are named `mempalace_search` and not
# `mcp__mempalace__search`. Confirmed on a second route — settings.json has
# `mcp__mempalace__search`. (So did pi-extensions' mcp-loader.ts until it was
# retired after v1.11.0 in favour of builtin:mcp. Servers in ~/.pi/agent/mcp.json
# ARE exposure-governed: they need `"exposure": "direct"` to be declared.) Confirmed on a second route — settings.json has
# neither an `mcpServers` block (builtin, exposure-governed) nor an `mcp` block.
# Extension-registered tools are declared like built-ins, so `exposure` cannot
# reach them.
@@ -258,7 +260,36 @@ ARG USER_NAME=developer
# ACTIVITY/READY/TOOLS in BOTH renderers (controls 0/3, every arm render-proofed);
# obsmem 022da5e registers /om:status and /om:view (control: none); and all 11
# pi-extensions a88fa99 extensions load with no error (control: no /aws-sso).
ARG PI_VERSION=1.1.0
#
# v1.11.2: 1.1.0 -> 1.2.0 (npm latest, 2026-10-12). MEASURED 2026-10-14 on
# tor-ms22 from an isolated npm prefix (nothing in /usr or ~/.pi touched):
# 1. FOUR `### Breaking Changes` in 1.2.0, none of which reaches us:
# - before_agent_start also fires for sendMessage(triggerTurn) runs and is
# a union on `messageType`; `systemPrompt` is on the shared base. Our 3
# handlers: ssh-controlmaster reads only systemPrompt, aws-sso and
# mempalace read no event field. Nothing in the stack uses triggerTurn.
# - the session counts as busy while a run starts (isIdle() false,
# sendUserMessage needs deliverAs). aws-sso already passes deliverAs;
# mempalace's /mempalace-diary is a command and runs while idle.
# - themes are schema-validated at startup and the schema moved to
# pi.dev. pi-studio's themes still name the old pi-mono $schema URL, but
# `--use-theme pi-studio-dark` applies (palette differs from the
# default) and prints no warning.
# - `pi mcp list --json` reports `resources: true`. Nothing parses it.
# 2. pi-agent-core 1.1.0 -> 1.2.0: dist is BYTE-IDENTICAL (24 files, same
# tree hash). The turn-cap probe drove the real 1.2.0 agentLoop: uncapped
# ran its chosen 19 turns, maxTurns=3 stopped at exactly 3 + agent_end.
# 3. 1.2.0's `-p` now exits 1 on a reply cut off at the output limit, in TEXT
# mode only (print-mode.js). pi-task and pi-fork run `--mode json`.
# 4. engines.node is still `>=22.19.0`; the image ships 24.x. Peer ranges:
# atelier >=0.84.0, fork >=0.75.0, obsmem and studio `*`.
# Exercised before tagging, in a real pty with -ne controls: atelier v0.15.0
# paints ACTIVITY/READY/TOOLS in BOTH renderers (controls 0/3, every arm
# render-proofed); obsmem 022da5e registers /om:status and /om:view (control:
# none); a copy of tor-ms22's real ~/.pi/agent (all packages, pi-extensions
# 750b2a8) starts with the same [Extensions] list as on 1.1.0 and no
# [Extension issues]; `pi -p` answers PONG via ERDC in 1.0 s, Bedrock in 5.2 s.
ARG PI_VERSION=1.2.0
ARG PI_TOOLKIT_REF=main
ARG PI_EXTENSIONS_REF=main
# Repo URLs default to the canonical gitea origin but are overridable so a
@@ -287,10 +318,14 @@ ARG PI_OBSMEM_REPO=https://github.com/elpapi42/pi-observational-memory.git
# dist/index.ts instead of 32 TS sources. The bundle is committed, so
# `npm install --omit=dev` below needs no build step. src/agents/ is
# UNCHANGED between the two SHAs, so the finishTurn migration is intact.
# KNOWN GAP, not fixed here: docker-publish.yml's resolve-versions still
# resolves obsmem from `master` and passes it as --build-arg, which overrides
# this default (verified two-sided on buildx 2026-10-12). CI builds therefore
# get the pin only while master == pin, as it is at this release.
# v1.11.2: unchanged (master is still 022da5e, 6 commits past the 3.1.4 tag).
# CI NOW BUILDS THIS PIN. Until v1.11.2 docker-publish.yml's resolve-versions
# resolved obsmem from `master` and passed it as --build-arg, which overrides
# this default (verified two-sided on buildx 2026-10-12), so CI got the pin only
# while master == pin. resolve-versions now reads this line, refuses anything
# but a 40-char SHA, and WARNS when master has moved past it. obsmem tags
# rarely and lands what pi needs on master (PR #83 never reached a tag), so
# adopting master is still the norm: bump this SHA, deliberately, after audit.
ARG PI_OBSMEM_REF=022da5e5554d04dfdc876ed7dac4a42c6b83dfd4
# pi-atelier (TUI sidebar: ordered panels, split-pane, themes) is PINNED TO A
# TAG, which CI resolves to that tag's commit SHA — same treatment as
@@ -379,6 +414,8 @@ ARG PI_ATELIER_REPO=https://github.com/michaelmjhhhh/pi-atelier.git
# panels and simplifies the Activity rows. The startup preference is still
# `showSidebarOnStartup` (default true, src/config.ts:38). Painted in both
# renderers on pi 1.1.0 before tagging (see the PI_VERSION note).
# v1.11.2: unchanged (still the newest tag); painted in both renderers on
# pi 1.2.0 before tagging.
ARG PI_ATELIER_REF=v0.15.0
# Human-readable tag PI_ATELIER_REF was resolved from; recorded as a label.
ARG PI_ATELIER_VERSION=v0.15.0
+2 -2
View File
@@ -1198,7 +1198,7 @@ persisted volumes survived, and pi runtime wiring is intact:
```bash
./scripts/recreate-sanity-check.sh # auto-detects variant
./scripts/recreate-sanity-check.sh --expected-image-version 1.8.9 # assert the pi-devbox release tag
./scripts/recreate-sanity-check.sh --expected-version 1.1.0 # assert the pi coding agent version
./scripts/recreate-sanity-check.sh --expected-version 1.2.0 # assert the pi coding agent version
```
Those are **two different versions**, and the flags are not interchangeable:
@@ -1237,7 +1237,7 @@ resolved to `latest` at build time:
| Component | Pin | Where |
|---|---|---|
| pi | `1.1.0` | `ARG PI_VERSION` — `Dockerfile.variant` |
| pi | `1.2.0` | `ARG PI_VERSION` — `Dockerfile.variant` |
| pi-obsmem | `022da5e5554d04dfdc876ed7dac4a42c6b83dfd4` | `ARG PI_OBSMEM_REF` — `Dockerfile.variant` |
| pi-atelier | `v0.15.0` | `ARG PI_ATELIER_REF` — `Dockerfile.variant` |
| mempalace | `3.10.0` | `ARG MEMPALACE_VERSION` — `Dockerfile.base` |
@@ -61,7 +61,7 @@ its skill file needed baking.
`docker build` records the same thing CI does.
Verifying "is this snapshot current?" is **not** a CI job and was deliberately
not made one — see the Unreleased CHANGELOG entry for why (private repo;
not made one — see the v1.8.8 CHANGELOG entry for why (private repo;
another repo's branch must not be able to fail this build; and the artefact it
would guard is read by no host on this fleet). The check belongs where the
skillset actually is: `vendor-mempalace-skill.sh --check` for a maintainer,
@@ -23,11 +23,13 @@ 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/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`, `ext-toggle`, `task`, `fork-gate`, `secret-guard`, `aws-sso`, `confirm-destructive` |
| `~/.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` |
**MCP servers are not an extension here.** pi's built-in MCP support (`builtin:mcp`) reads `~/.pi/agent/mcp.json`; manage it with `/mcp` or `pi mcp add|list`. Its tools are named `mcp__<server>__<tool>` and default to `codemode` exposure (not declared to the model) unless the server sets `"exposure": "direct"`. The `mempalace_*` tools are different: `mempalace.ts` registers them itself, so exposure never applies. The old `mcp-loader.ts` (an `mcp` block in `settings.json`) was retired on 2026-10-12, because an extension that registers `/mcp` replaces the built-in.
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.
+18
View File
@@ -303,6 +303,24 @@ else
fail "only $EXT_COUNT extensions deployed (expected ≥4)"
fi
# no dangling extension links. The count above includes them (ls lists a link
# whether or not its target exists) but they load nothing, and pi skips them
# without a word. That is exactly how a retired extension outlives its image on
# a preserved volume: mcp-loader.ts (retired after v1.11.0) left
# ~/.pi/agent/extensions/mcp-loader.ts -> /opt/pi-extensions/... behind. The
# pi-extensions install.sh run by the entrypoint now prunes links into ITS repo;
# anything this still finds points somewhere else, hence WARN, not FAIL.
_dangling=""
for _e in "$HOME"/.pi/agent/extensions/*.ts "$HOME"/.pi/agent/extensions/*.ts.off; do
if [ -L "$_e" ] && [ ! -e "$_e" ]; then _dangling="$_dangling $(basename "$_e")"; fi
done
if [ -z "$_dangling" ]; then
pass "no dangling extension links in ~/.pi/agent/extensions"
else
warn "dangling extension links (target gone, pi ignores them):$_dangling"
fi
unset _dangling _e
# mempalace.ts bridge symlink
if [ -L "$HOME/.pi/agent/extensions/mempalace.ts" ]; then
pass "~/.pi/agent/extensions/mempalace.ts bridge symlink"
+36 -3
View File
@@ -30,7 +30,8 @@
# client bundle present + registered via `pi install`
# - no foreign npm-11 platform packages (@esbuild, clipboard) beyond the host
# - no build-time npm cache (/root/.npm) shipped in the image
# - esbuild compiles everywhere; clipboard present iff pi declares it
# - esbuild present iff something declares it, and compiles everywhere;
# clipboard present iff pi declares it
# - image size within threshold
set -euo pipefail
@@ -985,8 +986,40 @@ run "no build-time npm cache shipped (/root/.npm)" \
# runbook as "if this fails, revert the release", and it duly failed for the
# wrong reason on the first machine that ran it. A check must fail only for the
# thing it is checking.
run "esbuild works at every install site (prune removed weight, not function)" \
'sites=$(find /usr/lib/node_modules /opt -type d -path "*/node_modules/esbuild" -prune 2>/dev/null); if [ -z "$sites" ]; then echo "no esbuild install found at all" >&2; exit 1; fi; for d in $sites; do node -e "require(\"$d\").transformSync(\"const x:number=1\",{loader:\"ts\"})" || { echo "esbuild broken at $d" >&2; exit 1; }; done; echo ok'
#
# PRESENT IFF DECLARED, like clipboard below. esbuild was never a direct pi
# dependency: it arrived through @earendil-works/chord (pi 1.1.0 > chord >
# esbuild 0.28.2, at both the pi and pi-fork sites). pi 1.2.0 dropped chord, so
# a correct 1.2.0 image has NO esbuild, and the old "must exist" form failed the
# v1.11.1 tag build (102/103 core, 105/106 studio, nothing published) against a
# good image — the same mistake the clipboard check made at v1.10.0. Because it
# is transitive, "declared" cannot be read from pi's package.json: it is
# derived from every INSTALLED package root (node_modules/<pkg>/package.json,
# node_modules/@scope/<pkg>/package.json) and every /opt repo root, counting
# dependencies, optionalDependencies and peerDependencies (devDependencies are
# not installed: --omit=dev). Anchoring on package roots keeps a test fixture's
# package.json from counting. Both directions fail: declared but missing (the
# prune removed function), and installed with nothing declaring it (residue).
run "esbuild: present iff something declares it, and works at every install site (prune removed weight, not function)" \
'declarers=$(find /usr/lib/node_modules /opt -regextype posix-extended -type f \( -regex ".*/node_modules/(@[^/]+/)?[^/]+/package\.json" -o -regex "/opt/[^/]+/package\.json" \) 2>/dev/null | node -e "
const fs = require(\"fs\"); const hit = [];
for (const f of fs.readFileSync(0, \"utf8\").split(String.fromCharCode(10)).filter(Boolean)) {
let j; try { j = JSON.parse(fs.readFileSync(f, \"utf8\")); } catch (e) { continue; }
if (!j || j.name === \"esbuild\") continue;
for (const k of [\"dependencies\", \"optionalDependencies\", \"peerDependencies\"]) {
if (j[k] && Object.prototype.hasOwnProperty.call(j[k], \"esbuild\")) { hit.push(f); break; }
}
}
console.log(hit.join(\" \"));") || { echo "declarer scan itself failed" >&2; exit 1; };
sites=$(find /usr/lib/node_modules /opt -type d -path "*/node_modules/esbuild" -prune 2>/dev/null);
if [ -n "$declarers" ]; then
[ -n "$sites" ] || { echo "declared by $declarers but NO install found - prune removed function" >&2; exit 1; };
for d in $sites; do node -e "require(\"$d\").transformSync(\"const x:number=1\",{loader:\"ts\"})" || { echo "esbuild broken at $d" >&2; exit 1; }; done;
echo "ok: declared ($(echo $declarers | wc -w) package(s)) and compiles at $(echo $sites | wc -w) site(s)";
else
[ -z "$sites" ] || { echo "nothing declares esbuild yet installs remain: $sites" >&2; exit 1; };
echo "ok: nothing declares esbuild (pi >= 1.2.0 dropped chord); no install expected";
fi'
# Clipboard is the family pruned second. Upstream pi 0.86.0 (#9163) REPLACED the
# external `@mariozechner/clipboard` dependency with bundled macOS/Windows/X11