sage: window plan for the 52f35e7a0 re-pin, rows 135, 139, 150 and 137

Re-pin 3190ad762 -> 52f35e7a0, no migration, per Mos's 20:58Z message.
The plan puts row 137's refs/heads/root write in the window. One working
repo, mosaic-stack/stack.git. Its seed root is the canonical checkout.
Jason's ~/.mosaic-dev/projects/mosaic-stack/stack is named and is not
touched. working-refs.sh records the refs before and after; root-rollback.sh
deletes only the seed-created refs/heads/root, matched by sha, and leaves
main alone. Copy and rollback set (sage-pin-rollback-3190ad762) are made
and checked. Nothing live ran.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-16 16:10:06 -05:00
co-authored by Claude Opus 5.5
parent 22dbc2b281
commit dc12dc9c0c
9 changed files with 545 additions and 0 deletions
@@ -0,0 +1,335 @@
# Re-pin for rows 135, 139, 150 and 137, no migration
Sage, 2026-10-16. Plan for Mos's window, per Mos's 20:58Z message: copy and
rollback set first, then the plan, nothing live before Mos's go. Both are
done and checked. Rows 139 and 137 share this re-pin and both SMOKE runs
(Mos's earlier ruling). Mos runs every step. Sage ran none of it; the
controls Sage did run are listed at the end.
No schema, migration, store.mjs, bus-service.sh, webui-service.sh,
stack-copy.sh, systemd, adapters, package*.json or RELEASE change between OLD
and NEW, so this is the `repin-56639c99b.md` shape. The one new part is the
working repository's `refs/heads/root`, which Mos asked to have in the plan.
It has its own section, its own pre and post record, and its own rollback.
- OLD, live now: `3190ad762b8fd5c09ad7515e4f7e609e81ae1af6`, bus schema 3f.
Bus (MainPID 3575632) and Console (3575635) started 2026-10-16 12:29:51
CDT. `stack-copy.sh check` at 21:06:55Z: bus, Console and CLI on OLD.
- NEW: `52f35e7a0933e9fa845f59860a5de1709f9e5756`, on origin `refactor`
since 20:59:59Z. Copy, made by Sage:
`/home/jwoltje/.local/share/mosaic-stack/copies/52f35e7a0933e9fa845f59860a5de1709f9e5756`.
Bus schema 3f, the same as OLD.
- Rollback set: `RS=/mnt/storage/scratch/sage-pin-rollback-3190ad762`, made by
Sage with `capture-rollback.sh`. MATCH × 3.
- Scripts: `P=/mnt/storage/scratch/sage-pin-52f35e7a0`. The reviewed copies
are committed beside this file in `repin-52f35e7a0/`, with
`SCRIPTS.sha256` (7 files). P0 checks that P matches them.
```
REPO=/mnt/storage/src/mosaic-stack
P=/mnt/storage/scratch/sage-pin-52f35e7a0
RS=/mnt/storage/scratch/sage-pin-rollback-3190ad762
D=/home/jwoltje/.mosaic-dev
OLD=3190ad762b8fd5c09ad7515e4f7e609e81ae1af6
NEW=52f35e7a0933e9fa845f59860a5de1709f9e5756
C=/home/jwoltje/.local/share/mosaic-stack/copies/$NEW
```
## What NEW carries over OLD
Four commits change the copy. Everything else between OLD and NEW is queue,
session, BUILD-LOG and packet commits outside COPY_PATHS.
- Row 135 (7473dcd2a): PRD 1.5 text and `packages/tasks/requirements.json`.
No runtime behavior change.
- Row 139 (63c82f1c4): a gated decision that cites a confirmed spec commit
goes to the business's arbiter for its domain instead of Jason. Hard waits
still go to Jason. Under Q169, a revoke of the assistant is Jason's or the
chief's alone. `GATED_ONLY` and `vocabulary.mjs` changed, which is why P4
loads the live business file under NEW.
- Row 150 (0f7bca083): `mosaic inbox --read <id>...` and `--read-all` mark
the human's messages read, and the Console's send section names them. The
Console's `public/bus.js` changes; Mos's decision 123 go on public diff
096d922c covers it.
- Row 137 (f1ee9a989): the `land_work` tool, the broker's `landHeld`
host op, called before every launch, and the new working-repository seed.
No role definition in NEW's `roles/` grants git.merge.protected, so no
launch can get `land_work` and nothing can land through the bus before
row 149.
## refs/heads/root (Mos's 20:58Z item 1)
### What writes it
`seed()` in `packages/seat/src/working.mjs:164`. Under NEW it runs
`fetch <root> +HEAD:refs/heads/root`, forced, into the working repository.
Then it creates `main` if `main` is absent, or moves `main` to the new root
with a compare-and-swap `update-ref` only when `main` is an ancestor of it.
Otherwise `main` stays where it is, with no log line (Dewey's finding 2,
below). OLD's seed instead forced `+HEAD:refs/heads/main` on every call.
The seed runs only inside a push (working.mjs:207), a review fetch (263) or a
land (317). It never runs at bus start, and no window step and no read-only
smoke step causes one. A land needs `land_work`, which no launch can have. So
the first write of `refs/heads/root` is the first coder push or reviewer
fetch through the git socket after the re-pin.
### Every repository involved
There is one working repository. Its seed root is the canonical checkout,
not Jason's project repo.
| Repository | Role | Now | After the first seed under NEW |
|---|---|---|---|
| `/home/jwoltje/.mosaic-dev/working/mosaic-stack/stack.git` | working repo the seed writes (`<dataRoot>/working/<business>/<project>.git`) | HEAD `refs/heads/master` (dangling); `main` fc5b342f839498b46499fe8f00c79dd78ba9e1e4; no `refs/heads/root`; 15 `refs/heads/work/*` refs | `refs/heads/root` := the seed root's HEAD at that moment; `main` fast-forwards fc5b342f8 → that commit; work refs untouched |
| `/mnt/storage/src/mosaic-stack` | seed root, `projects.stack.root` in `~/.config/mosaic-dev/businesses/mosaic-stack.json`; only read | HEAD 22dbc2b28169cf3652126b8c51bdfc8f7c433b57 at planning time | unchanged by the seed |
| `/home/jwoltje/.mosaic-dev/projects/mosaic-stack/stack` | Jason's project repo, row 138's spec repo (`packages/project/src/paths.mjs:32`) | HEAD ae0b17c9586e51da7bc1b7e548c6acf913a55665 ("Initialize project layout v1"); no `refs/heads/root` | **not touched.** The seed never reads or writes it. `read_spec` only reads it. |
Mos's 20:58Z premise was that the write could land in Jason's
`~/.mosaic-dev/projects/mosaic-stack/stack`. It can't. The seed's root is
`projects.<id>.root`, the canonical checkout. The pre and post records still
read Jason's repo and fail if its HEAD moves or a root ref appears there.
### The sha it gets
`refs/heads/root` gets the canonical checkout's HEAD at the moment of the
first seed. That was 22dbc2b28 at planning time, and the commit of this plan
moves it again. Every landing and every seat commit in the canonical checkout
moves it, including local commits not yet pushed. OLD's seed carried those
into `main` the same way. So the sha can't be fixed in advance. P5 records the
HEAD at window time; `working-refs.sh post` prints the actual sha as a
`ROOT_SHA` line, and the rollback deletes the ref only if it still names that
sha.
`main` is fc5b342f8, an ancestor of the canonical HEAD, so the first seed
fast-forwards `main` to the root. OLD's next push would force the same move.
The rollback leaves `main` where the seed put it, as Mos asked. The repo's
dangling `HEAD -> refs/heads/master` predates this window and NEW doesn't
change it. I saw it; I didn't investigate it.
### The record (P5 and step 7)
`working-refs.sh` is read-only: `git rev-parse`, `symbolic-ref`,
`for-each-ref` and `merge-base`, with no system or global git config and hooks
off. It reads only the business file's `projects` block, and never the bus
store.
- `pre OUT` lists every `$D/working/*/*.git` with its path, HEAD, `main`, root
ref and work-ref count. For each it lists the seed root and that root's HEAD
now, plus Jason's project repo with its HEAD and root ref. It prints PRE_STOP
and writes nothing if any of these repos already has `refs/heads/root`. It
prints the predicted first seed and writes OUT plus OUT.sha256. It refuses an
existing OUT.
- `post RECORD` checks the record's sum, then for each repo:
- `main` must be unchanged, or a fast-forward of the recorded `main`; else
DIFFER.
- `ROOT_SHA <repo> <sha>`, or "absent: no seed has run under NEW".
- Jason's repo must be unchanged with no root ref; else DIFFER.
It ends with POST_OK or POST_DIFFER.
### The root rollback
`root-rollback.sh RECORD REPO SHA` for each `ROOT_SHA` line from `post`:
- The record must pass its sum and name REPO, so the ref can only be one that
didn't exist at P5.
- The bus unit must be `inactive` or `failed`, and if
`$D/bus-host/host.lock` exists, its pid must not be running. The lock file
can outlive a stop, so the script checks the pid, not the file.
- If `refs/heads/root` is absent, it prints NOTHING and exits 0. If it names
another sha, it prints STOP and deletes nothing.
- Otherwise `git update-ref -d refs/heads/root SHA`, a compare-and-delete. It
then checks every other `refs/heads/*` ref, `main` included, is as it was,
and prints `ROOT_REMOVED <repo> <sha>`.
It never touches `main`, the work refs, the seed root or Jason's repo.
## Steps
### Before the window (P0 to P5, nothing stopped)
| # | Who | Step |
|---|-----|------|
| P0 | Mos | `cd $P && sha256sum -c $REPO/agents/sage/work/window-plan/repin-52f35e7a0/SCRIPTS.sha256`, 7 × OK. |
| P1 | Sage, done; Mos checks | The copy exists. Copy check below, rc 0 with no output. Then `grep -l git.merge.protected $C/roles/*.json` prints nothing (no role can land). |
| P2 | Sage, done; Mos checks | `bash $RS/verify-live.sh`, 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`. |
| P4 | Mos | `node $P/business-check.mjs $NEW` prints counts and LOAD_OK. REFUSED means the live business file fails under NEW's loader and roles; stop. |
| P5 | Mos | `bash $P/working-refs.sh pre $P/refs-window.tsv`. PRE_OK, one repo, the predicted first seed, and Jason's repo HEAD ae0b17c9 with no root ref. PRE_STOP ends the window before it starts. Run P5 last, close to step 1, so the recorded root HEAD is current. |
The P1 copy check:
```
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 P4 change nothing live and can run any time. The copy and the
rollback set stay if the window is postponed. P5 writes only under `$P`.
### 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 7 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 | `$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`. |
| 4 | Mos | `$REPO/scripts/stack-copy.sh wrapper $NEW`. |
| 5 | Mos | Start the bus, then the Console. |
| 6 | Mos | Step 6 checks, below. |
| 7 | Mos | `bash $P/working-refs.sh post $P/refs-window.tsv`. POST_OK, `main unchanged`, `refs/heads/root absent`, Jason's repo unchanged. |
| 8 | Mos | Smokes from tmux, below. |
### Step 6 checks
```
$REPO/scripts/stack-copy.sh check # bus, Console, CLI on NEW
bash $P/verify-new.sh # 5 x MATCH, rc 0
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
```
`verify-new.sh` checks that the live wrapper and both units equal the saved
3190ad762 files with only the copy id swapped, and that the units equal what
`bus-service.sh render` and `webui-service.sh render` give for NEW. It prints
DIFFER × 5 today, as it should before the window.
`check-launch.mjs` now also fails if `typed` has `mcp__mosaic__land_work`.
That is Dewey's row 137 step A for each launch. It keeps the earlier checks:
sources under NEW, the prompt digest, the row 140 text, `read_spec`, and
`request_staff` for pm. Its `write_record` INFO line is a fact to record, not
a pass condition.
### Step 8, smokes from tmux
Every `talk` starts with `[mos test]` and runs with `--tester`. Dewey's plans
say business "stack"; the live business is `mosaic-stack` and the project is
`stack`. Use the same `--business` the earlier windows used.
First, the pm launch:
```
mosaic talk pm "[mos test] re-pin 52f35e7a0: reply with one line, no staffing request" --tester
node $P/check-launch.mjs pm <step 1 stamp> $NEW # CHECK3_PASS, also request_staff, no land_work
```
**Row 139**, `agents/dewey/work/queue-139/SMOKE.md`. Mos checks its
preconditions read-only, including that `arbiters.delivery` names pm and that
pm's new manifest has `mcp__mosaic__resolve_decision` in `typed`.
- A, cited `policy.change` from chief: goes to pm, not Jason's inbox. pm
resolves it `no`. Run it.
- B, cited `network.expose`: goes to Jason's inbox. Needs Jason's agreement
first, and Jason resolves it `no`.
- C and D are optional. D, the assistant revoke, also puts an item in Jason's
inbox, so it also needs Jason.
**Row 137**, `agents/dewey/work/queue-137/SMOKE.md`, read-only as Mos ruled.
- Preconditions 1 to 6 as written, read-only. Precondition 5 is P5's record.
- A: every launch in this window has no `land_work`. `check-launch.mjs`
already checks chief and pm; it's a FAIL line there, not a separate step.
- B isn't in the window. No step here pushes or fetches. B is observed at the
first coder push or reviewer fetch after the window, with
`working-refs.sh post $P/refs-window.tsv`. That push fast-forwards `main`
fc5b342f8 → root HEAD in the live working repo. Dewey's plan puts it behind
Jason's agreement. OLD would force the same move today.
- C, the cited gated merge from coder, needs a coder relaunch. The Q134 PM
test limits keep that out, and it leaves an approved, unspent
git.merge.protected decision in the store (Dewey's finding 3). Skip it in
this window.
- D: Sage's next land-work landing shows `Resolved ... via the CLI`. It isn't
tied to the window.
## Stop conditions
Before step 2, any one ends the window with nothing changed:
- P0 to P5 fail. PRE_STOP is a stop and a report; nobody deletes a root ref
that was there before the window.
From step 5 on, any one means rollback:
- 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.
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.
- Step 7 prints POST_DIFFER.
A step 8 failure is a finding, not an automatic rollback, unless the live
talk path is broken. Mos decides. A `refs/heads/root` that appears before
step 8 means something pushed or fetched during the window; stop and report.
## Rollback
The 3190ad762 plan had rollbacks A and B. With no migration, only the B shape
applies, and the root step runs in both places Mos named.
1. Stop the Console, then the bus; both `inactive`.
2. `bash $P/working-refs.sh post $P/refs-window.tsv`. Write down each
`ROOT_SHA <repo> <sha>` line.
3. For each one: `bash $P/root-rollback.sh $P/refs-window.tsv <repo> <sha>`.
ROOT_REMOVED, or NOTHING if the ref is absent. STOP means diagnose; nobody
deletes the ref by hand.
4. Repeat steps 3 and 4 with `$OLD` in place of `$NEW`.
5. `bash $RS/verify-live.sh`, MATCH × 3. A DIFFER means stop and diff
against the saved files before starting anything.
6. Start the bus, then the Console. `stack-copy.sh check` shows OLD.
`main` stays where it is. If a seed ran, that is the fast-forward to the
root, which OLD's next push would force anyway. Nothing prunes copies, so
OLD and NEW stay on disk. Any decision row 139 routed to pm and pm resolved
under NEW stays resolved after the rollback.
The same `post` and `root-rollback.sh` steps apply to a rollback after the
window, once a seed has run, as long as `$P/refs-window.tsv` is kept. Keep it
with the rollback set.
## Notes
- Dewey's row 137 findings: 1, no live landing before row 149, goes into
row 149's plan with Jason. 2, the seed declines silently when `main` isn't
an ancestor of the root; follow-up issue, not a 137 blocker. 3, step C would
leave an approved, unspent merge decision; C is skipped here.
- Dewey's row 139 finding 1, the raise tool has no `domain` field, goes to
row 149.
- Keep the copies: OLD and NEW, plus all earlier rollback copies.
- Record for BUILD-LOG: step 1 stamp, the P5 record and its sum, the step 6
output, the step 7 output, the chief and pm manifest paths and prompt
digests, the smoke trails, and the downtime from step 2 to step 6.
## What Sage ran for this plan
Nothing live was stopped, started, installed or re-pinned. No store file was
opened. No working ref was written in the live repo.
- P1: `stack-copy.sh copy $NEW`, rc 0, and the `git archive` diff against
the copy, rc 0. `grep git.merge.protected` over NEW's `roles/*.json`: none.
- P2: `capture-rollback.sh` to `$RS`, MATCH × 3.
- P4: `business-check.mjs` under OLD and NEW, LOAD_OK both.
- `working-refs.sh pre` on the live repos to `$P/refs-plan.tsv`: PRE_OK, one
working repo, prediction "main fast-forwards fc5b342f8 -> 22dbc2b28".
Then `post` against it: POST_OK, root ref absent, Jason's repo unchanged.
A second `pre` to the same OUT refused, rc 1.
- `root-rollback.sh` on a fixture repo under `$P/fixture`, as `t.sh`: `sed`
changed 2 lines, the bus state to `inactive` and the lock path to a fixture
lock. Cases 1 to 6:
1. a root ref with another sha: STOP, nothing deleted, rc 1;
2. a repo not in the record: STOP, rc 1;
3. a lock whose pid is running: STOP, rc 1;
4. a stale lock and the matching sha: ROOT_REMOVED, `main` and the work ref
unchanged, rc 0;
5. the same again: NOTHING, rc 0;
6. a tampered record: STOP, fails its sum, rc 1.
Case 7 is the unchanged script on the fixture with the record restored and
the root ref recreated, while the live bus runs: "STOP the bus is active;
stop it first", rc 1, the ref still there.
- `verify-new.sh` now: DIFFER × 5, rc 1. With NEW set to OLD: MATCH × 5.
- `check-launch.mjs` against the live chief (rb9e5865e56ff) and pm
(r673f8b6b62ba) runs with `$OLD`: CHECK3_PASS both, `land_work` absent,
`write_record` present (INFO).
@@ -0,0 +1,7 @@
19bc98f6c2a24a69ddeeb4865d560d1e4099368abfaa9c3ec488daacd2801dce business-check.mjs
b74067b5573eeac37824c1c427115623b9a506cde6d2597aef770d62ca0f9bb5 capture-rollback.sh
7cff3b62dcfb32d7ade3218adb11ba689a817f488c71924a3adc8c12ed5d561c check-launch.mjs
110d9fd2d7bb4dddd431d3e94e2c8de26b8e1d8c24c44fcc5a7a1d75ac2932b5 root-rollback.sh
f1f02b49532afbef46250ed79b4805bb4d1d0020abdcaa3f83be456841abf038 verify-live.sh
caf8cd34324e49651de8d0675ef3f2efde74e174ba4870af179727867973b328 verify-new.sh
7d8eed95220ca3f777863d9309efeea4b7206def3618e6aac73246a9d42670f1 working-refs.sh
@@ -0,0 +1,17 @@
// Re-pin 52f35e7a0, P4: the live business file loads under COPY's loader and roles, the way the bus loads it at
// start. Row 139 changed GATED_ONLY and vocabulary.mjs, so a file OLD takes could refuse under NEW.
// Usage: node business-check.mjs COPY_ID. Read-only. Prints counts and LOAD_OK, or REFUSED with the loader's
// message, never a value from the file.
const [copyId] = process.argv.slice(2);
if (!/^[0-9a-f]{40}$/.test(copyId ?? '')) { console.error('usage: node business-check.mjs COPY_ID'); process.exit(4); }
const copy = `${process.env.HOME}/.local/share/mosaic-stack/copies/${copyId}`;
const { loadBusiness } = await import(`${copy}/packages/business/src/business.mjs`);
try {
const b = loadBusiness('mosaic-stack', { rolesDir: `${copy}/roles` });
const count = (o) => Object.keys(o ?? {}).length;
console.log(`copy ${copyId.slice(0, 9)}: roles ${count(b.roles)}, projects ${count(b.projects)}, arbiters ${count(b.arbiters)}`);
console.log('LOAD_OK');
} catch (e) {
console.log(`REFUSED ${e.message}`);
process.exit(1);
}
@@ -0,0 +1,21 @@
#!/bin/bash
# Re-pin 52f35e7a0, pre-window P2: save the live 3190ad762 wrapper and unit files as the rollback set.
# Read-only on live files. Usage: capture-rollback.sh [OUT]; OUT defaults to sage-pin-rollback-3190ad762
# and must not exist. Ends with verify-live.sh, which must print MATCH three times.
OUT=${1:-/mnt/storage/scratch/sage-pin-rollback-3190ad762}; H=$(cd "$(dirname "$0")" && pwd)
OLD=3190ad762b8fd5c09ad7515e4f7e609e81ae1af6; 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,38 @@
// Re-pin 52f35e7a0, check 3: the newest launch of INSTANCE since SINCE ran from COPY, still carries row 138's spec tool,
// and has no land_work, since no role in roles/ holds git.merge.protected before row 149 (row 137, Dewey's SMOKE.md step A).
// 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, read_spec and pm's request_staff stay as regression checks from the 56639c99b and 3190ad762 windows.
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`],
// Row 137: land_work goes only to a launch whose limits hold git.merge.protected, and no role grants it yet.
['typed has no land_work (row 137)', !typed.includes('mcp__mosaic__land_work'), `${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);
@@ -0,0 +1,32 @@
#!/bin/bash
# Re-pin 52f35e7a0, rollback A or B: delete the refs/heads/root that row 137's seed created in one working repo,
# matched by sha, and nothing else. main and every work ref stay where they are.
# Usage: root-rollback.sh RECORD REPO SHA, where RECORD is working-refs.sh's pre record (it proves REPO had no
# refs/heads/root before the window) and SHA is the ROOT_SHA line working-refs.sh post printed for REPO.
# The bus must be stopped: only the bus host writes a working repo, under its own lock.
rec=$1; repo=$2; sha=$3
[ $# -eq 3 ] && [[ "$sha" =~ ^[0-9a-f]{40}$ ]] || { echo "usage: root-rollback.sh RECORD REPO SHA (40-hex)"; exit 4; }
(cd "$(dirname "$rec")" && sha256sum -c --quiet "$(basename "$rec").sha256") || { echo "STOP record fails its sum"; exit 1; }
awk -F'\t' -v r="$repo" '$1 == r {f=1} END {exit !f}' "$rec" || { echo "STOP $repo is not in the pre record"; exit 1; }
state=$(systemctl --user is-active [email protected])
[ "$state" = inactive ] || [ "$state" = failed ] || { echo "STOP the bus is $state; stop it first"; exit 1; }
# host.lock can outlive a stop (host.mjs claims a stale one), so check its holder, not the file.
L=/home/jwoltje/.mosaic-dev/bus-host/host.lock
if [ -e "$L" ]; then
pid=$(node -e 'try { const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).pid; if (Number.isInteger(p)) console.log(p) } catch {}' "$L")
[ -n "$pid" ] || { echo "STOP $L has no readable pid"; exit 1; }
[ -e "/proc/$pid" ] && { echo "STOP the bus host lock's pid $pid is running"; exit 1; }
fi
g() { env -i HOME="$HOME" PATH=/usr/bin:/bin GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null \
git -c core.hooksPath=/dev/null -c core.fsmonitor=false --git-dir="$repo" "$@"; }
before=$(g for-each-ref --format='%(refname) %(objectname)' refs/heads | grep -v '^refs/heads/root ')
now=$(g rev-parse -q --verify refs/heads/root)
[ -n "$now" ] || { echo "NOTHING refs/heads/root is absent in $repo"; exit 0; }
[ "$now" = "$sha" ] || { echo "STOP refs/heads/root is $now, not $sha; nothing deleted"; exit 1; }
# Compare and delete: git deletes the ref only if it still names $sha.
g update-ref -d refs/heads/root "$sha" || { echo "STOP update-ref -d failed; nothing else touched"; exit 1; }
after=$(g for-each-ref --format='%(refname) %(objectname)' refs/heads)
[ "$before" = "$after" ] || { echo "DIFFER refs other than root changed:"; diff <(echo "$before") <(echo "$after"); exit 1; }
g rev-parse -q --verify refs/heads/root >/dev/null && { echo "STOP refs/heads/root still exists"; exit 1; }
echo "main $(g rev-parse -q --verify refs/heads/main) unchanged; $(echo "$after" | grep -c .) refs as before"
echo "ROOT_REMOVED $repo $sha"
@@ -0,0 +1,12 @@
#!/bin/bash
# Rollback B step 4: compare the live wrapper and unit files with the 3190ad762 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 52f35e7a0, step 8: the live wrapper and unit files are the 3190ad762 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. Nothing in bus-service.sh, webui-service.sh or stack-copy.sh changed between OLD and NEW.
OLD=3190ad762b8fd5c09ad7515e4f7e609e81ae1af6; NEW=52f35e7a0933e9fa845f59860a5de1709f9e5756
R=/mnt/storage/scratch/sage-pin-rollback-3190ad762; 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,67 @@
#!/bin/bash
# Re-pin 52f35e7a0: the refs/heads/root record Mos asked for at 20:58Z. Row 137's seed (working.mjs seed())
# fetches each project's root HEAD into <dataRoot>/working/<business>/<project>.git as refs/heads/root, forced,
# and moves main only by fast-forward. OLD's seed instead forced main to the root HEAD on every push and review.
# The seed runs only inside a push, a review fetch or a land, never at bus start.
#
# working-refs.sh pre OUT every working repo: path, HEAD, main, root ref (must be absent, else STOP),
# the project root it seeds from and that root's HEAD now, which is the sha
# refs/heads/root gets if the seed runs before the root moves. Also Jason's
# project repo <dataRoot>/projects/<business>/<project>, which the seed never
# reads or writes. Writes OUT (refuses if it exists) and OUT.sha256.
# working-refs.sh post RECORD the same repos now: main unchanged or fast-forwarded from the recorded main,
# refs/heads/root absent or its sha, Jason's repo HEAD unchanged with no root ref.
#
# Read-only: git rev-parse, symbolic-ref, for-each-ref and merge-base only, with no system or global config and
# hooks off. Reads the business file's projects block and nothing else of it. Never opens the bus store.
D=/home/jwoltje/.mosaic-dev; BF=$HOME/.config/mosaic-dev/businesses
g() { env -i HOME="$HOME" PATH=/usr/bin:/bin GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null \
git -c core.hooksPath=/dev/null -c core.fsmonitor=false "$@"; }
ref() { g --git-dir="$1" rev-parse -q --verify "$2^{commit}" 2>/dev/null; }
root_of() { node -e 'const [f,p]=process.argv.slice(1); const b=JSON.parse(require("fs").readFileSync(f,"utf8")); const e=(b.projects??{})[p]; if(!e?.root) process.exit(3); console.log(e.root)' "$BF/$1.json" "$2"; }
mode=$1; out=$2
[ "$mode" = pre ] || [ "$mode" = post ] || { echo "usage: working-refs.sh pre OUT | post RECORD"; exit 4; }
[ -n "$out" ] || { echo "usage: working-refs.sh pre OUT | post RECORD"; exit 4; }
if [ "$mode" = pre ]; then [ -e "$out" ] && { echo "refusing: $out exists"; exit 1; }; rec=$(mktemp); else
[ -f "$out" ] || { echo "no record at $out"; exit 4; }; (cd "$(dirname "$out")" && sha256sum -c --quiet "$(basename "$out").sha256") || { echo "STOP record fails its sum"; exit 1; }; fi
rc=0; n=0
for repo in "$D"/working/*/*.git; do
[ -d "$repo" ] || continue; n=$((n+1))
business=$(basename "$(dirname "$repo")"); project=$(basename "$repo" .git)
head=$(g --git-dir="$repo" symbolic-ref -q HEAD); main=$(ref "$repo" refs/heads/main); rootref=$(ref "$repo" refs/heads/root)
root=$(root_of "$business" "$project") || { echo "STOP $repo: the business file has no projects.$project.root"; rc=1; continue; }
roothead=$(g -C "$root" rev-parse -q --verify 'HEAD^{commit}')
jr="$D/projects/$business/$project"
echo "working repo: $repo"
echo " HEAD $head; main ${main:-absent}; refs/heads/root ${rootref:-absent}; work refs $(g --git-dir="$repo" for-each-ref --format=x refs/heads/work | wc -l)"
echo " seeds from projects.$project.root = $root, whose HEAD is now ${roothead:-unreadable}"
if [ -d "$jr" ]; then
jhead=$(g -C "$jr" rev-parse -q --verify 'HEAD^{commit}'); jroot=$(g -C "$jr" rev-parse -q --verify refs/heads/root)
echo " Jason's project repo $jr: HEAD ${jhead:-none}, refs/heads/root ${jroot:-absent}; the seed does not read or write it"
else jhead=none; jroot=; echo " Jason's project repo $jr: absent"; fi
if [ "$mode" = pre ]; then
[ -n "$rootref" ] && { echo "STOP $repo already has refs/heads/root at $rootref"; rc=1; }
[ -n "$jroot" ] && { echo "STOP $jr has refs/heads/root at $jroot"; rc=1; }
if [ -n "$main" ] && [ -n "$roothead" ]; then
if [ "$main" = "$roothead" ]; then echo " first seed under NEW: refs/heads/root := $roothead; main stays (equal)"
elif g --git-dir="$repo" merge-base --is-ancestor "$main" "$roothead" 2>/dev/null || g -C "$root" merge-base --is-ancestor "$main" "$roothead" 2>/dev/null; then
echo " first seed under NEW: refs/heads/root := $roothead; main fast-forwards $main -> $roothead (OLD's seed would force the same move)"
else echo " first seed under NEW: refs/heads/root := $roothead; main stays at $main (not an ancestor of the root)"; fi
fi
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$repo" "${main:-absent}" "$root" "${roothead:-none}" "$jr" "${jhead:-none}" >> "$rec"
else
IFS=$'\t' read -r _ m0 _ r0 _ j0 < <(awk -F'\t' -v r="$repo" '$1 == r' "$out")
[ -n "$m0" ] || { echo "STOP $repo is not in the record"; rc=1; continue; }
if [ "${main:-absent}" = "$m0" ]; then echo " main unchanged ($m0)"
elif [ "$m0" != absent ] && g --git-dir="$repo" merge-base --is-ancestor "$m0" "$main"; then echo " main fast-forwarded $m0 -> $main"
else echo " DIFFER main $m0 -> ${main:-absent} is not a fast-forward"; rc=1; fi
if [ -n "$rootref" ]; then echo " ROOT_SHA $repo $rootref (recorded root HEAD before the window: $r0)"; else echo " refs/heads/root absent: no seed has run under NEW"; fi
[ "${jhead:-none}" = "$j0" ] && [ -z "$jroot" ] && echo " Jason's project repo unchanged" || { echo " DIFFER Jason's project repo HEAD $j0 -> ${jhead:-none}, root ref ${jroot:-absent}"; rc=1; }
fi
done
[ $n -gt 0 ] || { echo "STOP no working repository under $D/working"; rc=1; }
if [ "$mode" = pre ]; then
if [ $rc -eq 0 ]; then mv "$rec" "$out" && (cd "$(dirname "$out")" && sha256sum "$(basename "$out")" > "$(basename "$out").sha256") && echo "PRE_OK $n repo(s), record $out"
else rm -f "$rec"; echo "PRE_STOP"; fi
else [ $rc -eq 0 ] && echo "POST_OK" || echo "POST_DIFFER"; fi
exit $rc