Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf265f6d54 | |||
| 4efa84bb42 | |||
| fa38e3e102 | |||
| 1d53e008d4 | |||
| 28e324d68f | |||
| 3da7312166 | |||
| 4ee94da523 | |||
| 3f46f5fd29 | |||
| 09ce100c92 | |||
| 11e512e497 | |||
| eb70928b16 | |||
| 32448c7353 | |||
| 56e3742a2d | |||
| 784fad78f3 |
@@ -169,6 +169,30 @@ GIT_USER_EMAIL=
|
|||||||
# AWS_ACCESS_KEY_ID=
|
# AWS_ACCESS_KEY_ID=
|
||||||
# AWS_SECRET_ACCESS_KEY=
|
# AWS_SECRET_ACCESS_KEY=
|
||||||
|
|
||||||
|
# ── AWS SSO keeper (pi-extensions aws-sso.ts; all optional) ──────────
|
||||||
|
# Renews the AWS SSO session for Bedrock when it ends (aws sso login); you or
|
||||||
|
# your signed-in browser still approve each login. Off until `/aws-sso on`.
|
||||||
|
# Guide: pi-extensions docs/aws-sso.md. Recreate the container after editing.
|
||||||
|
# PI_AWS_SSO_REFRESH=1 # force the keeper on (0 = force off)
|
||||||
|
# PI_AWS_SSO_MODE=local # local (default): browser on the Docker host
|
||||||
|
# # remote: device code + push (headless/ssh+tmux)
|
||||||
|
# # (a mode set with /aws-sso local|remote wins)
|
||||||
|
# PI_AWS_SSO_NTFY= # push target: https://ntfy.sh/<long-random-topic>
|
||||||
|
# # anyone knowing the topic can read the pushes
|
||||||
|
# PI_AWS_SSO_NTFY_TOKEN= # token for a protected topic (own ntfy server)
|
||||||
|
# PI_AWS_SSO_SSH_HOST=host # local: SSH alias of the Docker host
|
||||||
|
# PI_AWS_SSO_LOCAL_CMD= # local: command run on the host; {session} =
|
||||||
|
# # sso-session name. Default (POSIX host):
|
||||||
|
# # exec "${SHELL:-/bin/sh}" -lc 'aws sso login --sso-session {session}'
|
||||||
|
# # (Compose expands ${...} in .env: write $$ for a literal $)
|
||||||
|
|
||||||
|
# ── Shared custom model providers (pi models.json) ────────────────────
|
||||||
|
# If /opt/fleet-pi/models.json exists (mount a host DIRECTORY there, read-only),
|
||||||
|
# the entrypoint links ~/.pi/agent/models.json to it on every start. A real file
|
||||||
|
# or a link of your own is never replaced. README: "Custom model providers".
|
||||||
|
# Put API keys in this .env and reference them as "$NAME" in models.json.
|
||||||
|
# PI_MODELS_JSON=/opt/fleet-pi/models.json # another path; off = disable
|
||||||
|
|
||||||
# ── Skillset (agent skills and instructions) ─────────────────────────
|
# ── Skillset (agent skills and instructions) ─────────────────────────
|
||||||
# If you have a skillset repo, the entrypoint auto-deploys skills and
|
# If you have a skillset repo, the entrypoint auto-deploys skills and
|
||||||
# instructions on container start using relative symlinks.
|
# instructions on container start using relative symlinks.
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ name: Publish Docker Image
|
|||||||
# Pipeline shape:
|
# Pipeline shape:
|
||||||
# 1. base-decide compute base hash from Dockerfile.base + rootfs/
|
# 1. base-decide compute base hash from Dockerfile.base + rootfs/
|
||||||
# + entrypoints; probe Docker Hub for existing tag.
|
# + entrypoints; probe Docker Hub for existing tag.
|
||||||
# 2. resolve-versions resolve pi @ npm 'latest', pi-fork/pi-obsmem refs
|
# 2. resolve-versions pi and mempalace from their pins (checked on npm/
|
||||||
# to commit SHAs (defeats registry-buildcache
|
# PyPI); pi-obsmem and pi-atelier from their pins in
|
||||||
# cache-hit footgun on byte-identical build args).
|
# 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>.
|
# 3. build-base only if probe missed; multi-arch push of base-<hash>.
|
||||||
# 4. smoke amd64-only build of the variant FROMing the base
|
# 4. smoke amd64-only build of the variant FROMing the base
|
||||||
# tag; runs scripts/smoke-test.sh.
|
# tag; runs scripts/smoke-test.sh.
|
||||||
@@ -186,7 +190,7 @@ jobs:
|
|||||||
# gap -- and for it the gap is strictly worse. Shellcheck judges the TREE:
|
# 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.
|
# 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
|
# 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
|
# 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
|
# main carries no information about tag time, and that window is exactly where
|
||||||
# releases live. Worked example: pi-observational-memory moved cba0334 ->
|
# releases live. Worked example: pi-observational-memory moved cba0334 ->
|
||||||
@@ -222,6 +226,13 @@ jobs:
|
|||||||
- name: Components the next build would bake differently are named in the CHANGELOG
|
- name: Components the next build would bake differently are named in the CHANGELOG
|
||||||
run: bash scripts/check-doc-drift.sh
|
run: bash scripts/check-doc-drift.sh
|
||||||
|
|
||||||
|
# Runs HERE as well as in lint.yml deliberately. A guard that gates only
|
||||||
|
# `push` lets a tag regress — the adoption slip that let doc-drift land
|
||||||
|
# 27 h after the v1.9.3 tag. This one protects the tag build from the
|
||||||
|
# zero-failing-step failure mode that killed v1.10.0 and v1.10.1.
|
||||||
|
- name: Dockerfile.variant still opens with its BuildKit check directive
|
||||||
|
run: bash scripts/check-dockerfile-directives.sh Dockerfile.variant
|
||||||
|
|
||||||
resolve-versions:
|
resolve-versions:
|
||||||
# Gated: a defective tree must not reach a 46-minute base build.
|
# Gated: a defective tree must not reach a 46-minute base build.
|
||||||
needs: [lint-gate]
|
needs: [lint-gate]
|
||||||
@@ -379,15 +390,37 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "mempalace_version=${MEMPALACE_VERSION}" >> "$GITHUB_OUTPUT"
|
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" \
|
FORK_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
||||||
"https://api.github.com/repos/elpapi42/pi-fork/commits/master" || true)
|
"https://api.github.com/repos/elpapi42/pi-fork/commits/master" || true)
|
||||||
require_sha PI_FORK_REF "$FORK_REF"
|
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"
|
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"
|
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
|
# pi-atelier → the PINNED TAG's commit SHA. Unlike fork/obsmem
|
||||||
# (which track a branch head) atelier wraps pi's private TUI
|
# (which track a branch head) atelier wraps pi's private TUI
|
||||||
@@ -476,7 +509,7 @@ jobs:
|
|||||||
echo "Resolved PI_VERSION=${PI_VERSION} (pinned in Dockerfile.variant; npm latest is ${PI_NPM_LATEST:-unknown})"
|
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 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_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_TOOLKIT_REF=${TOOLKIT_REF}, PI_EXTENSIONS_REF=${EXTENSIONS_REF}"
|
||||||
echo "Resolved PI_STUDIO_REF=${STUDIO_REF} (pi-studio ${STUDIO_TAG})"
|
echo "Resolved PI_STUDIO_REF=${STUDIO_REF} (pi-studio ${STUDIO_TAG})"
|
||||||
echo "Resolved MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}"
|
echo "Resolved MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}"
|
||||||
|
|||||||
@@ -90,6 +90,18 @@ jobs:
|
|||||||
# actionlint so the more precise diagnostic surfaces first.
|
# actionlint so the more precise diagnostic surfaces first.
|
||||||
run: bash scripts/check-workflow-shell.sh .gitea/workflows
|
run: bash scripts/check-workflow-shell.sh .gitea/workflows
|
||||||
|
|
||||||
|
- name: link-pi-models.sh contract (two-sided, no docker needed)
|
||||||
|
# The entrypoint links ~/.pi/agent/models.json to a mounted shared file
|
||||||
|
# and must never replace a real file or a user's own link. Tested here
|
||||||
|
# on every push, because the smoke test only runs at release time.
|
||||||
|
run: bash scripts/test-link-pi-models.sh
|
||||||
|
|
||||||
|
- name: Dockerfile.variant still opens with its BuildKit check directive
|
||||||
|
# See scripts/check-dockerfile-directives.sh: without that first line the
|
||||||
|
# smoke jobs fail with `invalid format delimiter 'ghadelimiter_...'` and
|
||||||
|
# NO failing step. Also wired into docker-publish.yml's lint-gate.
|
||||||
|
run: bash scripts/check-dockerfile-directives.sh Dockerfile.variant
|
||||||
|
|
||||||
- name: Install actionlint (pinned)
|
- name: Install actionlint (pinned)
|
||||||
env:
|
env:
|
||||||
ACTIONLINT_VERSION: 1.7.12
|
ACTIONLINT_VERSION: 1.7.12
|
||||||
|
|||||||
+487
-12
@@ -11,21 +11,494 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.10.1 — 2026-10-02
|
## v1.11.2 — 2026-10-14
|
||||||
|
|
||||||
**v1.10.0 was tagged and never published.** Its tag build (run 704) failed one
|
**v1.11.1 was tagged and never published.** Run 724 failed one smoke assertion
|
||||||
assertion out of 104 — a smoke check that required a pi dependency upstream had
|
in each variant (`smoke` 102 passed / **1 failed**, `smoke-studio` 105 / **1**):
|
||||||
deliberately deleted — so every publish job skipped and no image, no `latest`,
|
*esbuild works at every install site* reported "no esbuild install found at
|
||||||
no Hub description was ever written. v1.10.1 is that same release plus the fix
|
all". The image was right and the assertion was wrong. esbuild was never a
|
||||||
to the assertion: **everything described under v1.10.0 below ships here**, and
|
direct pi dependency. It came through `@earendil-works/chord` (pi 1.1.0 → chord
|
||||||
that section remains the content record for this release. Nothing in it changed.
|
→ 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.
|
||||||
|
|
||||||
The tag number moves rather than being re-pointed because CI had already
|
Patch release. Headlines: **pi 1.2.0**, **pi-studio 0.10.1** (two editors, a
|
||||||
consumed v1.10.0; a version that failed its build should stay failed and
|
Prompt and a Document, by default), **`pi -p` returns in seconds** on a
|
||||||
readable, not be quietly overwritten with different bytes.
|
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
|
### 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
|
||||||
|
model context by default** (pi-extensions `secret-guard`), an opt-in **AWS SSO
|
||||||
|
keeper** for long Bedrock runs, and **`~/.pi/agent/models.json` linked to a
|
||||||
|
mounted shared file** at every start. Everything else moves to its newest
|
||||||
|
upstream: pi-observational-memory `022da5e`, pi-atelier `v0.15.0`, pi-studio
|
||||||
|
`v0.9.63`. The base image is rebuilt (rootfs and `entrypoint-user.sh` changed),
|
||||||
|
so the base tools pinned to `latest` re-resolve too.
|
||||||
|
|
||||||
|
### Components that move with this release
|
||||||
|
|
||||||
|
| Component | Baked in v1.10.2 | v1.11.0 | Why |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **pi** | `1.0.0` | **`1.1.0`** | audited, see *Changed* below |
|
||||||
|
| pi-obsmem | `731c3d4` | **`022da5e`** (pinned SHA, = `master`) | #81: compaction never drops unobserved context; #93: pi loads a prebuilt `dist/index.ts` (see *Changed*) |
|
||||||
|
| pi-atelier | `v0.13.0` (`34d26f1`) | **`v0.15.0`** (`3befe02`) | v0.14.0 Auto/Manual sidebar modes with auto-collapse; v0.15.0 starts in Manual, recolours panels, simpler Activity rows |
|
||||||
|
| skillset snapshot | `e9e45f7` | **`96b840e`** | provenance only: the vendored `SKILL.md` is byte-identical (sha256 `a1db86da…` at both refs) |
|
||||||
|
| pi-extensions | `143a214` | **`a88fa99`** (or later: CI resolves `main` at build time) | new `aws-sso.ts` (keeps the Bedrock SSO session alive) and `secret-guard.ts` (keeps secret env values out of model context); both below |
|
||||||
|
| pi-toolkit | `9c87ee8` | **`3760d90`** (or later: CI resolves `main` at build time) | `pi-task`: optional model fallback when the primary model fails auth before acting (below) |
|
||||||
|
| pi-studio | `v0.9.61` (`641aa32`) | **`v0.9.63`** (`fae3ad3`) | upstream releases (v0.9.62, v0.9.63), picked up by CI's newest-tag resolution; `-studio` variant only. Reviewed: browser-editor fixes only (WebKit cursor drift after re-wrap, re-layout after macOS autocorrect) plus `Ctrl+Shift+H` to toggle highlighting; peerDependencies unchanged: [compare](https://github.com/omaclaren/pi-studio/compare/641aa3288a6bfd4a7eb37a3a4847836de85e0593...fae3ad38d23c9666981bfc29f9a4e06d56f8de84) |
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Shared custom model providers: `~/.pi/agent/models.json` is linked to a
|
||||||
|
mounted file at every start.** Mount a host *directory* read-only at
|
||||||
|
`/opt/fleet-pi`, and new `rootfs/usr/local/lib/pi-devbox/link-pi-models.sh`
|
||||||
|
links `~/.pi/agent/models.json` to `/opt/fleet-pi/models.json`. It runs from
|
||||||
|
`entrypoint-user.sh`. `PI_MODELS_JSON` points at another path, and `=off`
|
||||||
|
disables it. It only ever creates a link where nothing exists: a real file or a
|
||||||
|
link of your own is left alone, with one `NOTE` line in `docker logs`.
|
||||||
|
|
||||||
|
Why this is needed: pi reads custom providers (company gateways, vLLM, Ollama,
|
||||||
|
…) from `~/.pi/agent/models.json`, and `~/.pi` is the `devbox-pi-config`
|
||||||
|
volume. A file baked into the image would only seed a fresh volume once, and a
|
||||||
|
hand-made link was one more step on every new volume. In October 2026 that was
|
||||||
|
the same `ln -s` once per device for one fleet-shared file. Mount the
|
||||||
|
directory, not the file. `git pull` replaces the file by rename, and a
|
||||||
|
single-file bind mount then shows "No such file" until the next recreate
|
||||||
|
(measured on Docker Desktop).
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- `scripts/test-link-pi-models.sh` has 13 two-sided cases and now runs in
|
||||||
|
`lint.yml` on every push. Every mutation of a rule was caught, except one
|
||||||
|
that the guards make unreachable.
|
||||||
|
- Two smoke-test checks: with nothing mounted no link appears, and the script
|
||||||
|
links a mounted file.
|
||||||
|
- `recreate-sanity-check.sh` checks the link whenever a shared file is mounted.
|
||||||
|
|
||||||
|
pi treats a dangling `models.json` exactly like a missing one, with no error,
|
||||||
|
so removing the mount later is harmless (measured with pi 1.0.0). README:
|
||||||
|
"Custom model providers (`models.json`)".
|
||||||
|
|
||||||
|
- **secret-guard (pi-extensions `secret-guard.ts`): secret env values never reach
|
||||||
|
the model, the session file or the palace feed.** It replaces the exact value
|
||||||
|
of every env var whose name says secret (`…TOKEN`, `…API_KEY`, `…PASSWORD`, …)
|
||||||
|
with `<redacted:NAME>`, the same rule and placeholder as mempalace-toolkit's
|
||||||
|
feed redactor. It applies to every tool result (MCP tools included), to the
|
||||||
|
context of each request (covering user `!` commands and older sessions), and it
|
||||||
|
blocks a `write`/`edit` that would write such a placeholder back over the real
|
||||||
|
value. `$NAME` still expands in commands; only the output coming back is
|
||||||
|
rewritten.
|
||||||
|
|
||||||
|
Measured motivation, 2026-10-12: an agent's own `sed` failed to redact
|
||||||
|
`MEMPALACE_REMOTE_TOKEN=…` from `env | grep` output. The raw value had been in
|
||||||
|
local session files for two months, so each of those sessions had sent it to
|
||||||
|
its model provider. The palace stayed clean only because the feed redactor
|
||||||
|
works.
|
||||||
|
|
||||||
|
Not covered:
|
||||||
|
- live streaming output while a tool runs;
|
||||||
|
- the stored entry of a user `!` command;
|
||||||
|
- secrets not in the environment;
|
||||||
|
- `pi-task` children, which run `--no-extensions`.
|
||||||
|
|
||||||
|
`PI_SECRET_GUARD=off` disables it.
|
||||||
|
|
||||||
|
- **`pi-task` model fallback (pi-toolkit `bin/pi-task`).** A
|
||||||
|
`pi-task.fallbackProfiles` block in `settings.json` (per effort tier:
|
||||||
|
provider/id/thinking) lets a task retry once on another model. The retry
|
||||||
|
happens only when the primary fails with an auth-class error (an expired SSO
|
||||||
|
token, an invalid key) **before** it has called a tool or changed a watched
|
||||||
|
file, and with at least 30 s of wall budget left. Both attempts are recorded in
|
||||||
|
`result.json` and the audit dir, and the result line is tagged `[FALLBACK]`.
|
||||||
|
Without the block, behaviour is unchanged. It was needed because pi reports
|
||||||
|
such a failure only inside its JSON stream: `stopReason=error`, rc 0. Before
|
||||||
|
this, an expired Bedrock session failed every task as "no fenced json
|
||||||
|
envelope". The `fork` tool does not read this key. Documented in pi-toolkit's
|
||||||
|
README. It complements `/aws-sso` (below), which by design acts only in
|
||||||
|
interactive sessions (`ctx.hasUI`; pi-extensions `docs/aws-sso.md`,
|
||||||
|
troubleshooting). A `task` child that meets an ended SSO session gets no help
|
||||||
|
from the keeper.
|
||||||
|
|
||||||
|
- **Long Bedrock runs no longer die at "Token is expired" when the AWS SSO
|
||||||
|
session ends: `/aws-sso` (pi-extensions `aws-sso.ts`) runs `aws sso login` for
|
||||||
|
you just after the session ends.** Off until `/aws-sso on` (or
|
||||||
|
`PI_AWS_SSO_REFRESH=1`), so other devices stay as they are.
|
||||||
|
|
||||||
|
Why this is needed: pi's AWS SDK already renews the 1-hour access token by itself.
|
||||||
|
The limit is the IAM Identity Center session behind it, the browser's AWS sign-in,
|
||||||
|
which the IdC admin caps (8 h here, measured three times). After it, AWS refuses
|
||||||
|
every renewal, and a long or overnight run stops. Renewing on a clock does not
|
||||||
|
work: a login while the browser sign-in is still valid completes silently but
|
||||||
|
*inherits* its end. Only a login after the end starts a new session.
|
||||||
|
|
||||||
|
How it finds the end: in the session's last hour AWS still issues tokens, but caps
|
||||||
|
them at the end. The keeper makes the SDK's own renewal attempt as soon as the
|
||||||
|
SDK's 5-minute window opens and reads the result from the token cache: a token
|
||||||
|
issued for under 55 min gives the end, and the login runs 30 s after it. A refused
|
||||||
|
renewal triggers the login at once. If a request fails anyway (pi closed, laptop
|
||||||
|
asleep), it logs in and sends `continue`, so the run resumes. It never handles
|
||||||
|
passwords or MFA, and tokens appear only as `sha256[:8]` fingerprints.
|
||||||
|
|
||||||
|
Two modes, set per device with `/aws-sso local|remote` or `PI_AWS_SSO_MODE`:
|
||||||
|
`local` (default) runs the login on the Docker host over `~/.ssh-local/config`,
|
||||||
|
so the host's signed-in browser normally completes it silently. `remote`
|
||||||
|
(headless server, ssh + tmux) uses the device-code flow in the container: link
|
||||||
|
and code shown in pi and pushed to the phone via [ntfy](https://ntfy.sh)
|
||||||
|
(`PI_AWS_SSO_NTFY`; the topic name is a secret, shown only as a fingerprint).
|
||||||
|
Pushes also report a `local` login waiting for you, a failed login and a renewal.
|
||||||
|
|
||||||
|
Measured on mbp-m1-2020 (macOS host, `local`), 2026-10-11/12: overnight, two
|
||||||
|
unattended silent logins with nobody at the Mac. Then, with the final rule: the
|
||||||
|
end was read from the cap at 08:08:57 (session end 08:47:55), and the login ran
|
||||||
|
at 08:48:40 and completed in 7 s with a new full-hour token, before any request
|
||||||
|
failed.
|
||||||
|
|
||||||
|
`.env.example` documents the `PI_AWS_SSO_*` variables. The setup guide (modes
|
||||||
|
per device type, ntfy, log reference, troubleshooting) is pi-extensions
|
||||||
|
[`docs/aws-sso.md`](https://gitea.jordbo.se/joakimp/pi-extensions/src/branch/main/docs/aws-sso.md).
|
||||||
|
`.env` changes need a container recreate. To try a variable without one, run a
|
||||||
|
second pi as the right user: `docker exec` defaults to **root** here (the image
|
||||||
|
sets no `USER`), and root has no `~/.aws` ("Could not load credentials from any
|
||||||
|
providers"). Use
|
||||||
|
`docker exec -it -u developer -e PI_AWS_SSO_NTFY=… pi-devbox bash -lc pi`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **pi `1.0.0` → `1.1.0`.** Audited 2026-10-12 against the published npm
|
||||||
|
tarballs, by the same method as the v1.10.2 audit (the full note is above
|
||||||
|
`ARG PI_VERSION` in `Dockerfile.variant`):
|
||||||
|
- The only `### Breaking Changes` section in 1.0.1..1.1.0 is 1.0.3's rename
|
||||||
|
of the `azure-openai-responses` provider to `azure`. Nothing here uses it.
|
||||||
|
- `finishTurn` handling in pi-agent-core is byte-identical between 1.0.0 and
|
||||||
|
1.1.0. The six named `pi.*` APIs our extensions call (`registerTool`,
|
||||||
|
`registerCommand`, `registerFlag`, `getFlag`, `sendMessage`,
|
||||||
|
`sendUserMessage`) appear in the same dist files in both versions, and so do
|
||||||
|
the TUI internals atelier hooks. `engines.node` is unchanged.
|
||||||
|
- User-visible: `Home`/`End` now always move the editor cursor (fullscreen
|
||||||
|
transcript top/bottom moved to `Ctrl+Home`/`Ctrl+End`). Terminals that
|
||||||
|
support OSC 7501 now see pi's working/blocked/done status; set
|
||||||
|
`PI_PROGRAM_STATUS=0` to turn that off.
|
||||||
|
- Exercised before tagging with pi 1.1.0 from a temp prefix, in a real pty,
|
||||||
|
with `--no-extensions` controls:
|
||||||
|
- The obsmem turn cap held on the real 1.1.0 agent loop: uncapped ran 11
|
||||||
|
turns, capped stopped at exactly 3.
|
||||||
|
- atelier v0.15.0 painted in both renderers (controls 0/3, every arm
|
||||||
|
render-proofed).
|
||||||
|
- obsmem `022da5e` registered `/om:status` and `/om:view`.
|
||||||
|
- All 11 pi-extensions `a88fa99` extensions loaded without error.
|
||||||
|
- **pi-observational-memory `731c3d4` → `022da5e`**, still a full-SHA pin; no
|
||||||
|
tag newer than 3.1.4 exists.
|
||||||
|
- #81 changes compaction. Entries the observer has not reached yet are no
|
||||||
|
longer cut out of context, and memory workers yield to the session. New
|
||||||
|
settings (`compactionMaxRetainedTokens`, `consolidateWhenIdle`,
|
||||||
|
`piAutoCompactionEnabled`) all have defaults, so nothing needs setting.
|
||||||
|
- #93 makes pi load a committed `dist/index.ts` bundle instead of 32 TS
|
||||||
|
sources, which is faster to start.
|
||||||
|
- `src/agents/` is unchanged between the two SHAs, so the workers'
|
||||||
|
`finishTurn` migration is intact.
|
||||||
|
- **pi-atelier `v0.13.0` → `v0.15.0`.** The sidebar now starts in **Manual**
|
||||||
|
mode. `/atelier sidebar auto` collapses it when the terminal is narrow and
|
||||||
|
reopens it when there is room. `showSidebarOnStartup` keeps its key and its
|
||||||
|
default. README's sidebar section describes both modes.
|
||||||
|
- **Base rebuilt** (`base-<hash>` changes because `rootfs/` and
|
||||||
|
`entrypoint-user.sh` changed). The base tools pinned to `latest` re-resolve.
|
||||||
|
Upstream on 2026-10-12: uv 0.13.0 (its breaking change makes Python 3.15 the
|
||||||
|
default *download*; mempalace still installs with `uv tool install` on
|
||||||
|
Debian's python3.13, which uv prefers because it is already installed), nvim
|
||||||
|
0.12.6, fzf 0.74.5. `# BASE_REBUILD_DATE:` is updated while it is free.
|
||||||
|
|
||||||
|
### Still open
|
||||||
|
|
||||||
|
- `remote` mode end to end (code → push → approve on the phone) is unit-tested and
|
||||||
|
its push path is verified against ntfy.sh, but no live device-code renewal has
|
||||||
|
run yet.
|
||||||
|
- `local` is verified on a macOS host only. On a Linux or Windows host, the login
|
||||||
|
started over SSH may not reach the desktop browser; `PI_AWS_SSO_LOCAL_CMD`
|
||||||
|
overrides the host command.
|
||||||
|
- AWS refused renewals twice with time left in the session (a probe 5 min before
|
||||||
|
the end, a failed request about 2 h before it). The cause is unknown; both times
|
||||||
|
a login then worked at once.
|
||||||
|
- The real fix is administrative: the IdC admin can raise the session length (up to
|
||||||
|
90 days), after which the keeper has almost nothing to do.
|
||||||
|
- **The pi-obsmem pin does not bind CI builds.** `resolve-versions` in
|
||||||
|
`docker-publish.yml` still resolves `pi-observational-memory` from `master`
|
||||||
|
and passes it as `--build-arg PI_OBSMEM_REF`, which overrides the Dockerfile
|
||||||
|
default. This was verified on both sides with buildx: without the build-arg
|
||||||
|
the pin is baked, with it `master` is. The pin reached v1.9.5, v1.10.2 and
|
||||||
|
this release only because `master` equalled the pin at build time.
|
||||||
|
`check-doc-drift.sh` reads the ARG, so it would not notice a divergence. Fix
|
||||||
|
it by reading the pin from `Dockerfile.variant` in `resolve-versions`, the way
|
||||||
|
pi-atelier's tag already is.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.10.2 — 2026-10-02
|
||||||
|
|
||||||
|
**v1.10.0 and v1.10.1 were both tagged and never published.** Neither failure
|
||||||
|
was in the image — both were in the test and CI harness, and each hid the other:
|
||||||
|
|
||||||
|
| tag | run | symptom | cause |
|
||||||
|
|---|---|---|---|
|
||||||
|
| v1.10.0 | 704 | `smoke` 100 passed / **1 failed**; `smoke-studio` 103 / **1** | a smoke assertion required a pi dependency upstream had deleted |
|
||||||
|
| v1.10.1 | 707 | `smoke` **101 passed / 0 failed**, `smoke-studio` **104 / 0**, and both jobs red anyway with **no failing step** | base64(`Dockerfile.variant`) in buildx's warning metadata crossed act_runner's 64 KiB line cap |
|
||||||
|
|
||||||
|
In both runs every publish job skipped, so no image, no `latest`, and no Hub
|
||||||
|
description was ever written — the gates did their job twice. **Everything
|
||||||
|
described under v1.10.0 below ships here**; that section remains the content
|
||||||
|
record and nothing in it changed.
|
||||||
|
|
||||||
|
The tag number moves each time rather than being re-pointed, because CI had
|
||||||
|
already consumed the previous one. A version that failed its build should stay
|
||||||
|
failed and readable, not be quietly overwritten with different bytes.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **A job can fail with every step green, a smoke suite reporting `0 failed`,
|
||||||
|
and no failing step anywhere — and it cost two tags** — `Dockerfile.variant`
|
||||||
|
now opens with `# check=skip=InvalidDefaultArgInFrom`, and
|
||||||
|
`scripts/check-dockerfile-directives.sh` keeps it there.
|
||||||
|
|
||||||
|
The chain, measured rather than reasoned: `ARG BASE_IMAGE` deliberately has no
|
||||||
|
default (the two-phase build always supplies it), which trips BuildKit's
|
||||||
|
`InvalidDefaultArgInFrom` check. buildx attaches that warning's source context
|
||||||
|
to the build metadata as `buildx.build.warnings[].sourceInfo.data` — **the
|
||||||
|
entire Dockerfile, base64, on one line**. `docker/build-push-action` writes
|
||||||
|
that metadata to `$GITHUB_OUTPUT` as a `name<<ghadelimiter_<uuid>` heredoc,
|
||||||
|
and Gitea's act_runner truncates any single line at exactly 65536 chars. Once
|
||||||
|
base64 of the file passed 64 KiB the closing delimiter was cut off and the
|
||||||
|
runner reported `invalid format delimiter 'ghadelimiter_...' not found before
|
||||||
|
end of file`, then failed the job while attributing it to nothing.
|
||||||
|
|
||||||
|
Numbers: the file was 42251 B at v1.9.4 → base64 56355 chars, fine; it is
|
||||||
|
50034 B now → base64 66712, truncated to exactly 65536 (2^16, i.e. cut *at*
|
||||||
|
the cap, not merely long). The cap corresponds to a 49152 B Dockerfile, so
|
||||||
|
this release's comment growth — much of it comments added while documenting
|
||||||
|
the v1.10.0 round — crossed it by **882 bytes**. Confirmed two ways: by
|
||||||
|
parsing the metadata out of both job logs, and independently by arithmetic on
|
||||||
|
the file size at each tag. Verified fixed by running `docker buildx build
|
||||||
|
--check` against the actual edited file: *"Check complete, no warnings
|
||||||
|
found."* With zero warnings there is no `sourceInfo`, so the longest metadata
|
||||||
|
line drops from 65536 to ~1936 chars and the Dockerfile's **size stops being
|
||||||
|
coupled to whether CI passes**.
|
||||||
|
|
||||||
|
Hypotheses measured and rejected on the way, each of which would have produced
|
||||||
|
a wrong fix: floating action tags moving (the act action-bundle hashes are
|
||||||
|
byte-identical between the green v1.9.4 run and the red one — all four); the
|
||||||
|
build-check annotation text changing (byte-identical); and `provenance: false`,
|
||||||
|
which was written, tested against a real buildx, and **reverted** when the
|
||||||
|
metadata turned out to carry the base64 under `buildx.build.warnings`, not
|
||||||
|
under provenance. The published images are unaffected either way: they are
|
||||||
|
built by raw `docker buildx build --push`, which never writes
|
||||||
|
`$GITHUB_OUTPUT` metadata.
|
||||||
|
|
||||||
|
The new guard runs in **both** `lint.yml` and the publish workflow's
|
||||||
|
`lint-gate`, because a check that gates only `push` lets a tag regress — the
|
||||||
|
same adoption slip that let doc-drift land 27 h after the v1.9.3 tag. It
|
||||||
|
also fails if `ARG BASE_IMAGE` ever gains a default, so the skip directive
|
||||||
|
cannot rot into pointing at a check that can no longer fire. Truth table:
|
||||||
|
directive present → rc=0; removed → rc=1; demoted to line 2 → rc=1; ARG given
|
||||||
|
a default → rc=1; file missing → rc=2 (cannot-run must not pass).
|
||||||
|
|
||||||
- **The smoke suite asserted a pi dependency that upstream deleted, and it cost
|
- **The smoke suite asserted a pi dependency that upstream deleted, and it cost
|
||||||
this release its first tag build** — `scripts/smoke-test.sh` required
|
this release its first tag build** — `scripts/smoke-test.sh` required
|
||||||
`@mariozechner/clipboard` to be installed and `require()`-able at every install
|
`@mariozechner/clipboard` to be installed and `require()`-able at every install
|
||||||
@@ -60,13 +533,15 @@ readable, not be quietly overwritten with different bytes.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.10.0 — 2026-10-02 (tagged, never published — superseded by v1.10.1)
|
## v1.10.0 — 2026-10-02 (tagged, never published — content shipped as v1.10.2)
|
||||||
|
|
||||||
> Tagged 2026-10-02 at commit `12f99c4`. Its build (run 704) went red on the
|
> Tagged 2026-10-02 at commit `12f99c4`. Its build (run 704) went red on the
|
||||||
> stale clipboard assertion described under v1.10.1, so `build-variant`,
|
> stale clipboard assertion described under v1.10.1, so `build-variant`,
|
||||||
> `build-variant-studio`, `promote-base-latest` and `update-description` all
|
> `build-variant-studio`, `promote-base-latest` and `update-description` all
|
||||||
> skipped and nothing reached the Hub. No image carries this tag. The content
|
> skipped and nothing reached the Hub. No image carries this tag. The content
|
||||||
> below is accurate and shipped as **v1.10.1**.
|
> below is accurate and shipped as **v1.10.2**. v1.10.1 (commit `c2c42c3`,
|
||||||
|
> run 707) was the first attempt at republishing it and died on the separate
|
||||||
|
> act_runner line-cap bug described under v1.10.2.
|
||||||
|
|
||||||
Three small fixes found by the v1.9.4 first-boot acceptance and the CI base
|
Three small fixes found by the v1.9.4 first-boot acceptance and the CI base
|
||||||
hash prediction, plus one boot-time wiring fix that stops a recurring `git push`
|
hash prediction, plus one boot-time wiring fix that stops a recurring `git push`
|
||||||
|
|||||||
+2
-1
@@ -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 `{{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-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-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
|
- **`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
|
- **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
|
- **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
@@ -14,7 +14,7 @@
|
|||||||
# content-addressed over this file, so any byte change invalidates the
|
# content-addressed over this file, so any byte change invalidates the
|
||||||
# cache. Recommended cadence: once per release for security updates.
|
# cache. Recommended cadence: once per release for security updates.
|
||||||
#
|
#
|
||||||
# BASE_REBUILD_DATE: 2026-09-22 (v1.9.4 — mempalace 3.9.0 -> 3.10.0 with ENV MEMPALACE_CONFIG_DIR pinning the layout, mempalace-toolkit 2167a1b explicit event_list order; previous marker 2026-09-19 / v1.9.3)
|
# 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 ─────────────────────────────────────────────────────
|
# ── Lineage note ─────────────────────────────────────────────────────
|
||||||
# Adapted from opencode-devbox/Dockerfile.base (commit before v1.16.2).
|
# Adapted from opencode-devbox/Dockerfile.base (commit before v1.16.2).
|
||||||
|
|||||||
+128
-10
@@ -1,3 +1,37 @@
|
|||||||
|
# check=skip=InvalidDefaultArgInFrom
|
||||||
|
#
|
||||||
|
# ^ MUST stay the FIRST line of this file, and it is load-bearing for CI, not
|
||||||
|
# style. BuildKit parses `# check=` only before any other line, so moving it
|
||||||
|
# below the title comment silently disables it.
|
||||||
|
#
|
||||||
|
# Why it exists: `ARG BASE_IMAGE` (below) deliberately has NO default — this
|
||||||
|
# file is only ever built by the two-phase CI with --build-arg BASE_IMAGE=
|
||||||
|
# <image>:base-<hash>. BuildKit's InvalidDefaultArgInFrom check flags that as
|
||||||
|
# "default value for global ARG results in an empty or invalid base image
|
||||||
|
# name". There is no honest default to give it: `scratch` would satisfy the
|
||||||
|
# linter while being a lie (nothing here can build FROM scratch), and it would
|
||||||
|
# convert today's instant "invalid reference format" into a failure deep in the
|
||||||
|
# build. So the check is skipped by name, with the reason written down.
|
||||||
|
#
|
||||||
|
# What the warning actually COST, which is why this is not cosmetic: buildx
|
||||||
|
# attaches the warning's source context to the build metadata as
|
||||||
|
# buildx.build.warnings[].sourceInfo.data — the ENTIRE Dockerfile, base64, on
|
||||||
|
# ONE line. docker/build-push-action writes that metadata to $GITHUB_OUTPUT as
|
||||||
|
# a `name<<ghadelimiter_<uuid>` heredoc, and Gitea's act_runner truncates any
|
||||||
|
# single line at exactly 65536 chars. Once base64(this file) crossed 64 KiB the
|
||||||
|
# closing delimiter was cut off, and the runner failed the job with
|
||||||
|
# invalid format delimiter 'ghadelimiter_...' not found before end of file
|
||||||
|
# and NO failing step: every step green, smoke suite "0 failed", job red.
|
||||||
|
# Measured: 42251 B at v1.9.4 -> base64 56355 (fine); 50034 B at v1.10.1 ->
|
||||||
|
# base64 66712, truncated to exactly 65536. The cap corresponds to a 49152 B
|
||||||
|
# Dockerfile, so this release's comment growth crossed it by 882 B. It killed
|
||||||
|
# v1.10.0 (run 704, where a stale clipboard assertion hid it) and v1.10.1
|
||||||
|
# (run 707). With zero warnings there is no sourceInfo, so the longest metadata
|
||||||
|
# line drops from 65536 to ~1936 chars and the Dockerfile's SIZE stops being
|
||||||
|
# coupled to CI passing at all.
|
||||||
|
#
|
||||||
|
# If this ever recurs — job red, zero failing steps, suite reporting 0 failed —
|
||||||
|
# grep the job log for `ghadelimiter` first.
|
||||||
# pi-devbox — variant image
|
# pi-devbox — variant image
|
||||||
#
|
#
|
||||||
# FROMs a base-<hash> image produced by Dockerfile.base and adds only
|
# FROMs a base-<hash> image produced by Dockerfile.base and adds only
|
||||||
@@ -154,11 +188,11 @@ ARG USER_NAME=developer
|
|||||||
# (e7d77dc -> 1529e14 -> 731c3d4 in the nine days to 2026-10-01), so waiting for
|
# (e7d77dc -> 1529e14 -> 731c3d4 in the nine days to 2026-10-01), so waiting for
|
||||||
# a tag means holding pi indefinitely. A full SHA keeps the one property
|
# a tag means holding pi indefinitely. A full SHA keeps the one property
|
||||||
# `master` does not have: rebuilding this tag later produces the SAME image.
|
# `master` does not have: rebuilding this tag later produces the SAME image.
|
||||||
# v1.9.5 SKIPPED 0.99.x and 1.0.0 for lack of evidence. v1.10.1 went and got the
|
# v1.9.5 SKIPPED 0.99.x and 1.0.0 for lack of evidence. v1.10.2 went and got the
|
||||||
# evidence instead of waiting for obsmem to mention a version, because "no issue
|
# evidence instead of waiting for obsmem to mention a version, because "no issue
|
||||||
# names 1.0" is an absence of a statement, not a measurement.
|
# names 1.0" is an absence of a statement, not a measurement.
|
||||||
#
|
#
|
||||||
# v1.10.1: 0.87.1 -> 1.0.0. MEASURED 2026-10-02 against the published npm
|
# v1.10.2: 0.87.1 -> 1.0.0. MEASURED 2026-10-02 against the published npm
|
||||||
# tarballs for 0.87.1 and 1.0.0, unpacked side by side:
|
# tarballs for 0.87.1 and 1.0.0, unpacked side by side:
|
||||||
# 1. NO `### Breaking Changes` SECTION IN 1.0.0 AT ALL. The changelog's
|
# 1. NO `### Breaking Changes` SECTION IN 1.0.0 AT ALL. The changelog's
|
||||||
# breaking sections belong to 0.87.0, 0.86.0, 0.84.3, 0.84.0, 0.83.0,
|
# breaking sections belong to 0.87.0, 0.86.0, 0.84.3, 0.84.0, 0.83.0,
|
||||||
@@ -178,9 +212,11 @@ ARG USER_NAME=developer
|
|||||||
# `exposure` is `codemode`, i.e. such tools are "neither declared to the model
|
# `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
|
# 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 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
|
# `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.
|
# neither an `mcpServers` block (builtin, exposure-governed) nor an `mcp` block.
|
||||||
# Extension-registered tools are declared like built-ins, so `exposure` cannot
|
# Extension-registered tools are declared like built-ins, so `exposure` cannot
|
||||||
# reach them.
|
# reach them.
|
||||||
@@ -197,7 +233,63 @@ ARG USER_NAME=developer
|
|||||||
# is documented (README -> "Terminal UI mode"): `"tuiMode": "regular"` in
|
# is documented (README -> "Terminal UI mode"): `"tuiMode": "regular"` in
|
||||||
# ~/.pi/agent/settings.json, `pi --tui-mode regular` for one session, or the
|
# ~/.pi/agent/settings.json, `pi --tui-mode regular` for one session, or the
|
||||||
# same key in a project's .pi/settings.json.
|
# same key in a project's .pi/settings.json.
|
||||||
ARG PI_VERSION=1.0.0
|
#
|
||||||
|
# v1.11.0: 1.0.0 -> 1.1.0. MEASURED 2026-10-12 against the published npm
|
||||||
|
# tarballs for 1.0.0 and 1.1.0, unpacked side by side, by the same method as
|
||||||
|
# the v1.10.2 audit above:
|
||||||
|
# 1. ONE `### Breaking Changes` section in 1.0.1..1.1.0: 1.0.3 renamed the
|
||||||
|
# provider `azure-openai-responses` -> `azure`. 0 uses here: grepped
|
||||||
|
# rootfs/, ~/.pi/agent/settings.json, pi-toolkit's settings.example.json
|
||||||
|
# and the fleet's shared models.json. Everything else is additive or a fix
|
||||||
|
# (OSC 7501 program status, `+name`/`-name` in --tools, `durationMs` and
|
||||||
|
# `outputPad` in the tool render context, `aborted` on agent_settled).
|
||||||
|
# Two user-visible changes: Home/End always move the editor cursor
|
||||||
|
# (fullscreen transcript top/bottom moved to Ctrl+Home/Ctrl+End, 1.0.3),
|
||||||
|
# and OSC 7501 status reporting (PI_PROGRAM_STATUS=0 turns it off, 1.1.0).
|
||||||
|
# 2. pi-agent-core 1.0.0 -> 1.1.0: the finishTurn context in dist is
|
||||||
|
# BYTE-IDENTICAL; the only agent-loop diff adds `durationMs`. The obsmem
|
||||||
|
# turn-cap probe drove the real 1.1.0 agentLoop: uncapped ran its chosen
|
||||||
|
# 11 turns, maxTurns=3 stopped at exactly 3 + agent_end.
|
||||||
|
# 3. The 6 named pi.* APIs (registerTool, registerCommand, registerFlag,
|
||||||
|
# getFlag, sendMessage, sendUserMessage) appear in the same number of dist
|
||||||
|
# files in 1.0.0 and 1.1.0. atelier's internals TuiMainScreen and
|
||||||
|
# renderLayoutFrame are both still in 1.1.0's dist.
|
||||||
|
# 4. engines.node is still `>=22.19.0`; the image ships 24.x.
|
||||||
|
# Exercised before tagging, from a temp prefix with pi 1.1.0 and a throwaway
|
||||||
|
# PI_CODING_AGENT_DIR, 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); and all 11
|
||||||
|
# pi-extensions a88fa99 extensions load with no error (control: no /aws-sso).
|
||||||
|
#
|
||||||
|
# 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_TOOLKIT_REF=main
|
||||||
ARG PI_EXTENSIONS_REF=main
|
ARG PI_EXTENSIONS_REF=main
|
||||||
# Repo URLs default to the canonical gitea origin but are overridable so a
|
# Repo URLs default to the canonical gitea origin but are overridable so a
|
||||||
@@ -219,7 +311,22 @@ ARG PI_OBSMEM_REPO=https://github.com/elpapi42/pi-observational-memory.git
|
|||||||
# 40-char match (SHA40), so a short pin would fall through to its
|
# 40-char match (SHA40), so a short pin would fall through to its
|
||||||
# branch-or-tag lookup, fail, and downgrade that component's drift check to a
|
# branch-or-tag lookup, fail, and downgrade that component's drift check to a
|
||||||
# silent SKIP — a pin that reads fine and is no longer verified.
|
# silent SKIP — a pin that reads fine and is no longer verified.
|
||||||
ARG PI_OBSMEM_REF=731c3d49288580f4d79cabdbfbc0d16b34db0f41
|
# v1.11.0: 731c3d4 -> 022da5e (master, 2026-10-05; still no newer tag than
|
||||||
|
# 3.1.4). Two commits: #81 never drops unobserved context at compaction
|
||||||
|
# (new settings compactionMaxRetainedTokens, consolidateWhenIdle,
|
||||||
|
# piAutoCompactionEnabled, all defaulted), and #93 makes pi load a prebuilt
|
||||||
|
# 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.
|
||||||
|
# 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
|
# 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
|
# TAG, which CI resolves to that tag's commit SHA — same treatment as
|
||||||
# pi-studio, for reproducibility plus cache-busting.
|
# pi-studio, for reproducibility plus cache-busting.
|
||||||
@@ -273,7 +380,7 @@ ARG PI_ATELIER_REPO=https://github.com/michaelmjhhhh/pi-atelier.git
|
|||||||
# pi >=0.84.0, so it still spans the pinned 0.85.1. 13 commits v0.10.1..v0.10.3,
|
# pi >=0.84.0, so it still spans the pinned 0.85.1. 13 commits v0.10.1..v0.10.3,
|
||||||
# all under src/ tests/ docs/ scripts/ plus metadata; no entry-point move.
|
# all under src/ tests/ docs/ scripts/ plus metadata; no entry-point move.
|
||||||
#
|
#
|
||||||
# v1.10.1: v0.10.3 -> v0.13.0, closing the two-minor gap v1.9.5 flagged as the
|
# v1.10.2: v0.10.3 -> v0.13.0, closing the two-minor gap v1.9.5 flagged as the
|
||||||
# residual risk of the pi bump. peerDependencies are UNCHANGED at pi >=0.84.0
|
# residual risk of the pi bump. peerDependencies are UNCHANGED at pi >=0.84.0
|
||||||
# across v0.10.3, v0.12.1 and v0.13.0 — still a FLOOR, so still not evidence of
|
# across v0.10.3, v0.12.1 and v0.13.0 — still a FLOOR, so still not evidence of
|
||||||
# anything; the floor is satisfied by 1.0.0 either way. What was actually
|
# anything; the floor is satisfied by 1.0.0 either way. What was actually
|
||||||
@@ -298,9 +405,20 @@ ARG PI_ATELIER_REPO=https://github.com/michaelmjhhhh/pi-atelier.git
|
|||||||
# - Residual: v0.13.0 is SAME-DAY upstream (2026-10-02). Acceptance proves
|
# - Residual: v0.13.0 is SAME-DAY upstream (2026-10-02). Acceptance proves
|
||||||
# the sidebar PAINTS with the two-sided check from 0.84.4/0.85.1 that can
|
# the sidebar PAINTS with the two-sided check from 0.84.4/0.85.1 that can
|
||||||
# tell "loaded" from "silently absent" — "no crash" is not the test.
|
# tell "loaded" from "silently absent" — "no crash" is not the test.
|
||||||
ARG PI_ATELIER_REF=v0.13.0
|
#
|
||||||
|
# v1.11.0: v0.13.0 -> v0.15.0 (v0.14.0 and v0.15.0 both 2026-10-03). The
|
||||||
|
# peerDependencies floor is unchanged (pi >=0.84.0), atelier still has zero
|
||||||
|
# runtime dependencies, and it still imports only the public package exports.
|
||||||
|
# Behaviour changes: v0.14.0 auto-collapses the Sidebar when the terminal is
|
||||||
|
# narrow (Auto/Manual modes). v0.15.0 starts in Manual mode, recolours the
|
||||||
|
# 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.
|
# Human-readable tag PI_ATELIER_REF was resolved from; recorded as a label.
|
||||||
ARG PI_ATELIER_VERSION=v0.13.0
|
ARG PI_ATELIER_VERSION=v0.15.0
|
||||||
|
|
||||||
RUN set -e && \
|
RUN set -e && \
|
||||||
# git_fetch_ref: clone-equivalent helper that accepts EITHER a branch name
|
# git_fetch_ref: clone-equivalent helper that accepts EITHER a branch name
|
||||||
@@ -625,7 +743,7 @@ ARG MEMPALACE_TOOLKIT_REF=main
|
|||||||
# no ~67-minute base rebuild. (scripts/check-base-hash.sh scans only
|
# no ~67-minute base rebuild. (scripts/check-base-hash.sh scans only
|
||||||
# Dockerfile.base, so no folding into the base hash is required — nor would
|
# Dockerfile.base, so no folding into the base hash is required — nor would
|
||||||
# it be correct, since this ARG changes nothing about the base's contents.)
|
# it be correct, since this ARG changes nothing about the base's contents.)
|
||||||
ARG SKILLSET_SNAPSHOT_REF=e9e45f7acdde490c3b5d24ce5f508bff8785c2c7
|
ARG SKILLSET_SNAPSHOT_REF=96b840ee43beca65a190772021a8ef370145cde1
|
||||||
|
|
||||||
# Dockerfile.base sets description="pi-devbox — base image (variant-independent)"
|
# Dockerfile.base sets description="pi-devbox — base image (variant-independent)"
|
||||||
# and every variant INHERITS it, so both published images used to advertise
|
# and every variant INHERITS it, so both published images used to advertise
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ DOT syntax errors instead of crashing. Then in Studio: open the PNG (or a
|
|||||||
`.md` that embeds it) and hit **refresh-from-disk** after each edit.
|
`.md` that embeds it) and hit **refresh-from-disk** after each edit.
|
||||||
Note: SVG is **not** in Studio's local-image-link allowlist — use PNG.
|
Note: SVG is **not** in Studio's local-image-link allowlist — use PNG.
|
||||||
|
|
||||||
## Terminal UI mode: fullscreen is the default (since v1.10.1)
|
## Terminal UI mode: fullscreen is the default (since v1.10.2)
|
||||||
|
|
||||||
pi **1.0.0** changed the default terminal UI mode to **fullscreen**, and this
|
pi **1.0.0** changed the default terminal UI mode to **fullscreen**, and this
|
||||||
image adopts upstream's default rather than overriding it. Fullscreen draws into
|
image adopts upstream's default rather than overriding it. Fullscreen draws into
|
||||||
@@ -464,7 +464,10 @@ pin the order yourself.
|
|||||||
|
|
||||||
The sidebar auto-hides below 92 terminal columns and keeps the main pane at
|
The sidebar auto-hides below 92 terminal columns and keeps the main pane at
|
||||||
least 64 columns wide, so a narrow terminal degrades to the plain TUI rather
|
least 64 columns wide, so a narrow terminal degrades to the plain TUI rather
|
||||||
than a squeezed one.
|
than a squeezed one. That is **Manual** mode, the default since atelier v0.15.0.
|
||||||
|
`/atelier sidebar auto` switches to **Auto** mode, which collapses the sidebar
|
||||||
|
when space is tight (below 124 columns at the default width) and reopens it at
|
||||||
|
132. Manual width adjustment is disabled in Auto mode.
|
||||||
|
|
||||||
## docker-compose.yml — basic shape
|
## docker-compose.yml — basic shape
|
||||||
|
|
||||||
@@ -879,6 +882,40 @@ If you use Bedrock as pi's LLM provider:
|
|||||||
The pi-toolkit AWS env loader (in `~/.pi/agent/`) prepares Bedrock
|
The pi-toolkit AWS env loader (in `~/.pi/agent/`) prepares Bedrock
|
||||||
inference-profile model IDs (with `eu.` / `us.` prefixes) automatically.
|
inference-profile model IDs (with `eu.` / `us.` prefixes) automatically.
|
||||||
|
|
||||||
|
## Custom model providers (`models.json`)
|
||||||
|
|
||||||
|
pi reads extra providers from `~/.pi/agent/models.json`. That can be any
|
||||||
|
OpenAI-, Anthropic- or Google-compatible endpoint: a company gateway, vLLM,
|
||||||
|
Ollama, LM Studio and so on (pi's
|
||||||
|
[models.md](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/models.md)).
|
||||||
|
`~/.pi` is the `devbox-pi-config` volume. A file you put there by hand survives
|
||||||
|
recreates, but every new volume starts without one: another device, or after
|
||||||
|
`down -v`.
|
||||||
|
|
||||||
|
To share one file across devices, keep it in a host directory and mount that
|
||||||
|
**directory** read-only at `/opt/fleet-pi`. At every start the entrypoint links
|
||||||
|
`~/.pi/agent/models.json` to `/opt/fleet-pi/models.json`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- ./pi-shared:/opt/fleet-pi:ro # a directory containing models.json
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Mount the directory, not the file.** `git pull` and most editors replace a
|
||||||
|
file by rename. A single-file bind mount then keeps showing the old one, or
|
||||||
|
"No such file", until the next recreate. A directory mount shows the new file
|
||||||
|
at once. pi reads `models.json` when it starts, so a pulled change reaches the
|
||||||
|
next pi session without a recreate.
|
||||||
|
- **Keep keys out of the file.** Write `"apiKey": "$MY_GATEWAY_KEY"` in
|
||||||
|
`models.json` and put the value in `.env`, which `env_file:` already passes
|
||||||
|
in. The file then holds no secret and can live in git.
|
||||||
|
- **Nothing of yours is replaced.** A real `~/.pi/agent/models.json`, or a link
|
||||||
|
of your own, is left alone; `docker logs` shows one `NOTE` line saying so. With
|
||||||
|
nothing mounted, nothing happens.
|
||||||
|
- `PI_MODELS_JSON` points at another path, and `PI_MODELS_JSON=off` disables the
|
||||||
|
link. Check with `pi --list-models <provider>`. `recreate-sanity-check.sh`
|
||||||
|
verifies the link whenever a shared file is mounted.
|
||||||
|
|
||||||
## Build pipeline
|
## Build pipeline
|
||||||
|
|
||||||
pi-devbox is built from this repo's CI in two phases:
|
pi-devbox is built from this repo's CI in two phases:
|
||||||
@@ -1161,7 +1198,7 @@ persisted volumes survived, and pi runtime wiring is intact:
|
|||||||
```bash
|
```bash
|
||||||
./scripts/recreate-sanity-check.sh # auto-detects variant
|
./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-image-version 1.8.9 # assert the pi-devbox release tag
|
||||||
./scripts/recreate-sanity-check.sh --expected-version 1.0.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:
|
Those are **two different versions**, and the flags are not interchangeable:
|
||||||
@@ -1200,9 +1237,9 @@ resolved to `latest` at build time:
|
|||||||
|
|
||||||
| Component | Pin | Where |
|
| Component | Pin | Where |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| pi | `1.0.0` | `ARG PI_VERSION` — `Dockerfile.variant` |
|
| pi | `1.2.0` | `ARG PI_VERSION` — `Dockerfile.variant` |
|
||||||
| pi-obsmem | `731c3d49288580f4d79cabdbfbc0d16b34db0f41` | `ARG PI_OBSMEM_REF` — `Dockerfile.variant` |
|
| pi-obsmem | `022da5e5554d04dfdc876ed7dac4a42c6b83dfd4` | `ARG PI_OBSMEM_REF` — `Dockerfile.variant` |
|
||||||
| pi-atelier | `v0.13.0` | `ARG PI_ATELIER_REF` — `Dockerfile.variant` |
|
| pi-atelier | `v0.15.0` | `ARG PI_ATELIER_REF` — `Dockerfile.variant` |
|
||||||
| mempalace | `3.10.0` | `ARG MEMPALACE_VERSION` — `Dockerfile.base` |
|
| mempalace | `3.10.0` | `ARG MEMPALACE_VERSION` — `Dockerfile.base` |
|
||||||
|
|
||||||
The objective is **not** to freeze versions. Bumping is routine — usually one
|
The objective is **not** to freeze versions. Bumping is routine — usually one
|
||||||
|
|||||||
@@ -371,6 +371,16 @@ if command -v pi &>/dev/null; then
|
|||||||
echo "WARN: pi-extensions install.sh failed (continuing)"
|
echo "WARN: pi-extensions install.sh failed (continuing)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Shared custom providers: link ~/.pi/agent/models.json to a models.json in a
|
||||||
|
# read-only mounted DIRECTORY (default /opt/fleet-pi/models.json; PI_MODELS_JSON
|
||||||
|
# overrides, =off disables). Only ever creates a link where nothing exists —
|
||||||
|
# a real file or a link of the user's own is left alone. Without this, every
|
||||||
|
# fresh devbox-pi-config volume needed a manual `ln -s` (2026-10-12: one per
|
||||||
|
# device for the same file). Rationale and cases: the script's header.
|
||||||
|
if [ -r /usr/local/lib/pi-devbox/link-pi-models.sh ]; then
|
||||||
|
bash /usr/local/lib/pi-devbox/link-pi-models.sh || true
|
||||||
|
fi
|
||||||
|
|
||||||
# Bootstrap settings.json from template if absent (pi rewrites this
|
# Bootstrap settings.json from template if absent (pi rewrites this
|
||||||
# file at runtime — lastChangelogVersion, etc — so we can't symlink it).
|
# file at runtime — lastChangelogVersion, etc — so we can't symlink it).
|
||||||
_pi_settings="$HOME/.pi/agent/settings.json"
|
_pi_settings="$HOME/.pi/agent/settings.json"
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# link-pi-models.sh — point ~/.pi/agent/models.json at a shared, mounted file.
|
||||||
|
#
|
||||||
|
# pi reads custom providers (any OpenAI-, Anthropic- or Google-compatible
|
||||||
|
# endpoint: a company gateway, vLLM, Ollama, LM Studio …) from
|
||||||
|
# ~/.pi/agent/models.json. In pi-devbox that directory is the devbox-pi-config
|
||||||
|
# named volume, which makes both obvious ways of providing the file wrong:
|
||||||
|
# - baked into the image, it would only seed a FRESH volume, once;
|
||||||
|
# - copied by hand into each volume, it is one more per-device step, and the
|
||||||
|
# copies drift the first time a price or model id changes.
|
||||||
|
# Instead, keep ONE models.json in a host directory, mount that directory
|
||||||
|
# read-only, and let this script link it in at every container start:
|
||||||
|
#
|
||||||
|
# volumes:
|
||||||
|
# - ./pi-shared:/opt/fleet-pi:ro # a directory containing models.json
|
||||||
|
#
|
||||||
|
# Mount the DIRECTORY, not the file. `git pull` (and most editors) replace a
|
||||||
|
# file by rename, and a single-file bind mount keeps the old inode and shows
|
||||||
|
# "No such file" until the next recreate (measured on Docker Desktop,
|
||||||
|
# 2026-10-12). A directory mount shows the new file at once, and pi re-reads
|
||||||
|
# models.json when it starts, so a pulled change reaches the next pi session.
|
||||||
|
#
|
||||||
|
# PI_MODELS_JSON overrides the path (default /opt/fleet-pi/models.json).
|
||||||
|
# PI_MODELS_JSON=off (or empty) disables this script.
|
||||||
|
#
|
||||||
|
# Never destructive. It only ever creates a link where nothing exists:
|
||||||
|
# - no file at the path → nothing (the case for most users)
|
||||||
|
# - models.json is already our link → nothing
|
||||||
|
# - models.json is a real file → left alone; one NOTE line says why
|
||||||
|
# - models.json links somewhere else → left alone; a deliberate choice
|
||||||
|
# A link this script made whose target later disappears (mount removed) is
|
||||||
|
# harmless: pi treats a dangling models.json exactly like a missing one —
|
||||||
|
# no error, no custom models (measured with pi 1.0.0, 2026-10-12).
|
||||||
|
#
|
||||||
|
# Called by entrypoint-user.sh. Safe to run by hand at any time.
|
||||||
|
set -u
|
||||||
|
|
||||||
|
src="${PI_MODELS_JSON-/opt/fleet-pi/models.json}"
|
||||||
|
case "$src" in
|
||||||
|
"" | off | OFF | Off) exit 0 ;;
|
||||||
|
esac
|
||||||
|
[ -f "$src" ] && [ -r "$src" ] || exit 0
|
||||||
|
|
||||||
|
agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
||||||
|
dest="$agent_dir/models.json"
|
||||||
|
|
||||||
|
if [ -L "$dest" ]; then
|
||||||
|
# Ours already, or someone else's deliberate link: either way, nothing to do.
|
||||||
|
[ "$(readlink "$dest")" = "$src" ] || \
|
||||||
|
echo "NOTE: $dest links to $(readlink "$dest"), not $src; left alone"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ -e "$dest" ]; then
|
||||||
|
echo "NOTE: $dest is a real file; not linking $src (move it away to use the shared one)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$agent_dir" 2>/dev/null || true
|
||||||
|
if ln -s "$src" "$dest" 2>/dev/null; then
|
||||||
|
echo "pi models.json: linked to $src"
|
||||||
|
else
|
||||||
|
echo "WARN: could not link $dest -> $src (continuing)"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
@@ -61,7 +61,7 @@ its skill file needed baking.
|
|||||||
`docker build` records the same thing CI does.
|
`docker build` records the same thing CI does.
|
||||||
|
|
||||||
Verifying "is this snapshot current?" is **not** a CI job and was deliberately
|
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
|
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
|
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,
|
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 |
|
| 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/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) |
|
| `~/.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` |
|
| `/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.
|
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.
|
**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.
|
||||||
|
|||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Guard: Dockerfile.variant must OPEN with its BuildKit `# check=` directive.
|
||||||
|
#
|
||||||
|
# Why this is a gate and not a comment. BuildKit parses `# check=` ONLY before
|
||||||
|
# any other line in the file, so moving it below the title comment — or dropping
|
||||||
|
# it during an unrelated header edit — silently re-enables the
|
||||||
|
# InvalidDefaultArgInFrom warning on `ARG BASE_IMAGE` / `FROM ${BASE_IMAGE}`.
|
||||||
|
#
|
||||||
|
# A re-enabled warning is not cosmetic. buildx attaches the warning's source
|
||||||
|
# context to the build metadata as buildx.build.warnings[].sourceInfo.data: the
|
||||||
|
# entire Dockerfile, base64, on ONE line. docker/build-push-action writes that
|
||||||
|
# metadata to $GITHUB_OUTPUT as a `name<<ghadelimiter_<uuid>` heredoc, and
|
||||||
|
# Gitea's act_runner truncates any single line at exactly 65536 chars. Since
|
||||||
|
# base64(Dockerfile.variant) passed 64 KiB (50034 B source -> 66712 chars, cut
|
||||||
|
# to 65536), the closing delimiter is lost and act_runner fails the job with
|
||||||
|
# invalid format delimiter 'ghadelimiter_...' not found before end of file
|
||||||
|
# while attributing it to NO step: every step reports Success, the smoke suite
|
||||||
|
# prints "0 failed", and the job is red regardless. That cost two tags —
|
||||||
|
# v1.10.0 (run 704, where a stale clipboard assertion masked it) and v1.10.1
|
||||||
|
# (run 707) — and most of a session to localise.
|
||||||
|
#
|
||||||
|
# So: one deterministic grep, run both on push AND in the publish workflow's
|
||||||
|
# lint-gate. A check that gates only `push` lets a tag regress — the same
|
||||||
|
# adoption slip that let doc-drift land 27 h after the v1.9.3 tag.
|
||||||
|
#
|
||||||
|
# Exit codes: 0 OK, 1 violation, 2 cannot-run (missing file) — matching
|
||||||
|
# lint-shell.sh / check-skill-floor.sh / check-doc-drift.sh, because a gate that
|
||||||
|
# cannot run must not pass.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DF="${1:-Dockerfile.variant}"
|
||||||
|
EXPECTED='# check=skip=InvalidDefaultArgInFrom'
|
||||||
|
|
||||||
|
if [ ! -f "$DF" ]; then
|
||||||
|
echo "::error::check-dockerfile-directives: '$DF' not found (cannot-run)" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
first="$(head -n 1 "$DF")"
|
||||||
|
if [ "$first" != "$EXPECTED" ]; then
|
||||||
|
{
|
||||||
|
echo "::error::$DF line 1 must be exactly: $EXPECTED"
|
||||||
|
echo "::error::found instead: ${first:-<empty>}"
|
||||||
|
echo "::error::"
|
||||||
|
echo "::error::BuildKit only honours '# check=' before any other line. Without it the"
|
||||||
|
echo "::error::InvalidDefaultArgInFrom warning returns, buildx embeds this whole file as"
|
||||||
|
echo "::error::base64 in buildx.build.warnings[].sourceInfo.data, that single line exceeds"
|
||||||
|
echo "::error::act_runner's 65536-char cap, and the smoke jobs fail with"
|
||||||
|
echo "::error:: invalid format delimiter 'ghadelimiter_...' not found before end of file"
|
||||||
|
echo "::error::and NO failing step. See the header of $DF for the full measurement."
|
||||||
|
} >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Second, independent assertion: the condition the directive exists FOR. If a
|
||||||
|
# later edit gives ARG BASE_IMAGE a default, the directive becomes dead weight
|
||||||
|
# and should be removed deliberately rather than left to rot — and if the ARG is
|
||||||
|
# renamed, this guard would otherwise keep passing while guarding nothing.
|
||||||
|
if ! grep -qE '^ARG BASE_IMAGE$' "$DF"; then
|
||||||
|
{
|
||||||
|
echo "::error::$DF no longer contains a bare 'ARG BASE_IMAGE' (no default)."
|
||||||
|
echo "::error::That is the only thing '$EXPECTED' suppresses. Either restore the bare ARG"
|
||||||
|
echo "::error::or drop the directive and this guard together — do not leave a skip"
|
||||||
|
echo "::error::directive pointing at a check that can no longer fire."
|
||||||
|
} >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Dockerfile directive guard OK — $DF opens with the check-skip directive and still declares a bare ARG BASE_IMAGE."
|
||||||
@@ -277,6 +277,24 @@ else
|
|||||||
fail "~/.pi/agent/AGENTS.md missing or not a symlink"
|
fail "~/.pi/agent/AGENTS.md missing or not a symlink"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# shared models.json (link-pi-models.sh) — checked only when a shared file is
|
||||||
|
# actually mounted, so a device without one is not judged by it.
|
||||||
|
_mj_src="${PI_MODELS_JSON-/opt/fleet-pi/models.json}"
|
||||||
|
case "$_mj_src" in "" | off | OFF | Off) _mj_src="" ;; esac
|
||||||
|
if [ -n "$_mj_src" ] && [ -f "$_mj_src" ]; then
|
||||||
|
_mj="$HOME/.pi/agent/models.json"
|
||||||
|
if [ -L "$_mj" ] && [ "$(readlink "$_mj")" = "$_mj_src" ]; then
|
||||||
|
pass "~/.pi/agent/models.json -> $_mj_src (shared custom providers)"
|
||||||
|
elif [ -L "$_mj" ]; then
|
||||||
|
warn "~/.pi/agent/models.json links to $(readlink "$_mj"), not the mounted $_mj_src"
|
||||||
|
elif [ -e "$_mj" ]; then
|
||||||
|
warn "~/.pi/agent/models.json is a real file, so the mounted $_mj_src is NOT in use"
|
||||||
|
else
|
||||||
|
fail "$_mj_src is mounted but ~/.pi/agent/models.json is missing (image without link-pi-models.sh? run: ln -s $_mj_src ~/.pi/agent/models.json)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
unset _mj _mj_src
|
||||||
|
|
||||||
# extensions deployed (pi-extensions) — expect ≥4 *.ts
|
# extensions deployed (pi-extensions) — expect ≥4 *.ts
|
||||||
EXT_COUNT=$(ls -1 "$HOME"/.pi/agent/extensions/*.ts 2>/dev/null | wc -l | tr -d ' ')
|
EXT_COUNT=$(ls -1 "$HOME"/.pi/agent/extensions/*.ts 2>/dev/null | wc -l | tr -d ' ')
|
||||||
if [ "$EXT_COUNT" -ge 4 ]; then
|
if [ "$EXT_COUNT" -ge 4 ]; then
|
||||||
@@ -285,6 +303,24 @@ else
|
|||||||
fail "only $EXT_COUNT extensions deployed (expected ≥4)"
|
fail "only $EXT_COUNT extensions deployed (expected ≥4)"
|
||||||
fi
|
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
|
# mempalace.ts bridge symlink
|
||||||
if [ -L "$HOME/.pi/agent/extensions/mempalace.ts" ]; then
|
if [ -L "$HOME/.pi/agent/extensions/mempalace.ts" ]; then
|
||||||
pass "~/.pi/agent/extensions/mempalace.ts bridge symlink"
|
pass "~/.pi/agent/extensions/mempalace.ts bridge symlink"
|
||||||
|
|||||||
+38
-3
@@ -30,7 +30,8 @@
|
|||||||
# client bundle present + registered via `pi install`
|
# client bundle present + registered via `pi install`
|
||||||
# - no foreign npm-11 platform packages (@esbuild, clipboard) beyond the host
|
# - no foreign npm-11 platform packages (@esbuild, clipboard) beyond the host
|
||||||
# - no build-time npm cache (/root/.npm) shipped in the image
|
# - 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
|
# - image size within threshold
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -672,6 +673,8 @@ exec_test "keybindings.json (pi-toolkit)" 'test -L $HOME/.pi/agent/keybi
|
|||||||
exec_test "extensions ≥ 4 (pi-extensions)" 'count=$(ls -1 $HOME/.pi/agent/extensions/*.ts 2>/dev/null | wc -l); [ $count -ge 4 ] && echo "$count extensions"'
|
exec_test "extensions ≥ 4 (pi-extensions)" 'count=$(ls -1 $HOME/.pi/agent/extensions/*.ts 2>/dev/null | wc -l); [ $count -ge 4 ] && echo "$count extensions"'
|
||||||
exec_test "mempalace.ts bridge" 'test -L $HOME/.pi/agent/extensions/mempalace.ts && echo ok'
|
exec_test "mempalace.ts bridge" 'test -L $HOME/.pi/agent/extensions/mempalace.ts && echo ok'
|
||||||
exec_test "settings.json bootstrapped" 'test -f $HOME/.pi/agent/settings.json && echo ok'
|
exec_test "settings.json bootstrapped" 'test -f $HOME/.pi/agent/settings.json && echo ok'
|
||||||
|
exec_test "no shared models.json mounted → none linked" 'test ! -e $HOME/.pi/agent/models.json && test ! -L $HOME/.pi/agent/models.json'
|
||||||
|
exec_test "link-pi-models.sh links a mounted models.json" 'mkdir -p /tmp/lpm && echo "{}" > /tmp/lpm/models.json && PI_MODELS_JSON=/tmp/lpm/models.json PI_CODING_AGENT_DIR=/tmp/lpm/agent bash /usr/local/lib/pi-devbox/link-pi-models.sh >/dev/null && test "$(readlink /tmp/lpm/agent/models.json)" = /tmp/lpm/models.json && rm -rf /tmp/lpm'
|
||||||
exec_test "pi-devbox-environment skill linked" 'test -L $HOME/.agents/skills/pi-devbox-environment && test -f $HOME/.agents/skills/pi-devbox-environment/SKILL.md && echo ok'
|
exec_test "pi-devbox-environment skill linked" 'test -L $HOME/.agents/skills/pi-devbox-environment && test -f $HOME/.agents/skills/pi-devbox-environment/SKILL.md && echo ok'
|
||||||
exec_test "pi-extensions skill linked (fallback)" 'test -L $HOME/.agents/skills/pi-extensions && test -f $HOME/.agents/skills/pi-extensions/SKILL.md && echo ok'
|
exec_test "pi-extensions skill linked (fallback)" 'test -L $HOME/.agents/skills/pi-extensions && test -f $HOME/.agents/skills/pi-extensions/SKILL.md && echo ok'
|
||||||
exec_test "mempalace skill linked (fallback)" 'test -L $HOME/.agents/skills/mempalace && test -f $HOME/.agents/skills/mempalace/SKILL.md && echo ok'
|
exec_test "mempalace skill linked (fallback)" 'test -L $HOME/.agents/skills/mempalace && test -f $HOME/.agents/skills/mempalace/SKILL.md && echo ok'
|
||||||
@@ -983,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
|
# 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
|
# wrong reason on the first machine that ran it. A check must fail only for the
|
||||||
# thing it is checking.
|
# 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
|
# Clipboard is the family pruned second. Upstream pi 0.86.0 (#9163) REPLACED the
|
||||||
# external `@mariozechner/clipboard` dependency with bundled macOS/Windows/X11
|
# external `@mariozechner/clipboard` dependency with bundled macOS/Windows/X11
|
||||||
|
|||||||
Executable
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# test-link-pi-models.sh — two-sided tests for rootfs/usr/local/lib/pi-devbox/link-pi-models.sh.
|
||||||
|
#
|
||||||
|
# Runs the real script against a throwaway HOME / PI_CODING_AGENT_DIR, so it never
|
||||||
|
# touches a live ~/.pi/agent. Every "must link" case has a "must NOT touch" twin:
|
||||||
|
# the script's whole contract is that it only ever creates a link where nothing
|
||||||
|
# exists, so the negative cases are the important half.
|
||||||
|
#
|
||||||
|
# bash scripts/test-link-pi-models.sh # CI (lint.yml) and locally
|
||||||
|
# LINK_PI_MODELS=/path/to/variant.sh bash scripts/test-link-pi-models.sh
|
||||||
|
set -u
|
||||||
|
here="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
SCRIPT="${LINK_PI_MODELS:-$here/rootfs/usr/local/lib/pi-devbox/link-pi-models.sh}"
|
||||||
|
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
ok() { printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); }
|
||||||
|
bad() { printf ' ❌ %s — %s\n' "$1" "$2"; FAIL=$((FAIL+1)); }
|
||||||
|
|
||||||
|
# One fresh world per case: shared dir with models.json, empty HOME, agent dir unset.
|
||||||
|
world() {
|
||||||
|
rm -rf "$T/w"; mkdir -p "$T/w/shared" "$T/w/home"
|
||||||
|
printf '{"providers":{}}\n' > "$T/w/shared/models.json"
|
||||||
|
SRC="$T/w/shared/models.json"; AG="$T/w/home/.pi/agent"; DEST="$AG/models.json"
|
||||||
|
}
|
||||||
|
# run [env assignments...] — run the script with HOME and PI_CODING_AGENT_DIR pointed into the world.
|
||||||
|
# "$@" goes FIRST so an option like `-u PI_MODELS_JSON` is still parsed as one by env.
|
||||||
|
run() { OUT="$(env "$@" HOME="$T/w/home" PI_CODING_AGENT_DIR="$AG" bash "$SCRIPT" 2>&1)"; RC=$?; }
|
||||||
|
|
||||||
|
echo "── link-pi-models.sh ($SCRIPT)"
|
||||||
|
|
||||||
|
world; run PI_MODELS_JSON="$SRC"
|
||||||
|
if [ "$RC" = 0 ] && [ -L "$DEST" ] && [ "$(readlink "$DEST")" = "$SRC" ] && [[ "$OUT" == *linked* ]]; then
|
||||||
|
ok "links models.json when the shared file exists and nothing is there (creates the agent dir)"
|
||||||
|
else bad "link when absent" "rc=$RC link=$(readlink "$DEST" 2>&1) out=[$OUT]"; fi
|
||||||
|
|
||||||
|
run PI_MODELS_JSON="$SRC"
|
||||||
|
if [ "$RC" = 0 ] && [ -z "$OUT" ] && [ "$(readlink "$DEST")" = "$SRC" ]; then
|
||||||
|
ok "second run is a silent no-op (every container start runs it)"
|
||||||
|
else bad "idempotent" "rc=$RC out=[$OUT]"; fi
|
||||||
|
|
||||||
|
world; mkdir -p "$AG"; printf 'my own\n' > "$DEST"; before="$(sha256sum "$DEST")"; run PI_MODELS_JSON="$SRC"
|
||||||
|
if [ "$RC" = 0 ] && [ ! -L "$DEST" ] && [ "$(sha256sum "$DEST")" = "$before" ] && [[ "$OUT" == NOTE*"real file"* ]]; then
|
||||||
|
ok "never replaces a real models.json (content unchanged, one NOTE line)"
|
||||||
|
else bad "real file kept" "rc=$RC islink=$([ -L "$DEST" ] && echo y) out=[$OUT]"; fi
|
||||||
|
|
||||||
|
world; mkdir -p "$AG"; ln -s /somewhere/else.json "$DEST"; run PI_MODELS_JSON="$SRC"
|
||||||
|
if [ "$RC" = 0 ] && [ "$(readlink "$DEST")" = /somewhere/else.json ] && [[ "$OUT" == NOTE*"left alone"* ]]; then
|
||||||
|
ok "never re-points a link the user made to another file"
|
||||||
|
else bad "foreign link kept" "rc=$RC link=$(readlink "$DEST") out=[$OUT]"; fi
|
||||||
|
|
||||||
|
world; rm -f "$SRC"; run PI_MODELS_JSON="$SRC"
|
||||||
|
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ] && [ -z "$OUT" ]; then
|
||||||
|
ok "no shared file → creates nothing and says nothing (the default for most users)"
|
||||||
|
else bad "absent source" "rc=$RC out=[$OUT]"; fi
|
||||||
|
|
||||||
|
world; rm -f "$SRC"; mkdir -p "$SRC"; run PI_MODELS_JSON="$SRC"
|
||||||
|
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ]; then
|
||||||
|
ok "a directory at the path is not a models.json → nothing"
|
||||||
|
else bad "directory source" "rc=$RC out=[$OUT]"; fi
|
||||||
|
|
||||||
|
for v in off OFF ""; do
|
||||||
|
world; run PI_MODELS_JSON="$v"
|
||||||
|
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ] && [ -z "$OUT" ]; then
|
||||||
|
ok "PI_MODELS_JSON='$v' disables it"
|
||||||
|
else bad "disable '$v'" "rc=$RC out=[$OUT]"; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
world; run PI_MODELS_JSON="$SRC"; rm -f "$SRC"; run PI_MODELS_JSON="$SRC"
|
||||||
|
if [ "$RC" = 0 ] && [ -L "$DEST" ] && [ "$(readlink "$DEST")" = "$SRC" ] && [ -z "$OUT" ]; then
|
||||||
|
ok "our link whose target vanished (mount removed) is left as is, silently (pi treats it as no file)"
|
||||||
|
else bad "dangling own link" "rc=$RC out=[$OUT]"; fi
|
||||||
|
|
||||||
|
# pi honours PI_CODING_AGENT_DIR, so the link must land THERE, not in $HOME/.pi/agent.
|
||||||
|
world; AG="$T/w/elsewhere/agent"; DEST="$AG/models.json"; run PI_MODELS_JSON="$SRC"
|
||||||
|
if [ "$RC" = 0 ] && [ "$(readlink "$DEST" 2>/dev/null)" = "$SRC" ] && [ ! -e "$T/w/home/.pi/agent/models.json" ]; then
|
||||||
|
ok "links into PI_CODING_AGENT_DIR when set (and leaves \$HOME/.pi/agent alone)"
|
||||||
|
else bad "PI_CODING_AGENT_DIR" "rc=$RC link=$(readlink "$DEST" 2>&1) out=[$OUT]"; fi
|
||||||
|
|
||||||
|
# 'off' must be a switch, not merely a path that happens not to exist: with a file
|
||||||
|
# literally named "off" in the working directory it must still disable.
|
||||||
|
world; printf '{}\n' > "$T/w/off"; OUT="$(cd "$T/w" && env PI_MODELS_JSON=off HOME="$T/w/home" PI_CODING_AGENT_DIR="$AG" bash "$SCRIPT" 2>&1)"; RC=$?
|
||||||
|
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ]; then
|
||||||
|
ok "PI_MODELS_JSON=off disables it even when a file named 'off' exists in the cwd"
|
||||||
|
else bad "off is a switch" "rc=$RC link=$(readlink "$DEST" 2>&1) out=[$OUT]"; fi
|
||||||
|
|
||||||
|
# The default path is /opt/fleet-pi/models.json. Whether it exists depends on where
|
||||||
|
# this runs (a fleet devbox: yes; CI: no), so derive the expectation, don't assume it.
|
||||||
|
world; run -u PI_MODELS_JSON
|
||||||
|
if [ -f /opt/fleet-pi/models.json ]; then
|
||||||
|
if [ "$RC" = 0 ] && [ "$(readlink "$DEST" 2>/dev/null)" = /opt/fleet-pi/models.json ]; then
|
||||||
|
ok "PI_MODELS_JSON unset → default /opt/fleet-pi/models.json (present here) is linked"
|
||||||
|
else bad "default path (present)" "rc=$RC link=$(readlink "$DEST" 2>&1) out=[$OUT]"; fi
|
||||||
|
else
|
||||||
|
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ] && [ -z "$OUT" ]; then
|
||||||
|
ok "PI_MODELS_JSON unset → default /opt/fleet-pi/models.json (absent here) → nothing"
|
||||||
|
else bad "default path (absent)" "rc=$RC out=[$OUT]"; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "── $PASS passed, $FAIL failed"
|
||||||
|
[ "$FAIL" = 0 ]
|
||||||
Reference in New Issue
Block a user