Compare commits
7 Commits
aa0fbc5ec0
...
v1.8.14
| Author | SHA1 | Date | |
|---|---|---|---|
| 361babd4fd | |||
| 70e675afee | |||
| 601fc98a49 | |||
| 7e0e66997d | |||
| 6bd8b79d3a | |||
| fabf1274aa | |||
| 5972a2c535 |
@@ -87,6 +87,35 @@ SSH_KEY_PATH=~/.ssh
|
|||||||
# MEMPALACE_PI_REMOTE_PATH=/data/feed
|
# MEMPALACE_PI_REMOTE_PATH=/data/feed
|
||||||
# MEMPALACE_PI_DEVICE=
|
# MEMPALACE_PI_DEVICE=
|
||||||
|
|
||||||
|
# ── Mailbox notification: MUST BE NAMED, auto-detect CANNOT work here ──
|
||||||
|
# The mempalace extension polls the logstream for fleet asks addressed to this
|
||||||
|
# device and queues them into the next turn. That part needs no config. The
|
||||||
|
# NOTIFICATION that tells the human it happened does, and unset means SILENT
|
||||||
|
# outside the pi TUI.
|
||||||
|
#
|
||||||
|
# Why there is no working default: terminal identity lives in env vars set by
|
||||||
|
# the emulator (KITTY_WINDOW_ID, TERM_PROGRAM) and `docker exec` does NOT
|
||||||
|
# forward them — inside the container pi sees only TERM=xterm-256color no matter
|
||||||
|
# what is rendering it. So "desktop" auto-detection always falls through to
|
||||||
|
# OSC 777, which Kitty does not implement, and the notification silently does
|
||||||
|
# nothing: the worst outcome for a feature whose only job is to break a silence.
|
||||||
|
# Naming the protocol is what makes it fire.
|
||||||
|
#
|
||||||
|
# kitty OSC 99 desktop notification (correct for Kitty, incl. over SSH)
|
||||||
|
# osc777 OSC 777 (tmux/iTerm2/foot and others)
|
||||||
|
# desktop OSC 99 if KITTY_WINDOW_ID is visible, else OSC 777 — inside a
|
||||||
|
# container that means effectively always OSC 777, so prefer naming
|
||||||
|
# 0 / off suppress entirely (in-TUI notify still shows)
|
||||||
|
# MEMPALACE_MAILBOX_NOTIFY=kitty
|
||||||
|
#
|
||||||
|
# Cadence, if the delivery ever feels late: the poll is coupled to session
|
||||||
|
# activity (it runs when the agent settles), NOT to a wall clock.
|
||||||
|
# MEMPALACE_MAILBOX_POLL_MS is therefore a FLOOR BETWEEN POLLS (default 300000),
|
||||||
|
# not a promise of one every 5 minutes — an idle session polls zero times, and
|
||||||
|
# session start does the first look.
|
||||||
|
# MEMPALACE_MAILBOX_POLL_MS=300000
|
||||||
|
# MEMPALACE_MAILBOX_RESURFACE_MS=3600000
|
||||||
|
|
||||||
# ── LAN access from the container (host-OS-agnostic) ─────────────────
|
# ── LAN access from the container (host-OS-agnostic) ─────────────────
|
||||||
# On VM-backed hosts (macOS OrbStack / Docker Desktop) the container can't
|
# On VM-backed hosts (macOS OrbStack / Docker Desktop) the container can't
|
||||||
# reach the host's directly-attached LAN peers by default. The entrypoint
|
# reach the host's directly-attached LAN peers by default. The entrypoint
|
||||||
|
|||||||
@@ -157,7 +157,41 @@ jobs:
|
|||||||
# buildcache silently reuses the layer from whatever pi version was
|
# buildcache silently reuses the layer from whatever pi version was
|
||||||
# current when the cache was first populated. Same class of bug as
|
# current when the cache was first populated. Same class of bug as
|
||||||
# pi-devbox v0.74.0..v0.75.5 (fixed in v0.75.5b 2026-05-23).
|
# pi-devbox v0.74.0..v0.75.5 (fixed in v0.75.5b 2026-05-23).
|
||||||
|
# ── release gate ──────────────────────────────────────────────
|
||||||
|
# Refuse to spend a base build on a tree whose own shell scripts do not lint.
|
||||||
|
#
|
||||||
|
# v1.8.14's first attempt is why this exists. smoke and smoke-studio both failed
|
||||||
|
# at scripts/smoke-test.sh:770 AFTER build-base had already spent ~46 minutes,
|
||||||
|
# on a defect shellcheck had flagged as SC2289 (severity error) a day earlier:
|
||||||
|
# the lint workflow went red on the very push that introduced it (run 186) and
|
||||||
|
# stayed red for runs 187 and 188, unread.
|
||||||
|
#
|
||||||
|
# lint.yml deliberately does not run on tag pushes, and its reasoning is sound
|
||||||
|
# (the tagged tree was already linted on main; a tag-ref lint run sorts above
|
||||||
|
# the publish run and makes a release look finished before anything ships). The
|
||||||
|
# missing invariant was never "lint the tag" -- it was "do not RELEASE a tree
|
||||||
|
# whose lint failed", and only a job inside THIS workflow can enforce that.
|
||||||
|
#
|
||||||
|
# ~40 s, ahead of everything expensive, and it runs scripts/lint-shell.sh --
|
||||||
|
# the same file lint.yml calls, not a second copy that drifts.
|
||||||
|
lint-gate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install shellcheck
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --no-install-recommends shellcheck
|
||||||
|
|
||||||
|
- name: "Shellcheck + syntax-check repository scripts (severity: error)"
|
||||||
|
run: bash scripts/lint-shell.sh
|
||||||
|
|
||||||
resolve-versions:
|
resolve-versions:
|
||||||
|
# Gated: a defective tree must not reach a 46-minute base build.
|
||||||
|
needs: [lint-gate]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -543,7 +577,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
EXPECTED_MEMPALACE_VERSION: ${{ needs.resolve-versions.outputs.mempalace_version }}
|
EXPECTED_MEMPALACE_VERSION: ${{ needs.resolve-versions.outputs.mempalace_version }}
|
||||||
run: bash scripts/smoke-test.sh pi-devbox:smoke
|
run: |
|
||||||
|
# Single source of truth for the node major is Dockerfile.base's ARG.
|
||||||
|
# Asserting the BUILT image matches it also catches a stale cached layer.
|
||||||
|
EXPECTED_NODE_MAJOR=$(sed -n 's/^ARG NODE_VERSION=\([0-9][0-9]*\).*/\1/p' Dockerfile.base)
|
||||||
|
export EXPECTED_NODE_MAJOR
|
||||||
|
bash scripts/smoke-test.sh pi-devbox:smoke
|
||||||
|
|
||||||
# ── Phase 3b: amd64 smoke for the studio variant ────────────────────
|
# ── Phase 3b: amd64 smoke for the studio variant ────────────────────
|
||||||
# Additive + independent of the core `smoke` job: gates ONLY
|
# Additive + independent of the core `smoke` job: gates ONLY
|
||||||
@@ -606,7 +645,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
EXPECTED_MEMPALACE_VERSION: ${{ needs.resolve-versions.outputs.mempalace_version }}
|
EXPECTED_MEMPALACE_VERSION: ${{ needs.resolve-versions.outputs.mempalace_version }}
|
||||||
run: bash scripts/smoke-test.sh pi-devbox:smoke-studio
|
run: |
|
||||||
|
# Single source of truth for the node major is Dockerfile.base's ARG.
|
||||||
|
# Asserting the BUILT image matches it also catches a stale cached layer.
|
||||||
|
EXPECTED_NODE_MAJOR=$(sed -n 's/^ARG NODE_VERSION=\([0-9][0-9]*\).*/\1/p' Dockerfile.base)
|
||||||
|
export EXPECTED_NODE_MAJOR
|
||||||
|
bash scripts/smoke-test.sh pi-devbox:smoke-studio
|
||||||
|
|
||||||
# ── Phase 4: multi-arch publish ─────────────────────────────────────
|
# ── Phase 4: multi-arch publish ─────────────────────────────────────
|
||||||
build-variant:
|
build-variant:
|
||||||
|
|||||||
@@ -75,31 +75,11 @@ jobs:
|
|||||||
# are shell scripts with no extension. -print0/mapfile -d '' so a path
|
# are shell scripts with no extension. -print0/mapfile -d '' so a path
|
||||||
# with a space cannot silently split, and the file count is asserted
|
# with a space cannot silently split, and the file count is asserted
|
||||||
# non-zero — a green tick over an empty file set is not a check.
|
# non-zero — a green tick over an empty file set is not a check.
|
||||||
run: |
|
#
|
||||||
# Union of two signals, because either alone misses a real case:
|
# The implementation moved to scripts/lint-shell.sh on 2026-09-08 so the
|
||||||
# a shebang scan misses a sourced fragment with no shebang, and a
|
# release gate in docker-publish.yml runs the SAME code rather than a
|
||||||
# *.sh glob misses the extensionless tools in rootfs/usr/local/bin/.
|
# second copy that drifts. Edit the script, not a copy of it.
|
||||||
# Silent skipping is precisely the failure mode this gate exists to
|
run: bash scripts/lint-shell.sh
|
||||||
# prevent, so err toward over-collecting.
|
|
||||||
mapfile -d '' -t all_files < <(find . -not -path './.git/*' -type f -print0)
|
|
||||||
sh_files=()
|
|
||||||
for f in "${all_files[@]}"; do
|
|
||||||
case "$f" in *.sh) sh_files+=("$f"); continue;; esac
|
|
||||||
if head -n1 "$f" 2>/dev/null | grep -qE '^#!.*\b(bash|sh)\b'; then
|
|
||||||
sh_files+=("$f")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "Checking ${#sh_files[@]} shell file(s)"
|
|
||||||
if [ "${#sh_files[@]}" -eq 0 ]; then
|
|
||||||
echo "::error::no shell files found — the shebang scan or the checkout is wrong"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
shellcheck -S error -f gcc "${sh_files[@]}"
|
|
||||||
rc=0
|
|
||||||
for f in "${sh_files[@]}"; do
|
|
||||||
bash -n "$f" || { echo "::error file=$f::bash -n failed"; rc=1; }
|
|
||||||
done
|
|
||||||
exit "$rc"
|
|
||||||
|
|
||||||
- name: Gitea shell guard (catches the actionlint blind spot)
|
- name: Gitea shell guard (catches the actionlint blind spot)
|
||||||
# actionlint models GitHub Actions, where the default run shell is
|
# actionlint models GitHub Actions, where the default run shell is
|
||||||
|
|||||||
+170
-3
@@ -11,6 +11,167 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v1.8.14 — 2026-09-08
|
||||||
|
|
||||||
|
> **First release attempt failed; fixed in this same entry.** The `smoke` and
|
||||||
|
> `smoke-studio` jobs both failed at `scripts/smoke-test.sh:770` with
|
||||||
|
> `agent-browser: command not found`, after `build-base` had already succeeded
|
||||||
|
> (~46 min spent). Root cause was in the agent-browser execution guard added the
|
||||||
|
> day before: the explanatory comment inside the **single-quoted** `exec_test`
|
||||||
|
> body contained an apostrophe (`the fleet\'s`). Inside `'...'` bash treats a
|
||||||
|
> backslash literally, so `\'` does not escape — it **closes the string**. The
|
||||||
|
> body silently truncated (measured: `exec_test` received **12** arguments
|
||||||
|
> instead of 2), and the remaining lines, including the `agent-browser --version`
|
||||||
|
> assertion, were parsed by the **runner's** shell instead of executing inside
|
||||||
|
> the image — and the runner has no agent-browser. The prose now lives above the
|
||||||
|
> call, where an apostrophe is harmless.
|
||||||
|
>
|
||||||
|
> **The lint job had already caught this, and it went unread for 24 hours.**
|
||||||
|
> `shellcheck` flagged it as `SC2289` at severity *error*, so the `actionlint`
|
||||||
|
> job went red at run 186 on 2026-09-07 21:21 — the exact push that introduced
|
||||||
|
> the guard — and stayed red for runs 187 and 188. `lint.yml` deliberately
|
||||||
|
> excludes tag pushes (documented: the tagged tree was already linted on main,
|
||||||
|
> and a tag-ref lint run would sort above the publish run), which is sound; the
|
||||||
|
> broken assumption was different, namely that a tree whose lint FAILED would not
|
||||||
|
> then be released. `docker-publish.yml` has no dependency on lint, so it built
|
||||||
|
> for 50 minutes on a tree known to be defective.
|
||||||
|
>
|
||||||
|
> **Fixed, then gated.** The prose moved above the `exec_test` call so an
|
||||||
|
> apostrophe cannot terminate anything, and the shell-lint logic moved out of
|
||||||
|
> `lint.yml` into **`scripts/lint-shell.sh`** — now called by both `lint.yml` and
|
||||||
|
> a new `lint-gate` job here that `resolve-versions` depends on. A release with a
|
||||||
|
> lint error refuses in ~40 s instead of failing after fifty minutes. One copy,
|
||||||
|
> not two: a duplicated check that drifts is the failure this repo keeps paying
|
||||||
|
> for. The script also refuses to pass when `shellcheck` is absent, inheriting
|
||||||
|
> the existing principle that a gate which cannot run must not pass.
|
||||||
|
>
|
||||||
|
> **`v1.8.14` was re-pointed** from `601fc98` to the fix commit. Nothing had
|
||||||
|
> consumed the original tag — no `v1.8.14` image was ever published, only the
|
||||||
|
> content-addressed `base-a365dd24de21`. `scripts/` does not feed the base hash,
|
||||||
|
> so the re-run reuses that base and skips the 46-minute rebuild.
|
||||||
|
|
||||||
|
**A test that was quietly checking nothing, and a version number that was wrong.**
|
||||||
|
Both found by delegating a read-only audit of this repo to a headless worker
|
||||||
|
(`pi-toolkit` `bin/pi-task`) and then spot-checking its pointers from the
|
||||||
|
filesystem — 5 of 5 held, and it also corrected a false premise planted in its
|
||||||
|
own brief.
|
||||||
|
|
||||||
|
**The node major is now asserted, not merely printed.**
|
||||||
|
`scripts/smoke-test.sh` ran `run "node" "node --version"`, which asserts only
|
||||||
|
that the binary exists and exits 0 — the printed version was compared to
|
||||||
|
nothing. The line above it has always used `run_expect` against
|
||||||
|
`$EXPECTED_PI_VERSION` for `pi`, so the suite *looked* like it covered node.
|
||||||
|
**A node major bump would have passed the whole smoke suite silently.** Worse,
|
||||||
|
this is where the "node v22.23.2 verified" line in the v1.8.13 recreate notes
|
||||||
|
came from: printed output, not an assertion — an expectation stated up front and
|
||||||
|
then falsified by the check.
|
||||||
|
|
||||||
|
Now gated on `EXPECTED_NODE_MAJOR`, which CI derives from `Dockerfile.base`'s
|
||||||
|
`ARG NODE_VERSION` — the single source of truth, and the *only* hard node pin in
|
||||||
|
the repo (`Dockerfile.variant` has no node install at all, so the two Dockerfiles
|
||||||
|
cannot disagree). That also catches a stale cached layer whose node disagrees
|
||||||
|
with the declared ARG. Unset ⇒ previous behaviour, so nothing breaks for anyone
|
||||||
|
running the suite by hand.
|
||||||
|
|
||||||
|
Verified two-sided, because a silent failure here reintroduces the exact bug it
|
||||||
|
fixes: the `sed` derivation yields `22` (an empty result would disable the
|
||||||
|
assertion silently); `grep -Fq "v22."` matches `v22.23.2`; `"v24."` does **not**
|
||||||
|
match, so a wrong major is caught; `"v2."` does not prefix-collide. The workflow
|
||||||
|
YAML was re-parsed after editing (9 jobs).
|
||||||
|
|
||||||
|
**v1.8.13's agent-browser version was wrong.** That entry said "the image's own
|
||||||
|
0.35.2". The image ships **0.36.0** — `/usr/lib/node_modules/agent-browser` at
|
||||||
|
0.36.0 with `engines.node >=24.0.0`, and no 0.35.2 exists anywhere in the image.
|
||||||
|
The sentence was also internally incoherent, contrasting 0.36.0 against a version
|
||||||
|
that is not present. Corrected in place with a visible note, since that entry is
|
||||||
|
already released. **The reasoning survives untouched**: the engines floor really
|
||||||
|
is vestigial, because `/usr/bin/agent-browser` is a prebuilt aarch64 ELF invoked
|
||||||
|
directly and never through node — which is exactly why 0.36.0 runs fine on
|
||||||
|
22.23.2, consistent with the runtime proof collected on 2026-09-07 and with the
|
||||||
|
retraction of the earlier false "0.36.0 requires node >= 24" alert.
|
||||||
|
|
||||||
|
No image content changes: `NODE_VERSION` still 22, no pins moved. This is a test
|
||||||
|
and a docs correction only.
|
||||||
|
|
||||||
|
**The same bug class, twice in one file — and the second one was throwing away a
|
||||||
|
proof the fleet cannot obtain any other way.** `scripts/smoke-test.sh`'s
|
||||||
|
agent-browser guard captured the version *inside an `echo`, with `2>/dev/null`*:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
echo "resolved=[$r] version=[$(agent-browser --version 2>/dev/null | head -n1)]" >&2
|
||||||
|
```
|
||||||
|
|
||||||
|
The exit code was discarded, so a binary that could not execute at all still
|
||||||
|
**passed**, printing `version=[]`. Verified two-sided: a stub exiting 127 passes
|
||||||
|
the old form and is caught by the new one.
|
||||||
|
|
||||||
|
Why that exit code matters more than most: smoke runs `platforms: linux/amd64` on
|
||||||
|
an x86 runner, i.e. **native amd64**, making this line the fleet's only recurring
|
||||||
|
amd64 runtime proof for agent-browser's `linux-x64` ELF. **No devbox can ever
|
||||||
|
supply one** — every machine in the pi fleet is an Apple Silicon Mac
|
||||||
|
(`mbp-m1-2020`; `tor-ms22` = Mac Studio `Mac13,1` M1 Max, verified 2026-08-17 by
|
||||||
|
`system_profiler`; `emb-7kj4vr4g` = Apple Silicon, verified 4 ways 2026-09-07).
|
||||||
|
The "amd64 runtime proof still needed" item that was sent to two devices was
|
||||||
|
therefore asking for the impossible, while CI already had the answer and was
|
||||||
|
discarding it. `Dockerfile.base:607` does assert it (`agent-browser --version &&`),
|
||||||
|
but only when the base actually rebuilds — and v1.8.13's base was cached.
|
||||||
|
|
||||||
|
**The mailbox now announces replies that CLOSE your own asks.** `mempalace-toolkit`
|
||||||
|
`21023e7` → `e45f6b4`, which adds `deriveClosed()` alongside `deriveOwed()`. The old
|
||||||
|
path queried `status: open` and joined for a reply, which by construction can only
|
||||||
|
surface asks *you owe someone else*; a terminal reply carries `status: applied`
|
||||||
|
(or `blocked`/`failed`), so **the answer to your own question was structurally
|
||||||
|
invisible** — the one notification a human actually wants. Measured: `emb-7kj4vr4g`
|
||||||
|
closed the v1.8.13 rollout ask at 18:31Z with `status=applied`, the operator
|
||||||
|
reasonably expected to hear about it, and the mailbox stayed silent while being
|
||||||
|
correct by its own definition. Nine closed correlations were sitting unannounced.
|
||||||
|
Shares the 1-hour resurface floor, so a close is announced once and is news rather
|
||||||
|
than a nag.
|
||||||
|
|
||||||
|
This lands **because the base rebuilds**, which is worth stating explicitly: the
|
||||||
|
CI-resolved `mempalace-toolkit` SHA is folded into the content-addressed base tag
|
||||||
|
(`base-decide`), precisely so a toolkit-only fix cannot silently fail to land
|
||||||
|
behind an unchanged `Dockerfile.base`. The floating `main` ref was left alone on
|
||||||
|
purpose — the toolkit moving *forces* the rebuild rather than waiting for one.
|
||||||
|
|
||||||
|
**A consequence worth noting for the amd64 item above: this release actually
|
||||||
|
collects that proof.** v1.8.13's base was cached, which is why
|
||||||
|
`Dockerfile.base:607`'s `agent-browser --version &&` never ran. v1.8.14's base is
|
||||||
|
not cached, so both that assertion and the new `EXPECTED_NODE_MAJOR` gate execute
|
||||||
|
on a native `linux/amd64` runner. The fleet's first *kept* amd64 runtime proof for
|
||||||
|
the `linux-x64` ELF should be an artefact of this build rather than something
|
||||||
|
asked of a device that cannot supply it.
|
||||||
|
|
||||||
|
**Subtask delegation is documented — including the rung nobody built.** The image
|
||||||
|
picks these up through their resolved refs (`pi-toolkit` `adfb553`,
|
||||||
|
`pi-extensions` `c64c122`, the latter also refreshing the baked fallback skill):
|
||||||
|
|
||||||
|
- an operator-facing decision guide in pi-toolkit's `README.md`, built on the
|
||||||
|
L0–L4 context ladder — how much of the parent session a child can see is the
|
||||||
|
axis that explains nearly every observed good and bad behaviour;
|
||||||
|
- the canonical `pi-extensions` skill gains the same ladder next to *Boundary
|
||||||
|
discipline*, which until now diagnosed why an inherited transcript defeats a
|
||||||
|
brief without offering any alternative to "don't fork that";
|
||||||
|
- one bullet in the global `AGENTS.md`, so the choice is visible without loading
|
||||||
|
a skill, and naming `pi-task` as a **CLI** — an agent hunting for a `pi_task`
|
||||||
|
tool finds none and concludes it is unavailable.
|
||||||
|
|
||||||
|
What the ladder records: **L0/L1/L2 exist** in `pi-task` (`context.facts` /
|
||||||
|
`.files` / `.commands`), **L4** is `fork`'s only behaviour (`getHeader()` +
|
||||||
|
`getBranch()`, no offset or limit anywhere in the call chain), and **L3** — a
|
||||||
|
truncated branch — **is not implemented by anything**, which is now written down
|
||||||
|
instead of being a design idea somebody remembers.
|
||||||
|
|
||||||
|
Also recorded, found while writing the above: `pi-fork/src/runner.ts:188` reads
|
||||||
|
`if (extensions !== null) args.push("--no-extensions")`. So `extensions: []` turns
|
||||||
|
the capability floor **on** and `null` turns it **off** — and `null` is the
|
||||||
|
documented way to "restore normal extension loading", so tidying `[]` to `null`
|
||||||
|
as a no-op re-arms palace writes inside every fork child. `pi-task` hardcodes the
|
||||||
|
flag and cannot drift this way. Documented in three places because the edit that
|
||||||
|
triggers it looks harmless.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.8.13 — 2026-09-06
|
## v1.8.13 — 2026-09-06
|
||||||
|
|
||||||
**Version audit + three pins moved, one deliberately not moved.** `pi`
|
**Version audit + three pins moved, one deliberately not moved.** `pi`
|
||||||
@@ -56,9 +217,15 @@ identically to the 0.84.4 control, so "alive" could be distinguished from
|
|||||||
safe: all five prebuilt native addons in pi use NAPI (ABI-stable, no
|
safe: all five prebuilt native addons in pi use NAPI (ABI-stable, no
|
||||||
NODE_MODULE_VERSION lock, no binding.gyp), nothing in the image declares a node
|
NODE_MODULE_VERSION lock, no binding.gyp), nothing in the image declares a node
|
||||||
CEILING, and the install is one token (`setup_${NODE_VERSION}.x`). agent-browser
|
CEILING, and the install is one token (`setup_${NODE_VERSION}.x`). agent-browser
|
||||||
0.36.0 declares `engines.node >=24`, but that field is vestigial for the
|
0.36.0 declares `engines.node >=24.0.0`, but that field is vestigial for the
|
||||||
artifact actually shipped: the image's own 0.35.2 declares the same floor and
|
artifact actually shipped: `/usr/bin/agent-browser` is the prebuilt aarch64 ELF
|
||||||
runs fine on 22.23.2 as a prebuilt aarch64 ELF. The reason to wait is
|
`bin/agent-browser-linux-arm64`, invoked directly and never through node, so npm's
|
||||||
|
engines floor is never enforced at runtime — verified running under 22.23.2 in
|
||||||
|
this image. (Corrected 2026-09-07: this paragraph originally said "the image's own
|
||||||
|
0.35.2 declares the same floor". That was wrong and incoherent — it contrasted
|
||||||
|
0.36.0 against a 0.35.2 that does not exist in the image. There is exactly one
|
||||||
|
agent-browser present, `/usr/lib/node_modules/agent-browser` at 0.36.0. The
|
||||||
|
argument is unaffected; only the version was wrong.) The reason to wait is
|
||||||
attribution, not compatibility — this release already moves pi a minor,
|
attribution, not compatibility — this release already moves pi a minor,
|
||||||
mempalace a minor and bakes a Studio RC, so adding a node major would leave four
|
mempalace a minor and bakes a Studio RC, so adding a node major would leave four
|
||||||
suspects if the image misbehaves. Worth doing as its own release with the smoke
|
suspects if the image misbehaves. Worth doing as its own release with the smoke
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shellcheck + syntax-check every shell script in this repo. Severity: error.
|
||||||
|
#
|
||||||
|
# SINGLE SOURCE OF TRUTH for two callers:
|
||||||
|
# .gitea/workflows/lint.yml — advisory, every branch push and PR
|
||||||
|
# .gitea/workflows/docker-publish.yml — the release GATE (lint-gate job)
|
||||||
|
# Extracted from lint.yml on 2026-09-08 rather than copied, because a second
|
||||||
|
# copy is exactly the drift this repo has been bitten by (see skillset's
|
||||||
|
# pi-extensions mirror, refreshed the same evening after sitting 9579 B behind).
|
||||||
|
#
|
||||||
|
# WHY THIS CHECK EXISTS AT ALL
|
||||||
|
# actionlint shellchecks workflow `run:` steps only. The repo's own scripts —
|
||||||
|
# entrypoint.sh, scripts/*.sh, and the extensionless tools under
|
||||||
|
# rootfs/usr/local/bin/ — were never shellchecked. A sibling repo with the same
|
||||||
|
# gap shipped a broken `echo "$json" | python3 <<'EOF' ... json.load(sys.stdin)`
|
||||||
|
# for two months: with no script argument python reads its SCRIPT from stdin,
|
||||||
|
# so the heredoc IS stdin and json.load hits EOF. shellcheck flags that at
|
||||||
|
# severity error (SC2259); nothing ever ran it.
|
||||||
|
#
|
||||||
|
# WHY THE RELEASE GATES ON IT (added 2026-09-08, the expensive way round)
|
||||||
|
# v1.8.14's first attempt failed after build-base had already spent ~46 min:
|
||||||
|
# scripts/smoke-test.sh had an apostrophe inside a single-quoted exec_test body
|
||||||
|
# ("the fleet\'s"), which CLOSES the string, so the body truncated and its tail
|
||||||
|
# ran on the CI runner instead of inside the image. shellcheck had already
|
||||||
|
# caught it as SC2289 at severity error — the lint job went red on the very
|
||||||
|
# push that introduced it and stayed red for 24 hours, unread. lint.yml
|
||||||
|
# deliberately does not run on tag pushes (sound: the tagged tree was linted on
|
||||||
|
# main, and a tag-ref lint run sorts above the publish run and makes a release
|
||||||
|
# look finished early). The gap was never "lint the tag" — it was that a tree
|
||||||
|
# whose lint FAILED could still be released. Hence a gate inside the publish
|
||||||
|
# workflow, ~40 s, ahead of everything expensive.
|
||||||
|
#
|
||||||
|
# SEVERITY CHOICE
|
||||||
|
# -S error is 0 findings across this repo when clean, so it is free to add.
|
||||||
|
# -S warning is NOT free here (19x SC2088 tilde-in-quotes in
|
||||||
|
# recreate-sanity-check.sh, plus assorted SC2016 — both intentional), and a
|
||||||
|
# noisy gate trains people to ignore it. Error-only, matching the
|
||||||
|
# SHELLCHECK_OPTS philosophy in lint.yml.
|
||||||
|
#
|
||||||
|
# Usage: bash scripts/lint-shell.sh [root] (default root: repo top level)
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
root="${1:-}"
|
||||||
|
if [ -z "$root" ]; then
|
||||||
|
root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||||
|
fi
|
||||||
|
cd "$root" || { echo "::error::cannot cd to $root"; exit 2; }
|
||||||
|
|
||||||
|
# A gate that cannot run must not pass. Without this, a machine (or a CI job
|
||||||
|
# whose install step was reordered away) without shellcheck would sail through
|
||||||
|
# printing nothing, which is the failure mode this whole file exists to prevent.
|
||||||
|
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
echo "::error::shellcheck not found — the gate cannot run, so it must not pass" >&2
|
||||||
|
echo " install it (apt-get install -y shellcheck) or run this in CI" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Union of two signals, because either alone misses a real case: a shebang scan
|
||||||
|
# misses a sourced fragment with no shebang, and a *.sh glob misses the
|
||||||
|
# extensionless tools in rootfs/usr/local/bin/. Silent skipping is precisely the
|
||||||
|
# failure mode this gate exists to prevent, so err toward over-collecting.
|
||||||
|
# -print0/mapfile -d '' so a path containing a space cannot silently split.
|
||||||
|
mapfile -d '' -t all_files < <(find . -not -path './.git/*' -type f -print0)
|
||||||
|
sh_files=()
|
||||||
|
for f in "${all_files[@]}"; do
|
||||||
|
case "$f" in *.sh) sh_files+=("$f"); continue;; esac
|
||||||
|
if head -n1 "$f" 2>/dev/null | grep -qE '^#!.*\b(bash|sh)\b'; then
|
||||||
|
sh_files+=("$f")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Checking ${#sh_files[@]} shell file(s) with $(shellcheck --version | awk '/version:/{print $2}')"
|
||||||
|
# A green tick over an empty file set is not a check.
|
||||||
|
if [ "${#sh_files[@]}" -eq 0 ]; then
|
||||||
|
echo "::error::no shell files found — the shebang scan or the checkout is wrong"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
shellcheck -S error -f gcc "${sh_files[@]}" || rc=1
|
||||||
|
|
||||||
|
# bash -n catches a different class than shellcheck (unbalanced constructs it
|
||||||
|
# declines to parse), so both run and both count.
|
||||||
|
for f in "${sh_files[@]}"; do
|
||||||
|
bash -n "$f" || { echo "::error file=$f::bash -n failed"; rc=1; }
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
echo "OK: ${#sh_files[@]} shell file(s) clean at severity error"
|
||||||
|
fi
|
||||||
|
exit "$rc"
|
||||||
+37
-2
@@ -5,6 +5,7 @@
|
|||||||
#
|
#
|
||||||
# Verifies:
|
# Verifies:
|
||||||
# - pi binary present and (if EXPECTED_PI_VERSION set) matches CI's resolved version
|
# - pi binary present and (if EXPECTED_PI_VERSION set) matches CI's resolved version
|
||||||
|
# - node MAJOR matches Dockerfile.base's ARG NODE_VERSION (if EXPECTED_NODE_MAJOR set)
|
||||||
# - mempalace core matches the audited pin (if EXPECTED_MEMPALACE_VERSION set)
|
# - mempalace core matches the audited pin (if EXPECTED_MEMPALACE_VERSION set)
|
||||||
# - new v1.0.0 base additions (pandoc, graphviz, imagemagick, yq, tealdeer)
|
# - new v1.0.0 base additions (pandoc, graphviz, imagemagick, yq, tealdeer)
|
||||||
# - typst PDF engine for pandoc (v1.4.0) — `pandoc --pdf-engine=typst`
|
# - typst PDF engine for pandoc (v1.4.0) — `pandoc --pdf-engine=typst`
|
||||||
@@ -91,7 +92,18 @@ if [ -n "${EXPECTED_PI_VERSION:-}" ]; then
|
|||||||
else
|
else
|
||||||
run "pi" "pi --version"
|
run "pi" "pi --version"
|
||||||
fi
|
fi
|
||||||
run "node" "node --version"
|
# Until 2026-09-07 this was a bare `run "node" "node --version"`, which asserts
|
||||||
|
# only that the binary exists and exits 0 — the printed version was never
|
||||||
|
# compared to anything. A node major bump would therefore have passed this suite
|
||||||
|
# SILENTLY, while a reader skimming it would reasonably assume node regressions
|
||||||
|
# were covered. EXPECTED_NODE_MAJOR closes that: CI derives it from
|
||||||
|
# Dockerfile.base's ARG NODE_VERSION (the single source of truth), so this also
|
||||||
|
# catches a stale cached layer whose node does not match the declared ARG.
|
||||||
|
if [ -n "${EXPECTED_NODE_MAJOR:-}" ]; then
|
||||||
|
run_expect "node major matches Dockerfile ARG" "node --version" "v${EXPECTED_NODE_MAJOR}."
|
||||||
|
else
|
||||||
|
run "node" "node --version"
|
||||||
|
fi
|
||||||
run "git" "git --version"
|
run "git" "git --version"
|
||||||
run "aws" "aws --version"
|
run "aws" "aws --version"
|
||||||
run "uv" "uv --version"
|
run "uv" "uv --version"
|
||||||
@@ -739,10 +751,33 @@ exec_test "pi-atelier registered from /opt, not npm: (volume-shadowing guard)" \
|
|||||||
# shadows it. The check that actually bites lives in
|
# shadows it. The check that actually bites lives in
|
||||||
# recreate-sanity-check.sh, which runs where the volume is real — that is
|
# recreate-sanity-check.sh, which runs where the volume is real — that is
|
||||||
# where a 7-week-old 0.27.0 was caught shadowing 0.35.2 on 2026-09-06.
|
# where a 7-week-old 0.27.0 was caught shadowing 0.35.2 on 2026-09-06.
|
||||||
|
# EXECUTION is ASSERTED here, not printed. Until 2026-09-07 the version was
|
||||||
|
# captured inside an echo with 2>/dev/null, so a binary that could not run at all
|
||||||
|
# still PASSED and simply printed version=[] -- the same failure class as the bare
|
||||||
|
# `node --version` two hundred lines up: a value displayed rather than compared.
|
||||||
|
#
|
||||||
|
# Why this exit code matters more than most: smoke runs `platforms: linux/amd64`
|
||||||
|
# on an x86 runner, i.e. NATIVE amd64, so this is the fleet's only recurring
|
||||||
|
# amd64 runtime proof for the linux-x64 ELF. No devbox can supply one -- every
|
||||||
|
# machine in the pi fleet is an Apple Silicon Mac (mbp-m1-2020; tor-ms22 = Mac
|
||||||
|
# Studio Mac13,1 M1 Max, verified 2026-08-17 by system_profiler; emb-7kj4vr4g =
|
||||||
|
# Apple Silicon, 4 routes 2026-09-07). Asking a device for that proof is asking
|
||||||
|
# for the impossible; CI already had it and was discarding it.
|
||||||
|
#
|
||||||
|
# KEEP PROSE OUT OF THE QUOTED BODY BELOW. On 2026-09-07 this explanation lived
|
||||||
|
# INSIDE the single-quoted argument and contained an apostrophe ("the fleet's").
|
||||||
|
# Inside '...' bash treats a backslash literally, so \' does not escape -- it
|
||||||
|
# CLOSES the string. The body silently truncated, the remaining lines were parsed
|
||||||
|
# by the RUNNER's shell instead of the container's, and `agent-browser --version`
|
||||||
|
# ran on a host that has no agent-browser: "line 770: command not found", release
|
||||||
|
# v1.8.14's smoke job failed after the base had already built. shellcheck caught
|
||||||
|
# it as SC2289 the same day and the red lint job went unread for 24h.
|
||||||
exec_test "agent-browser resolves under /usr (volume-shadowing guard, build-time half)" '
|
exec_test "agent-browser resolves under /usr (volume-shadowing guard, build-time half)" '
|
||||||
p=$(command -v agent-browser) || { echo "agent-browser not on PATH" >&2; exit 1; }
|
p=$(command -v agent-browser) || { echo "agent-browser not on PATH" >&2; exit 1; }
|
||||||
r=$(readlink -f "$p")
|
r=$(readlink -f "$p")
|
||||||
echo "resolved=[$r] version=[$(agent-browser --version 2>/dev/null | head -n1)]" >&2
|
v=$(agent-browser --version) || { echo "agent-browser did not EXECUTE" >&2; exit 1; }
|
||||||
|
test -n "$v" || { echo "agent-browser --version produced no output" >&2; exit 1; }
|
||||||
|
echo "resolved=[$r] version=[$(printf %s "$v" | head -n1)]" >&2
|
||||||
case "$r" in /usr/*) ;; *) exit 1 ;; esac
|
case "$r" in /usr/*) ;; *) exit 1 ;; esac
|
||||||
test ! -d "$HOME/.pi/npm-global/lib/node_modules/agent-browser" || exit 1
|
test ! -d "$HOME/.pi/npm-global/lib/node_modules/agent-browser" || exit 1
|
||||||
echo ok
|
echo ok
|
||||||
|
|||||||
Reference in New Issue
Block a user