agents: Sage window plan for the re-pin to 3190ad762 (rows 152, 138, bus 3e to 3f)

The plan for Mos's A4 window: stopped-bus backup with sha256, rehearsal on
disposable copies, live migration with migrate-schema.mjs, NEW units and
wrapper, verify-new.sh and check-launch.mjs for the NEW copy (read_spec now
required), and restore steps A and B. Nine scripts committed with
SCRIPTS.sha256 so Mos can check the scratch copies before running. Not run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-15 21:09:07 -05:00
co-authored by Claude Opus 5.5
parent 7158d62ec1
commit 4eca829205
12 changed files with 513 additions and 0 deletions
@@ -0,0 +1,309 @@
# Re-pin for rows 152 and 138, with the bus 3e to 3f migration
Sage, 2026-10-16. Plan for Mos's window, on Mos's directive re sage-0494.
Mos runs every step, with Jason present (A4). Sage ran none of it; the
controls and fixture tests Sage did run are listed at the end.
This window has a schema change, so it adds a stopped-bus backup, a rehearsal
and a live migration to the shape of `repin-56639c99b.md`. The procedure is
Rocko's row 138 MIGRATION.md, which lives at
`agents/rocko/work/queue-138/r4/MIGRATION.md` (there is no
`packages/bus/MIGRATION.md`), with the "Schema and verification" section of
`packages/bus/README.md`.
- OLD, live now: `56639c99b03ddca8f5fa84826b19322cd09a713e`. Bus, Console
and CLI all on it per `stack-copy.sh check` at 02:05Z. Bus and Console
started 2026-10-16T00:48:12Z. Bus schema 3e.
- NEW: `3190ad762b8fd5c09ad7515e4f7e609e81ae1af6`, committed
2026-10-16T01:49:44Z, origin `refactor` per `git ls-remote`. Copy, made in P1:
`/home/jwoltje/.local/share/mosaic-stack/copies/3190ad762b8fd5c09ad7515e4f7e609e81ae1af6`.
Bus schema 3f.
- Data root: `/home/jwoltje/.mosaic-dev` (config.json `dataRoot`). The
store is `bus/bus.sqlite` with its -wal and -shm files.
- Scripts: `P=/mnt/storage/scratch/sage-pin-3190ad762`. The reviewed copies
are committed beside this file in `repin-3190ad762/`, with
`SCRIPTS.sha256`. P0 checks that P matches them.
## What NEW carries over OLD
Nothing else landed after 138. `git log 56639c99b..3190ad762` holds rows 152
and 138 plus queue, session and packet commits that change nothing in the
copy. The one commit after 3190ad762, 7158d62ec (queue revisions 1510-1511),
touches only `docs/plans/queue.json` and `docs/plans/QUEUE.md`, which are
outside COPY_PATHS and change no schema. The window pins 3190ad762, so it is
not in the copy either way.
- Row 152 (26a0b0fab): when a message waits for a role that `launch.by`
can't launch because it isn't in the launch block, the bus says so at once.
`broker.mjs` marks the waiting message `launchable`; `talk` prints
"<role> isn't running and isn't in the launch block, so <launcher> can't
launch it". No schema change.
- Row 138 (3190ad762): file-backed north-star specs.
`packages/project/src/read.mjs` reads a project's committed spec. The bus
serves `spec.read` and `record.write`. The harness gives every role the
`read_spec` typed tool and gives `write_record` only to a launch whose
narrowed limits hold `record.write`. Eleven `roles/*.json` gained
`record.write` in `withinRole`; `vocabulary.mjs` gained the word. Schema
3e to 3f: `migrations/3e-3f.sql` adds nullable `decisions.spec_path` and
`decisions.spec_commit`. Nothing else in the store changes.
Spec reads and record writes stay inert live. The live business has no
`projects.<id>.repo`, so `spec.read` and `record.write` refuse until Jason
runs `mosaic project init`, which is a separate human operation and not part
of this window. The `read_spec` tool will be in the chief and pm manifests;
calling it will refuse.
NEW opens nothing older than 3f. A normal bus start under NEW on the 3e store
refuses with `schema-mismatch` and changes nothing. So the migration has to
run before the bus starts on NEW, and OLD must never open the migrated file:
OLD refuses 3f the same way. Both directions are in the rehearsal.
## Rollback set
P2 captures it at `/mnt/storage/scratch/sage-pin-rollback-56639c99b/`, the
same layout as the 02ee7f640 set: the `mosaic` wrapper,
`[email protected]`, `mosaic-webui.service`, both `systemctl --user show`
outputs, DROPINS.txt, SOURCES.txt, SHA256SUMS and `verify-live.sh`.
`capture-rollback.sh` refuses if the live files do not name OLD or if the
directory exists. It ends by running `verify-live.sh`, which must print
MATCH three times.
The bus backup from step 3 is the second half of the rollback set. It records
the rollback set's path and SHA256SUMS, so the backup receipt names the code
and launch settings it pairs with (MIGRATION.md step 3).
## Scripts
All in `$P`. None of them reads or prints a row of the store.
| Script | Used in | What it does |
|---|---|---|
| `capture-rollback.sh [OUT]` | P2 | Saves the OLD wrapper and units, as above. Read-only on live files. |
| `stopped-check.sh DATA_ROOT` | 3, R | Fails if `bus/writer.lock` exists or any process has a file under `bus/` open (scans `/proc/*/fd`). Prints STOPPED_OK. Never opens the store and never removes a lock. |
| `backup.sh DATA_ROOT OUT ROLLBACK_SET` | 3 | Refuses if OUT exists or the rollback set fails its sums. Runs stopped-check, then `cp -p` of `bus.sqlite` and any `-wal`/`-shm` into `OUT/bus` (umask 077, dirs 0700). Writes SOURCE.sha256 (sums of the live files) and SHA256SUMS (sums of the copies) and requires them equal. Writes MODES.txt, LISTING.txt, DATA_ROOT, STAMP, ROLLBACK_SET and ROLLBACK_SET.sha256. Runs stopped-check again, prints the sums and BACKUP_OK. |
| `rehearse.sh BACKUP NEW_COPY OLD_COPY` | 4 | Checks the backup against its sums, makes two 0700 copies under `BACKUP/rehearsal-*`, then checks (a) NEW refuses the 3e copy at a normal open, (b) `migrate-schema.mjs` exits 0 ending `bus schema 3f ready`, with its run time, (c) NEW opens the migrated copy at 3f, (d) a second migrate is a validated no-op, (e) OLD refuses the 3f copy, (f) the backup is unchanged. Prints REHEARSAL_PASS or REHEARSAL_FAIL. Leaves the rehearsal copies. |
| `verify-open.mjs COPY DATA_ROOT` | 4 | Opens a store with COPY's `store.mjs` the way the bus does at start and prints `OPEN_OK <version>` or `REFUSED <code>`. Refuses to run on `~/.mosaic-dev`. |
| `restore.sh DATA_ROOT BACKUP` | R | Refuses a backup taken from another data root. Runs stopped-check, checks the backup sums, renames `bus` to `bus.post-attempt-<UTC stamp>` (deletes nothing), makes a 0700 `bus`, `cp -p` the backup files in, checks the sums and that the directory holds exactly the backup's files. Prints RESTORE_OK. |
| `verify-new.sh` | 8 | The live wrapper and both units equal the P2 set with only the copy id swapped, and both units equal what `bus-service.sh render` and `webui-service.sh render` give for NEW. The renders run at check time because they need the NEW copy. 5 × MATCH. |
| `check-launch.mjs INSTANCE SINCE COPY_ID` | 8, 9 | As in the 56639c99b window, plus a required check that the manifest's `typed` includes `mcp__mosaic__read_spec`. Keeps the row 140 text and pm `request_staff` checks. Prints `write_record` present or absent as INFO, never as a pass condition. CHECK3_PASS or CHECK3_FAIL; never prompt text. |
| `verify-live.sh` | R | Copied into the rollback set by P2. MATCH per file against its SHA256SUMS. |
Nothing under `scripts/`, `adapters/`, `package*.json`, `RELEASE`,
`packages/webui` or `packages/cli/systemd` changed between OLD and NEW. The
OLD renders equal the live units byte for byte, so the NEW render is a fair
reference.
## Steps
```
REPO=/mnt/storage/src/mosaic-stack
P=/mnt/storage/scratch/sage-pin-3190ad762
D=/home/jwoltje/.mosaic-dev
OLD=56639c99b03ddca8f5fa84826b19322cd09a713e
NEW=3190ad762b8fd5c09ad7515e4f7e609e81ae1af6
COPIES=/home/jwoltje/.local/share/mosaic-stack/copies
C=$COPIES/$NEW; OC=$COPIES/$OLD
B=$P/backup # the bus backup; holds live bus data, owner-only, nobody but Mos opens it
RS=/mnt/storage/scratch/sage-pin-rollback-56639c99b
```
### Before the window (P0 to P3, nothing stopped)
| # | Who | Step |
|---|-----|------|
| P0 | Mos | `cd $P && sha256sum -c $REPO/agents/sage/work/window-plan/repin-3190ad762/SCRIPTS.sha256`, 9 × OK. |
| P1 | Mos | `$REPO/scripts/stack-copy.sh copy $NEW`, rc 0. Then the copy check below, rc 0 with no output. Then `test -f $C/packages/bus/src/migrations/3e-3f.sql && test -f $C/packages/project/src/read.mjs && echo NEW_COPY_HAS_138`. |
| P2 | Mos | `bash $P/capture-rollback.sh`. DROPINS lines, then MATCH × 3, rc 0. |
| P3 | Mos | `node -e 'console.log(JSON.parse(require("fs").readFileSync(process.env.HOME+"/.config/mosaic-dev/config.json","utf8")).dataRoot)'` prints a path that resolves to `$D`. `test -e $B && echo B_EXISTS` prints nothing. |
The P1 copy check, the same `git archive` diff as last time:
```
paths=$(sed -n '/^COPY_PATHS=(/,/^)/p' $REPO/scripts/stack-copy.sh | grep -v '^ *#' | sed '1d;$d')
T=$(mktemp -d) && git -C $REPO archive $NEW -- $paths | tar -xf - -C $T && diff -rq --exclude=node_modules $T $C; echo "rc=$?"
```
P0 to P3 change nothing live and can run any time before the window. The copy
and the rollback set stay if the window is postponed.
### The window
| # | Who | Step |
|---|-----|------|
| 1 | Jason, then Mos | PM-idle check. Jason, in his own shell: `mosaic agents --business mosaic-stack`; chief and pm must not be `working`. Mos: `mosaic launches list`. Nobody talks to chief or pm until step 8 passes. Write down the UTC start stamp: `date -u +%FT%TZ`. |
| 2 | Mos | Stop the Console, then the bus: `systemctl --user stop mosaic-webui.service`, then `systemctl --user stop [email protected]`. `systemctl --user is-active mosaic-webui.service [email protected]` prints `inactive` twice. |
| 3 | Mos | `bash $P/stopped-check.sh $D` prints STOPPED_OK. Then `bash $P/backup.sh $D $B $RS`, BACKUP_OK. Record the `bus.sqlite` sha256 line it prints, and the -wal and -shm lines if any. A clean bus stop usually leaves only `bus.sqlite`. |
| 4 | Mos | `bash $P/rehearse.sh $B $C $OC`. REHEARSAL_PASS, six PASS lines. Record the migrate run time. Any FAIL ends the window here: start the bus and the Console again on OLD, nothing else changed. |
| 5 | Mos | Live migration, bus still stopped: `node $C/packages/bus/src/migrate-schema.mjs $D; echo "rc=$?"`. rc 0, last line `bus schema 3f ready`. Then `(cd $B/bus && sha256sum -c --quiet ../SHA256SUMS) && echo BACKUP_INTACT`. |
| 6 | Mos | From the canonical checkout: `$REPO/scripts/bus-service.sh install --copy $NEW --pm --claude /home/jwoltje/.local/bin/claude`, then `$REPO/scripts/webui-service.sh install --copy $NEW --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --send`. |
| 7 | Mos | `$REPO/scripts/stack-copy.sh wrapper $NEW`. |
| 8 | Mos | Start the bus, then the Console. Run the checks below in order. |
| 9 | Mos | Smoke from tmux, below. |
Between steps 5 and 7 the live store is 3f while the wrapper and units still
name OLD. Run no `mosaic` command in that gap. If the bus unit were started by
mistake on OLD it would refuse 3f and change nothing, which is a stop
condition, not damage.
The migration takes the store's exclusive writer lock, checks the 3e source
digest, schema version, WAL, quick_check, foreign keys and timestamps, then
runs the 3e-3f step in one transaction. A refusal prints `migration refused:
<code>` and exits 1. The step's transaction rolls back, so the store is still
3e; restore anyway (rollback A), because the backup is the known state.
### Step 8 checks
```
$REPO/scripts/stack-copy.sh check # bus, Console, CLI on NEW
bash $P/verify-new.sh # 5 x MATCH, rc 0
test -f $C/packages/project/src/read.mjs && echo SPEC_READER_PRESENT
systemctl --user status [email protected] mosaic-webui.service
curl -fsS http://127.0.0.1:7332/healthz
mosaic launches list # one running chief, launched by the bus start
node $P/check-launch.mjs chief <step 1 stamp> $NEW # CHECK3_PASS
```
The bus start is itself the check that the live store is 3f. A normal open
under NEW verifies the 3f digest, version, WAL, quick_check, foreign keys and
timestamps; on any mismatch the bus does not start. The bus does not log a
schema line at start.
`check-launch.mjs chief` must show sources under NEW, the prompt digest, the
row 140 line and `mcp__mosaic__read_spec` in `typed`. Its INFO line says
whether `write_record` is present. That depends on the launch block's limits
for chief. Either answer is a fact to record, not a failure.
### Step 9, smoke from tmux
```
mosaic talk chief "[mos test] re-pin 3190ad762: reply with one line, no staffing request"
mosaic talk pm "[mos test] re-pin 3190ad762: reply with one line, no staffing request"
node $P/check-launch.mjs pm <step 1 stamp> $NEW # CHECK3_PASS, also needs request_staff
```
If pm is stopped, `talk pm` prints the row 146 waiting line and chief
launches pm, as before. Row 152's new line appears only for a role outside
the launch block. Exercising it live means talking to such a role, which the
PM test limits keep out, so row 152 has no live smoke here. Its CLI and bus
tests ran green in both landing gates. No `read_spec` call is part of the
smoke: with no project repo it can only refuse.
## Stop conditions
Before step 5, any one ends the window with nothing changed. Start the bus
and the Console again on OLD and report:
- P0 to P3 fail, or `stopped-check.sh` fails in step 3. A writer.lock with
the bus stopped goes to the stale-lock procedure; nobody deletes it.
- `backup.sh` prints anything but BACKUP_OK.
- `rehearse.sh` prints REHEARSAL_FAIL.
From step 5 on, any one means rollback:
- Step 5 exits non-zero or does not end `bus schema 3f ready`: rollback A.
- The bus fails to start on NEW, or the newest launch-log line for this start
is `"event":"refused"`, or `journalctl --user -u mosaic-bus@mosaic-stack
--since "<UTC stamp> UTC"` shows `PM launch refused` or a store code
such as `schema-mismatch` or `integrity-failed`: rollback B. journalctl reads `--since` in local time
unless the stamp says UTC.
- `/healthz` fails, `stack-copy.sh check` shows anything not on NEW,
`verify-new.sh` prints any DIFFER, or `check-launch.mjs chief` prints
CHECK3_FAIL: rollback B.
A step 9 failure is a finding, not an automatic rollback, unless the live
talk path is broken. Mos decides.
## Rollback
Never start the bus after a restore that did not print RESTORE_OK. Under
either copy, a bus that starts on a data root with no `bus/bus.sqlite`
creates a fresh empty store.
### A: the migration failed (step 5), units and wrapper still on OLD
1. Bus and Console stay stopped.
2. `bash $P/restore.sh $D $B`. RESTORE_OK. Note the `bus.post-attempt-*`
path it prints; keep it for diagnosis.
3. `bash $RS/verify-live.sh`, MATCH × 3 (nothing was installed yet).
4. Start the bus, then the Console, on OLD. `stack-copy.sh check` shows OLD.
5. Report the refusal code. Nobody retries the migration until it is
diagnosed.
### B: after the start on NEW
1. Stop the Console, then the bus; both `inactive`.
2. `bash $P/restore.sh $D $B`. RESTORE_OK. The post-attempt store, with
everything NEW recorded, stays at the printed `bus.post-attempt-*` path.
3. Repeat steps 6 and 7 with `$OLD` in place of `$NEW`.
4. `bash $RS/verify-live.sh`, MATCH × 3. A DIFFER means stop and diff
against the saved files before starting anything.
5. Start the bus, then the Console. `stack-copy.sh check` shows OLD.
OLD never opens the 3f file: step 2 moves it aside before step 5 starts the
bus. Data boundary: anything the bus recorded after the step 8 start (at the
least, the chief launch) is in the post-attempt store only and is gone from
the active one. Mos and Jason decide what happens to it before anyone
reconciles. Nobody strips columns or edits evidence in place.
Fixing forward is the other option after a step 8 failure that is not about
the store: the store is 3f and healthy, so another NEW-based copy can be
pinned without a restore. That needs its own plan.
## Notes
- Land-work gap. From 3190ad762 on, the canonical checkout's
`scripts/land-work/read.mjs` imports the 3f SCHEMA_VERSION and refuses
against a 3e bus with `bus-schema`. Until step 5, land-work refuses on the
live store. Any landing in that gap uses conductor-apply. After a rollback
B the gap reopens.
- Keep the copies: OLD and NEW, plus the earlier rollback copies. Nothing
prunes copies.
- Keep `$B` and any `bus.post-attempt-*` directory until Mos says otherwise.
They hold live bus data, so they stay 0700 and nobody but Mos opens them.
Deleting them is a data deletion and goes to Jason.
- The rehearsal copies under `$B/rehearsal-*` also hold live bus data. Same
rule.
- Record for BUILD-LOG (MIGRATION.md step 7): step 1 stamp, the backup sums,
the rehearsal output, the migrate exit and last line, the step 8 output,
the chief and pm manifest paths and prompt digests, and the downtime from
step 2 to step 8.
## What Sage ran for this plan
No part of the plan ran. Nothing live was stopped, started, installed,
copied or migrated. No live store file was opened or read. P1 and P2 did not
run; the NEW copy and the 56639c99b rollback set do not exist yet.
Read-only on live state:
- `stack-copy.sh check` and `systemctl --user show` for the header.
- `bus-service.sh render` and `webui-service.sh render` for OLD equal the
live units byte for byte.
- `verify-new.sh` before the window: 5 × DIFFER, rc 1, as it should be with
no rollback set and no NEW copy.
- `check-launch.mjs` against the live chief (r3da8da09850d) and pm
(r95d7c818d9c7) runs. With `$OLD`: sources, digest, row 140 text and pm
request_staff PASS; `read_spec` FAIL; CHECK3_FAIL. With `$NEW`: the
sources check fails as well. `write_record` INFO prints absent.
- `capture-rollback.sh` to a fixture path, not `$RS`: MATCH × 3 and sums OK.
It read the live wrapper and units and ran `systemctl --user show`.
- `verify-new.sh` logic with NEW set to OLD against that fixture set:
5 × MATCH, so the render commands and paths are right.
- The P1 copy check against the OLD copy with `$OLD`: rc 0, no output.
On fixture data roots under `$P/fixture`, made fresh at 3e by the OLD copy's
`store.mjs`, with the canonical checkout (3190ad762 plus the queue commit) as
NEW:
- `stopped-check.sh` fails on a store held open by a process and on a
writer.lock, and passes once both are gone.
- `backup.sh` refuses a locked store, an existing OUT and a bad rollback set;
backs up a store with a leftover WAL and SHM; BACKUP_OK.
- `rehearse.sh`: REHEARSAL_PASS twice, migrate about 42 ms on the fixture.
The live store is about 1.9 MB plus a 0.5 MB WAL.
- A stand-in step 5 on the fixture: rc 0, `bus schema 3f ready`, backup
intact.
- `restore.sh` refuses a backup from another data root and a locked store;
then RESTORE_OK. The restored store opens at 3e under OLD and refuses
under NEW; the post-attempt directory is kept.
- `verify-open.mjs` refuses `~/.mosaic-dev` before opening anything.
@@ -0,0 +1,9 @@
1fd4e10f1847fbae6e64ca5d9277ead5a68e2c575594993b919181c09d0b6b35 backup.sh
979e4327603c53c484bdd1964a133dda9192b492b1089772b6c57b37dad9219a capture-rollback.sh
fc41a82f78da8809977b57275b7d72cc1fa155f5dc83c797b412fad6f2d97ca7 rehearse.sh
88fdb8624221bc8988d780f9212465c096df2980a46ac21c3799769d87bd5877 restore.sh
422021b2e9fff30c3747d5fff7d1e8bd2ad16ed15cd3e11c97472c75b74e8efc stopped-check.sh
8bf05f2d05166c535f87738c5a4bf2692e2324e107ee7e2e7f58af4658f212a9 verify-live.sh
f2754716f7811cb0786af2d9476ebe6bf4a894a43e0a57cf6b74bd5d5353197d verify-new.sh
0697d38cf2d97d2891be57a5feec55331c46de670e5cf25b452c67db619d77a5 check-launch.mjs
775f57263ac236f22de0c3b12e5e19ef7ca66493dc0a4c4212cebb11a4e66b53 verify-open.mjs
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# Re-pin 3190ad762, step 3: an owner-only copy of the stopped bus store with sha256 sums.
# Usage: backup.sh DATA_ROOT OUT ROLLBACK_SET. Copies bus.sqlite and any -wal/-shm file; never opens them.
# OUT must not exist. ROLLBACK_SET is the P2 capture of the OLD wrapper and units; its path and SHA256SUMS
# go into OUT so the backup receipt names the code and launch settings it pairs with (MIGRATION.md step 3).
# Prints the sums and BACKUP_OK.
set -o pipefail
U='usage: backup.sh DATA_ROOT OUT ROLLBACK_SET'
D=${1:?$U}; O=${2:?$U}; RS=${3:?$U}; H=$(cd "$(dirname "$0")" && pwd)
[ -e "$O" ] && { echo "refusing: $O exists"; exit 1; }
(cd "$RS" && sha256sum -c --quiet SHA256SUMS) || { echo "refusing: $RS is not an intact rollback set"; exit 1; }
bash "$H/stopped-check.sh" "$D" || exit 1
[ -f "$D/bus/bus.sqlite" ] || { echo "refusing: no $D/bus/bus.sqlite"; exit 1; }
files=(); for f in bus.sqlite bus.sqlite-wal bus.sqlite-shm; do [ -f "$D/bus/$f" ] && files+=("$f"); done
umask 077
mkdir -m 700 "$O" "$O/bus" || exit 1
for f in "${files[@]}"; do cp -p "$D/bus/$f" "$O/bus/$f" || { echo "FAIL copy of $f"; exit 1; }; done
(cd "$D/bus" && sha256sum "${files[@]}") > "$O/SOURCE.sha256" || exit 1
(cd "$O/bus" && sha256sum "${files[@]}") > "$O/SHA256SUMS" || exit 1
cmp -s "$O/SOURCE.sha256" "$O/SHA256SUMS" || { echo "FAIL the copy differs from the source"; exit 1; }
(cd "$D/bus" && stat -c '%n %a %U %s %y' . "${files[@]}") > "$O/MODES.txt"
ls -1A "$D/bus" > "$O/LISTING.txt"
echo "$D" > "$O/DATA_ROOT"; date -u +%FT%TZ > "$O/STAMP"
echo "$RS" > "$O/ROLLBACK_SET"; cp "$RS/SHA256SUMS" "$O/ROLLBACK_SET.sha256"
bash "$H/stopped-check.sh" "$D" > /dev/null || { echo "FAIL a holder appeared during the copy; this backup is not consistent"; exit 1; }
cat "$O/SHA256SUMS"
echo BACKUP_OK
@@ -0,0 +1,21 @@
#!/bin/bash
# Re-pin 3190ad762, pre-window P2: save the live 56639c99b wrapper and unit files as the rollback set.
# Read-only on live files. Usage: capture-rollback.sh [OUT]; OUT defaults to sage-pin-rollback-56639c99b
# and must not exist. Ends with verify-live.sh, which must print MATCH three times.
OUT=${1:-/mnt/storage/scratch/sage-pin-rollback-56639c99b}; H=$(cd "$(dirname "$0")" && pwd)
OLD=56639c99b03ddca8f5fa84826b19322cd09a713e; U=$HOME/.config/systemd/user; W=$HOME/.npm-global/bin/mosaic
[ -e "$OUT" ] && { echo "refusing: $OUT exists"; exit 1; }
for f in "$W" "$U/[email protected]" "$U/mosaic-webui.service"; do
grep -q "$OLD" "$f" || { echo "refusing: $f does not name $OLD; the live pin is not the expected one"; exit 1; }
done
mkdir "$OUT" || exit 1
cp -p "$W" "$OUT/mosaic" && cp -p "$U/[email protected]" "$U/mosaic-webui.service" "$OUT/" || exit 1
for u in [email protected] mosaic-webui.service; do systemctl --user show "$u" > "$OUT/show-$u.txt"; done
for d in "$U/[email protected]" "$U/[email protected]" "$U/mosaic-webui.service.d"; do
if [ -e "$d" ]; then echo "$d: present"; ls -la "$d"; else echo "$d: absent"; fi
done > "$OUT/DROPINS.txt"
printf 'mosaic\t%s\[email protected]\t%s\nmosaic-webui.service\t%s\n' "$W" "$U/[email protected]" "$U/mosaic-webui.service" > "$OUT/SOURCES.txt"
cp "$H/verify-live.sh" "$OUT/verify-live.sh" && chmod 755 "$OUT/verify-live.sh"
(cd "$OUT" && sha256sum mosaic [email protected] mosaic-webui.service DROPINS.txt SOURCES.txt verify-live.sh show-*.txt) > "$OUT/SHA256SUMS"
cat "$OUT/DROPINS.txt"
bash "$OUT/verify-live.sh"
@@ -0,0 +1,35 @@
// Re-pin 3190ad762, check 3: the newest launch of INSTANCE since SINCE ran from COPY and carries row 138's spec tool.
// Usage: node check-launch.mjs INSTANCE SINCE_ISO COPY_ID. Prints hashes and PASS/FAIL/INFO lines only, never prompt text.
// The row 140 text and pm's request_staff stay as regression checks from the 56639c99b window.
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { dirname, join } from 'node:path';
const [instance, since, copyId] = process.argv.slice(2);
if (!instance || !since || !/^[0-9a-f]{40}$/.test(copyId ?? '')) { console.error('usage: node check-launch.mjs INSTANCE SINCE_ISO COPY_ID'); process.exit(4); }
const home = process.env.HOME, copy = `${home}/.local/share/mosaic-stack/copies/${copyId}`;
const log = readFileSync(`${home}/.mosaic-dev/launches/mosaic-stack.jsonl`, 'utf8').trim().split('\n').map(l => JSON.parse(l));
const launch = log.filter(l => l.event === 'launch' && l.instance === instance && l.at >= since).at(-1);
if (!launch) { console.log(`FAIL no launch of ${instance} since ${since}`); process.exit(1); }
const manifest = JSON.parse(readFileSync(launch.manifest, 'utf8')); // the log field is the path (9056fe5ad)
const prompt = readFileSync(join(dirname(launch.manifest), 'prompt.md'), 'utf8');
const sha = createHash('sha256').update(prompt).digest('hex');
// One line row 140 added to each contract (git diff 02ee7f640 56639c99b -- roles/), as the prompt carries it.
const want = {
chief: { file: 'roles/chief-of-staff.md', text: 'A request never widens the limits.' },
pm: { file: 'roles/pm.md', text: 'with the `request_staff` tool: name the task and each role with how' },
}[instance];
const typed = manifest.typed ?? [];
const checks = [
['run', true, `${launch.run} at ${launch.at} launchedBy ${launch.launchedBy} manifest ${launch.manifest}`],
['sources from copy', Object.values(manifest.sources).every(s => s.path.startsWith(copy + '/')), Object.values(manifest.sources).map(s => s.path.split('/copies/')[1]?.slice(0, 9)).join(',')],
['prompt.md digest matches manifest', manifest.files['prompt.md'] === sha, sha],
[`prompt carries ${want?.file} row 140 text`, !!want && prompt.includes(want.text), want?.text ?? 'no expectation for this instance'],
// Row 138: harness tools.mjs adds read_spec for every role, so its absence means the launch did not use NEW.
['typed has read_spec (row 138)', typed.includes('mcp__mosaic__read_spec'), `${typed.length} typed tools`],
];
if (instance === 'pm') checks.push(['pm typed has request_staff', typed.includes('mcp__mosaic__request_staff'), `${typed.length} typed tools`]);
let ok = true;
for (const [name, pass, detail] of checks) { ok &&= pass; console.log(`${pass ? 'PASS' : 'FAIL'} ${name}: ${detail}`); }
// write_record appears only when the launch's narrowed limits hold record.write, which the business launch block decides.
console.log(`INFO write_record ${typed.includes('mcp__mosaic__write_record') ? 'present' : 'absent'} (depends on the launch limits; not a pass condition)`);
console.log(ok ? 'CHECK3_PASS' : 'CHECK3_FAIL'); process.exit(ok ? 0 : 1);
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Re-pin 3190ad762, step 4: the 3e to 3f migration on disposable copies of the step 3 backup.
# Usage: rehearse.sh BACKUP NEW_COPY OLD_COPY. Never touches the live store or the backup itself.
# The rehearsal copies stay under BACKUP/rehearsal-*; they hold bus data, so they are 0700 like the backup.
set -o pipefail
O=${1:?usage: rehearse.sh BACKUP NEW_COPY OLD_COPY}; C=${2:?usage}; OC=${3:?usage}; H=$(cd "$(dirname "$0")" && pwd)
(cd "$O/bus" && sha256sum -c --quiet ../SHA256SUMS) || { echo "FAIL the backup does not match its SHA256SUMS"; exit 1; }
umask 077
mk() { local r; r=$(mktemp -d "$O/rehearsal-$1.XXXXXX") && mkdir -m 700 "$r/bus" && cp -p "$O"/bus/* "$r/bus/" && echo "$r"; }
R1=$(mk refuse) && R2=$(mk migrate) || { echo "FAIL could not make the rehearsal copies"; exit 1; }
rc=0
pass() { echo "PASS $1"; }
fail() { echo "FAIL $1"; rc=1; }
out=$(node "$H/verify-open.mjs" "$C" "$R1" 2>&1)
[ "$out" = "REFUSED schema-mismatch" ] && pass "NEW refuses the 3e store at a normal open (no auto-migrate)" || fail "NEW open of the 3e copy: $out"
t0=$(date +%s%N)
out=$(node "$C/packages/bus/src/migrate-schema.mjs" "$R2" 2>&1); mrc=$?
ms=$(( ($(date +%s%N) - t0) / 1000000 ))
printf '%s\n' "$out" | sed 's/^/ migrate: /'
[ $mrc = 0 ] && [ "$(printf '%s\n' "$out" | tail -1)" = "bus schema 3f ready" ] && pass "migrate-schema rc 0, bus schema 3f ready, ${ms} ms" || fail "migrate-schema rc $mrc"
out=$(node "$H/verify-open.mjs" "$C" "$R2" 2>&1)
[ "$out" = "OPEN_OK 3f" ] && pass "NEW opens the migrated copy: $out" || fail "NEW open of the migrated copy: $out"
out=$(node "$C/packages/bus/src/migrate-schema.mjs" "$R2" 2>&1); mrc=$?
[ $mrc = 0 ] && [ "$(printf '%s\n' "$out" | tail -1)" = "bus schema 3f ready" ] && pass "a second run is a validated no-op" || fail "second migrate-schema run rc $mrc: $out"
out=$(node "$H/verify-open.mjs" "$OC" "$R2" 2>&1)
[ "$out" = "REFUSED schema-mismatch" ] && pass "OLD refuses the 3f store" || fail "OLD open of the migrated copy: $out"
(cd "$O/bus" && sha256sum -c --quiet ../SHA256SUMS) && pass "backup unchanged" || fail "the backup changed"
echo "rehearsal copies: $R1 $R2"
[ $rc = 0 ] && echo REHEARSAL_PASS || echo REHEARSAL_FAIL
exit $rc
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# Re-pin 3190ad762, rollback R2: put the step 3 backup back as DATA_ROOT's bus store.
# Usage: restore.sh DATA_ROOT BACKUP. Deletes nothing: the post-attempt bus directory is renamed to
# DATA_ROOT/bus.post-attempt-<UTC stamp> first, so it stays for reconciliation. Prints RESTORE_OK.
set -o pipefail
D=${1:?usage: restore.sh DATA_ROOT BACKUP}; O=${2:?usage: restore.sh DATA_ROOT BACKUP}; H=$(cd "$(dirname "$0")" && pwd)
[ "$(cat "$O/DATA_ROOT" 2>/dev/null)" = "$D" ] || { echo "refusing: $O was not taken from $D"; exit 1; }
bash "$H/stopped-check.sh" "$D" || exit 1
(cd "$O/bus" && sha256sum -c --quiet ../SHA256SUMS) || { echo "FAIL the backup does not match its SHA256SUMS; stop and diagnose"; exit 1; }
A="$D/bus.post-attempt-$(date -u +%Y%m%dT%H%M%SZ)"
[ -e "$A" ] && { echo "refusing: $A exists"; exit 1; }
mv "$D/bus" "$A" || { echo "FAIL could not move $D/bus aside"; exit 1; }
echo "post-attempt store kept at $A"
umask 077
mkdir -m 700 "$D/bus" && cp -p "$O"/bus/* "$D/bus/" || { echo "FAIL copy-in; the post-attempt store is still at $A"; exit 1; }
(cd "$D/bus" && sha256sum -c --quiet "$O/SHA256SUMS") || { echo "FAIL the restored files do not match the backup"; exit 1; }
want=$(awk '{print $2}' "$O/SHA256SUMS" | sort); got=$(ls -1A "$D/bus" | sort)
[ "$want" = "$got" ] || { echo "FAIL $D/bus holds files the backup does not"; exit 1; }
echo RESTORE_OK
@@ -0,0 +1,15 @@
#!/bin/bash
# Re-pin 3190ad762, step 3a: nothing holds DATA_ROOT's bus store. Read-only; it never opens the store.
# Prints STOPPED_OK, or a FAIL line per problem.
D=${1:?usage: stopped-check.sh DATA_ROOT}; B="$D/bus"; rc=0
[ -d "$B" ] || { echo "FAIL no $B"; exit 1; }
if [ -e "$B/writer.lock" ]; then
echo "FAIL $B/writer.lock is present: diagnose it under the stale-lock procedure, never delete it"; rc=1
fi
holders=$(for f in /proc/[0-9]*/fd/*; do
t=$(readlink "$f" 2>/dev/null) || continue
case "$t" in "$B"/*) f=${f#/proc/}; echo "${f%%/*}";; esac
done | sort -u | tr '\n' ' ')
if [ -n "$holders" ]; then echo "FAIL files under $B are open by pid $holders"; rc=1; fi
[ $rc = 0 ] && echo STOPPED_OK
exit $rc
@@ -0,0 +1,12 @@
#!/bin/bash
# Rollback B step 4: compare the live wrapper and unit files with the 56639c99b set saved before the window.
# MATCH on all three means the reinstall reproduced the pre-window files byte for byte.
R=$(cd "$(dirname "$0")" && pwd)
cd "$R" || exit 9
rc=0
while IFS=$'\t' read -r name live; do
want=$(awk -v f="$name" '$2 == f {print $1}' SHA256SUMS)
got=$(sha256sum "$live" | cut -d' ' -f1)
if [ -n "$want" ] && [ "$want" = "$got" ]; then echo "MATCH $name"; else echo "DIFFER $name ($live)"; rc=1; fi
done < SOURCES.txt
exit $rc
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# Re-pin 3190ad762, step 8: the live wrapper and unit files are the 56639c99b set with only the copy id changed,
# and the units equal what bus-service.sh and webui-service.sh render for NEW. The renders run at check time,
# because they need the NEW copy, which P1 makes. Nothing under scripts/ changed between OLD and NEW.
OLD=56639c99b03ddca8f5fa84826b19322cd09a713e; NEW=3190ad762b8fd5c09ad7515e4f7e609e81ae1af6
R=/mnt/storage/scratch/sage-pin-rollback-56639c99b; S=/mnt/storage/src/mosaic-stack/scripts; U=$HOME/.config/systemd/user
rc=0
chk() { if sed "s/$OLD/$NEW/g" "$R/$1" | cmp -s - "$2"; then echo "MATCH $1 (id swapped)"; else echo "DIFFER $1 ($2)"; rc=1; fi; }
chk mosaic "$HOME/.npm-global/bin/mosaic"
chk [email protected] "$U/[email protected]"
chk mosaic-webui.service "$U/mosaic-webui.service"
"$S/bus-service.sh" render --copy "$NEW" --pm --claude /home/jwoltje/.local/bin/claude 2>&1 | cmp -s - "$U/[email protected]" \
&& echo "MATCH bus render" || { echo "DIFFER bus render"; rc=1; }
"$S/webui-service.sh" render --copy "$NEW" --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username \
--allow-user jason.woltje --send 2>&1 | cmp -s - "$U/mosaic-webui.service" && echo "MATCH webui render" || { echo "DIFFER webui render"; rc=1; }
exit $rc
@@ -0,0 +1,19 @@
// Re-pin 3190ad762: open DATA_ROOT's bus store with COPY's store.mjs the way the bus does at start (no migration).
// Usage: node verify-open.mjs COPY DATA_ROOT. Prints OPEN_OK <schema_version> or REFUSED <code>. Never reads rows.
// Refuses the live data root: this is for disposable rehearsal copies only.
import { realpathSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const [copy, root] = process.argv.slice(2);
if (!copy || !root) { console.error('usage: node verify-open.mjs COPY DATA_ROOT'); process.exit(4); }
if (realpathSync(root) === realpathSync(join(process.env.HOME, '.mosaic-dev'))) {
console.error('refusing: verify-open.mjs never opens the live data root'); process.exit(4);
}
const { Store } = await import(pathToFileURL(join(copy, 'packages/bus/src/store.mjs')).href);
let store;
try {
store = new Store(root);
console.log(`OPEN_OK ${store.get("SELECT value FROM meta WHERE key='schema_version'").value}`);
} catch (e) {
console.log(`REFUSED ${e.message}`); process.exitCode = 1;
} finally { store?.close(); }
+1
View File
@@ -1059,3 +1059,4 @@ are never rewritten or removed; corrections are new entries.
2026-10-16 01:37:53 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 138 #1632 review round 2 | r4 on 26a0b0fab; D1-D4 tests, async socket deadline and wording fixes; project 30/bus 294/business 109/harness 105/cli 215 green, 59/59 mutants killed; 43 hashes restored, process sweep empty; slot clear 01:37:17Z; candidate e3f9a4008c8cd0098f028aa17e9cbde8b15d3b64bc13f3ee306632070a318f70; no live action.
2026-10-16T01:29:26Z | Dewey (Claude Opus 5.5, 84fb346c) | row 138 round 1 review | Request changes on 75e07b65 (42 paths, base 26a0b0fab): design, grants, migration, adapter injection, snapshots and row 152 all hold; 16 touched files singly 118/118; four mutants D1-D4 survived full bus 289/289 and project 29/29 suites in the sage-0491 slot (01:24:34-01:25:25Z), so four tests are asked for. Comment 28752 as dewey; rev 1504 uncommitted for Sage's sweep; packet ff86a4473, not pushed; slot cleared 01:26:53Z.
2026-10-16T01:43:02Z | Dewey (Claude Opus 5.5, 84fb346c) | row 138 round 2 review | Approve on e3f9a400 (43 paths, base 26a0b0fab): my round 1 D1-D4 mutants, rerun unchanged, and D5 (removes the N-a setTimeout(120_000)) all killed, each by its new test only, full bus 294 and project 30 suites, in my slot 01:40:40-01:41:55Z; 17 touched files singly 124/124; N-a confined to the async project branch. Notes: 120 s covers one git process, not a whole write; set before admission. Comment 28754 as dewey; rev 1510 uncommitted for Sage's sweep; packet de3e93ca6, not pushed.
2026-10-16T02:08:59Z | Sage (Claude Opus 5.5, 1ef1e4f8) | window plan for the re-pin to 3190ad762 (rows 152, 138, bus 3e to 3f) | Wrote agents/sage/work/window-plan/repin-3190ad762.md and its 9 scripts with SCRIPTS.sha256, on Mos directive re sage-0494. Tested on fixture stores and read-only live controls; ran no step of the plan.