Compare commits

...

8 Commits

Author SHA1 Message Date
Joakim Persson 12f99c49e3 docs(v1.10.0): adopt pi's fullscreen default and document the way back
Lint / skill-floor (push) Successful in 7s
Lint / hadolint (push) Successful in 15s
Lint / actionlint (push) Successful in 17s
Lint / doc-drift (push) Successful in 14s
Publish Docker Image / lint-gate (push) Successful in 21s
Publish Docker Image / resolve-versions (push) Successful in 10s
Publish Docker Image / base-decide (push) Successful in 9s
Publish Docker Image / build-base (push) Successful in 55m32s
Publish Docker Image / smoke (push) Failing after 8m50s
Publish Docker Image / build-variant (push) Has been skipped
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
Publish Docker Image / smoke-studio (push) Failing after 9m55s
Publish Docker Image / build-variant-studio (push) Has been skipped
Decision: keep pi 1.0.0's default (tuiMode "fullscreen"). No tuiMode is baked,
so the image follows upstream rather than pinning the fleet to either mode -
but "we inherited a changed default" is only acceptable if the revert is
written down, so it now is.

README gains a "Terminal UI mode" section ahead of the pi-atelier section,
because the two interact. It gives all three scopes, taken from pi 1.0.0's own
docs/settings.md and docs/cli.md rather than from the changelog prose:

  - permanent: "tuiMode": "regular" in ~/.pi/agent/settings.json
  - one session: pi --tui-mode regular   (the flag is real: cli.md:221)
  - one project: the same key in .pi/settings.json, which overrides the agent
    directory

It also documents the four related settings (fullscreenExitOutput,
fullscreenScrollbar, fullscreenCopyOnSelect, fullscreenWheelScrollLines) and
two things specific to this image:

  - tmux: fullscreen uses the alternate screen, so tmux copy-mode shows the
    pane history AROUND pi, not pi's transcript. That is the concrete reason
    someone here would want "regular" back.
  - fullscreenWheelScrollLines "auto" behaves differently over SSH (caps fast
    wheel spins at 6 lines/event) than in a local macOS terminal (1 line) -
    worth naming because this container is normally driven over SSH.
  - pi-atelier works in BOTH modes (upstream has handled regular vs fullscreen
    renderers separately since pi 0.84), so reverting costs nothing. Stated so
    nobody assumes the sidebar is the price of the old scrollback.

The python3 merge snippet in that section was RUN before being documented:
against a realistic settings.json under an overridden HOME, twice, confirming
it is idempotent and preserves sibling keys including nested objects and the
_comment fields the seeded file uses. Documented code that has never been
executed is a guess.

DOCKER_HUB.md gets a short version of the same note, because CI reads it from
the TAG and POSTs it to Docker Hub as full_description - a behaviour change
this visible should not require reading the repo to undo.

Gates: doc-drift 23 OK / 0 DRIFT / 0 SKIP / 0 FAIL; base-hash, workflow-shell,
skill-floor, lint-shell all rc=0.
2026-10-02 09:00:51 +02:00
Joakim Persson 93eb3dbca7 docs(v1.10.0): name sqlite3/bc/dc/column in the Hub description tool list
Lint / skill-floor (push) Successful in 7s
Lint / actionlint (push) Successful in 18s
Lint / hadolint (push) Successful in 15s
Lint / doc-drift (push) Successful in 12s
DOCKER_HUB.md is POSTed to Docker Hub as full_description by the
update-description job, and CI reads it from the TAG, not from main. Its
"Modern CLI tooling" list said "Data: jq, yq" while this release adds sqlite3,
bc, dc and column to the base - so the public page would have shipped
incomplete for the whole v1.10.0 cycle and the fix could not land until the
next tag.

Caught by the pre-tag doc sweep rather than by a gate: check-doc-drift.sh
verifies version CLAIMS (pins, Node major) against the build files, but it
cannot know that a hand-written feature list grew stale, because nothing
declares that list's contents. Same failure shape as the v1.9.0 Node 22/24
mismatch that shipped eight releases running.

README carries no equivalent list, so there is nothing to mirror.
2026-10-02 08:20:01 +02:00
Joakim Persson e026f6f65e release(v1.10.0): pi 1.0.0 and pi-atelier v0.13.0, on measured evidence
Lint / hadolint (push) Successful in 10s
Lint / skill-floor (push) Successful in 12s
Lint / actionlint (push) Successful in 19s
Lint / doc-drift (push) Successful in 19s
Renames the unpublished v1.9.5 section to v1.10.0 and adopts the two bumps
that section had deliberately deferred. v1.9.5 was never tagged or published,
so nothing shipped under that name.

A minor, not a patch. v1.9.5 was numbered under the "patch - pi version bumps"
rule, but this release carries a pi MAJOR, a three-minor pi-atelier jump, four
new base packages and a new mailbox feature. The release carrying a 1.0.0
should not be the one numbered as a patch.

pi 0.85.1 -> 1.0.0. v1.9.5 held at 0.87.1 because obsmem's only compatibility
work names Pi 0.87 and no upstream issue mentions 0.99 or 1.0. That reasoning
had a hole worth naming: "no issue mentions 1.0" is an ABSENCE OF A STATEMENT,
not a measurement. So 1.0.0 was measured, against the published npm tarballs
for 0.87.1 and 1.0.0 unpacked side by side:

  - 1.0.0 has NO "### Breaking Changes" section at all. Those belong to 0.87.0,
    0.86.0, 0.84.3, 0.84.0, 0.83.0, 0.80.8, 0.80.7, 0.75.0 - none to anything
    between 0.88 and 1.0.0. The major is a milestone (fullscreen default,
    leaner codemode), not an API break.
  - finishTurn is still in 1.0.0's dist, so the pinned obsmem SHA keeps the API
    it migrated to.
  - All 8 pi.* APIs our extensions call exist in 1.0.0's dist: registerTool,
    registerCommand, registerFlag, getFlag, on, exec, sendMessage,
    sendUserMessage.
  - engines.node >=22.19.0 on both; the image ships 24.x.

The 0.99.2 change that looked fatal and is not: from 0.99.2 the DEFAULT MCP
exposure is `codemode`, so such tools are "neither declared to the model nor
listed" and must be found with searchTools(). That would gut the MemPalace
protocol if MemPalace were a builtin-MCP server. It is not - mempalace.ts and
mcp-loader.ts each run their own MCP client and register tools via
pi.registerTool(), which is why they are named mempalace_search and not
mcp__mempalace__search. Second route: settings.json has neither an mcpServers
block nor an mcp block. Filed upstream under "Changed", not "Breaking", so it
would have been easy to meet the hard way.

pi-atelier v0.10.3 (ed3837b) -> v0.13.0 (34d26f1), closing the gap v1.9.5
flagged as the pi bump's residual risk. peerDependencies are unchanged at pi
>=0.84.0 across v0.10.3/v0.12.1/v0.13.0 - a FLOOR, so not evidence of
anything. What was checked instead: v0.13.0 carries its OWN breaking change
(the entry point no longer exports the internal registry and layout helpers),
which reaches nothing of ours - grepping pi-atelier and registerSidebarPanel
across mempalace-toolkit, pi-devbox, pi-extensions, pi-fork,
pi-observational-memory and pi-studio returns ZERO matches in all six. v0.12.0
removed the showSessionActions setting (0 references here) and moved the
Control Center shortcut Alt+A -> F6 (0 references here, so no doc goes stale).
showSidebarAgent/showSidebarTodos, the only atelier keys README names, are
both still in v0.13.0's README. TuiMainScreen and renderLayoutFrame both still
exist in pi 1.0.0's dist.

PI_ATELIER_VERSION is bumped with PI_ATELIER_REF; it is a separate ARG and the
image label would otherwise have lied.

v0.13.0 is an ANNOTATED tag: `ls-remote --tags` reports the tag object
(dd06971), not the commit (34d26f1). check-doc-drift.sh resolves the commit, so
that is what the CHANGELOG names. The gate caught the wrong SHA on the first
attempt - noted in the CHANGELOG for future bumps.

NOT PROVEN, and stated so acceptance does not mistake it for cleared: grepping
dist shows the SYMBOLS survive, not that their SIGNATURES are unchanged -
necessary, not sufficient. 1.0.0 is four days old and neither obsmem nor
atelier has a commit naming it. Acceptance must prove the obsmem workers CAP
TURNS (peerDeps are *, so a mismatch is silent) and that the atelier sidebar
PAINTS.

User-visible behaviour change, deliberately NOT overridden: 1.0.0 makes the TUI
fullscreen by default, replacing the terminal's normal scrollback.
tuiMode: "regular" restores the old behaviour. No baked default is set, so the
image inherits upstream's choice rather than silently pinning the fleet.

Gates: check-doc-drift 23 OK / 0 DRIFT / 0 SKIP / 0 FAIL; base-hash,
workflow-shell, skill-floor, lint-shell all rc=0.
2026-10-02 08:16:37 +02:00
Joakim Persson 9d0b3dec0b release(v1.9.5): pi 0.87.1 with pi-obsmem pinned to the merged finishTurn fix
Lint / skill-floor (push) Successful in 7s
Lint / hadolint (push) Successful in 14s
Lint / actionlint (push) Successful in 19s
Lint / doc-drift (push) Successful in 12s
v1.9.4 held pi at 0.85.1 because 0.87.0 REMOVED `shouldStopAfterTurn`, which
pi-observational-memory 3.1.4 still used in all three workers. Its peerDeps are
`*`, so nothing refuses at install time and the breakage is silent at runtime --
turn caps ignored, workers losing their specialised prompts. PR #83 fixes that
and merged 2026-09-23.

Re-measured 2026-10-01 across src/agents/{observer,reflector,dropper}/agent.ts,
deliberately reusing the v1.9.4 audit's counting method so the numbers compare:

  e7d77dc (3.1.4, baked in v1.9.4): shouldStopAfterTurn 3, finishTurn 0, systemPrompt 3
  731c3d4 (pinned here)           : shouldStopAfterTurn 0, finishTurn 3, systemPrompt 0

All three workers migrated, and the 0.86.0 AgentContext.systemPrompt reads are
gone too. The coupling is asymmetric and that is why these move in ONE commit:
3.1.4 + 0.87.1 silently ignores turn caps, and 731c3d4 + 0.85.1 breaks the
workers outright, because finishTurn does not exist before 0.87.0.

Pinned to a SHA rather than waiting for a tag, departing from the v1.9.4
instruction to wait for a release: #83 is merged but the newest obsmem tag is
still 3.1.4, cut 2026-09-20, BEFORE the merge. Upstream tags slowly and moves
master often (e7d77dc -> 1529e14 -> 731c3d4 in nine days), so waiting means
holding pi indefinitely. A pinned SHA keeps the property `master` lacks:
rebuilding this tag later produces the same image.

The 40-char form is load-bearing, not pedantry. check-doc-drift.sh recognises a
literal SHA only through a 40-char match, so a 7-char pin would fall through to
its branch-or-tag lookup, fail to resolve, and downgrade pi-obsmem's drift check
to a silent SKIP -- a pin that reads correctly and is no longer verified. Full
gate run after this change: 23 OK, 0 DRIFT, 0 SKIP, 0 FAIL.

0.99.0/0.99.1/0.99.2 and 1.0.0 all exist upstream and are deliberately skipped:
obsmem's only compatibility work names Pi 0.87 (2b1dc1c) and a repo-wide
issue/PR search for 0.99 or 1.0 returns zero matches. 1.0.0 is its own round.

pi-atelier stays at v0.10.3 and is the residual risk. Its peerDeps declare pi
>=0.84.0 -- a floor, satisfied -- on both v0.10.3 and the current v0.12.1, so it
spans this bump. But atelier hooks pi TUI internals that a declared floor does
not protect, and an under-declared floor is exactly what failed to warn anyone
at pi 0.84. Acceptance must confirm the sidebar PAINTS, using the two-sided
check from 0.84.4/0.85.1 that distinguishes "loaded" from "silently absent".

Also here:
- check-doc-drift.sh gains a pi-obsmem pin check. The ref-move check covers the
  same component, but for a pinned SHA it can only answer "upstream did not
  move", never "the table still says what we bake".
- scripts/lint-shell.sh mode 100644 -> 100755. Pre-existing since f25efa0 and
  the only non-executable script in scripts/; it was latent because both CI
  steps call it as `bash scripts/lint-shell.sh`, but it failed rc=126 "bad
  interpreter" when invoked directly. Same dropped-exec-bit signature recorded
  on 2026-09-22, found the same way: by RUNNING it, not by reading a diff.
- Unreleased section renamed to `## v1.9.5 — 2026-10-02`, satisfying the
  release-gate rule that a tag's CHANGELOG must name its own version.
2026-10-02 00:23:28 +02:00
Joakim Persson cb8969ef2c changelog: adopt pi-studio v0.9.61 and mempalace-toolkit 975ab92
Lint / hadolint (push) Successful in 14s
Lint / actionlint (push) Successful in 18s
Lint / doc-drift (push) Failing after 17s
Lint / skill-floor (push) Has been cancelled
Both are floating refs that moved since v1.9.4, so the next tag would bake them
either way; the gate's complaint was that nothing in the repo said so. Naming
them is the fix the gate actually asks for.

mempalace-toolkit 2167a1b -> 975ab92 adds mailbox dormancy: an ask may declare a
`dormant_unless` predicate and is withheld from the ANNOUNCED owed-set while
every condition still matches its baseline. It is backward compatible by
construction -- an ask without the key behaves exactly as before -- and
fail-visible: any predicate that cannot be evaluated announces the ask rather
than hiding it, because the dangerous failure is work that disappears, not a
spurious nag.

pi-studio v0.9.60 -> v0.9.61 is adopted as-is. NOT pinned, deliberately, and the
reason is worth recording: check-doc-drift.sh gives pi-studio kind `studio`,
which resolves the highest semver TAG of the upstream repo and ignores
PI_STUDIO_REF entirely (resolve_studio, ~line 551). Setting
PI_STUDIO_REF=v0.9.61 would therefore not be tracked by the gate -- the moment
upstream tags v0.9.62 the gate would report DRIFT against a value we no longer
bake, i.e. a false positive by construction. Pinning pi-studio is a coherent
thing to want, but it requires moving it from kind `studio` to kind `ref` in the
gate at the same time, which changes release-gate semantics and is a separate
decision from adopting this release.

Still drifting, and left drifting on purpose: pi-obsmem e7d77dc -> 731c3d4.
PI_OBSMEM_REF=master is CI-resolved at build time, master is 4 commits ahead of
the 3.1.4 tag, and the newest tag remains 3.1.4 -- so PR #83 ("Fix Pi 0.87
memory worker compatibility", merged 2026-09-23) is still reachable only from
master. Whether to bake an unreleased master or stay on 3.1.4 and hold pi is a
pin decision with a silent failure mode behind it (obsmem's peerDeps are all
`*`), so it is not being made in a changelog commit.
2026-10-02 00:01:24 +02:00
Joakim Persson 31f36182bf feat(base): add sqlite3, bc, dc and bsdextrautils
Lint / skill-floor (push) Successful in 10s
Lint / doc-drift (push) Failing after 13s
Lint / actionlint (push) Successful in 20s
Lint / hadolint (push) Successful in 21s
sqlite3 is the only one of the four that was genuinely missing rather than
merely absent. MemPalace keeps both the palace and the logstream as SQLite
files (palace/chroma.sqlite3, logstream.sqlite3) and mempalace_status reports a
sqlite_integrity block, so every integrity or forensics check on this fleet has
so far gone through a python3 -c one-liner because the CLI was not in the image.

bc and dc are low value on their own and the CHANGELOG says so plainly: awk,
python3 and perl are all already baked and each is strictly more capable. They
are here because copy-pasted shell snippets assume bc exists. What actually went
wrong on 2026-09-28 was NOT bc's absence: printf '%6.2f' was handed the empty
output of the missing bc and rendered it as a confident "0.00 days" for a figure
that was really 4.81 days. No package fixes that failure mode -- only not taking
a formatted number on trust does.

bsdextrautils ships /usr/bin/column.

Cost MEASURED rather than estimated: 1311 KB total (587 + 236 + 149 + 339) and
ZERO transitive packages. libsqlite3-0, libreadline8t64, zlib1g, libsmartcols1
and libtinfo6 each already report "install ok installed", so nothing new is
pulled under --no-install-recommends.

Package names were verified with apt-cache and dpkg -S on a real trixie host
instead of being inferred, which caught two traps that would each have produced
either a build failure or a silently missing binary: dc is a SEPARATE binary
package from bc on Debian, and column ships in bsdextrautils, not in the
pre-bullseye bsdmainutils where it used to live.

Declined at the same time, recorded so the omission reads as a decision rather
than an oversight: datamash and xsv/csvkit. Python's stdlib csv module handled a
real 7-file Excel-export concatenation that day -- UTF-8 BOM, no trailing
newlines, and bare CR/LF inside quoted fields -- correctly and without them.

Dockerfile.base is in the base hash, so the next tag rebuilds the base (~64 min)
regardless of what else it carries.
2026-10-01 23:31:03 +02:00
joakimp 2278b22ba7 fix(ssh): wire git core.sshCommand to the writable sidecar, and assert it
Lint / hadolint (push) Successful in 13s
Lint / skill-floor (push) Successful in 14s
Lint / doc-drift (push) Successful in 18s
Lint / actionlint (push) Successful in 29s
~/.ssh is commonly bind-mounted READ-ONLY from the host, so a per-host
`ControlPath ~/.ssh/cm/%r@%h:%p` — the standard CGNAT multiplexing recipe, and
correct on the host — resolves inside an unwritable dir in the container. Every
push dies `unix_listener: cannot bind to path ...: Read-only file system`,
behind git's misleading "make sure you have the correct access rights".

setup-lan-access.sh already writes the fix: ~/.ssh-local/config overrides
ControlPath into the writable ~/.ssh-local/cm BEFORE `Include ~/.ssh/config`,
so -F repairs the socket path and keeps every per-host User/Port/IdentityFile.
entrypoint-user.sh now points git at it, guarded on the sidecar existing —
setup-lan-access.sh writes none on native Linux Docker, where -F at a missing
file would break every git-over-ssh call instead of fixing one. An existing
core.sshCommand is left alone (first-wins, as for the three git settings above).

Why code and not another doc line: the remedy was already in the global
AGENTS.md, in pi-devbox-environment SKILL.md §3, in 24 MemPalace drawers from
three devices, and printed verbatim by recreate-sanity-check.sh — and an agent
that had run that script two hours earlier still hit the failure and reinvented
a /tmp/sshcm workaround. A fifth copy was not the missing piece.

Assertions, each where it can actually pass:
- smoke-test.sh: two STATIC greps (wiring line + its [ -r ] guard). `run` uses
  --entrypoint="", so asserting the runtime value there would repeat the v1.8.0
  mistake of an assertion that cannot pass, unvalidated until the next tag.
- smoke-test.sh runtime phase: a BICONDITIONAL — sidecar present => must route
  through it; absent => must be unset. The absent arm is the one CI exercises
  (native Linux runner), so "is set" would have failed CI for a correct image.
- recreate-sanity-check.sh: the runtime assertion, plus an explicit fail for the
  inverted state (set while the sidecar is missing). The permanent "default ssh
  precedence" warning keeps its severity but now states that it is structural and
  can never reach zero, and whether git is wired, unwired, or has no sidecar.

All five arms exercised against the real script before commit; that caught a
defect in the first draft, which reported "git IS wired ... unaffected" about a
state where the sidecar was gone and every git-over-ssh call failed.

Host ~/.ssh/config needs no change: the same line is right on the host and
unusable through a read-only mount, so the fix belongs in the container layer.
2026-09-22 23:08:59 +02:00
joakimp 37fcbfcf04 fix: three v1.9.4-acceptance findings (installer WARN, init sentinel, hash collation)
Lint / hadolint (push) Successful in 10s
Lint / skill-floor (push) Successful in 9s
Lint / actionlint (push) Successful in 20s
Lint / doc-drift (push) Successful in 18s
Unreleased; no pin moves except pi-extensions master 25c1265 -> 143a214.

1. entrypoint-user.sh: first-run sentinel is now config.json, which is what
   `mempalace init` writes. The old test on palace/ (created by mining, not
   init) re-fired "Initializing MemPalace" on every boot of a container that
   never mined locally: v1.9.4 acceptance measured 1 line after one boot, 2
   after a restart. Idempotent, so harmless; the comment lied. This file is a
   base-hash input, so the next tag rebuilds the base (326fb7c03949 predicted
   with the pinned sort below; f40c4b7b103d before).

2. Dockerfile.variant: `git config --system --add safe.directory` for the
   seven root-owned /opt clones (listed, not `*`). Since git 2.35.2 any git
   command in a repo owned by another user fails "dubious ownership"; as
   `developer` that made `git -C /opt/pi-atelier rev-parse` print nothing
   (one false FAIL in the v1.9.4 acceptance) and is the root cause of the
   per-boot "WARN: pi-extensions install.sh failed" (fixed at the source in
   pi-extensions 143a214; this is the belt to that suspender). Verified live
   in a v1.9.3 container: add -> rev-parse prints 25c1265, unset -> fatal
   again; /etc/gitconfig restored to its 5 lines afterwards.

3. docker-publish.yml: `find -print0 | LC_ALL=C sort -z` in base-decide.
   sort collates per locale; identical rootfs hashed to base-f40c4b7b103d
   under C/C.UTF-8 (== run 695) and base-d8df62216a81 under sv_SE/en_US.UTF-8.
   The runner exports LANG=C.UTF-8, so the hash was stable by accident. Pin is
   hash-neutral: pinned sort + HEAD inputs reproduces f40c4b7b103d exactly.
   Per-command prefix only; nobody's locale changes.

CHANGELOG: new `## Unreleased` above v1.9.4 naming pi-extensions 143a214
(check 9 rc=0), the three fixes, and the synlig 3.10.0 hub upgrade that
v1.9.4 listed as still open. One invented URL org (gwpl) caught before commit;
upstream is elpapi42, as Dockerfile.variant:151 says.

Gates: check-doc-drift rc=0, check-base-hash rc=0, lint-shell rc=0 (16 files),
hadolint 2.15.1 rc=0, YAML parses (10 jobs), bash -n rc=0.
2026-09-22 17:20:10 +02:00
11 changed files with 694 additions and 12 deletions
+9 -2
View File
@@ -108,7 +108,14 @@ jobs:
id: compute
run: |
# Hash inputs that determine the base image's contents.
# Order is fixed via `find -print0 | sort -z` for reproducibility.
# Order is fixed via `find -print0 | LC_ALL=C sort -z`. The LC_ALL=C is
# load-bearing: sort collates per locale, and a dictionary locale
# (sv_SE/en_US.UTF-8) orders rootfs differently from byte order, giving
# a different hash for identical content (measured 2026-09-22:
# base-f40c4b7b103d under C/C.UTF-8 vs base-d8df62216a81 under
# sv_SE.UTF-8). The runner ships LANG=C.UTF-8 today, so this pin
# changes nothing now; it stops the hash depending on that accident.
# Predicting base_tag locally MUST use the same prefix on this sort.
# Junk filters: __pycache__/*.pyc and macOS metadata are gitignored
# locally but still picked up by `find rootfs -type f` on a clean CI
# checkout. Exclude them defensively.
@@ -120,7 +127,7 @@ jobs:
! -name '*.pyc' \
! -name '.DS_Store' \
! -name '._*' \
-print0 2>/dev/null | sort -z | xargs -0 cat 2>/dev/null
-print0 2>/dev/null | LC_ALL=C sort -z | xargs -0 cat 2>/dev/null
cat entrypoint.sh entrypoint-user.sh
# mempalace-toolkit is cloned in Dockerfile.base at a ref CI
# resolves to a SHA; fold it in so base_tag changes when the
+331
View File
@@ -11,6 +11,337 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
---
## v1.10.0 — 2026-10-02
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`
failure inside the container, four small base packages, **pi 1.0.0**, and
**pi-atelier v0.13.0**. `entrypoint-user.sh` and `Dockerfile.base` are both in
the base hash, so this tag rebuilds the base (~64 min) regardless of what else
it carries.
A **minor**, not a patch: this was prepared as `v1.9.5` under the "patch — pi
version bumps" rule, but it now carries a pi MAJOR, a three-minor pi-atelier
jump, four new base packages and a new mailbox feature. The release carrying a
1.0.0 should not be the one numbered as a patch.
### Components that move with the next build
| Component | Baked in v1.9.4 | This release | Why |
|---|---|---|---|
| pi | `0.85.1` | **`1.0.0`** | the hold is over: obsmem's `finishTurn` migration shipped, and 1.0.0 ships no breaking change that reaches us (below) |
| pi-obsmem | `e7d77dc` (`master`) | **`731c3d4`** (pinned SHA) | PR #83 is merged but unreleased; pinned so a rebuild is reproducible (below) |
| pi-atelier | `v0.10.3` (`ed3837b`) | **`v0.13.0`** (`34d26f1`) | closes the gap v1.9.5 flagged as the pi bump's residual risk; its own breaking change reaches nothing of ours (below) |
| pi-extensions | `25c1265` | **`143a214`** | `install.sh`: skip hook activation on a clone this user cannot configure (below) |
| mempalace-toolkit | `2167a1b` | **`975ab92`** | mailbox: an ask can declare `dormant_unless`, so deliberately-waiting work stops being announced (below) |
| pi-studio | `v0.9.60` (`e04fc7a`) | **`v0.9.61`** (`641aa32`) | upstream release, adopted as-is; `-studio` variant only |
### Changed
- **pi `0.85.1` → `1.0.0`, and pi-obsmem off `master` onto the pinned SHA
`731c3d4` — in one commit, because neither is safe alone.** v1.9.4 held pi at
0.85.1 because 0.87.0 *removed* `shouldStopAfterTurn`, which
pi-observational-memory 3.1.4 still used; its `peerDependencies` are `*`, so
nothing would refuse at install time and the breakage would be silent at
runtime (turn caps ignored, workers losing their specialised prompts). PR #83
merged 2026-09-23 and fixes exactly that. Re-measured 2026-10-01 across
`src/agents/{observer,reflector,dropper}/agent.ts`, using the same counting
method as the v1.9.4 audit so the two are comparable:
| ref | `shouldStopAfterTurn` | `finishTurn` | `systemPrompt` |
|---|---|---|---|
| `e7d77dc` (3.1.4, baked in v1.9.4) | 3 | 0 | 3 |
| `731c3d4` (pinned here) | 0 | 3 | 0 |
All three workers migrated, and the 0.86.0 `AgentContext.systemPrompt` reads
are gone too. The direction of the coupling is worth stating because it is not
symmetric: 3.1.4 + 0.87.1 ignores turn caps, and `731c3d4` + 0.85.1 breaks the
workers outright, since `finishTurn` does not exist before 0.87.0. They move
together or not at all.
**Pinned to a SHA rather than waiting for a tag**, departing from the v1.9.4
instruction to bump "once #83 has shipped in a release". The newest obsmem tag
is still 3.1.4, cut 2026-09-20 — *before* the merge — and upstream tags slowly
while moving `master` often: `e7d77dc` → `1529e14` → `731c3d4` in the nine days
to 2026-10-01. Waiting for a tag means holding pi indefinitely. The full
40-char form is deliberate: `check-doc-drift.sh` recognises a literal SHA only
via a 40-char match, so a short pin would fall through to its branch-or-tag
lookup and quietly downgrade that component's drift check to a SKIP.
**0.99.x was skipped and 1.0.0 was ADOPTED, because the evidence arrived.**
v1.9.5 was prepared holding at 0.87.1 on the grounds that obsmem's only
compatibility work names Pi 0.87 (`2b1dc1c`) and a repo-wide issue/PR search
for `0.99` or `1.0` returns zero matches. That reasoning had a hole worth
naming: *no issue mentions 1.0* is an absence of a statement, not a
measurement. So 1.0.0 was measured directly, against the published npm
tarballs for 0.87.1 and 1.0.0 unpacked side by side (2026-10-02):
- **1.0.0 has no `### Breaking Changes` section at all.** The changelog's
breaking sections belong to 0.87.0, 0.86.0, 0.84.3, 0.84.0, 0.83.0, 0.80.8,
0.80.7 and 0.75.0 — none to anything between 0.88 and 1.0.0. The major is a
milestone (fullscreen default, leaner codemode), not an API break. The last
break that touched us was 0.87.0's `shouldStopAfterTurn` removal, absorbed
above.
- **`finishTurn` is still in 1.0.0's dist**, so the pinned obsmem SHA keeps
the API it migrated to.
- **All 8 `pi.*` APIs our extensions call exist in 1.0.0's dist** —
`registerTool`, `registerCommand`, `registerFlag`, `getFlag`, `on`, `exec`,
`sendMessage`, `sendUserMessage`, extracted from `mempalace.ts`, the
pi-extensions tree and obsmem's `src/`.
- `engines.node` is `>=22.19.0` on both; the image ships 24.x.
**The 0.99.2 change that looked fatal and is not.** From 0.99.2 the default
MCP `exposure` is `codemode`, meaning such tools are "neither declared to the
model nor listed in the codemode description" and must be found with
`searchTools()`. That would gut the MemPalace protocol — every skill says to
call `mempalace_search` — if MemPalace were a builtin-MCP server. It is not:
`mempalace.ts` and `mcp-loader.ts` each run their own MCP client and surface
tools through `pi.registerTool()`, which is why they are named
`mempalace_search` rather than `mcp__mempalace__search`. Confirmed on a second
route: `settings.json` has neither an `mcpServers` block (builtin,
exposure-governed) nor an `mcp` block. Extension-registered tools are declared
like built-ins, so `exposure` cannot reach them. Noted here because the change
is filed under "Changed", not "Breaking", and would be easy to meet the hard
way.
**What is NOT proven.** Grepping dist shows the symbols survive, not that
their signatures are unchanged — necessary, not sufficient. 1.0.0 is four days
old (published 2026-10-01T19:15Z) and neither obsmem nor atelier has a commit
naming it. Acceptance must prove the obsmem workers **cap turns** (peerDeps
are `*`, so a mismatch is silent at runtime) and that the atelier sidebar
**paints**.
**User-visible behaviour change: the TUI is now fullscreen.** 1.0.0 changed the
default `tuiMode` to `"fullscreen"`, which draws into the terminal's alternate
screen — so the transcript no longer accumulates in the terminal's (or tmux's)
native scrollback. **Upstream's default is adopted deliberately**, not
inherited by omission: no `tuiMode` is baked, so the image follows pi rather
than pinning the fleet to either mode. The way back is documented in README
→ *Terminal UI mode*: `"tuiMode": "regular"` in `~/.pi/agent/settings.json`
for every session, `pi --tui-mode regular` for one, or the same key in a
project's `.pi/settings.json` for one project. That section also covers the
four related `fullscreen*` settings — including `fullscreenWheelScrollLines`,
whose `"auto"` behaviour differs over SSH, which is how this container is
normally driven — and records that pi-atelier's sidebar works in both modes,
so reverting costs nothing.
- **pi-atelier `v0.10.3` (`ed3837b`) → `v0.13.0` (`34d26f1`)** — closes the
two-minor gap v1.9.5 named as the pi bump's residual risk. `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 anything; 1.0.0 satisfies it either way. What
was actually checked:
- **v0.13.0 carries its own breaking change, unrelated to pi:** "the package
entry point now exports only the sidebar contribution protocol
(`registerSidebarPanel`, the ID and request guards, size limits, event
types). The internal registry and layout helpers are no longer exported."
That reaches nothing of ours — grepping `pi-atelier` and
`registerSidebarPanel` across mempalace-toolkit, pi-devbox, pi-extensions,
pi-fork, pi-observational-memory and pi-studio returns **zero matches in all
six**. atelier is a leaf here: it registers its own sidebar, and nothing
consumes its API.
- **Settings churn in the gap, checked against our own tree:** v0.12.0 removed
the `showSessionActions` setting (zero references here) and migrated the
Control Center shortcut Alt+A → F6 (zero references here, so no doc goes
stale). `showSidebarAgent` / `showSidebarTodos`, the only atelier keys README
names, are both still documented in v0.13.0's README.
- The TUI internals atelier patches (`TuiMainScreen`, `renderLayoutFrame`) are
both still present in pi 1.0.0's dist, and atelier's changelog shows it has
handled fullscreen vs regular renderers explicitly since 0.84 — so
fullscreen-by-default is terrain it already covers.
- **Residual:** v0.13.0 is a same-day upstream release (2026-10-02).
Acceptance proves 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.
- **Note for future bumps:** `v0.13.0` is an *annotated* tag, so
`git ls-remote --tags` reports the tag OBJECT (`dd06971`), not the commit.
The commit is `34d26f1`, via `ls-remote 'refs/tags/v0.13.0^{}'` — which is
what `check-doc-drift.sh` resolves and therefore what must be named here.
The gate caught the wrong SHA on the first attempt.
- **`check-doc-drift.sh` now checks the pi-obsmem pin** against README's
version-pin table, the same way it already checks pi, pi-atelier and
mempalace. The ref-move check also covers pi-obsmem, but for a pinned SHA it
can only ever answer "upstream did not move" — never "the table still says what
we bake", which is the claim a reader of the table actually relies on.
### Added
- **Mailbox: an ask can declare its own dormancy (`mempalace-toolkit` `975ab92`)**
— a first-boot acceptance ask is planted deliberately unanswerable, describing
work that becomes possible only when the device is next recreated, and it must
stay owed until then because closing it early to tidy the mailbox is how that
work gets lost. `deriveOwed` could not tell that apart from neglected work, so
such an ask was announced at every session start and every poll for as long as
it was correctly waiting — three days running on `emb-7kj4vr4g`
(2026-09-28 → 2026-10-01) and across three consecutive releases before that.
An ask may now carry a `dormant_unless` predicate (`file_mtime` / `json_field`
against local paths) and is withheld from the *announced* owed-set while every
condition still matches its baseline, returning the moment any differs.
Withheld from the announcement, never from the mailbox: the wake-up injection
still lists dormant asks once per session. Unevaluable predicates announce the
ask — dormancy is proven, never assumed — and an ask without the key behaves
exactly as before. Nothing in the image changes except the bridge the
`MEMPALACE_TOOLKIT_REF=main` clone already tracks.
- **Four base packages: `sqlite3`, `bc`, `dc`, `bsdextrautils`** — requested
2026-09-28 after a session had to reach for `bc` and found it absent. 1311 KB
total, and ZERO transitive packages: `libsqlite3-0`, `libreadline8t64`,
`zlib1g`, `libsmartcols1` and `libtinfo6` are each already in the image
(verified with `dpkg-query`, all five `install ok installed`), so nothing new
is pulled under `--no-install-recommends`. Package names were verified with
`apt-cache` and `dpkg -S` on a real trixie host rather than assumed, which
caught two traps: `dc` is a SEPARATE binary package from `bc` on Debian, and
`column` ships in `bsdextrautils`, not in the pre-bullseye `bsdmainutils`.
`sqlite3` is the one that was genuinely missing rather than merely absent —
MemPalace stores both the palace and the logstream as SQLite files
(`palace/chroma.sqlite3`, `logstream.sqlite3`) and `mempalace_status` reports a
`sqlite_integrity` block, so every integrity check on this fleet had been done
through a `python3 -c` one-liner. `bc`/`dc` are frankly low value on their own
(`awk`, `python3` and `perl` are all already baked and each is strictly more
capable) and are here only because copy-pasted shell snippets assume `bc`
exists; the incident that prompted the request was NOT `bc` being absent but
`printf '%6.2f'` rendering the missing tool's empty output as a confident
`0.00 days` for a figure that was really 4.81 days. Declined at the same time:
`datamash` and `xsv`/`csvkit`, because Python's stdlib `csv` module correctly
handled a real 7-file Excel-export concatenation (BOM, CRLF, embedded newlines
inside quoted fields) without them.
### Fixed
- **`git push` from inside the container dies on a read-only ControlPath, and no
amount of documentation was fixing it** — `entrypoint-user.sh` now sets
`core.sshCommand` to `ssh -F $HOME/.ssh-local/config` when that sidecar exists.
`~/.ssh` is commonly bind-mounted **read-only** from the host, so a per-host
`ControlPath ~/.ssh/cm/%r@%h:%p` — the standard CGNAT multiplexing recipe, and
correct *on the host* — resolves inside an unwritable directory here. Every
push then dies `unix_listener: cannot bind to path ~/.ssh/cm/...: Read-only
file system`, hidden behind git's misleading `Please make sure you have the
correct access rights`, which sends the reader hunting for a key problem that
does not exist. `setup-lan-access.sh` already wrote the writable sidecar, whose
leading `Host *` block overrides `ControlPath` into `~/.ssh-local/cm` and only
*then* `Include`s the user's own config, so `-F` repairs the socket path while
keeping every per-host `User`/`Port`/`IdentityFile`. Measured on tor-ms22
(2026-09-22): `ssh -G -F` yields `user gitea`, `port 22`, `egl_ed25519`
inherited from the host's own block, with a writable ControlPath; a master
socket is created and multiplexing is preserved, which is the whole point of
the host's setting.
The reason this is code and not a doc line: the remedy was already in the
global `AGENTS.md`, in `pi-devbox-environment` SKILL.md §3, in 24 MemPalace
drawers from three devices, **and printed verbatim by
`recreate-sanity-check.sh`** — and an agent that had run that script two hours
earlier still hit the failure and reinvented a `/tmp/sshcm` workaround. A fifth
copy of the text was not the missing piece; removing the need to know was.
The `[ -r ]` guard is load-bearing: `setup-lan-access.sh` writes no sidecar on
native Linux Docker, and `-F` at a missing file would break *every*
git-over-ssh call instead of fixing one. An existing `core.sshCommand` is left
alone, matching the first-wins convention of the three git settings above it.
Host configs need no change — the same line that is right on the host is simply
unusable through a read-only mount, so the fix belongs in the container layer.
- **Boot log: `WARN: pi-extensions install.sh failed (continuing)` on every start
of every device** — pi-extensions `143a214`. `/opt/pi-extensions` is root-owned
and `install.sh` runs as `developer`; git refuses the repo ("dubious
ownership"), `git config --local` fails silently, and the unguarded
`git config core.hooksPath hooks` exited 128 under `set -e`, aborting the
installer one line before `Done`. Damage was nil (all nine extension symlinks
were already created) but the WARN fired on v1.9.3 and v1.9.4 alike and would
have masked a real installer failure. `activate_hooks` now resolves the git dir
and requires a writable `.git/config`, else prints one note and returns 0 —
hooks are for clones you commit from. Measured with the function extracted and
run under `set -euo pipefail`: old function rc=128 on `/opt/pi-extensions` and
on a root-owned throwaway; new function rc=0 on those two, on a dir without
`.git`, and on a clone with `.git/config` mode 444; still activates on a
writable unset clone (config reads back `hooks`) and reports already-active on
a preset one; no root-owned config was touched.
- **`Initializing MemPalace for workspace` re-fired on every boot of a container
that had never mined locally** — `entrypoint-user.sh`. The first-run test
checked `$PALACE_DIR/palace`, but `mempalace init` writes `config.json` and
never creates `palace/` (mining does). v1.9.4 acceptance measured 1
"Initializing" line after one boot and 2 after a restart. Idempotent, so
harmless, but the comment claimed it skipped. Sentinel is now `config.json`
(what init writes; also a 3.10.0 legacy-layout marker). Populated volumes skip
either way.
- **`developer` can now run git in the root-owned `/opt` clones** —
`Dockerfile.variant` adds `git config --system --add safe.directory` for
`/opt/{pi-toolkit,pi-extensions,pi-fork,pi-observational-memory,pi-atelier,mempalace-toolkit,pi-studio}`
(paths listed, not `*`, so the ownership check still protects `/workspace`;
the pi-studio entry is inert in the plain variant). Before: `git -C
/opt/pi-atelier rev-parse --short HEAD` as `developer` printed nothing, which
produced one false FAIL in the v1.9.4 acceptance and an earlier
"dubious ownership" wall on `/opt/mempalace-toolkit`. Mechanism verified live
in a v1.9.3 container (add → `25c1265`, unset → fatal again).
### CI
- **`base-decide` pins the collation of the hash input order:
`find -print0 | LC_ALL=C sort -z`** — `docker-publish.yml`. `sort` collates per
locale, and the order of `rootfs` files under a dictionary locale differs from
byte order, so identical content hashed differently: measured
`base-f40c4b7b103d` under `C` and `C.UTF-8` (== CI run 695) versus
`base-d8df62216a81` under `sv_SE.UTF-8` and `en_US.UTF-8`. The runner image
happens to export `LANG=C.UTF-8`, so the hash was stable by accident and the
comment "order is fixed via `sort -z`" was not true as written. The pin is
hash-neutral today (no spurious base rebuild from this commit; the workflow
file is not a hash input) and removes the dependency on the runner's locale.
Predicting `base_tag` locally must use the same per-command prefix — it is
scoped to that one `sort`, not to the shell, so it leaves `sv_SE.UTF-8`
everywhere else alone.
- **`smoke-test.sh` + `recreate-sanity-check.sh` assert the sidecar wiring, and
the permanent ControlPath warning now says whether it matters** — three
assertions, each placed where it can actually pass. `smoke-test.sh` gets two
**static** greps against the image's `entrypoint-user.sh` (the wiring line, and
the `[ -r ]` guard around it), because `run` executes
`docker run --entrypoint=""`, so the entrypoint never runs there and asserting
the runtime *value* would repeat the v1.8.0 mistake of an assertion that cannot
pass, unvalidated until the next tag. Its runtime-deployment phase gets a
**biconditional**: sidecar present ⇒ `core.sshCommand` must route through it;
sidecar absent ⇒ it must be unset. Both arms are real, and the absent arm is
the one CI exercises, since a Gitea runner is native Linux Docker where
`setup-lan-access.sh` writes no sidecar — so "is set" would have failed CI for a
correct image.
`recreate-sanity-check.sh` gets the runtime assertion proper, with an explicit
`fail` for the inverted state (`core.sshCommand` set while the sidecar is
missing — which breaks every git-over-ssh call). The existing
"default ssh precedence" warning is unchanged in severity but now states its own
scope: it is **structural and can never reach zero** while a bind-mounted
config pins `ControlPath` inside the read-only `~/.ssh`, and it reports whether
git is wired, not wired, or has no sidecar to point at. That matters because the
script's own comment already warned that "a check that fires benignly every time
is one you learn to ignore" — and on 2026-09-22 that is exactly what happened,
with the remedy sitting inside the dismissed line. All five arms were exercised
against the real script before commit; doing so caught a defect in the first
draft, which reported "git IS wired … unaffected" about a state where the
sidecar was gone and every git-over-ssh call failed.
### Still open
- **pi 1.0.0 + pi-observational-memory, now adopted but not yet witnessed** —
the v1.9.4/v1.9.5 blocker (upstream
[PR #83](https://github.com/elpapi42/pi-observational-memory/pull/83),
`shouldStopAfterTurn` → `finishTurn`) is merged and pinned, and 1.0.0 measures
clean against every API we use. What remains is runtime proof, and it cannot
be had before the build: obsmem's `peerDependencies` are `*`, so an
incompatibility is silent — the workers simply stop capping turns. The newest
obsmem *tag* is still 3.1.4 (2026-09-20, before the merge), so this ships on a
pinned master commit; a 3.1.5 that tags the fix would let the pin become a
version again.
- **pi-atelier v0.13.0 and pi 1.0.0 are both days old** — adopted on measured
evidence (no breaking section, symbols present, no importers of the removed
exports), not on anyone's report of running them together. The three
first-boot acceptance asks carry both checks: workers **cap turns**, sidebar
**paints**.
- **synlig hub** — upgraded to mempalace 3.10.0 on 2026-09-22T14:42:53Z (uv
tool, hot backup first, 3 s downtime, 40996 embedding rows before == after).
Client-visible: `event_list` without a cursor now returns newest first;
search results carry `filed_at` / `authored_at_source` / `content_date`.
Recorded here because v1.9.4's "still open" listed it.
---
## v1.9.4 — 2026-09-22
### Dependency audit (2026-09-22)
+3 -1
View File
@@ -56,6 +56,8 @@ Full setup guide — authentication for each provider (Anthropic, OpenAI, Gemini
The entrypoint deploys/registers all of these on first container start. Re-running is idempotent and preserves user edits.
**Terminal UI mode — fullscreen by default.** pi 1.0.0 made the TUI fullscreen, and this image adopts upstream's default. Fullscreen uses the terminal's alternate screen, so the transcript no longer lands in your terminal's (or tmux's) native scrollback. To get the previous behaviour back, set `"tuiMode": "regular"` in `~/.pi/agent/settings.json`, or pass `pi --tui-mode regular` for a single session. The bundled pi-atelier sidebar works in both modes. See the README for the related `fullscreenExitOutput` / `fullscreenScrollbar` / `fullscreenCopyOnSelect` / `fullscreenWheelScrollLines` settings — the last one behaves differently over SSH, which is how this container is usually driven.
### MemPalace (persistent agent memory)
- **MemPalace** + MCP server — semantic search over conversation history, knowledge graph, diary; queryable via 29 `mempalace_*` tools inside pi
@@ -80,7 +82,7 @@ The entrypoint deploys/registers all of these on first container start. Re-runni
- **Editor**: neovim (system-wide `termguicolors` default; bring your own config/plugins), tmux (configured for 0-indexed sessions)
- **Search/nav**: ripgrep, fd, fzf, zoxide
- **Display**: bat, eza, htop, tree
- **Data**: jq, yq
- **Data**: jq, yq, sqlite3, bc/dc, column
- **Help**: tldr (tealdeer — Rust port; run `tldr --update` once to populate cache)
- **Git**: git-lfs, git-crypt, gitleaks (for pre-commit secret scanning)
- **Build**: gcc, g++, make, patch
+40
View File
@@ -162,6 +162,42 @@ ENV DEBIAN_FRONTEND=noninteractive
# /dev/tcp does reachability checks with zero packages
# (verified against gitea.egl.lan:3000). Recorded here so the
# omission reads as a decision rather than an oversight.
# sqlite3 — the `sqlite3` CLI. Added 2026-09-28 at ALC's request. 587 KB.
# This is the one that was genuinely missing rather than merely
# absent: MemPalace keeps BOTH the palace and the logstream as
# SQLite files (palace/chroma.sqlite3, logstream.sqlite3),
# mempalace_status reports a sqlite_integrity block, and every
# integrity or forensics check on this fleet has so far been
# done through a python3 -c one-liner because the CLI did not
# exist in the image.
# bc, dc — arbitrary-precision calculators. 236 KB + 149 KB. SEPARATE
# binary packages on Debian (dc split out of bc before
# bookworm), so both must be named — verified with apt-cache on
# a trixie host, not assumed. Added 2026-09-28. HONEST
# RATIONALE: low value on their own, because awk, python3 and
# perl are all already baked and each is strictly more capable.
# They are here because copy-pasted shell snippets assume bc
# exists. Note what actually went wrong on 2026-09-28, since it
# was NOT bc's absence: `printf '%6.2f'` was handed the empty
# output of the missing bc and rendered it as a confident
# "0.00 days" for a figure that was really 4.81 days. A missing
# tool that formats as a plausible number is worse than one
# that fails loudly, and no package fixes that — only not
# taking a formatted value on trust does.
# bsdextrautils— ships /usr/bin/column. 339 KB. Added 2026-09-28. Confirmed
# with `dpkg -S /usr/bin/column` on trixie rather than inferred
# from the pre-bullseye bsdmainutils name, which is where column
# used to live and is the obvious way to get this wrong.
# MEASURED COST OF ALL FOUR: 1311 KB total and ZERO transitive
# packages — libsqlite3-0, libreadline8t64, zlib1g,
# libsmartcols1 and libtinfo6 are each already present in the
# image (checked with dpkg-query, all five report
# "install ok installed"), so under --no-install-recommends
# nothing new is pulled.
# NOT added — datamash, xsv/csvkit: considered 2026-09-28 and
# declined. Python's stdlib csv module handled a real 7-file
# Excel-export concatenation that day (BOM, CRLF, embedded
# newlines inside quoted fields) correctly and without them.
# python3-yaml — PyYAML. Added 2026-09-10 for precisely the same reason as
# shellcheck above: a gate this repo ALREADY OWNS could not be
# run locally by anyone. scripts/check-workflow-shell.sh — the
@@ -220,6 +256,10 @@ RUN apt-get update && \
ldap-utils \
xxd \
python3-yaml \
sqlite3 \
bc \
dc \
bsdextrautils \
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
+116 -4
View File
@@ -134,7 +134,70 @@ ARG USER_NAME=developer
# contract) and its registerTool calls spread the built-in tools so they
# carry parameter schemas (#9300). 0.86.1 as an intermediate is untested and
# not worth the pty matrix for a stop that #83 will make moot.
ARG PI_VERSION=0.85.1
#
# v1.9.5: 0.85.1 -> 0.87.1, and pi-obsmem moves off `master` to a pinned SHA in
# the SAME commit, because neither is safe alone. 0.87.0 REMOVED
# `shouldStopAfterTurn`, which 3.1.4 still used; the pinned tip uses
# `finishTurn`, which does not exist before 0.87.0. So 3.1.4 + 0.87.1 silently
# ignores turn caps, and the new tip + 0.85.1 breaks the workers outright — the
# pair only works together, exactly as the v1.9.4 note predicted.
# MEASURED 2026-10-01 across src/agents/{observer,reflector,dropper}/agent.ts,
# deliberately the same counting method as the v1.9.4 audit above so the two
# numbers are comparable:
# 3.1.4 (e7d77dc): shouldStopAfterTurn 3, finishTurn 0, systemPrompt 3
# 731c3d4 (pinned) : shouldStopAfterTurn 0, finishTurn 3, systemPrompt 0
# i.e. all three workers migrated, and the 0.86.0 `AgentContext.systemPrompt`
# reads are gone too. #82/#83 merged 2026-09-23.
# WHY A SHA AND NOT A TAG, departing from the v1.9.4 instruction to wait for a
# release: #83 is merged, but the newest obsmem tag is STILL 3.1.4, cut
# 2026-09-20 — before the merge. Upstream tags slowly while moving master often
# (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
# `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.0 went and got the
# 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.
#
# v1.10.0: 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:
# 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,
# 0.80.8, 0.80.7 and 0.75.0 — none to 0.88+..1.0.0. The major is a
# milestone (fullscreen default, leaner codemode), not an API break. The
# last break that touched us was 0.87.0's `shouldStopAfterTurn` removal,
# which v1.9.5 already absorbed.
# 2. `finishTurn` IS STILL IN 1.0.0's dist, so the obsmem SHA pinned below
# keeps the API it migrated to. (`shouldStopAfterTurn`: absent from both
# 0.87.1 and 1.0.0, as expected after its 0.87.0 removal.)
# 3. EVERY pi.* API our extensions call exists in 1.0.0's dist — all 8 of
# registerTool, registerCommand, registerFlag, getFlag, on, exec,
# sendMessage, sendUserMessage, extracted from mempalace.ts, the
# pi-extensions tree and obsmem's src/.
# 4. engines.node is `>=22.19.0` on both; the image ships 24.x.
# THE 0.99.2 CHANGE THAT LOOKED FATAL AND IS NOT: from 0.99.2 the DEFAULT MCP
# `exposure` is `codemode`, i.e. such tools are "neither declared to the model
# nor listed" and must be found with `searchTools()`. That would gut the
# MemPalace protocol if MemPalace were a builtin-MCP server. It is not: both
# mempalace.ts and mcp-loader.ts run their OWN MCP client and surface tools via
# `pi.registerTool()`, which is why they are named `mempalace_search` and not
# `mcp__mempalace__search`. Confirmed on a second route — settings.json has
# neither an `mcpServers` block (builtin, exposure-governed) nor an `mcp` block.
# Extension-registered tools are declared like built-ins, so `exposure` cannot
# reach them.
# WHAT IS NOT PROVEN, stated so acceptance does not mistake this for cleared:
# grepping dist shows the SYMBOLS survive, not that their SIGNATURES are
# unchanged — necessary, not sufficient. 1.0.0 is also four days of upstream old
# (published 2026-10-01T19:15Z) and neither obsmem nor atelier has a commit
# naming it. Acceptance must prove the obsmem workers CAP TURNS (peerDeps are
# `*`, so a mismatch is silent) and that the atelier sidebar PAINTS.
# USER-VISIBLE BEHAVIOUR CHANGE, decided rather than inherited: 1.0.0 makes the
# TUI fullscreen by default, which replaces the terminal's normal scrollback.
# Upstream's default is ADOPTED on purpose and no `tuiMode` is baked here, so
# the image follows pi instead of pinning the fleet to either mode. The revert
# is documented (README -> "Terminal UI mode"): `"tuiMode": "regular"` in
# ~/.pi/agent/settings.json, `pi --tui-mode regular` for one session, or the
# same key in a project's .pi/settings.json.
ARG PI_VERSION=1.0.0
ARG PI_TOOLKIT_REF=main
ARG PI_EXTENSIONS_REF=main
# Repo URLs default to the canonical gitea origin but are overridable so a
@@ -149,7 +212,14 @@ ARG PI_EXTENSIONS_REPO=https://gitea.jordbo.se/joakimp/pi-extensions.git
ARG PI_FORK_REPO=https://github.com/elpapi42/pi-fork.git
ARG PI_FORK_REF=master
ARG PI_OBSMEM_REPO=https://github.com/elpapi42/pi-observational-memory.git
ARG PI_OBSMEM_REF=master
# PINNED to a full 40-char SHA as of v1.9.5, not `master` — see the PI_VERSION
# note above for the measurement and the reasoning. Moves together with
# PI_VERSION by necessity, not by convention. The width matters: 731c3d4 is the
# same commit, but check-doc-drift.sh recognises a literal SHA only via a
# 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
# silent SKIP — a pin that reads fine and is no longer verified.
ARG PI_OBSMEM_REF=731c3d49288580f4d79cabdbfbc0d16b34db0f41
# 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
# pi-studio, for reproducibility plus cache-busting.
@@ -202,9 +272,35 @@ ARG PI_ATELIER_REPO=https://github.com/michaelmjhhhh/pi-atelier.git
# the no-`npm install` reasoning above holds) and peerDependencies are still
# 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.
ARG PI_ATELIER_REF=v0.10.3
#
# v1.10.0: 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
# 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
# checked, 2026-10-02:
# - v0.13.0 CARRIES ITS OWN BREAKING CHANGE, unrelated to pi: "The package
# entry point now exports only the sidebar contribution protocol
# (`registerSidebarPanel`, guards, size limits, event types). The internal
# registry and layout helpers are no longer exported." Harmless HERE only
# because nothing of ours imports them: a grep for `pi-atelier` and
# `registerSidebarPanel` across mempalace-toolkit, pi-devbox,
# pi-extensions, pi-fork, pi-observational-memory and pi-studio returns
# ZERO matches in all six. atelier is a leaf here — it registers its own
# sidebar and no one consumes its API.
# - Settings churn in the gap, checked against our own tree: v0.12.0 REMOVED
# the `showSessionActions` setting (0 references here) and migrated the
# Control Center shortcut Alt+A -> F6 (0 references here, so no doc goes
# stale). `showSidebarAgent` / `showSidebarTodos`, the only atelier keys
# README names, are both still documented in v0.13.0's README.
# - The TUI internals atelier patches (`TuiMainScreen`, `renderLayoutFrame`)
# are both still present in pi 1.0.0's dist, and atelier's changelog shows
# it has handled fullscreen vs regular renderers explicitly since 0.84.
# - 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
# tell "loaded" from "silently absent" — "no crash" is not the test.
ARG PI_ATELIER_REF=v0.13.0
# Human-readable tag PI_ATELIER_REF was resolved from; recorded as a label.
ARG PI_ATELIER_VERSION=v0.10.3
ARG PI_ATELIER_VERSION=v0.13.0
RUN set -e && \
# git_fetch_ref: clone-equivalent helper that accepts EITHER a branch name
@@ -314,6 +410,22 @@ RUN set -e && \
echo "pi-observational-memory at $(cd /opt/pi-observational-memory && git rev-parse --short HEAD)" && \
echo "pi-atelier at $(cd /opt/pi-atelier && git rev-parse --short HEAD) (${PI_ATELIER_VERSION})"
# ── git: let the unprivileged user read the root-owned /opt clones ──────────
# The clones above (and /opt/mempalace-toolkit from the base, /opt/pi-studio in
# the studio variant) are root-owned; the container runs as `developer`. Since
# git 2.35.2 (CVE-2022-24765) any git command in a repo owned by another user
# fails with "dubious ownership" — so `git -C /opt/pi-extensions rev-parse`
# returns nothing, `install.sh` used to abort on it, and acceptance checks that
# read a baked ref via git silently measured "" (v1.9.4 first-boot run: one
# false FAIL from exactly this). Listing the paths (not `*`) keeps the check
# meaningful for everything else, e.g. the virtiofs-mounted /workspace.
# Entries for paths absent in a variant (pi-studio) are inert.
RUN for d in pi-toolkit pi-extensions pi-fork pi-observational-memory pi-atelier \
mempalace-toolkit pi-studio; do \
git config --system --add safe.directory "/opt/${d}"; \
done && \
git config --system --get-all safe.directory
# ── Image-baked skill refresh: pi-extensions (Option 1 over Option 2) ──
# rootfs ships a VENDORED snapshot of the pi-extensions skill at
# /usr/local/share/pi-devbox/skills/pi-extensions/ (the "floor" — guarantees the
+59 -3
View File
@@ -358,6 +358,61 @@ 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.
Note: SVG is **not** in Studio's local-image-link allowlist — use PNG.
## Terminal UI mode: fullscreen is the default (since v1.10.0)
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
the terminal's alternate screen, so pi's transcript no longer accumulates in your
terminal's native scrollback — you scroll inside pi instead, and on exit pi
prints the transcript (`fullscreenExitOutput`).
That is a real behaviour change if you were used to the old mode, so here is the
way back. Nothing in the image is pinned, so all three routes below work:
| Scope | How |
|---|---|
| **Permanently, for every session** | add `"tuiMode": "regular"` to `~/.pi/agent/settings.json` |
| **One session** | `pi --tui-mode regular` |
| **One project only** | add `"tuiMode": "regular"` to that project's `.pi/settings.json` — project settings override the agent directory |
The settings file is plain JSON and `tuiMode` is top-level, so the minimal
permanent change is:
```bash
# merge the key without disturbing the rest of the file (python3 is always present)
python3 - <<'PY'
import json, pathlib
p = pathlib.Path.home() / ".pi/agent/settings.json"
d = json.loads(p.read_text()) if p.exists() else {}
d["tuiMode"] = "regular" # "fullscreen" is pi's default
p.write_text(json.dumps(d, indent=2) + "\n")
PY
```
If you stay on fullscreen, four related settings are worth knowing — all
documented in pi's own `docs/settings.md` under *Terminal and display*:
- `fullscreenExitOutput` — `"transcript"` (default) or `"resume-hint"`: what pi
leaves behind in the terminal when fullscreen exits.
- `fullscreenScrollbar` — `"auto"` (default), `"always"`, `"hidden"`.
- `fullscreenCopyOnSelect` — `true` by default; selecting text copies it.
- `fullscreenWheelScrollLines` — `"auto"` by default. Relevant **here** in
particular: this container is normally driven over SSH, and over SSH `"auto"`
accelerates fast wheel spins to at most 6 lines per event (local macOS
terminals accelerate on their own, so there it moves one line). `Alt`+wheel
moves five times as far.
Two notes specific to this image:
- **tmux.** Fullscreen uses the alternate screen, so `tmux` copy-mode scrollback
shows the pane's history *around* pi, not pi's transcript. Scroll within pi,
or use `"tuiMode": "regular"` if you rely on tmux copy-mode to search the
conversation.
- **pi-atelier.** The bundled sidebar works in both modes — upstream has handled
regular and fullscreen renderers separately since pi 0.84, including
fullscreen divider dragging and keeping sidebar text out of fullscreen
selection — so switching back to `regular` does not cost you the sidebar.
## Using pi-atelier (TUI sidebar)
`pi-atelier` is bundled in **both** variants (vendored at `/opt/pi-atelier`,
@@ -1106,7 +1161,7 @@ persisted volumes survived, and pi runtime wiring is intact:
```bash
./scripts/recreate-sanity-check.sh # auto-detects variant
./scripts/recreate-sanity-check.sh --expected-image-version 1.8.9 # assert the pi-devbox release tag
./scripts/recreate-sanity-check.sh --expected-version 0.85.1 # assert the pi coding agent version
./scripts/recreate-sanity-check.sh --expected-version 1.0.0 # assert the pi coding agent version
```
Those are **two different versions**, and the flags are not interchangeable:
@@ -1145,8 +1200,9 @@ resolved to `latest` at build time:
| Component | Pin | Where |
|---|---|---|
| pi | `0.85.1` | `ARG PI_VERSION` — `Dockerfile.variant` |
| pi-atelier | `v0.10.3` | `ARG PI_ATELIER_REF` — `Dockerfile.variant` |
| pi | `1.0.0` | `ARG PI_VERSION` — `Dockerfile.variant` |
| pi-obsmem | `731c3d49288580f4d79cabdbfbc0d16b34db0f41` | `ARG PI_OBSMEM_REF` — `Dockerfile.variant` |
| pi-atelier | `v0.13.0` | `ARG PI_ATELIER_REF` — `Dockerfile.variant` |
| mempalace | `3.10.0` | `ARG MEMPALACE_VERSION` — `Dockerfile.base` |
The objective is **not** to freeze versions. Bumping is routine — usually one
+36 -1
View File
@@ -108,7 +108,13 @@ if command -v mempalace &>/dev/null && [ -d /workspace ]; then
# own resolution can no longer disagree about where the palace lives — a
# disagreement that would make this branch fire on every start.
PALACE_DIR="${MEMPALACE_CONFIG_DIR:-${HOME}/.mempalace}"
if [ ! -d "$PALACE_DIR/palace" ]; then
# Sentinel = config.json, because that is what `mempalace init` writes.
# It does NOT create palace/ — mining does — so the earlier test on palace/
# re-fired on every start of a container that had never mined locally
# (v1.9.4 acceptance: 1 "Initializing" line after one boot, 2 after a
# restart). Harmless (init is idempotent) but the log lied. Populated
# volumes (config.json present) skip either way.
if [ ! -f "$PALACE_DIR/config.json" ]; then
echo "Initializing MemPalace for workspace (non-interactive)..."
# </dev/null: mempalace init has an interactive "Mine this directory
# now? [Y/n]" prompt that --yes does not auto-answer in all paths.
@@ -313,6 +319,35 @@ fi
if [ -f "$HOME/.gitignore_global" ] && ! git config --global core.excludesFile &>/dev/null; then
git config --global core.excludesFile "$HOME/.gitignore_global"
fi
# Route git-over-ssh through the WRITABLE ssh sidecar. ~/.ssh is commonly
# bind-mounted read-only from the host, and a per-host
# ControlPath ~/.ssh/cm/%r@%h:%p
# inherited from that config (the standard CGNAT multiplexing recipe) kills every
# push with
# unix_listener: cannot bind to path ~/.ssh/cm/...: Read-only file system
# hidden behind git's misleading "Please make sure you have the correct access
# rights", which sends the reader hunting for a key problem that does not exist.
# setup-lan-access.sh (run near the top of this script) already wrote
# ~/.ssh-local/config, whose leading `Host *` block overrides ControlPath into
# the writable ~/.ssh-local/cm and only THEN `Include`s the user's own config —
# so -F repairs the socket path while keeping every per-host User/Port/
# IdentityFile. Wiring it here means no caller has to know any of that.
#
# WHY THIS IS NOT LEFT TO DOCUMENTATION: measured 2026-09-22 on tor-ms22, an
# agent with the remedy in its system prompt, in a loaded skill, in 24 palace
# drawers, AND printed verbatim by recreate-sanity-check.sh two hours earlier
# still hit this and reinvented a /tmp/sshcm workaround. The knowledge was
# available four times over, so a fifth copy is not the fix — removing the need
# to know is.
#
# The [ -r ] guard is load-bearing, not decoration: setup-lan-access.sh only
# writes the sidecar on VM-backed hosts (OrbStack / Docker Desktop). On native
# Linux Docker there is none, and pointing -F at a missing file would break EVERY
# git-over-ssh operation instead of fixing one. Respect a value the user already
# set — same first-wins convention as the three settings above.
if [ -r "$HOME/.ssh-local/config" ] && ! git config --global core.sshCommand &>/dev/null; then
git config --global core.sshCommand "ssh -F $HOME/.ssh-local/config"
fi
# ── pi: deploy toolkit + extensions + mempalace bridge ─────────────
# pi is always installed in pi-devbox; no INSTALL_PI guard needed.
+6
View File
@@ -181,8 +181,14 @@ check_pin() {
PI_ACTUAL="$(read_arg "$DF_VARIANT" PI_VERSION)"
ATELIER_ACTUAL="$(read_arg "$DF_VARIANT" PI_ATELIER_REF)"
MEMPALACE_ACTUAL="$(read_arg "$DF_BASE" MEMPALACE_VERSION)"
# pi-obsmem became a PIN in v1.9.5 (was the floating `master`), so it joins the
# reviewable table. It is also covered by the ref-move check below, but that one
# can only ever report "unchanged" for a pinned SHA -- it answers "did upstream
# move?", never "does the table still say what we bake?", which is this check.
OBSMEM_PIN_ACTUAL="$(read_arg "$DF_VARIANT" PI_OBSMEM_REF)"
check_pin pi "$(read_pin_row pi)" "$PI_ACTUAL" "ARG PI_VERSION in $DF_VARIANT"
check_pin pi-obsmem "$(read_pin_row pi-obsmem)" "$OBSMEM_PIN_ACTUAL" "ARG PI_OBSMEM_REF in $DF_VARIANT"
check_pin pi-atelier "$(read_pin_row pi-atelier)" "$ATELIER_ACTUAL" "ARG PI_ATELIER_REF in $DF_VARIANT"
check_pin mempalace "$(read_pin_row mempalace)" "$MEMPALACE_ACTUAL" "ARG MEMPALACE_VERSION in $DF_BASE"
Regular → Executable
View File
+61 -1
View File
@@ -502,13 +502,33 @@ _ssh_cm_probe() {
elif [ "${#bad[@]}" -eq 0 ]; then
pass "$label: $n ControlMaster host(s), every ControlPath dir exists and is writable"
elif [ "$sev" = "warn" ]; then
warn "$label: ${#bad[@]}/$n host(s) resolve ControlPath to a missing or unwritable dir [$shown] — expected when ~/.ssh/config pins ControlPath inside the read-only ~/.ssh; use 'ssh -F ~/.ssh-local/config' (see Dockerfile.base CAVEAT)"
warn "$label: ${#bad[@]}/$n host(s) resolve ControlPath to a missing or unwritable dir [$shown] — STRUCTURAL and permanent while ~/.ssh/config pins ControlPath inside the read-only ~/.ssh, so this line can never reach zero and is not a to-do; $_git_ssh_note (see Dockerfile.base CAVEAT)"
else
fail "$label: ${#bad[@]}/$n host(s) resolve ControlPath to a missing or unwritable dir [$shown] — ssh dies rc=255 'unix_listener: cannot bind to path' and the remote command never runs"
fi
}
if command -v ssh >/dev/null 2>&1; then
# Whether git-over-ssh already routes through the sidecar decides how much the
# permanent default-route warning below actually matters, so state it IN that
# message rather than leaving each reader to work it out. Asserted properly as
# its own pass/fail in the next section.
#
# THREE states, not two, and the [ -r ] test is why: core.sshCommand NAMING the
# sidecar does not mean the sidecar EXISTS. Without that test, the arm where the
# path is wired but the file is gone printed "git IS wired ... unaffected" about
# a state in which every single git-over-ssh call fails. Found by exercising all
# five arms of this check rather than only the healthy one.
if [ ! -r "$HOME/.ssh-local/config" ]; then
_git_ssh_note="there is no ssh sidecar on this host, so nothing for -F to point at and these hosts cannot multiplex at all — see the git-over-ssh check below"
elif command -v git >/dev/null 2>&1 &&
git config --global --get core.sshCommand 2>/dev/null |
grep -qF -- "-F $HOME/.ssh-local/config"; then
_git_ssh_note="git IS wired to the sidecar (core.sshCommand), so git push/fetch is unaffected; bare 'ssh' to these hosts still needs 'ssh -F ~/.ssh-local/config'"
else
_git_ssh_note="git is NOT wired to the sidecar (see the git-over-ssh check below), so both git and bare 'ssh' need 'ssh -F ~/.ssh-local/config'"
fi
# Concrete Host aliases only: patterns (*, ?) and negations (!) are not
# connectable targets, so `ssh -G` on them proves nothing.
_cm_cfgs=()
@@ -535,6 +555,46 @@ else
warn "ssh not on PATH — effective ControlPath not verified"
fi
echo
echo "-- git-over-ssh routed through the writable ssh sidecar --"
# The one question in this area that is BINARY, FIXABLE, and therefore worth a
# check that can reach zero and stay there.
#
# The ControlPath warning above cannot: while a bind-mounted ~/.ssh/config pins
# ControlPath inside the read-only ~/.ssh, the default route will ALWAYS resolve
# to an unwritable dir, so that line fires benignly on every run of every device
# forever — and the script's own comment above says why that is dangerous: it is
# a warning you learn to skip. MEASURED 2026-09-22, exactly that: an agent ran
# this script, recorded "two by-design warnings", then two hours later hit
# `unix_listener: cannot bind ... Read-only file system` on `git push`, failed to
# connect it to the warning it had already read, and reinvented a /tmp/sshcm
# workaround — while the remedy string sat inside the dismissed warning.
# entrypoint-user.sh now wires core.sshCommand to the sidecar so nobody has to
# know; this section asserts the wiring actually happened, which is the part a
# reader can act on.
if ! command -v git >/dev/null 2>&1; then
warn "git not on PATH — sidecar wiring not verified"
elif [ ! -r "$HOME/.ssh-local/config" ]; then
# No sidecar (native Linux Docker, where setup-lan-access.sh writes none). The
# correct state is UNSET: -F pointing at a missing file breaks every
# git-over-ssh call, which is worse than the problem being solved.
if [ -z "$(git config --global --get core.sshCommand 2>/dev/null || true)" ]; then
pass "no ssh sidecar on this host and core.sshCommand correctly unset (guard holds)"
else
fail "core.sshCommand is set but ~/.ssh-local/config does not exist — every git-over-ssh call dies on a missing -F file; entrypoint-user.sh's [ -r ] guard did not hold"
fi
else
_git_ssh_cmd=$(git config --global --get core.sshCommand 2>/dev/null || true)
case "$_git_ssh_cmd" in
*"-F $HOME/.ssh-local/config"*)
pass "git core.sshCommand routes through the sidecar ($_git_ssh_cmd)" ;;
'')
fail "a sidecar exists but git core.sshCommand is unset — 'git push' to any host whose config pins ControlPath inside the read-only ~/.ssh dies rc=255 behind git's misleading 'correct access rights'. entrypoint-user.sh should set it; expected on images built before that wiring landed, where the fix is: git config --global core.sshCommand \"ssh -F \$HOME/.ssh-local/config\"" ;;
*)
warn "git core.sshCommand set to something else and left alone (first-wins, deliberate): $_git_ssh_cmd" ;;
esac
fi
echo
echo "-- Shell defaults re-seeded from /etc/skel-devbox --"
if [ -f "$HOME/.bash_aliases" ]; then
+33
View File
@@ -250,6 +250,22 @@ run_expect "remote-palace-without-inbox skip is announced, not silent" \
"MemPalace catch-up skipped"
run "...and the skip notice names the variable that fixes it" \
"grep -A6 'MemPalace catch-up skipped' /usr/local/bin/entrypoint-user.sh | grep -q 'MEMPALACE_PI_SSH_TARGET'"
# git-over-ssh must be wired to the writable ssh sidecar, because ~/.ssh is
# commonly bind-mounted READ-ONLY and a per-host `ControlPath ~/.ssh/cm/...`
# inherited from it kills every push with `unix_listener: cannot bind ...:
# Read-only file system` behind git's misleading "correct access rights".
#
# STATIC assertion, deliberately. `run` executes `docker run --entrypoint=""`, so
# entrypoint-user.sh never runs here and `git config --global core.sshCommand` is
# necessarily unset — asserting the VALUE in this harness would repeat the v1.8.0
# mistake (an assertion that cannot pass, unvalidated until the next tag). What
# IS checkable at build time is that the wiring code shipped in the image. The
# runtime value is asserted below in the Runtime deployment section, where the
# real entrypoint chain has run.
run "entrypoint wires git core.sshCommand to the ssh sidecar" \
"grep -q 'core.sshCommand \"ssh -F' /usr/local/bin/entrypoint-user.sh"
run "...and guards it on the sidecar existing (native Linux has none)" \
"grep -B2 'core.sshCommand \"ssh -F' /usr/local/bin/entrypoint-user.sh | grep -q '\\[ -r \"\$HOME/.ssh-local/config\" \\]'"
# A remote mine that FAILS must not report success. MCP answers a hard tool
# failure with HTTP 200 and the tool's own JSON escaped inside
# result.content[].text, so the feeder's old `'\"error\"' in body` check could
@@ -659,6 +675,23 @@ exec_test "settings.json bootstrapped" 'test -f $HOME/.pi/agent/sett
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 "mempalace skill linked (fallback)" 'test -L $HOME/.agents/skills/mempalace && test -f $HOME/.agents/skills/mempalace/SKILL.md && echo ok'
# git-over-ssh sidecar wiring, asserted as a BICONDITIONAL rather than "is set".
# setup-lan-access.sh writes ~/.ssh-local/config only on VM-backed hosts, and a
# CI runner is native Linux Docker — so "core.sshCommand is set" would fail here
# for a correct image, which is precisely the v1.8.0 trap (an assertion whose
# environment was never checked, unvalidated until the next tag). Both arms are
# real: sidecar present => must route through it; sidecar absent => must be UNSET,
# because -F pointing at a missing file breaks every git-over-ssh call and is
# worse than the problem being fixed. This arm is the one CI actually exercises,
# so CI validates the guard; the other is covered by the static greps above and
# by scripts/recreate-sanity-check.sh on a real device.
exec_test "git core.sshCommand matches sidecar presence" '
cmd=$(git config --global --get core.sshCommand 2>/dev/null || true)
if [ -r "$HOME/.ssh-local/config" ]; then
case "$cmd" in *"-F $HOME/.ssh-local/config"*) echo "wired: $cmd" ;; *) exit 1 ;; esac
else
[ -z "$cmd" ] || exit 1; echo "no sidecar, correctly unset"
fi'
# The vendored mempalace snapshot is refreshed MANUALLY per release (see
# rootfs/usr/local/share/pi-devbox/skills/VENDORED.md). Through v1.8.4 it also
# silently SHADOWED the live skillset copy, so staleness was invisible — and the