9d0b3dec0b
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.
98 lines
4.8 KiB
Bash
Executable File
98 lines
4.8 KiB
Bash
Executable File
#!/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 (20x 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.
|
|
# Reproduce the count before editing it (the `$ ` prefix is load-bearing: a
|
|
# comment whose first word is "shellcheck" is parsed as a DIRECTIVE, and a
|
|
# malformed one is SC1072/SC1073 at severity error — this gate caught exactly
|
|
# that when the line was first written without it):
|
|
# $ shellcheck -S warning -f gcc scripts/*.sh rootfs/usr/local/bin/* \
|
|
# entrypoint*.sh hooks/* | grep -c SC2088
|
|
#
|
|
# 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"
|