Compare commits
8 Commits
v1.8.13
...
15a3728ae9
| Author | SHA1 | Date | |
|---|---|---|---|
| 15a3728ae9 | |||
| 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
|
||||||
|
|||||||
+253
-3
@@ -11,6 +11,250 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
**`shellcheck` is now in the image, because the release gate it depends on could
|
||||||
|
not be run by anyone.** v1.8.14 made shell lint a release gate: `scripts/lint-shell.sh`
|
||||||
|
became the single source of truth for `lint.yml` and a new `lint-gate` job that
|
||||||
|
`resolve-versions` depends on, and it deliberately exits 2 when `shellcheck` is
|
||||||
|
absent — *a gate that cannot run must not pass*. Measured on v1.8.14 on
|
||||||
|
2026-09-09, by three routes (`command -v`, `dpkg -l`, a filesystem search):
|
||||||
|
**`shellcheck` was not in the image at all.** So `bash scripts/lint-shell.sh`
|
||||||
|
exited 2 in every devbox container, and the only place the gate could ever run
|
||||||
|
was CI. The developer loop was therefore write-shell → push → wait for CI →
|
||||||
|
discover — which is the loop the gate was added to shorten, after v1.8.14's first
|
||||||
|
attempt burned ~46 minutes on a tree whose lint had already been red for 24
|
||||||
|
hours. Added to the `apt-get` block in `Dockerfile.base`: `shellcheck 0.10.0-1`,
|
||||||
|
~39 MB installed (`Installed-Size` 40112 KB), and measured to pull **zero**
|
||||||
|
additional packages under `--no-install-recommends` because its three deps
|
||||||
|
(`libc6`, `libffi8`, `libgmp10`) are already present. **This forces one full base
|
||||||
|
rebuild** — `base-decide` hashes `Dockerfile.base` + `rootfs/`, so unlike a
|
||||||
|
`scripts/` change it cannot reuse the existing `base-` layer.
|
||||||
|
|
||||||
|
**A client-side pre-push lint gate: `hooks/pre-push`.** Opt-in per clone with
|
||||||
|
`git config core.hooksPath hooks`, bypass with `git push --no-verify`, matching
|
||||||
|
the idiom the `skillset` and `myconfigs` repos already use. It is a thin wrapper
|
||||||
|
that `exec`s `scripts/lint-shell.sh` — the same script CI runs, one copy, because
|
||||||
|
a duplicated check that drifts is the failure this repo keeps paying for (the
|
||||||
|
`pi-extensions` skill mirror sat 9579 B behind for weeks; the shell-lint logic
|
||||||
|
was extracted to one file for exactly this reason).
|
||||||
|
|
||||||
|
> **Why this repo had no hooks at all, which is worth stating because it was
|
||||||
|
> reported as drift and is not.** A fleet peer asked `tor-ms22` to report
|
||||||
|
> `git config core.hooksPath` per clone on the premise that an unset value meant
|
||||||
|
> "no secret-scan and no shell-lint hook locally", leaving the drift/secret gates
|
||||||
|
> unverified. Measured: `pi-devbox` **unset**, `skillset` `hooks`, `myconfigs`
|
||||||
|
> `common/hooks`, `pi-toolkit` **unset**. But `git ls-files | grep -i hook` is
|
||||||
|
> **empty** in both `pi-devbox` and `pi-toolkit` — neither repo tracked a single
|
||||||
|
> hook file, so there was nothing for `core.hooksPath` to point at on any machine
|
||||||
|
> and unset was the only correct value. The two repos that do ship hooks were
|
||||||
|
> already wired correctly. This entry closes the real half of that gap for
|
||||||
|
> `pi-devbox`; `pi-toolkit` still ships none.
|
||||||
|
|
||||||
|
**The hook is verified to catch the defect that motivated it, not merely to
|
||||||
|
exist.** Three measurements, each with the expected result written down first:
|
||||||
|
|
||||||
|
- **Refusal paths.** With `shellcheck` absent (the state of every container built
|
||||||
|
before this change) the hook exits **2** and names the remedy; with
|
||||||
|
`scripts/lint-shell.sh` missing it also exits **2**. It never waves a push
|
||||||
|
through on the assumption that CI will catch it.
|
||||||
|
- **The hook is actually in the scan set.** `lint-shell.sh` reports `Checking 14
|
||||||
|
shell file(s)` with `hooks/pre-push` present and **13** with it moved aside — so
|
||||||
|
the extensionless file is discovered by the shebang half of the linter's
|
||||||
|
two-signal union, rather than being silently skipped. This check exists because
|
||||||
|
the first attempt at it was ambiguous: a planted `[ $UNSET_VAR = "x" ]` was not
|
||||||
|
reported, which could equally have meant "file not scanned" or "defect below
|
||||||
|
`-S error`". It was the latter. A count that moves is unambiguous; a clean run
|
||||||
|
is not.
|
||||||
|
- **It catches the real v1.8.14 defect.** Planting the exact failing shape — an
|
||||||
|
apostrophe inside a single-quoted string, `echo 'the fleet\'s thing'` — in
|
||||||
|
`hooks/pre-push` produces `SC1073`/`SC1072` at severity **error** and `rc=1`.
|
||||||
|
That is the defect that closed a string, truncated an `exec_test` body, sent its
|
||||||
|
tail to the runner's shell, and cost a 46-minute build.
|
||||||
|
|
||||||
|
**The gate earned its keep inside the commit that added it.** The first version of
|
||||||
|
the `smoke-test.sh` assertion above carried a comment beginning `# shellcheck is a
|
||||||
|
GATE DEPENDENCY…`. A comment whose first word is the tool's name is parsed as a
|
||||||
|
**shellcheck directive**, not a comment, so the new gate immediately failed with
|
||||||
|
`SC1073`/`SC1072` at severity error — on the change that introduced it. Same family
|
||||||
|
as the v1.8.14 apostrophe: a line that reads as prose to a human and as syntax to
|
||||||
|
the parser. Before this change that defect would have been discovered in CI.
|
||||||
|
|
||||||
|
**Also queued, not yet pinned:** the `mempalace-toolkit` owed-set derivation now
|
||||||
|
honours a requester withdrawing its *own* ask (`isWithdrawn`, RFC 003 §3.3
|
||||||
|
clause 4, with `scripts/test-owed-withdrawal.sh`) — toolkit commit **`e2b060a`**,
|
||||||
|
which is the minimum revision for the behaviour. This image still pins `e45f6b4`.
|
||||||
|
Until an image bakes `e2b060a` or later, a sender must assume its withdrawal has
|
||||||
|
no effect on the recipient's mailbox — measured cost of the gap: a withdrawn
|
||||||
|
v1.8.13 rollout ask was still being reported as owed on `tor-ms22` 41 hours later,
|
||||||
|
for a release that device never installed. The same commit also anchors the
|
||||||
|
derivation's `mine` query at the newest end (`order: "desc"`); with the previous
|
||||||
|
default `asc` + `limit: 100`, a device passing 100 authored events would have its
|
||||||
|
recent replies fall out of the join window and see answered asks resurface.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 +300,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
|
||||||
|
|||||||
@@ -101,6 +101,27 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|||||||
# container: `ss` lands at /usr/bin/ss, `ip` at /usr/sbin/ip
|
# container: `ss` lands at /usr/bin/ss, `ip` at /usr/sbin/ip
|
||||||
# (both already on the developer PATH), and `portcheck --all`
|
# (both already on the developer PATH), and `portcheck --all`
|
||||||
# then correctly identifies the socat listener on 8765.
|
# then correctly identifies the socat listener on 8765.
|
||||||
|
# shellcheck — shell linter. Added 2026-09-09 to close a CAPABILITY gap, not
|
||||||
|
# a style preference. `scripts/lint-shell.sh` is the release
|
||||||
|
# GATE (the `lint-gate` job that `resolve-versions` depends
|
||||||
|
# on), and it correctly refuses to pass when shellcheck is
|
||||||
|
# missing — "a gate that cannot run must not pass". Measured on
|
||||||
|
# v1.8.14: shellcheck was absent from this image by all three
|
||||||
|
# routes (PATH, dpkg, filesystem), so `bash
|
||||||
|
# scripts/lint-shell.sh` exited 2 in EVERY devbox container and
|
||||||
|
# no developer could run the release gate locally at all. The
|
||||||
|
# loop was therefore write-shell → push → wait for CI → discover,
|
||||||
|
# which is the loop the gate was added to shorten: v1.8.14's
|
||||||
|
# first attempt burned ~46 min on a tree whose lint had already
|
||||||
|
# been red for 24 h. This is also what makes a client-side
|
||||||
|
# pre-push hook possible (see hooks/pre-push); without the
|
||||||
|
# binary that hook would refuse every push. ~39 MB installed
|
||||||
|
# (Installed-Size 40112 KB, shellcheck 0.10.0-1) and measured
|
||||||
|
# to pull ZERO additional packages under
|
||||||
|
# --no-install-recommends: its deps (libc6, libffi8, libgmp10)
|
||||||
|
# are already present. NOTE this file feeds the base-decide
|
||||||
|
# hash (Dockerfile.base + rootfs/), so adding it forces one
|
||||||
|
# full base rebuild.
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get upgrade -y --no-install-recommends && \
|
apt-get upgrade -y --no-install-recommends && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
@@ -120,6 +141,7 @@ RUN apt-get update && \
|
|||||||
make \
|
make \
|
||||||
patch \
|
patch \
|
||||||
diffutils \
|
diffutils \
|
||||||
|
shellcheck \
|
||||||
git-crypt \
|
git-crypt \
|
||||||
age \
|
age \
|
||||||
file \
|
file \
|
||||||
|
|||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pre-push gate for pi-devbox: shellcheck every shell script before it leaves
|
||||||
|
# this clone. Thin wrapper — all logic lives in scripts/lint-shell.sh, which is
|
||||||
|
# the SAME script the CI release gate runs. One copy, not two: a duplicated
|
||||||
|
# check that drifts is the failure this repo keeps paying for.
|
||||||
|
#
|
||||||
|
# Install per clone: git config core.hooksPath hooks
|
||||||
|
# Bypass this gate: git push --no-verify (a guard, not a wall)
|
||||||
|
#
|
||||||
|
# WHY THIS HOOK EXISTS
|
||||||
|
# v1.8.14's first release attempt died at scripts/smoke-test.sh:770 after
|
||||||
|
# build-base had already spent ~46 minutes. shellcheck had ALREADY caught the
|
||||||
|
# defect — SC2289 at severity error, on the very push that introduced it — and
|
||||||
|
# the lint job stayed red for 24 hours, unread, across three runs. The fix at
|
||||||
|
# the time was to gate the release on the same script (the `lint-gate` job).
|
||||||
|
# This hook is the cheaper end of that: the same finding, before the push,
|
||||||
|
# in seconds rather than after a 40 s CI gate or a 46 min build.
|
||||||
|
#
|
||||||
|
# WHY IT COULD NOT EXIST UNTIL NOW
|
||||||
|
# Measured on v1.8.14 (2026-09-09): shellcheck was absent from the devbox
|
||||||
|
# image by all three routes — PATH, dpkg and a filesystem search. So
|
||||||
|
# lint-shell.sh exited 2 in every container, and a hook calling it would have
|
||||||
|
# refused EVERY push rather than gating anything. `shellcheck` was added to
|
||||||
|
# Dockerfile.base in the same change that added this file; on an image built
|
||||||
|
# before that, enable this hook and you will simply be told the gate cannot
|
||||||
|
# run. That is the correct behaviour, but it is not a working hook — so do not
|
||||||
|
# set core.hooksPath on a container older than the release that bakes it.
|
||||||
|
#
|
||||||
|
# NOTE ON SCOPE: this lints the WORKING TREE, not the exact commit range being
|
||||||
|
# pushed. That is deliberate and matches what the CI gate does to the tagged
|
||||||
|
# tree. It means a defect you have staged-but-not-committed is also reported,
|
||||||
|
# which is noisy in the safe direction.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
|
||||||
|
LINTER="$REPO_ROOT/scripts/lint-shell.sh"
|
||||||
|
tag="[lint-shell]"
|
||||||
|
|
||||||
|
# Same rule the gate itself applies, applied one level up: a missing check is
|
||||||
|
# not a pass. If the script is gone, the push is refused rather than waved
|
||||||
|
# through on the assumption that CI will catch it.
|
||||||
|
if [ ! -r "$LINTER" ]; then
|
||||||
|
echo "$tag refusing the push: $LINTER is missing, so the gate cannot" >&2
|
||||||
|
echo "$tag run. A gate that cannot run must not pass." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Point the message at the actual remedy when the binary is absent, because the
|
||||||
|
# linter's own message ("install it or run this in CI") is written for a CI
|
||||||
|
# runner and is misleading inside a container the developer cannot apt-install
|
||||||
|
# into persistently.
|
||||||
|
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
echo "$tag refusing the push: shellcheck is not installed, so the gate" >&2
|
||||||
|
echo "$tag cannot run. A gate that cannot run must not pass." >&2
|
||||||
|
echo "$tag" >&2
|
||||||
|
echo "$tag This container predates the image that bakes shellcheck." >&2
|
||||||
|
echo "$tag Either recreate onto an image that has it, or unset the hook:" >&2
|
||||||
|
echo "$tag git config --unset core.hooksPath" >&2
|
||||||
|
echo "$tag To push this once without the gate: git push --no-verify" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec bash "$LINTER" "$REPO_ROOT"
|
||||||
@@ -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"
|
||||||
+49
-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,8 +92,31 @@ 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"
|
||||||
|
# NOTE: the shellcheck binary is a GATE DEPENDENCY, not a convenience.
|
||||||
|
# scripts/lint-shell.sh is the release gate (the lint-gate job resolve-versions
|
||||||
|
# depends on) and it exits 2 when the binary is missing, by design — "a gate that
|
||||||
|
# cannot run must not pass". Measured on v1.8.14: it was absent from the image, so
|
||||||
|
# that gate could not be run by a developer in ANY container, only in CI.
|
||||||
|
# Asserted here so its absence fails a build instead of being discovered by a hook
|
||||||
|
# that then refuses every push (hooks/pre-push).
|
||||||
|
#
|
||||||
|
# This comment must not BEGIN with the tool's name: a line starting with
|
||||||
|
# `# shellcheck` is parsed as a DIRECTIVE, not a comment (SC1073/SC1072). The
|
||||||
|
# gate added in this same change caught that here, before the push.
|
||||||
|
run "shellcheck (lint gate dependency)" "shellcheck --version | grep -qE '^version: [0-9]'"
|
||||||
run "aws" "aws --version"
|
run "aws" "aws --version"
|
||||||
run "uv" "uv --version"
|
run "uv" "uv --version"
|
||||||
run "nvim" "nvim --version"
|
run "nvim" "nvim --version"
|
||||||
@@ -739,10 +763,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