darkwing: row 162 packet, rounds 1-2, scratch plans and follow-up draft (#1659)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-18 15:39:38 -05:00
co-authored by Claude Opus 5.5
parent 7e7f8e208e
commit d132b369ea
17 changed files with 5012 additions and 0 deletions
+864
View File
@@ -0,0 +1,864 @@
# Row 162 (D1, #1659): stack activate and rollback, health-gated
Owner: Darkwing. Reviewer: Filbert. Gate owner: Sage.
Brief: `docs/plans/2026-10-18_stack-release.md`, section "## Stack activate
and rollback, health-gated (D1)", commit cd9c071ae, blob cfc468c9df88.
Base: df4d5702f for round 2 (origin/refactor after row 125 landed; see
"Candidate round 2"). Round 1 was on cfde62f9f (origin/refactor after row 164 landed). The draft
started on cd9c071ae and moved up with `git checkout --detach`, through
70504d23a (row 124) to cfde62f9f. None of the 162 paths is among row
164's 21; the only base change on them is row 129's appended "Console
Chat" section in `docs/TOOLS.md`, and the TOOLS.md patch applied cleanly.
The store schema on refactor is 3h. The live copy 52f35e7a0 is at 3f,
not 3g as this packet said before 2026-10-18T19:40Z: 3g came with row
141 (de352dfaf), which isn't live yet.
Worktree: `~/darkwing-scratch/r162/wt`.
Status: built and tested on Sage's slot (19:25:10Z to 19:46:14Z on
2026-10-18). `stack-release.test.mjs` 58/58 and `stack-copy.test.mjs`
12/12, each run singly. 47 mutants killed before the steps change, then
M41, M45b, M46 and M47 to M49 killed after it. The scratch-root plan for
Mos is in `scratch-plan.txt` next to this file.
## Files
| File | Change |
|---|---|
| `scripts/stack-release.sh` | new: `adopt`, `activate`, `rollback`, `status` |
| `scripts/stack-copy.sh` | a `reachable` function split out of `copy_commit`, and a guard so the file can be sourced for its helpers |
| `packages/cli/tests/stack-release.test.mjs` | new |
| `docs/TOOLS.md` | the "Pinned stack update" section only |
| `packages/cli/README.md` | the pinned-stack paragraph only |
| this packet | |
Nothing else changes. `bus-service.sh` and `webui-service.sh` are called
as they are.
## Shape of the script
```
scripts/stack-release.sh adopt BUSINESS
scripts/stack-release.sh activate ID BUSINESS [--pm] [--migrate] [--timeout S] [--plan]
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
scripts/stack-release.sh status BUSINESS
common options: --copies DIR, --bin DIR (passed to stack-copy.sh and the install scripts)
```
Exit codes: 0 ok; 1 the gate failed and the rollback inside the run passed;
2 `rollback-failed`, the stack is left stopped; 3 a refusal, nothing
changed; 4 usage. Usage errors write no log line. Every other outcome
writes exactly one final line.
`--pm` on `activate` must match the pointer's `pm`. The brief has
`activate` install "exactly the pointer's `--pm`", so the flag is a check
the operator states, not a way to change it. A mismatch refuses
`unit-flags-drift`.
The header comment states decision 96's four conditions (item 10) and
points at decisions 94 and 102 for who starts the Console.
### Where things come from
- Data root: `load_config` from `scripts/common.sh`, run from the canonical
checkout (`MOSAIC_CONFIG` is honoured, as everywhere). The bus resolves
its root the same way, so both see one root.
- The canonical checkout: the `REPO` of this script. `bus-service.sh`,
`webui-service.sh` and `stack-copy.sh` are this checkout's, as in the
window plans. `webui-service.sh install` already refuses outside the
canonical checkout.
- Unit names: `[email protected]` and `mosaic-webui.service`.
Unit directory: `${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user`, the
default both install scripts write to.
- `claude`: `MOSAIC_CLAUDE_BIN=` from `systemctl --user show
mosaic-bus@B.service -p Environment`.
- `pm`: a `--pm` token after `bus start B` in the bus unit's `ExecStart`
`argv[]`.
- `console`: the `argv[]` tokens after `serve` in the Console unit's
`ExecStart`, minus `--checkout X`. The install script adds `--checkout`
itself.
- The copy's `SCHEMA_VERSION` and chain: text read from the copy's
`packages/bus/src/store.mjs`. `export const SCHEMA_VERSION = 'X';` must
match exactly once, and the chain is the `from: 'X'` lines in file
order. The copy's code is never imported or run for this. A copy where
either read fails refuses `copy-schema-unknown`. The migration file
names would also work, but `3b.sql` is a base, not a step, so the
store.mjs lines are the cleaner source.
### Sharing with stack-copy.sh
`copy_commit`'s reachability loop moves into `reachable FULL`, and
`copy_commit` calls it. The dispatch at the bottom gets
`[ "${BASH_SOURCE[0]}" = "$0" ] || return 0`, so sourcing defines the
functions and runs nothing. `stack-release.sh` sources it inside a
subshell function, so stack-copy's `die`, `parse` and globals never mix
with its own:
```
copy_ok() ( source "$REPO/scripts/stack-copy.sh"; ID="$1"; COPIES="$COPIES_DIR"; copy_root >/dev/null && reachable "$ID" )
```
`check` and `wrapper` run as commands, as an operator would run them.
## Pointer and log (item 1)
`<dataRoot>/state/stack-active.json`, written to a temp file in `state/`
and renamed:
```json
{"copy":"<40 hex>","business":"mosaic-stack","pm":true,
"claude":"/home/u/.local/bin/claude","console":["--port","7332","--business","mosaic-stack"],
"schema":"3g","activation":"20261018T170000Z-52f35e7a0abc","at":"2026-10-18T17:00:00Z"}
```
`schema` is the copy's `SCHEMA_VERSION` at the time it went live. On
`adopt` that is the adopted copy's, since a running host proves the store
is at its copy's schema.
`<dataRoot>/state/stack-activation-log.jsonl`, appended with `>>`, one
JSON object per line:
```json
{"at":"...","op":"activate","from":"<copy>","to":"<copy>","backup":null,"result":"ok"}
{"at":"...","op":"activate","from":"...","to":"...","backup":null,"result":"refused","code":"unit-flags-drift"}
{"at":"...","op":"activate","from":"...","to":"...","backup":"<activation>","result":"gate-failed","code":"healthz","rollback":"ok"}
{"at":"...","op":"activate","from":"...","to":"...","backup":"<activation>","result":"rollback-failed","code":"bus-status","rollback":"failed","rollbackCode":"healthz"}
```
Brief item 4 says "each step is logged". Ruling 3: a run that gets past
the refusals writes `result: "started"` before step 1 and one final line
at the end. The per-step detail goes to stdout and to `steps.log` inside
the run's rollback set. `rollback` reads final lines only.
A `started` line with no final line after it for the same activation is
an interrupted run. `status` shows it, and every later `adopt`,
`activate` or `rollback` refuses `interrupted-run`, naming the
activation. The script has no verb to clear it. Clearing it is Jason's or
Mos's call (ruling 3).
Every `adopt`, `activate` and `rollback` takes `flock -n` on
`state/stack-release.lock` first and refuses `run-locked` when another
run holds it. `status` and `--plan` take no lock (ruling 6).
Activation id: `<UTC %Y%m%dT%H%M%SZ>-<first 12 of the target copy>`. The
rollback set directory is made with `mkdir -m 0700` and no `-p`, so a
clash fails before anything changes.
## adopt (item 2)
1. The run lock, else `run-locked`. No `interrupted-run` in the log.
2. No pointer, else `already-adopted`.
3. `stack-copy.sh check --business B` passes, else `check-failed` with
its output.
4. Read `claude`, `pm` and `console` from `systemctl show`. No
`MOSAIC_CLAUDE_BIN` refuses `no-claude-pin` (ruling 5).
5. Write the pointer, then log `ok`.
It stops, starts and installs nothing.
## activate (items 3 to 6)
### Refusals before anything changes (item 3)
The checks run in this order. Each one gives exit 3, one log line with its
code, and no other write:
1. `run-locked`, then `interrupted-run`. `run-locked` writes no log
line, because the lock holder may be writing the log.
2. `no-pointer`, then `business-mismatch`: the pointer names another
business. The brief names no code for it; the pointer holds one
business, so a run for another one would install units the pointer
doesn't describe.
3. `copy-missing` (`copy_root` fails) or `copy-unreachable` (`reachable`
fails). The brief names no codes for these two, so these are mine.
4. `copy-schema-unknown`, from the store.mjs read above.
5. `already-active`: ID is the pointer's copy.
6. `instance-busy` (ruling 1): another business's `mosaic-bus@` instance
is in any state but inactive or failed, since the copy and wrapper
switch reach it too; or this business's instance is activating,
deactivating or reloading. The message names the instance and its
state. The read is `install_guard`'s `list-units` call. A literal
reading (refuse any running instance) would refuse every activate,
because the bus is running when activate starts.
7. `drop-in-override`: the `install_guard` scan of
`systemd-analyze --user unit-paths`, naming the file.
8. `no-claude-pin` (ruling 5): the effective bus unit has no
`MOSAIC_CLAUDE_BIN`.
9. `unit-flags-drift`: effective `pm`, `claude` or `console` differ from
the pointer's, or `--pm` differs from the pointer's `pm`. Prints
`pointer:` and `unit:` lines for each field that differs.
### The run (item 4)
1. Capture to `state/stack-rollback/<activation>/` (0700): the two unit
files from the unit directory, the drop-in directories
`[email protected]`, `mosaic-bus@B.service.d` and
`mosaic-webui.service.d` under that directory when present, the
wrapper `$BIN/mosaic` with `stat -c %a` in `WRAPPER_MODE`, the
`systemctl show` output for both units (it includes `DropInPaths`), and
`SHA256SUMS` over the captured files. `cp -p` throughout. This is
`capture-rollback.sh`. The `started` line is already written, so a
failure here writes the final line `refused` with code
`capture-failed` and exits 3. Nothing has stopped, and only the
partial rollback set exists.
2. `systemctl --user stop mosaic-webui.service`, then the bus unit. Poll
`is-active` for both to read inactive or failed, then the stopped
check: no `<dataRoot>/bus/writer.lock`, and no `/proc/*/fd` link
resolving under `<dataRoot>/bus/`. Bounded by `--timeout`. A timeout
goes to item 6 with code `stop-timeout`.
3. Schema read: see "The store read" below. A `-wal` or `-shm` beside
the store goes to item 6 with `store-unclean`.
- The same as the copy's `SCHEMA_VERSION`: no backup.
- In the chain and older, no `--migrate`: `migration-needs-flag`,
item 6.
- Older with `--migrate`: back up, then `node
<copy>/packages/bus/src/migrate-schema.mjs <dataRoot>`. A non-zero
exit goes to item 6 with code `migrate-failed` and the store
restored.
- Newer, or not in the chain: `store-newer`, item 6.
- No `bus.sqlite` at all (a fresh root): no backup, the new host
creates the store.
4. `bus-service.sh install --copy ID --copies DIR [--pm] --claude PATH`,
then `webui-service.sh install --copy ID --copies DIR <console
flags>`, then `stack-copy.sh wrapper ID --copies DIR --bin DIR`. Any
non-zero exit goes to item 6 with code `install-bus`, `install-console`
or `wrapper`.
5. Start the bus, then the Console.
From step 2 on, `trap` on INT, TERM and HUP sends the run to item 6, once.
A Ctrl-C mid-window leaves the old stack started, not a stopped one. The
one exception is after a `rollback --restore` has swapped the store; see
ruling 4 under rollback.
Most of the script's functions run as `f || handler`. Bash turns `set -e`
off inside such a call, so every step in them that can fail carries its
own `|| return 1` or sits in an `&&` chain.
### The backup
`<dataRoot>/backups/stack/<activation>/`, as `backup.sh` does it: umask
077, `mkdir -m 700` for the directory and `bus/`, `cp -p` of `bus.sqlite`
(only that file: a `-wal` or `-shm` was already refused as
`store-unclean`), `SOURCE.sha256` taken before the copy and
checked against `SHA256SUMS` after it, `MODES.txt`, `LISTING.txt`,
`DATA_ROOT`, `STAMP` (UTC ISO with milliseconds, the same form as
`events.at`), `SCHEMA` (the version read in step 3) and `ROLLBACK_SET`.
`backup.sh`, `restore.sh` and `stopped-check.sh` here mean Sage's
committed window scripts in `agents/sage/work/window-plan/repin-de352dfaf/`.
`stack-release.sh` carries its own copies of their steps and doesn't call
them. The stopped check runs again after the copy. Any failure goes to item 6
before the migration runs.
### The store read
`node -e` with `node:sqlite`:
`` new DatabaseSync(`${pathToFileURL(file).href}?immutable=1`, { readOnly: true }) ``,
then `SELECT value FROM meta WHERE key='schema_version'`, then close.
The probe settled the open mode. It ran on Sage's slot (sage-0738),
16:02:53Z to 16:03:08Z, node v26.8.1, on a WAL-mode fixture store in
`~/darkwing-scratch/r162/probe/store/`, never the live store or a copy of
it. The fixture was rebuilt before each trial and closed cleanly, so only
`bus.sqlite` was present at the start of each one.
| Open | Read | Files after | Main file mtime, sha256 |
|---|---|---|---|
| `{ readOnly: true }`, store stopped | version and count right | `bus.sqlite-shm` and `bus.sqlite-wal` created and left behind | unchanged |
| `file:...?immutable=1` with `readOnly`, store stopped | version and count right | none created | unchanged |
| `{ readOnly: true }` while a WAL connection holds the store | sees the holder's row still in the WAL | the holder's `-wal` and `-shm`, nothing new | not compared |
A plain read-only open of a stopped store writes two files next to it, so
it fails Sage's condition, and immutable is the mode. Immutable tells
SQLite the file can't change and makes it skip the WAL entirely. That is
sound only when there is no WAL to skip. So the rule (Sage approved it in
sage-0739):
- Before every stopped-store read, `bus.sqlite-wal` and `bus.sqlite-shm`
must both be absent. A clean close checkpoints and removes them. One
left behind means the bus did not close the store cleanly, and an
immutable read could return a stale version or count. The run refuses
`store-unclean`, naming the file, and goes to item 6. `stack-release.sh`
never opens, copies or moves that store. Whatever happens to it next is
Jason's or Mos's call. There is no copy fallback.
- With both absent, the read uses immutable.
- An unreadable version goes to item 6 with code `schema-unreadable`.
The `--restore` count in item 7 step 5 is taken before the stop, from a
store the running bus holds. There the `-wal` and `-shm` belong to the
bus, the bus unit must be active, and the read is `{ readOnly: true }`,
which the probe shows adds no file. If the bus unit isn't active, the
count is a stopped-store read under the rule above.
The no-write test below catches a read that creates a file: it compares
the file set and the sums under `bus/` before and after.
### The health gate (item 5)
Polled every second until all pass or `--timeout` (default 60) runs out.
The failing check named at the end is the first one still failing:
| Check | Code |
|---|---|
| `systemctl --user is-active` for both units | `units-inactive` |
| `<copy>/scripts/mosaic bus status --json` exits 0, `host.business` is B, `host.live` is true, `host.pid` is the bus unit's `MainPID`, `boot.ok` is true, `boot.business` is B, `boot.at` is at or after the moment step 5 began | `bus-status` |
| `GET http://127.0.0.1:<port>/healthz` returns 200, port from the `--port` flag in `console`, else 7332 (the `webui-service.sh` default) | `healthz` |
| `stack-copy.sh check --business B` exits 0 | `copy-check` |
| with `pm`: exactly one `launched` line in the bus unit's journal since step 5, and its run is live in `<copy>/scripts/mosaic launches list --json`, for B, with `startedAt` at or after the moment step 5 began | `pm-launch-unconfirmed` |
Ruling 2 (sage-0736, refined in sage-0738): the check must match the PM
role without reading the business file (Jason's org file) and without
naming `launch.by`. The session entries carry `instance`, but nothing in
them or in `launches/<business>.jsonl` marks a launch as the PM's. The
`bus start --pm` path is the only code that knows. It prints
`launched <instance>: run <run>, <harness> <model>` to stdout
(`packages/cli/src/cli.mjs:503`, the only place that prints such a line),
and under the unit that stdout is the journal.
So the gate reads `journalctl --user -u mosaic-bus@B.service --since
@<epoch seconds of step 5> -o cat`. The `@` form is unambiguous. This
host's journalctl reads a bare time as local CDT, and in Q14 an empty
window passed for a clean start that way. Of the lines it returns, it
keeps those starting with `launched `. It passes only when:
- there is exactly one such line;
- it matches `^launched (\S+): run (\S+), `;
- that run is live in `launches list --json` for B, with `startedAt` at
or after step 5.
Zero lines, two or more, a line that doesn't match, or a run that isn't
live all give the one code `pm-launch-unconfirmed`, and the message says
which. A human `mosaic launch` in the same window writes no line to the
unit's journal, so it can't pass for the PM.
The review should know that this check depends on the wording of that
stdout line. A change to it in `cli.mjs` makes the gate fail closed, not
pass. A test pins the dependency: T10b's unparseable-line subtest feeds a
line that still starts with `launched ` but has a later word changed
(`job` for `run`) and expects the refusal.
Follow-up, not in D1 (Sage files it if wanted): a bus-side PM marker, for
example a `pm: true` field on the launch event the `--pm` path writes,
would let the gate drop the journal read and the stdout-format
dependency.
`MainPID` equals `host.pid` because `scripts/mosaic` ends in `exec node
...cli.mjs`. Every `bus start` that loads the business calls
`recordBootOk` (`packages/cli/src/cli.mjs:494`), which writes `boot.at` as
the current time, so a `boot.at` from before step 5 means the status
describes an earlier start.
The healthz fetch is `node -e fetch(...)` with a 5 s abort, as
`webui-service.sh status` does it.
On a pass: rename the pointer into place, log `ok`, exit 0.
### Rollback inside the run (item 6)
1. Stop the Console, then the bus, and wait as in step 2.
2. Put back the two unit files and the drop-in directories from the
rollback set (temp file and rename each), and the wrapper bytes and
mode. Check them against `SHA256SUMS`. `daemon-reload`.
3. If step 3 of this run took a backup, restore it as `restore.sh` does
(a `rollback --restore` backup is never restored here): check
`DATA_ROOT`, the stopped check and `sha256sum -c`; rename `bus` to
`bus.post-attempt-<UTC stamp>`; `mkdir -m 700 bus`; copy in; check the
sums and that the file set matches exactly. Nothing is deleted.
4. Start the bus, then the Console. Run the gate against the pointer's
copy.
5. Log `gate-failed` (or the step's code) with `rollback: ok`, exit 1. If
the rollback's gate fails: print both results, stop both units, log
`rollback-failed` with both codes, exit 2. No retry.
## rollback (item 7)
1. `no-pointer`.
2. Target: scan the log newest first for `result: "ok"` lines with
`op` adopt, activate or rollback, and take the first `to` that isn't
the pointer's copy. None found refuses `no-previous`. This is
`release.sh`'s rule, and it shares that rule's property: two rollbacks
in a row go back and forth between two copies.
3. The item 3 refusals apply to the target (copy, schema, busy,
drop-ins, flag drift).
4. `schema-differs`: the pointer's `schema` is not the target's
`SCHEMA_VERSION` and there's no `--restore`. Decided from the pointer
before anything stops.
5. With `--restore ACTIVATION`: `backups/stack/ACTIVATION/` must exist
with `SHA256SUMS`, a `DATA_ROOT` equal to this root, and a `SCHEMA`
equal to the target's `SCHEMA_VERSION`, else `restore-mismatch`. Print
the post-attempt path the run will keep, and the count of events with
`at` after the backup's `STAMP`, from a read-only open of the store
before anything stops. Count above 0 and `--events N` not equal to it
refuses `post-backup-events`. No change.
6. Steps 4.1 to 4.5 with the target. In step 3, the stopped store's
version must equal the pointer's `schema`, else `store-differs`, item
6. With `--restore`, the count is read again after the stop. A
different count goes to item 6 with `post-backup-events`, since an
event landed between the check and the stop. Then the restore runs in
place of the migration.
7. The gate, as for activate. A pass renames the pointer (with the
target's schema) and logs `ok` with `backup: ACTIVATION`.
Ruling 4 (sage-0736, confirmed in sage-0738) has two cases:
- A `rollback --restore` that fails after the restore has moved the old
`bus/` aside: the run does not go through item 6. That covers the gate
and also an install, start or signal failure after the swap; the script
treats every one the same and logs `rollbackCode: "not-attempted"`. A
restore that fails before the move goes through item 6 like any other
step, since the store hasn't changed. Putting the newer store back would
be a second swap, which is a retry under another name. The run prints
both results, stops both units, logs `rollback-failed` and exits 2. The
post-attempt directory and the restored `bus` stay where they are for
Jason. The unit files and wrapper also stay as the run installed them,
since they match the restored store. Nothing is swapped back. Tested in
T14b.
- A `rollback` without `--restore` whose gate fails: the store was never
swapped, so the run goes through item 6 as `activate` does. It puts back
the unit files and wrapper from its rollback set, restarts the pointer's
copy and runs the gate on it. Exit 1 with `rollback: ok`, or exit 2 with
`rollback-failed` if that gate fails too. Tested in T14c.
## --plan (item 8)
It reads the pointer, the log, the target copy's store.mjs and the
store's schema version. It writes nothing: no lock, no log line, no file
under the data root.
Mos takes the `--plan` output to Jason before the combined re-pin
(mos-0741, sage-0745), so a migration must be stated plainly. For a
store at 3f and refactor at 3h, `activate --plan` prints:
```
store schema: 3f (read from the stopped store)
target schema: 3h
migration: 3f to 3h, after a backup of the stopped store
migration steps: 3f to 3g, 3g to 3h
needs: --migrate (not given; the run would roll back with migration-needs-flag)
needs: Jason present for this run (decision 96 condition 2)
```
I added the `migration steps:` line in the slot on 2026-10-18, before the
scratch-root run (see "The scratch-root plan for Mos"). It walks the target store.mjs's `from:` and
`to:` pairs from the store's version, as text, the same way
`copy_schema` reads the version, so nothing in the copy runs. When the
lines don't pair up, or the walk doesn't reach the target's version, it
prints `migration steps: cannot be read from <file>` and the rest of the
plan is unchanged. The run itself still calls `migrate-schema.mjs`, which
applies the copy's own chain; the steps line only names it.
The store version comes from the store itself. While the bus unit is
active the read is `{ readOnly: true }`, which adds no file next to a
store the bus holds (the probe). With the bus stopped and no `-wal` or
`-shm`, it is the immutable read. With no store file, a `-wal` or `-shm`
present, or a failed read, it falls back to the pointer's `schema` and
says which case applied. When the store and the pointer disagree, a
`note:` line says so; the run itself goes by the stopped store.
Other cases print `migration: none` (same version), a `store-newer`
warning (not in the target's chain) or, for rollback, `restore: ... needs
--restore ACTIVATION` with the activation the log's backup field names
for the pointer's copy. Then the exact `bus-service.sh`,
`webui-service.sh` and `stack-copy.sh wrapper` command lines. Exit 0.
The refusals it can see before a run (no-pointer, business-mismatch,
copy, already-active, no-previous) still print, with exit 3 and no log
line.
After the migration or restore lines comes the `commits:` block
(sage-0778): `commits: the run adds N` when the pointer's copy is an
ancestor of the target, `commits: the run takes away N` when the target
is an ancestor of the pointer's copy (a rollback), then the
`git log --oneline --no-merges` lines, newest first, at most 200. When
neither copy is an ancestor of the other, `--plan` refuses
`copies-unrelated`. My 16:53Z message to Sage said "refuse if FROM isn't
an ancestor of TO". The code is wider than that on purpose, so a
rollback still lists what it takes away. T25 and T26 test it.
### The scratch-root plan for Mos (sage-0837)
Mos asked (mos-0765) for the combined re-pin's `--plan` output with the
3g to 3h migration and rows 148, 141, 143, 128, 124 and 149. Before any
live adopt there is no pointer, so I run it against a scratch root under
`~/darkwing-scratch/r162/`: a data root, a config naming it, a
hand-written pointer and a copies directory, all scratch. Nothing under
`~/.mosaic-dev`, the live copies directory or the units is written.
- The live copy id: `systemctl --user show mosaic-bus@mosaic-stack.service
-p WorkingDirectory` at 2026-10-18T19:32:16Z gave
`.../copies/52f35e7a0933e9fa845f59860a5de1709f9e5756`. It reads the
unit's property and writes nothing. sage-0859 also allowed a read-only
`systemctl --user is-active` on the bus unit name; the run never made
that call, because with no store file `plan_store` doesn't reach it.
- The pointer's `schema` is that copy's `SCHEMA_VERSION`, read with
`git show 52f35e7a0:packages/bus/src/store.mjs`: `3f`. I had expected
3g. That is why the steps line exists: `migration: 3f to 3h` alone
doesn't show the 3g to 3h step Mos asked for, and hand-writing 3g would
have been a false pointer.
- The target copy, from `scratch-plan-steps.log`:
`(cd ~/darkwing-scratch/r162/wt && scripts/stack-copy.sh copy
606fafd43154ded993a503933681198e737987cd --copies
~/darkwing-scratch/r162/plan/copies --allow-worktree)` at 19:45:54Z,
copied by 19:45:56Z. Removed at 19:45:56Z with `chmod -R u+w` and
`rm -rf` of that one copy directory; the copies dir was left with 0
entries. 606fafd43 was origin/refactor at the run; it moved past
cfde62f9f after sage-0859.
- A header I put above the script's output names the scratch root and
says the pointer is hand-written. The script prints no such header
itself.
- The scratch root has no `bus/bus.sqlite`, so the store schema line
reads `from the pointer; there is no store file`. `--plan` runs only
`check_target` and `plan`, not the unit checks, so the only other reads
are git in the checkout and the copies directory. The data root held
only `state/stack-active.json` and an empty `bus/` after the run.
The result, `scratch-plan.txt` (134 lines, exit 0): `migration: 3f to
3h`, `migration steps: 3f to 3g, 3g to 3h`, both `needs:` lines, then
`commits: the run adds 112`. All six rows are in the list (grep hits per
row: 148 7, 141 5, 143 8, 128 9, 124 9, 149 18). The range also carries
row 164, the row 165 brief and everything else since 52f35e7a0.
Mos then narrowed the window target to `8e7ea89a1`, the six rows only
(mos-0810 D1-D2, sage-0871). I reran the same plan in a new slot with
`r1/plan-8e7.sh`, a copy of plan.sh with the target, the scratch root
(`plan-8e7`) and the output file changed. It ran from this worktree
while it still matched the r1 manifest. The result is
`scratch-plan-8e7ea89a1.txt` (114 lines, exit 0). The `migration` and
`migration steps` lines are unchanged. The plan lists 92 commits, the
same count as `git rev-list --count 52f35e7a0..8e7ea89a1`. Each of the
six landing commits appears once. Row 164's `008746b6e` and `850484a7d`
are not in it. Of the 92, only the six touch code paths. The step log
is `scratch-plan-8e7ea89a1-steps.log`. Its "plan: written to" line names
the wrong file, and `live-reads.md` explains why. Both 606fafd43 files
stay as they were, since this review uses them.
Sage forwards the output to Mos as a scratch-root plan. The real
`--plan` against the live pointer runs again in Mos's window before any
adopt.
## status (item 9)
The pointer, the last five log lines, any interrupted run,
`stack-copy.sh check` output and result, `<pointer copy>/scripts/mosaic
bus status`, whether the unit flags match the pointer, and the drift:
whether the copy is an ancestor of `refactor` and
`git rev-list --count COPY..refactor`. It only reads.
## Tests (`packages/cli/tests/stack-release.test.mjs`)
Fixture, built once per file:
- A temp canonical repo, made the way `stack-copy.test.mjs` `setup` does
it: `git ls-files -co` of scripts and packages copied in, `git init -b
refactor`, the light package.json. I copy the setup code rather than
import it, because importing a test file runs its tests too. The base
commit is F.
- On `refactor`, empty commits A, B, C1 and C (subjects `row 901: A` to
`row 904: C`). A side branch from A holds S and merges into `refactor`
as M, with `--no-ff`. A branch named `unreached` from A holds U.
`refactor` and `origin/refactor` both point at M.
- Read-only stub copies under a temp `copies/<id>/` for F, A, B, C1, C, S
and U. M has no copy, so it is the `copy-missing` case. Each copy has
`scripts/mosaic`, `packages/webui/src/cli.mjs` (present only, never
run), `packages/bus/src/store.mjs` and
`packages/bus/src/migrate-schema.mjs`. The store file in A, B, C1, S
and U says `3g` with a `from: '3f'` step. C says `3h` with `3f` and
`3g` steps. F has no `SCHEMA_VERSION`, which gives
`copy-schema-unknown`.
- The stub migrate records the `backups/stack/` entries it saw, and
whether each has `SHA256SUMS`, in `migrate.json`, then sets the store
to `3h` with `node:sqlite`. The `migrate-fails` fault sets
`3h-partial` and exits 1.
- The stand-ins `systemctl`, `journalctl` and `systemd-analyze` are node
scripts with a `#!<node>` shebang in a `sbin/` directory that comes
first on `PATH`. All three exist, so the real ones never run. A
`package.json` with `"type":"commonjs"` in the fixture's base
directory covers them, the holder and the copies' `scripts/mosaic`.
Per test: a temp `HOME`, `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, data root,
`MOSAIC_CONFIG` (a valid config naming the temp data root), `bin/` with
the Q81 wrapper, `mosaic-old` and a fake `claude`, and a stand-in state
directory with a `faults/` directory. A fault is a file named for the
copy it hits, such as `healthz-<id>`, so one fault can fail the new copy
and leave the rollback target alone.
`seed` builds the stack the way an operator would: a real WAL-mode
`bus.sqlite` with a `meta` row and three events, then the real
`bus-service.sh install`, `webui-service.sh install` and
`stack-copy.sh wrapper` against the fixture copy, then a start of both
units. The pointer comes from a real `adopt`, or is written by hand with
no log (T23, T25), or is left out.
The stand-in `systemctl`:
- `show UNIT -p ...` reads the installed unit file and its drop-ins
(`[email protected]` and `<unit>.d`), fills in `%i` and `%h`, and
answers `LoadState`, `WorkingDirectory`, `ExecStart={ path=... ;
argv[]=... ; }`, `Environment`, `MainPID` and `DropInPaths`. The
`check-<id>` fault moves the Console's `WorkingDirectory`, so
`stack-copy.sh check` fails.
- `list-units`, `is-active`, `is-enabled`, `start`, `stop` and
`daemon-reload` keep unit state in `units.json` (written through a temp
file and a rename) and append every call to `calls.log`, so the order
is testable.
- `start` of the bus runs a holder process. The holder opens
`bus.sqlite` in WAL mode and keeps it open, writes `bus/writer.lock`
and a `host.json` with the business, its pid and a boot status, and
closes the store and removes the lock on SIGTERM. That gives the real
`-wal` and `-shm` files of a held store, and a clean close on stop. The
holder starts through `bash -c` with every fd above 2 closed, so it
never inherits the script's fd 9 lock. `SD_CLOCK_OFFSET` backdates the
seeded host and its PM run by two minutes, so they predate any run's
start.
- `start` of the bus with `--pm` in the unit's ExecStart appends the
`launched` line, as `cli.mjs:503` writes it, to the stand-in journal,
and adds the run to `launches.json`. A `pm-<id>` fault picks another
case: no line, two lines, a changed word, a run missing from the list,
an early `startedAt`, a human launch with no line, a run for another
business, a dead run.
- `stop` sends SIGTERM and waits for the holder to exit. The one-shot
`stop-leaves-wal` and `stop-leaves-shm` faults leave an empty `-wal` or
`-shm` beside the store, as an unclean close would. Each bus start and
stop records the name, size, mtime and sha256 of every file under
`bus/` in `snap.jsonl`, so T22 can compare the store before and after
the script's read.
- `start-dies-<id>` sets the bus to `failed`.
The stub `scripts/mosaic` prints `bus status --json` from `host.json`,
with `live` recomputed from `/proc`, and `launches list --json` from
`launches.json`. The `status-<id>` and `launches-<id>` faults make it
fail. The Console is an HTTP server inside the test process on a free
port. It answers 200 when the stand-in says the Console is active, and
500 when a `healthz-<id>` fault names the Console unit's current copy.
That is why the test runs the script with async `spawn`: `spawnSync`
would block the server.
`systemd-analyze --user unit-paths` prints the temp unit directory.
The stand-in `journalctl` logs the `--since` argument it got and prints
the stand-in journal lines for the unit at or after it. It accepts
`--since` only as `@<epoch seconds>` or `YYYY-MM-DD HH:MM:SS UTC`, and
exits 1 on any other form, so a bare local time gives no lines.
Every `run` call also checks T17: the listing of `backups/stack/`,
`state/stack-rollback/`, `bus.post-attempt-*` and `copies/` taken before
the run is a subset of the one after.
Clean-up kills only the holder pids the stand-in logged, after checking
each one's `/proc/<pid>/cmdline` names this file's holder.
Gate failure cases run with `--timeout 3`.
| # | Case | Brief |
|---|---|---|
| T1 | `adopt` writes the pointer (claude and console from `show`) and one `ok` line; a second adopt refuses `already-adopted`; a failing check refuses `check-failed` with no pointer | item 2 |
| T2 | `activate` installs exactly the pointer's `--pm`, `--claude` and Console flags (unit file text) | items 3, 4 |
| T3 | a hand-edited unit (each of pm, claude, a console flag) refuses `unit-flags-drift`, prints the difference, changes nothing | item 3 |
| T4 | each item 3 refusal: units, wrapper, pointer and store unchanged by sums, exactly one new log line with the code, no `start` or `stop` in `calls.log`; `instance-busy` has two subtests (another business's instance active, this one's activating) and both name the instance and state | item 3, ruling 1 |
| T5 | no-schema activate: `calls.log` shows stop Console, stop bus, daemon-reloads, start bus, start Console in order; pointer names C1; no `backups/stack/` | item 4 |
| T6 | B to C without `--migrate`: `migration-needs-flag`, old stack started, pointer unchanged | item 4.3 |
| T7 | B to C with `--migrate`: backup exists before the migrate stub ran (the stub records whether it saw the backup's sums), pointer schema `3h` | item 4.3 |
| T8 | B to C with `--migrate` and a failing migration: post-attempt dir exists, restored files match the backup's sums, old stack started | item 6 |
| T9 | `store-newer` (store at a version outside C's chain) | item 4.3 |
| T10 | each gate check failing alone (five subtests): rollback, pointer unchanged, unit files and wrapper back byte for byte, log names the check | items 5, 6 |
| T10b | the PM check, one subtest per case, each refusing `pm-launch-unconfirmed` with the case named: no `launched` line; two lines; a line that starts with `launched ` but has a later word changed (`launched mosaic-pm: job R, ...`); one line whose run isn't in `launches list`; one line whose run has `startedAt` before step 5; a live human `mosaic launch` session after step 5 with no journal line; a run for another business; a run that isn't live. A last subtest passes with one well-formed line and a live run, and checks the `--since` argument the stand-in received starts with `@` | item 5, ruling 2 |
| T11 | rollback gate fails too: `rollback-failed`, exit 2, both units stopped, no `start` after the failing one | item 6 |
| T12 | `rollback` finds the previous copy from the log | item 7 |
| T13 | `rollback` across a migration refuses `schema-differs`; with `--restore` it restores and goes live on the old copy | item 7 |
| T14 | `--restore` with post-backup events and no or a wrong `--events N` refuses `post-backup-events`; the right N proceeds | item 7 |
| T14b | a `rollback --restore` whose gate fails: `rollback-failed`, exit 2, both units stopped, the post-attempt dir and the restored `bus` in place, no rename after the restore | item 7, ruling 4 |
| T14c | a `rollback` without `--restore` whose gate fails: item 6 runs, the unit files and wrapper are back byte for byte from the run's rollback set, the pointer's copy is started and gated, exit 1 with `rollback: ok`; the store's file set and sums are unchanged | item 7, ruling 4 |
| T15 | `--plan` on activate and rollback prints the migration or restore and the flags; units, pointer, log and store unchanged. After an activate to C with `--migrate`, `rollback --plan` prints, line for line, the restore with the activation and `commits: the run takes away 2` | item 8 |
| T25 | sage-0778 ask 1: pointer on B, `activate S --plan` (S is on a side branch, so neither copy is an ancestor of the other) exits 3 with exactly `stack-release: refusing copies-unrelated: neither <B> nor <S> is an ancestor of the other, so the plan cannot list the commits`, empty stdout, no log, no lock | item 8, sage-0778 |
| T26 | sage-0778 ask 2: `activate C --plan`, with and without `--migrate`, prints the whole output line for line: the migration line, the `--migrate` need, the Jason line, then `commits: the run adds 2` and the two `row 904` and `row 903` lines, then the three install lines. `activate C1 --plan` prints `migration: none` | item 8, sage-0778 |
| T27 | the steps line: store at `3f`, `activate C --plan` prints `migration: 3f to 3h` then `migration steps: 3f to 3g, 3g to 3h`; `activate C1 --plan` (C1's fixture store.mjs has `from:` lines and no `to:` lines) prints `migration steps: cannot be read from <C1>/packages/bus/src/store.mjs` and still exits 0; `activate A --plan` (A's `to:` names `3x`, so the walk stops short of `3g`) prints the same for A | item 8, sage-0859 |
| T23 | sage-0745: the store at `3g` and stopped, the pointer on B, target C at `3h`. `activate C B --plan` exits 0 and prints `3g`, `3h`, `--migrate` and `Jason present ... decision 96 condition 2`. Afterwards the store's file set, mtime and sha256, the listing under the data root, the unit files and the wrapper are unchanged; there is no log file and no lock file. A second subtest with the bus active reads the version through the held read and prints the same lines | item 8, mos-0741 |
| T24 | `business-mismatch` (pointer for B, run for another business) and `capture-failed` (the rollback set's parent made read-only): each exits 3 with one `refused` line, no `stop` in `calls.log`; `capture-failed` follows a `started` line, so `status` shows no interrupted run afterwards. An activate ID that isn't 40 hex is a usage error, exit 4, no log | item 3, item 4.1 |
| T16 | `status` prints the pointer, five log lines, check, bus status and drift, and writes nothing | item 9 |
| T17 | in every `run` call: nothing under `backups/stack/`, `state/stack-rollback/`, `bus.post-attempt-*` or `copies/` is ever removed (the listing before the run is a subset of the one after). The store sums are checked in each test through its own fingerprint | item 10 |
| T18 | `stack-copy.test.mjs` still passes with `reachable` split out and the source guard | stack-copy.sh change |
| T19 | a `started` line with no final line: `status` shows the interrupted activation; `adopt`, `activate` and `rollback` each refuse `interrupted-run` naming it, with nothing changed | ruling 3 |
| T20 | a held `state/stack-release.lock`: `adopt`, `activate` and `rollback` refuse `run-locked` and change nothing; `status` and `--plan` still run | ruling 6 |
| T21 | a bus unit with no `MOSAIC_CLAUDE_BIN`: `adopt` and `activate` refuse `no-claude-pin` | ruling 5 |
| T22 | the store read (sage-0739), three subtests. A `bus.sqlite-wal` left after the stop refuses `store-unclean` naming the file, goes to item 6, and leaves every file under `bus/` with the same name, size, mtime and sha256. The same with a `bus.sqlite-shm`. The clean case: an activate reads the version, and afterwards `bus/` holds only `bus.sqlite`, with its mtime and sha256 the same as before the read | item 4.3, the store read |
## Mutants
One per behavior. Each is a single edit to a scratch copy of
`stack-release.sh` or `stack-copy.sh`, run against the file and expected
to fail the named test. The run script puts each mutant in its own
process group and kills the group on timeout.
| M | Mutation | Killed by |
|---|---|---|
| M1 | adopt skips the already-adopted check | T1 |
| M2 | adopt reads console flags without dropping `--checkout` | T1, T2 |
| M3 | activate installs without `--claude` | T2 |
| M4 | drift check ignores console flags | T3 |
| M5 | `instance-busy` check removed | T4 |
| M6 | `drop-in-override` check removed | T4 |
| M7 | `copy-unreachable` check removed | T4 |
| M8 | a refusal logs nothing | T4 |
| M9 | the bus stops before the Console | T5 |
| M10 | the Console starts before the bus | T5 |
| M11 | the migration runs without `--migrate` | T6 |
| M12 | the backup is taken after the migration | T7 |
| M13 | item 6 skips the store restore | T8 |
| M14 | `store-newer` treated as same | T9 |
| M15 to M19 | each gate check made to always pass | T10, one subtest each |
| M20 | the pointer is written before the gate | T10 |
| M21 | item 6 skips the wrapper restore | T10 |
| M22 | a failed rollback gate retries | T11 |
| M23 | rollback picks the newest `to` without skipping the current copy | T12 |
| M24 | `schema-differs` check removed | T13 |
| M25 | `--events` not compared | T14 |
| M26 | `--plan` writes a log line | T15 |
| M27 | restore deletes the post-attempt directory | T13, T8 (the run killed it there, not in T17) |
| M28 | `reachable` returns 0 always | T4, T18 |
| M29 | `instance-busy` ignores this business's transitional state | T4 (activating subtest) |
| M30 | the PM check accepts any live session started after step 5 | T10b (human launch subtest) |
| M31 | a `rollback --restore` gate failure goes through item 6 | T14b |
| M32 | the interrupted-run check removed | T19 |
| M33 | the run lock removed | T20 |
| M34 | the `no-claude-pin` check removed | T21 |
| M35 | `--since` given a bare local time instead of `@<epoch>` | T10b (pass subtest) |
| M36 | the PM check takes the first `launched` line instead of requiring exactly one | T10b (two-lines subtest) |
| M37 | the PM check skips the `startedAt` comparison | T10b (early-run subtest) |
| M38 | the `store-unclean` check removed | T22 (`-wal` and `-shm` subtests) |
| M39 | the store read uses `{ readOnly: true }` without immutable | T22 (clean subtest) |
| M40 | a `rollback` without `--restore` whose gate fails exits 2 without item 6 | T14c |
| M41 | `--plan` omits the `Jason present` line | T23 |
| M42 | `--plan` takes the run lock or logs `started` | T23, T20 |
| M43 | `--plan` reads the store with `{ readOnly: true }` while stopped, without immutable | T23 (file set) |
| M44 | a capture failure leaves the `started` line without a final line | T24 |
| M45 | the `copies-unrelated` refusal removed, or the `commits:` block left out | T25, T26 |
| M46 | `--plan` omits the migration line | T26, T23 |
| M47 | `--plan` omits the `migration steps:` line | T26, T23, T27 |
| M48 | `plan_steps` drops the odd-line-count check | T27 |
| M49 | `plan_steps` prints a partial walk that doesn't reach the target | T27 (the A case) |
## Candidate round 1
`r1/build.patch` against BASE `cfde62f9f` (sha256
`f5d28a751cb77c4054c598b36dd4dd996d7372bb47a0a8030c39bc8517b8a397`).
`r1/candidate-manifest.sha256` (sha256
`091693c51d137e4cd31e75ee22aef4f22c307e15abce6f91f6c518e3ff78fdd6`) lists
the 5 files in "Files". I applied `r1/build.patch` with `git apply
--index` to a fresh worktree at cfde62f9f: `sha256sum -c` passed on
every line, `git diff --cached --check` was clean, the staged names match
the manifest (5 files, +1604 -45), and the worktree was removed after.
`scripts/stack-release.sh` is mode 100755, the test file 100644. The
TOOLS.md diff is one hunk, inside "Pinned stack update".
### Checks round 1
On Sage's slot, 2026-10-18T19:25:10Z to 19:46:14Z, worktree at
cfde62f9f, TMPDIR `~/darkwing-scratch/r162/tmp`, each file run singly
with `timeout 600 node --test --test-reporter=spec`:
| Test file | Pass | Fail | Run |
|---|---|---|---|
| `packages/cli/tests/stack-release.test.mjs` | 58 | 0 | 19:43:19Z, after the steps change |
| `packages/cli/tests/stack-copy.test.mjs` | 12 | 0 | 19:45:27Z |
The first run in the slot, before the capture `mkdir` fix, was 56/57
with T24 failing (see "Notes"); the second was 57/57; the steps change
added T27. Mutants: all 47 killed against the 57-test file, then M41,
M45b, M46 and the new M47 to M49 killed against the 58-test file. The
mutant runner restores each file from a pristine copy and checks its
sha256 after every mutant. `sweep.sh` found no process of mine after.
Logs: `r1/stack-release-test.log`, `r1/stack-copy-test.log`,
`r1/mutants.summary`, `r1/mutants-steps.summary` and the runner
`r1/mutants.py`.
Not run here: the cli, bus and webui suites and the `scripts/test-*.sh`.
They are Sage's gate at landing.
## Candidate round 2 (sage-0882)
Row 125 landed as aa625565f, with its packet at df4d5702f (origin/refactor). It changed two of
this row's five paths, `docs/TOOLS.md` and `packages/cli/README.md`, so the r1 manifest no
longer described the tree this row would land on. Round 2 makes no source change.
I applied `r1/build.patch` with `git apply --index` to a fresh worktree at df4d5702f. It
applied without conflict, `git diff --cached --check` was clean, and the staged names are the
same 5 files (+1604 -45). The modes are unchanged: both scripts 100755, the test file 100644.
The worktree was removed after.
`r2/build.patch` against BASE `df4d5702f` (sha256
`336503b59a894c8ee0449dba5c36feb242a80404c9f262e96740def231fa541b`).
`r2/candidate-manifest.sha256` (sha256
`c213a801681b5dd9029520d606b3303cb30ed2fbfff8e320b0f7d79d483bd15c`).
What moved from r1:
- In `r2/build.patch`, only the two `index` lines for `docs/TOOLS.md` and
`packages/cli/README.md`, the base and result blob ids. Every hunk header, context line and
added or removed line is the same as in r1, because row 125's additions to both files sit
below this row's hunks.
- In the manifest, only the sha256 of those two files, since the files now carry row 125's
text too. `stack-release.test.mjs`, `stack-copy.sh` and `stack-release.sh` keep their r1
sha256s.
Nothing was rerun. `stack-release.test.mjs` and `stack-copy.test.mjs` read neither doc file,
and the scripts are byte-identical to r1, so the round 1 checks stand.
## Rulings (Sage, sage-0736, with sage-0738, sage-0739 and sage-0745)
1. `instance-busy`: approved as read. Refuse when another business's
`mosaic-bus@` instance is active, because the copy and wrapper switch
reach it too, or this business's is activating, deactivating or
reloading. The message names the instance and state. Test both cases.
2. PM check: approved without reading the business file, and it must
match the PM role: a live launch for B whose role is the PM, started at
or after this run's bus start. Don't name `launch.by`. sage-0738
accepted the journal reading in "The health gate" above, with an
unambiguous `--since`, exactly one line, and the one code
`pm-launch-unconfirmed` for every failure. Each refusal is tested
(T10b), and the stdout-line dependency is stated for the review. The
bus-side PM marker is a follow-up outside D1.
3. Logging: `started` and a final line in the jsonl, step detail in
`steps.log`. An unfinished `started` is an interrupted run: `status`
shows it and the next `adopt`, `activate` or `rollback` refuses
`interrupted-run`, naming the activation. Clearing it is Jason's or
Mos's. Tested.
4. A rollback whose own gate fails stops, prints both results, leaves the
stack stopped and writes `rollback-failed`. No further renames: a
second swap is a retry under another name. The post-attempt dir and
the restored bus stay for Jason. sage-0738 confirmed my reading: after
a restore, the unit files and wrapper stay as installed, and a
rollback that didn't restore goes through item 6 (T14b, T14c).
5. `no-claude-pin`: approved for both `adopt` and `activate`.
6. Run lock: approved, `flock -n` on `state/stack-release.lock`, code
`run-locked`. `status` and `--plan` take no lock.
SQLite probe: on Sage's slot (Rocko first, then me), on a fixture store
in my scratch, never the live store or a copy of it. Try `readOnly` and
`file:...?immutable=1`, and check no `-shm` or `-wal` appears and the
main file's mtime and hash hold. If neither works, the copy fallback.
Result in "The store read": readOnly left `-shm` and `-wal`, immutable
left nothing. sage-0739 approved immutable only with no `-wal` or `-shm`
present, `store-unclean` otherwise, no copy fallback, tested in T22.
sage-0745 (mos-0741): Mos takes the combined re-pin's `--plan` preview to
Jason, so `activate --plan` names the store's version and the target's
(3g to 3h there), that `--migrate` is needed, and that Jason must be
present under decision 96 condition 2. Tested in T23 with a fixture
store one version behind, which also checks that `--plan` writes
nothing. Base moved to 70504d23a, since row 124 touched
`packages/bus/src/store.mjs` and `schema.sql`.
## Notes
- T24 failed on the first run in the slot: `mkdir`'s "Permission denied"
reached stderr ahead of the `capture-failed` refusal. Both `mkdir`
calls in `capture` now send stderr to /dev/null; the refusal names the
path. The rerun passed.
- The install scripts render from the canonical checkout's templates. A
later rollback to an older copy renders that copy's units with today's
templates, and the in-run rollback restores the captured bytes. The
window plans had the same property.
- Nothing here prunes. Disk use grows by one rollback set per run and one
store copy per migrating run.
- The new TOOLS.md "Pinned stack update" section numbers the drop-in
removal as step 4, so the `drop-in-override` message says
`TOOLS.md step 4`. The brief's item 3 text still says step 3, from the
old seven-step list.
@@ -0,0 +1,59 @@
# Follow-up row brief: stack-release.sh hardening after D1 (row 162 review notes)
Draft by Darkwing for Sage (sage-0882), from Filbert's five non-blocking notes on row 162
round 1, #1659 comment 28997. Line numbers are `scripts/stack-release.sh` and the brief as of
row 162's candidate; the script is byte-identical in r1 and r2. This row starts only after
row 162 lands, and it rebases on that landing.
Suggested gate: Filbert approves the candidate manifest; cli suite and every
`scripts/test-*.sh` green on Sage's gate. Suggested owner: Darkwing, reviewer Filbert, gate
owner Sage.
None of the five is a known failure today. Items 1 to 4 each fail closed or can't be reached
with the current pin. The fix is to make each one refuse earlier, or tell the truth sooner,
with codes row 162 already has. No new refusal code.
## Items
1. Precheck unreadable drop-ins. `drop_in_override` (:503) scans the same three directories as
`install_guard` in `scripts/bus-service.sh` (:100), but skips a directory or file it can't
read (`[ -d ]`, `grep 2>/dev/null`). `install_guard` dies on that same path, after the stop,
so the run goes through item 6 and the live stack stopped for nothing. The precheck refuses
`drop-in-override` with the path when a unit path, drop-in directory or `.conf` file exists
but can't be read, using `install_guard`'s own tests (`-r`, `-x`, a failed read). Test: a drop-in file with mode 000 (and a
directory with mode 000) refuses before any `systemctl stop` in the stand-in log, and the
units stay active.
2. Close the `is-active` to read race. `plan_store` (:573, check at :577) and the `--restore`
event count (:716) choose the held-store read or the stopped-store read from one
`is-active` call. If the bus stops between the check and the readOnly open, the open can
leave `-wal` or `-shm` beside a stopped store, and the next run refuses `store-unclean`.
Fix: after a held-store read, check `is-active` again. If the bus stopped meanwhile, report
it (`--plan`: say the store state changed during the read and print no migration line;
rollback: refuse `restore-mismatch` before anything stops) instead of using the count.
Test: a stand-in `systemctl` that answers active then inactive.
3. Compare the copy check with the target. The `copy-check` gate step (:373) runs
`stack-copy.sh check --business`, which proves the bus, Console and CLI agree on one copy,
not that the copy is the one this run installed. Pass the expected copy id (or compare the
check's reported id with the target) and fail the gate with `copy-check` on a mismatch.
Test: a stand-in check that reports agreement on another copy fails the gate.
4. Don't split a quoted `Environment=`. `read_units` (:148) reads the `systemctl show`
output (`show` at :144) and splits its `Environment=` line on spaces with `read -r -a`
(:157). systemd quotes a value that holds a space, so a `MOSAIC_CLAUDE_BIN` path with a
space reads as a prefix of itself. Smallest fix: `read_units` returns 1 when that line holds
a quote or backslash, so its callers refuse as they do for an unreadable unit
(`unit-flags-drift` at :535, `check-failed` at :561; the status view at :743 then skips its
unit flags line). Parsing systemd's
quoting is the larger option. The same split reads `ExecStart` argv (`argv_of`, :145, used
at :153 and :159); cover it the same way. Test: a stand-in unit with
`Environment="MOSAIC_CLAUDE_BIN=/a b/claude"` refuses by name and never reads `/a`.
5. Re-pin brief item 8. `docs/plans/2026-10-18_stack-release.md` item 8 (:180-185) says the
plan reads the schema "with no store read". sage-0745 and mos-0741 replaced that with the
held or immutable read that row 162 implements. Sage re-pins the brief text; no code.
## Out of scope
The bus-side PM marker (ruling 2's follow-up) and anything in Mos's live window.
@@ -0,0 +1,68 @@
# Row 162 live reads for the scratch-root plan (sage-0837, sage-0859)
One live read, at 2026-10-18T19:32:16Z, by Darkwing:
```
systemctl --user show [email protected] -p WorkingDirectory
```
Its output, as recorded in the session transcript:
```
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/52f35e7a0933e9fa845f59860a5de1709f9e5756
```
`systemctl show` reads the
unit's properties from the user manager. It writes nothing: no unit
file, no drop-in, no state under `~/.mosaic-dev` and no change to the
unit's state.
sage-0859 also allowed a read-only `systemctl --user is-active` on the
bus unit name. It was never called. The scratch data root has no store
file, so `plan_store` sets the store schema from the pointer before it
reaches the `is-active` branch.
The pointer's schema, `3f`, came from git in the canonical checkout, not
from anything live:
```
git show 52f35e7a0933e9fa845f59860a5de1709f9e5756:packages/bus/src/store.mjs
```
Nothing under `~/.mosaic-dev`, the live copies directory or the systemd
units was written. The scratch copy and its removal are in
`scratch-plan-steps.log`; the plan output is `scratch-plan.txt`.
## Rerun for target 8e7ea89a1 (sage-0871, mos-0810 D1-D2)
One more live read, at 2026-10-18T19:51:55Z, by Darkwing, at the start of
the sage-0871 slot:
```
systemctl --user show [email protected] -p WorkingDirectory
```
Its output:
```
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/52f35e7a0933e9fa845f59860a5de1709f9e5756
```
The live copy had not changed since the 19:32:16Z read. No `is-active`
read was made this time either. The pointer is hand-written as before:
copy `52f35e7a0`, schema `3f` from `git show` of that copy's store.mjs.
The script was `r1/plan-8e7.sh`, a copy of plan.sh with three changes:
the target is `8e7ea89a1` instead of `origin/refactor`, the scratch root
is `~/darkwing-scratch/r162/plan-8e7` and the output file is
`scratch-plan-8e7ea89a1.txt`. It ran from the r1 candidate worktree on
cfde62f9f, which matched `r1/candidate-manifest.sha256` at 19:51Z. The
scratch copy was made at 19:51:56Z and removed at 19:51:58Z, leaving 0
entries.
One defect in the step log: its "plan: written to" line names
`out/scratch-plan.txt`. My edit changed only the quoted output path, not
the unquoted one in that log line. The output went to
`out/scratch-plan-8e7ea89a1.txt`. The 606fafd43 output kept its sha256
(0586168e…), so the run did not touch it. The step log is kept as
written.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
0e21117afe4acf0460fe10beecfddf4c90d1ceb30b6bb4f366b5bf5b3a5897c6 docs/TOOLS.md
7d5b4b7a7fcf075914438024fe30083d3ba03c11c834da729d87581b6d897419 packages/cli/README.md
73cdd6c2e2eceffa223d25c1af5f549c369d3cb68d906613ddd6f08c05db1bb0 packages/cli/tests/stack-release.test.mjs
19473e30b35c19cf2167fbf0d7aa0d3ed46562d50e44b738bb611d95cd123254 scripts/stack-copy.sh
6a2c828fefef0f467a1bea7c51f927790ee8990d58cfd26372c521ac64c53d7e scripts/stack-release.sh
@@ -0,0 +1,7 @@
M41 KILLED rc=1 tests=3 fail=3 1s failed=['T23 --plan reads the store without changing a file, stopped or held', 'held store', 'stopped store']
M45b KILLED rc=1 tests=1 fail=1 1s failed=['T26 --plan lists the commits an activate adds, after the migration lines']
M46 KILLED rc=1 tests=4 fail=4 2s failed=['T23 --plan reads the store without changing a file, stopped or held', 'T26 --plan lists the commits an activate adds, after the migration lines', 'held store', 'stopped store']
M47 KILLED rc=1 tests=5 fail=5 3s failed=['T23 --plan reads the store without changing a file, stopped or held', 'T26 --plan lists the commits an activate adds, after the migration lines', 'T27 --plan names each migration step, or says the steps cannot be read', 'held store', 'stopped store']
M48 KILLED rc=1 tests=1 fail=1 1s failed=['T27 --plan names each migration step, or says the steps cannot be read']
M49 KILLED rc=1 tests=1 fail=1 1s failed=['T27 --plan names each migration step, or says the steps cannot be read']
DONE 6 mutants, 6 killed, survived: []
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
# Row 162 mutants M1-M46 (agents/darkwing/work/queue-162/BUILD.md). Each mutant is exact-string
# edits to one file in the r162 worktree; the named tests must fail. The file is restored from a
# pristine copy after every mutant and checked by sha256. Run with python3 -I.
import hashlib, os, re, shutil, signal, subprocess, sys, time
HOME = os.environ["HOME"]
R = f"{HOME}/darkwing-scratch/r162"
WT = f"{R}/wt"
OUT = f"{R}/out/mutants"
TEST = "packages/cli/tests/stack-release.test.mjs"
SR = "scripts/stack-release.sh"
SC = "scripts/stack-copy.sh"
def sha(p):
return hashlib.sha256(open(p, "rb").read()).hexdigest()
M = []
def mut(mid, pattern, edits, f=SR, expect=None):
M.append((mid, pattern, edits, f, expect))
mut("M1", "^T1 ", [(' [ ! -f "$POINTER" ] || { read_pointer; FROM="$P_COPY"; refuse already-adopted "$POINTER exists (copy $P_COPY)"; }\n', "")])
mut("M2", "^T[12] ", [(' if [ "${argv[i]}" = --checkout ]; then i=$((i + 1)); continue; fi\n', "")])
mut("M3", "^T2 ", [(' --claude "$claude" >> "$STEPS"', ' >> "$STEPS"')])
mut("M4", "^T3 ", [(' [ "${P_CONSOLE[*]}" = "${U_CONSOLE[*]}" ] || d+=$\'\\n\'" console:', ' true || d+=$\'\\n\'" console:')])
mut("M5", "^T4 ", [(" instance_busy\n drop_in_override\n", " drop_in_override\n")])
mut("M6", "^T4 ", [(" instance_busy\n drop_in_override\n", " instance_busy\n")])
mut("M7", "^T4 ", [('copy_ok "$TO" 2>/dev/null || refuse copy-unreachable', 'true || refuse copy-unreachable')])
mut("M8", "^T4 ", [('|| log_line result refused code "$code"', "|| true")])
mut("M9", "^T5 ", [(' systemctl --user stop "$CONSOLE_UNIT" || return 1\n systemctl --user stop "$BUS_UNIT" || return 1\n',
' systemctl --user stop "$BUS_UNIT" || return 1\n systemctl --user stop "$CONSOLE_UNIT" || return 1\n')])
mut("M10", "^T5 ", [('systemctl --user start "$BUS_UNIT" && systemctl --user start "$CONSOLE_UNIT"',
'systemctl --user start "$CONSOLE_UNIT" && systemctl --user start "$BUS_UNIT"')])
mut("M11", "^T6 ", [(' [ "$MIGRATE" = 1 ] || rollback_in_run migration-needs-flag\n', "")])
mut("M12", "^T7 ", [(' backup_store || rollback_in_run backup\n node "$COPIES/$TO/packages/bus/src/migrate-schema.mjs" "$DATA" >> "$STEPS" 2>&1 || rollback_in_run migrate-failed\n',
' node "$COPIES/$TO/packages/bus/src/migrate-schema.mjs" "$DATA" >> "$STEPS" 2>&1 || rollback_in_run migrate-failed\n backup_store || rollback_in_run backup\n')])
mut("M13", "^T8 ", [(' elif [ -n "$TAKEN" ] && ! restore_store "$TAKEN"; then rcode=restore\n', "")])
mut("M14", "^T9 ", [(' if [ "$STORE_SCHEMA" != "$T_SCHEMA" ]; then\n', ' if [ "$STORE_SCHEMA" != "$T_SCHEMA" ] && in_chain "$STORE_SCHEMA"; then\n')])
mut("M15", "^T10 ", [(' systemctl --user is-active --quiet "$BUS_UNIT" && systemctl --user is-active --quiet "$CONSOLE_UNIT" || return 1\n GATE=bus-status\n',
' true || return 1\n GATE=bus-status\n')], expect="units-inactive")
mut("M16", "^T10 ", [(" GATE=bus-status\n", " GATE=bus-status\n if false; then\n"),
(' [ -z "$GATE_WHY" ] || return 1\n GATE=healthz\n', ' fi\n GATE=healthz\n')], expect="bus-status")
mut("M17", "^T10 ", [('-- "$(console_port)" || return 1', '-- "$(console_port)" || true')], expect="healthz")
mut("M18", "^T10 ", [('--business "$BUSINESS" > /dev/null 2>&1 || return 1', '--business "$BUSINESS" > /dev/null 2>&1 || true')], expect="copy-check")
mut("M19", "^T10 ", [('pm_launched "$copy" || return 1', 'pm_launched "$copy" || true')], expect="pm-launch-unconfirmed")
mut("M20", "^T10 ", [(' gate "$TO" "$P_PM" || after_fail "$GATE"\n trap - INT TERM HUP\n write_pointer "$TO" "$P_PM" "$P_CLAUDE" "$T_SCHEMA" "${P_CONSOLE[@]}"\n',
' write_pointer "$TO" "$P_PM" "$P_CLAUDE" "$T_SCHEMA" "${P_CONSOLE[@]}"\n gate "$TO" "$P_PM" || after_fail "$GATE"\n trap - INT TERM HUP\n')])
mut("M21", "^T10 ", [(' tmp="$(mktemp "$BIN/.mosaic.XXXXXX")" && cp -p "$RS/wrapper" "$tmp" && chmod "$(cat "$RS/WRAPPER_MODE")" "$tmp" && mv -f "$tmp" "$BIN/mosaic" || return 1\n', ""),
(' [ "$(sha256sum < "$BIN/mosaic" | cut -d\' \' -f1)" = "$(sum_of "$RS" wrapper)" ] || return 1\n [ "$(stat -c %a "$BIN/mosaic")" = "$(cat "$RS/WRAPPER_MODE")" ] || return 1\n', "")])
mut("M22", "^T11 ", [(' elif ! gate "$P_COPY" "$P_PM"; then rcode="$GATE"\n',
' elif ! gate "$P_COPY" "$P_PM" && ! { start_both && gate "$P_COPY" "$P_PM"; }; then rcode="$GATE"\n')])
mut("M23", "^T12 ", [('if(mode==="previous"&&o.to!==current)', 'if(mode==="previous")')])
mut("M24", "^T13 ", [('[ "$P_SCHEMA" = "$T_SCHEMA" ] || [ -n "$RESTORE" ] || refuse schema-differs', 'true || refuse schema-differs')])
mut("M25", "^T14 ", [('[ "$COUNT" = 0 ] || [ "$EVENTS" = "$COUNT" ] || refuse post-backup-events', '[ "$COUNT" = 0 ] || [ -n "$EVENTS" ] || refuse post-backup-events')])
mut("M26", "^T15 ", [('if [ "$PLAN" = 1 ]; then check_target; plan; exit 0; fi', 'if [ "$PLAN" = 1 ]; then log_line result planned; check_target; plan; exit 0; fi', 2)])
mut("M27", "^T(8|13) ", [(' mv "$DATA/bus" "$POST" || return 1\n', ' mv "$DATA/bus" "$POST" && rm -rf "$POST" || return 1\n')])
mut("M28", "^T4 ", [('git -C "$REPO" merge-base --is-ancestor "$1" "$ref" || die', 'true || die')], f=SC)
mut("M29", "^T4 ", [('case "$state" in activating|deactivating|reloading) refuse instance-busy "$unit is $state" ;; esac', ":")])
mut("M30", "^T10b ", [(' [ "${#lines[@]}" -gt 0 ] || { GATE_WHY="no launched line since the start"; return 1; }\n',
' [ "${#lines[@]}" -gt 0 ] || lines=("launched x: run $("$1/scripts/mosaic" launches list --json 2>/dev/null | node -e \'const s=JSON.parse(require("fs").readFileSync(0,"utf8")).find((x)=>x.live);process.stdout.write(s?s.run:"none")\'), y")\n')],
expect="a human launch with no journal line")
mut("M31", "^T14b ", [(' [ -z "$RESTORED" ] || rollback_failed "$1" not-attempted\n', "")])
mut("M32", "^T19 ", [('[ -z "$open" ] || refuse interrupted-run', 'true || refuse interrupted-run')])
mut("M33", "^T20 ", [("flock -n 9 || refuse run-locked", "true || refuse run-locked")])
mut("M34", "^T21 ", [('[ -n "$U_CLAUDE" ] || refuse no-claude-pin', 'true || refuse no-claude-pin', 2)])
mut("M35", "^T10b ", [('--since "@$START_EPOCH"', '--since "$(date -d "@$START_EPOCH" \'+%Y-%m-%d %H:%M:%S\')"')], expect="one well-formed line and a live run pass")
mut("M36", "^T10b ", [(' [ "${#lines[@]}" = 1 ] || { GATE_WHY="${#lines[@]} launched lines since the start"; return 1; }\n', "")], expect="two lines")
mut("M37", "^T10b ", [(':!(Date.parse(s.startedAt)>=Number(since))?`run ${run} started before this start`', "")], expect="run started before the start")
mut("M38", "^T22 ", [('if [ -e "$f" ] || [ -L "$f" ]; then UNCLEAN="$f"; return 0; fi', ":")])
mut("M39", "^T22 ", [('new DatabaseSync(pathToFileURL(file).href+"?immutable=1",{readOnly:true})', "new DatabaseSync(file,{readOnly:true})")], expect="clean")
mut("M40", "^T14c ", [(' [ -z "$RESTORED" ] || rollback_failed "$1" not-attempted\n', ' [ -z "$RESTORED" ] && [ "$OP" != rollback ] || rollback_failed "$1" not-attempted\n')])
mut("M41", "^T23 ", [(' echo "needs: Jason present for this run (decision 96 condition 2)"\n', "")])
mut("M42", "^T(23|20) ", [(' [ "$PLAN" = 1 ] || lock\n', " lock\n", 2)])
mut("M43", "^T23 ", [(' S_SCHEMA="$(store_read version 2>/dev/null)" || S_SCHEMA=""\n', ' S_SCHEMA="$(node -e "$STORE_JS" -- heldversion "$STORE" 2>/dev/null)" || S_SCHEMA=""\n')])
mut("M44", "^T24 ", [(' capture || refuse capture-failed "could not write the rollback set $RS; nothing was stopped"\n',
' capture || { echo "stack-release: refusing capture-failed: could not write the rollback set $RS; nothing was stopped" >&2; exit 3; }\n')])
mut("M45a", "^T25 ", [(' else refuse copies-unrelated "neither $P_COPY nor $TO is an ancestor of the other, so the plan cannot list the commits"\n',
' else RANGE="$P_COPY..$TO" VERB=adds\n')])
mut("M45b", "^T26 ", [(" plan_commits\n", "")])
mut("M46", "^T(26|23) ", [(' echo "migration: $S_SCHEMA to $T_SCHEMA, after a backup of the stopped store"\n', "")])
mut("M47", "^T(26|23|27) ", [(' echo "migration steps: $(plan_steps)"\n', "")])
mut("M48", "^T27 ", [(' (( ${#l[@]} % 2 == 0 )) || { echo "cannot be read from $f"; return; }\n', "")])
mut("M49", "^T27 ", [(' if [ "$v" = "$T_SCHEMA" ] && [ -n "$out" ]; then', ' if [ -n "$out" ]; then')])
def main():
only = set(sys.argv[1:])
os.makedirs(OUT, exist_ok=True)
pristine = {}
for f in (SR, SC):
p = f"{R}/edit/pristine-{os.path.basename(f)}"
shutil.copy2(f"{WT}/{f}", p)
if sha(p) != sha(f"{WT}/{f}"):
sys.exit(f"ABORT: backup of {f} failed")
pristine[f] = (p, sha(p))
env = dict(os.environ, TMPDIR=f"{R}/tmp")
results = []
try:
for mid, pattern, edits, f, expect in M:
if only and mid not in only:
continue
src = open(pristine[f][0]).read()
text = src
for e in edits:
old, new = e[0], e[1]
n = e[2] if len(e) > 2 else 1
if text.count(old) != n:
sys.exit(f"ABORT {mid}: expected {n} match(es) of {old!r} in {f}, found {text.count(old)}")
text = text.replace(old, new)
if text == src:
sys.exit(f"ABORT {mid}: no change")
if os.environ.get("DRY"):
print(f"{mid} ok", flush=True); continue
open(f"{WT}/{f}", "w").write(text)
log = f"{OUT}/{mid}.log"
t0 = time.time()
with open(log, "w") as fh:
p = subprocess.Popen(["node", "--test", "--test-reporter=spec", f"--test-name-pattern={pattern}", TEST],
cwd=WT, env=env, stdout=fh, stderr=subprocess.STDOUT, start_new_session=True)
try:
rc = p.wait(timeout=600)
except subprocess.TimeoutExpired:
os.killpg(p.pid, signal.SIGKILL); p.wait(); rc = "timeout"
shutil.copy2(pristine[f][0], f"{WT}/{f}")
if sha(f"{WT}/{f}") != pristine[f][1]:
sys.exit(f"ABORT {mid}: restore of {f} failed")
body = open(log).read()
ran = re.search(r"^ℹ tests (\d+)", body, re.M)
fails = re.search(r"^ℹ fail (\d+)", body, re.M)
nfail = int(fails.group(1)) if fails else -1
failed_names = re.findall(r"^\s*✖ (.+?) \(\d", body, re.M)
killed = rc != 0 and nfail > 0
if expect and killed:
killed = any(n == expect for n in failed_names)
verdict = "KILLED" if killed else "SURVIVED"
line = f"{mid} {verdict} rc={rc} tests={ran.group(1) if ran else '?'} fail={nfail} {time.time()-t0:.0f}s failed={sorted(set(failed_names))[:6]}"
print(line, flush=True)
results.append((mid, verdict))
finally:
for f, (p, s) in pristine.items():
shutil.copy2(p, f"{WT}/{f}")
if sha(f"{WT}/{f}") != s:
print(f"RESTORE FAILED {f}", flush=True)
surv = [m for m, v in results if v != "KILLED"]
print(f"DONE {len(results)} mutants, {len(results)-len(surv)} killed, survived: {surv}", flush=True)
sys.exit(1 if surv else 0)
main()
@@ -0,0 +1,48 @@
M1 KILLED rc=1 tests=1 fail=1 1s failed=['T1 adopt writes the pointer from the units once; a failing check refuses check-failed']
M2 KILLED rc=1 tests=2 fail=2 3s failed=['T1 adopt writes the pointer from the units once; a failing check refuses check-failed', "T2 activate installs exactly the pointer's pm, claude and Console flags"]
M3 KILLED rc=1 tests=1 fail=1 2s failed=["T2 activate installs exactly the pointer's pm, claude and Console flags"]
M4 KILLED rc=1 tests=4 fail=2 4s failed=['T3 a hand-edited unit refuses unit-flags-drift, names the difference and changes nothing', 'console']
M5 KILLED rc=1 tests=9 fail=3 7s failed=['T4 each item 3 refusal changes nothing and logs one line with its code', 'instance-busy (another business active)', 'instance-busy (this business activating)']
M6 KILLED rc=1 tests=9 fail=2 67s failed=['T4 each item 3 refusal changes nothing and logs one line with its code', 'drop-in-override']
M7 KILLED rc=1 tests=9 fail=2 6s failed=['T4 each item 3 refusal changes nothing and logs one line with its code', 'copy-unreachable']
M8 KILLED rc=1 tests=9 fail=9 5s failed=['T4 each item 3 refusal changes nothing and logs one line with its code', 'already-active', 'copy-missing', 'copy-schema-unknown', 'copy-unreachable', 'drop-in-override']
M9 KILLED rc=1 tests=1 fail=1 2s failed=['T5 an activate with no migration stops the Console then the bus, installs, starts the bus then the Console']
M10 KILLED rc=1 tests=1 fail=1 2s failed=['T5 an activate with no migration stops the Console then the bus, installs, starts the bus then the Console']
M11 KILLED rc=1 tests=1 fail=1 2s failed=['T6 a migration without --migrate rolls back with migration-needs-flag']
M12 KILLED rc=1 tests=1 fail=1 2s failed=['T7 --migrate takes the backup before the migration runs']
M13 KILLED rc=1 tests=1 fail=1 2s failed=['T8 a failed migration restores the backup and keeps the store it replaced']
M14 KILLED rc=1 tests=1 fail=1 2s failed=["T9 a store outside the target's chain rolls back with store-newer"]
M15 KILLED rc=1 tests=6 fail=2 25s failed=['T10 each gate check failing alone rolls back and puts every file back', 'units-inactive']
M16 KILLED rc=1 tests=6 fail=2 21s failed=['T10 each gate check failing alone rolls back and puts every file back', 'bus-status']
M17 KILLED rc=1 tests=6 fail=2 21s failed=['T10 each gate check failing alone rolls back and puts every file back', 'healthz']
M18 KILLED rc=1 tests=6 fail=2 21s failed=['T10 each gate check failing alone rolls back and puts every file back', 'copy-check']
M19 KILLED rc=1 tests=6 fail=2 22s failed=['T10 each gate check failing alone rolls back and puts every file back', 'pm-launch-unconfirmed']
M20 KILLED rc=1 tests=6 fail=6 25s failed=['T10 each gate check failing alone rolls back and puts every file back', 'bus-status', 'copy-check', 'healthz', 'pm-launch-unconfirmed', 'units-inactive']
M21 KILLED rc=1 tests=6 fail=6 39s failed=['T10 each gate check failing alone rolls back and puts every file back', 'bus-status', 'copy-check', 'healthz', 'pm-launch-unconfirmed', 'units-inactive']
M22 KILLED rc=1 tests=1 fail=1 12s failed=['T11 a rollback whose gate fails too: rollback-failed, exit 2, both units stopped, no retry']
M23 KILLED rc=1 tests=1 fail=1 2s failed=['T12 rollback takes the previous copy from the log']
M24 KILLED rc=1 tests=1 fail=1 3s failed=['T13 a rollback across a migration needs --restore, then restores and goes live']
M25 KILLED rc=1 tests=1 fail=1 3s failed=['T14 events written after the backup need --events N with the right N']
M26 KILLED rc=1 tests=1 fail=1 1s failed=['T15 --plan names the migration and the restore and changes nothing']
M27 KILLED rc=1 tests=2 fail=2 5s failed=['T13 a rollback across a migration needs --restore, then restores and goes live', 'T8 a failed migration restores the backup and keeps the store it replaced']
M28 KILLED rc=1 tests=9 fail=2 6s failed=['T4 each item 3 refusal changes nothing and logs one line with its code', 'copy-unreachable']
M29 KILLED rc=1 tests=9 fail=2 6s failed=['T4 each item 3 refusal changes nothing and logs one line with its code', 'instance-busy (this business activating)']
M30 KILLED rc=1 tests=10 fail=3 44s failed=['T10b the PM check needs exactly one launched line for a live run of this start', 'a human launch with no journal line', 'no launched line']
M31 KILLED rc=1 tests=1 fail=1 6s failed=['T14b a rollback --restore whose gate fails stops both units and skips item 6']
M32 KILLED rc=1 tests=1 fail=1 1s failed=['T19 a started line with no final line: status shows it and every changing verb refuses']
M33 KILLED rc=1 tests=1 fail=1 1s failed=['T20 a held run lock refuses run-locked without a log line; status and --plan still run']
M34 KILLED rc=1 tests=1 fail=1 1s failed=['T21 a bus unit with no MOSAIC_CLAUDE_BIN: adopt and activate refuse no-claude-pin']
M35 KILLED rc=1 tests=10 fail=10 183s failed=['T10b the PM check needs exactly one launched line for a live run of this start', 'a changed word', 'a human launch with no journal line', 'no launched line', 'one well-formed line and a live run pass', 'run for another business']
M36 KILLED rc=1 tests=10 fail=2 39s failed=['T10b the PM check needs exactly one launched line for a live run of this start', 'two lines']
M37 KILLED rc=1 tests=10 fail=2 42s failed=['T10b the PM check needs exactly one launched line for a live run of this start', 'run started before the start']
M38 KILLED rc=1 tests=4 fail=3 5s failed=['T22 the stopped store read: a -wal or -shm refuses store-unclean and is left alone; a clean read adds no file', 'bus.sqlite-shm', 'bus.sqlite-wal']
M39 KILLED rc=1 tests=4 fail=2 5s failed=['T22 the stopped store read: a -wal or -shm refuses store-unclean and is left alone; a clean read adds no file', 'clean']
M40 KILLED rc=1 tests=1 fail=1 6s failed=['T14c a rollback without --restore whose gate fails runs item 6']
M41 KILLED rc=1 tests=3 fail=3 1s failed=['T23 --plan reads the store without changing a file, stopped or held', 'held store', 'stopped store']
M42 KILLED rc=1 tests=4 fail=4 2s failed=['T20 a held run lock refuses run-locked without a log line; status and --plan still run', 'T23 --plan reads the store without changing a file, stopped or held', 'held store', 'stopped store']
M43 KILLED rc=1 tests=3 fail=2 1s failed=['T23 --plan reads the store without changing a file, stopped or held', 'stopped store']
M44 KILLED rc=1 tests=1 fail=1 1s failed=['T24 business-mismatch and capture-failed refuse with exit 3; a short id is a usage error']
M45a KILLED rc=1 tests=1 fail=1 1s failed=['T25 --plan refuses copies-unrelated when neither copy is an ancestor of the other']
M45b KILLED rc=1 tests=1 fail=1 1s failed=['T26 --plan lists the commits an activate adds, after the migration lines']
M46 KILLED rc=1 tests=4 fail=4 2s failed=['T23 --plan reads the store without changing a file, stopped or held', 'T26 --plan lists the commits an activate adds, after the migration lines', 'held store', 'stopped store']
DONE 47 mutants, 47 killed, survived: []
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Row 162, sage-0837, rerun on sage-0871 (mos-0810 D1-D2, target 8e7ea89a1): activate --plan against a scratch data root with a hand-written pointer.
# Everything written lives under $P. The only live reads were made before this script, by hand, the second at the start of the sage-0871 slot:
# `systemctl --user show [email protected] -p WorkingDirectory` (19:32:16Z).
set -euo pipefail
LIVE=52f35e7a0933e9fa845f59860a5de1709f9e5756
TO="$(git -C /mnt/storage/src/mosaic-stack rev-parse --verify 8e7ea89a1^{commit})"
R="$HOME/darkwing-scratch/r162"
P="$R/plan-8e7"
WT="$R/wt"
export TMPDIR="$R/tmp"
[ ! -e "$P" ] || { echo "plan.sh: $P exists; remove it first" >&2; exit 1; }
mkdir -p "$P/cfg" "$P/data/state" "$P/data/bus" "$P/copies" "$P/bin"
step() { echo "$(date -u +%FT%TZ) $*" >> "$P/steps.log"; }
cat > "$P/cfg/config.json" <<EOF
{"configVersion":1,"environment":"development","dataRoot":"$P/data","execution":{"backend":"docker","provider":"zai","model":"glm-5.3-flash","adapter":"mock"}}
EOF
# The schema adopt would record for the live copy: SCHEMA_VERSION in its store.mjs, read from git.
SCHEMA="$(git -C /mnt/storage/src/mosaic-stack show "$LIVE:packages/bus/src/store.mjs" | sed -n "s/^export const SCHEMA_VERSION = '\([A-Za-z0-9]*\)';\$/\1/p")"
node -e '
const [copy,schema]=process.argv.slice(1);
process.stdout.write(JSON.stringify({copy,business:"mosaic-stack",pm:false,claude:"/hand-written/no-claude-pin-read",console:[],schema,activation:"hand-written",at:new Date().toISOString().replace(/\.\d+Z$/,"Z")})+"\n");' -- "$LIVE" "$SCHEMA" > "$P/data/state/stack-active.json"
step "pointer: hand-written, copy $LIVE, schema $SCHEMA"
step "copy: (cd $WT && scripts/stack-copy.sh copy $TO --copies $P/copies --allow-worktree)"
(cd "$WT" && scripts/stack-copy.sh copy "$TO" --copies "$P/copies" --allow-worktree) > "$P/copy.out" 2>&1 || { step "copy failed"; tail -20 "$P/copy.out"; exit 1; }
step "copy: $(tail -1 "$P/copy.out")"
{
echo "# SCRATCH ROOT, NOT LIVE. activate --plan run by darkwing at $(date -u +%FT%TZ) in the row 162 slot."
echo "# Data root: $P/data (config $P/cfg/config.json). There is no store file, so the store schema"
echo "# comes from the pointer. The pointer is HAND-WRITTEN: copy $LIVE is the live bus unit's"
echo "# WorkingDirectory; schema $SCHEMA is that copy's SCHEMA_VERSION read with git show. pm, claude"
echo "# and console are placeholders, so the bus and Console lines below are not the live flags."
echo "# Copies: $P/copies (a scratch copy of $TO). The real --plan against the live pointer runs"
echo "# again in Mos's window before any adopt."
echo "# Command: MOSAIC_CONFIG=$P/cfg/config.json scripts/stack-release.sh activate $TO mosaic-stack --plan --copies $P/copies --bin $P/bin"
set +e
(cd "$WT" && MOSAIC_CONFIG="$P/cfg/config.json" scripts/stack-release.sh activate "$TO" mosaic-stack --plan --copies "$P/copies" --bin "$P/bin" 2>&1)
echo "# exit $?"
} > "$R/out/scratch-plan-8e7ea89a1.txt"
step "plan: written to $R/out/scratch-plan.txt"
step "files under the data root after the plan: $(cd "$P/data" && find . | sort | tr '\n' ' ')"
chmod -R u+w "$P/copies/$TO"
rm -rf "$P/copies/$TO"
step "removed: chmod -R u+w $P/copies/$TO && rm -rf $P/copies/$TO; left: $(ls -A "$P/copies" | wc -l) entries"
cat "$P/steps.log"
@@ -0,0 +1,20 @@
✔ shared copy: npm ci, pushed reachability, readonly/existing copy, webui delegation (423.822635ms)
✔ wrapper preserves Q81 lease and marker, rejects foreign/symlink; check uses effective roots (443.799606ms)
✔ bus unit copy/pm pins, install refuses all busy states and failed reads, drop-in refusal (417.363696ms)
✔ missing package manifest refuses before npm; npm failure removes the unpublished temp copy (313.097858ms)
✔ real pinned install loads bus and Console; same-copy mosaic talk passes proof, other trees refuse (3467.083425ms)
✔ bus status reads effective copy/pm/drop-ins without writes; check refuses an overridden ExecStart (494.000458ms)
✔ drop-ins in either override field and malformed metadata refuse; uninstall also checks --no-reload (196.85006ms)
✔ install never shows a template name on systemd 261; template drop-ins still refuse (188.126705ms)
✔ unlisted instance drop-in is found in another unit lookup directory; path discovery errors refuse (172.544517ms)
✔ inherited mosaic-.service.d override refuses without changing the unit (193.256243ms)
✔ inherited service.d override refuses without changing the unit (196.067886ms)
✔ inherited drop-in unreadable directory or file refuses before writing (185.272649ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 6755.065602
@@ -0,0 +1,72 @@
✔ T1 adopt writes the pointer from the units once; a failing check refuses check-failed (1073.366997ms)
✔ T2 activate installs exactly the pointer's pm, claude and Console flags (1516.662712ms)
▶ T3 a hand-edited unit refuses unit-flags-drift, names the difference and changes nothing
✔ pm (718.027528ms)
✔ claude (742.624633ms)
✔ console (718.380886ms)
✔ T3 a hand-edited unit refuses unit-flags-drift, names the difference and changes nothing (2179.878901ms)
▶ T4 each item 3 refusal changes nothing and logs one line with its code
✔ copy-missing (634.789281ms)
✔ copy-unreachable (640.551556ms)
✔ copy-schema-unknown (603.6274ms)
✔ already-active (721.483462ms)
✔ instance-busy (another business active) (639.947543ms)
✔ instance-busy (this business activating) (638.806649ms)
✔ drop-in-override (665.313306ms)
✔ no-pointer (347.822109ms)
✔ T4 each item 3 refusal changes nothing and logs one line with its code (4893.830749ms)
✔ T5 an activate with no migration stops the Console then the bus, installs, starts the bus then the Console (1516.133038ms)
✔ T6 a migration without --migrate rolls back with migration-needs-flag (1499.97179ms)
✔ T7 --migrate takes the backup before the migration runs (1611.488791ms)
✔ T8 a failed migration restores the backup and keeps the store it replaced (1693.972865ms)
✔ T9 a store outside the target's chain rolls back with store-newer (1624.299305ms)
▶ T10 each gate check failing alone rolls back and puts every file back
✔ units-inactive (4846.794113ms)
✔ bus-status (5224.664321ms)
✔ healthz (4427.925603ms)
✔ copy-check (4527.114561ms)
✔ pm-launch-unconfirmed (4676.65493ms)
✔ T10 each gate check failing alone rolls back and puts every file back (23704.096449ms)
▶ T10b the PM check needs exactly one launched line for a live run of this start
✔ no launched line (5937.72793ms)
✔ two lines (5852.839829ms)
✔ a changed word (4747.354719ms)
✔ run not in launches list (5206.082662ms)
✔ run started before the start (5014.009245ms)
✔ a human launch with no journal line (4636.092869ms)
✔ run for another business (4860.736291ms)
✔ run not live (4726.01952ms)
✔ one well-formed line and a live run pass (1585.348096ms)
✔ T10b the PM check needs exactly one launched line for a live run of this start (42567.84089ms)
✔ T11 a rollback whose gate fails too: rollback-failed, exit 2, both units stopped, no retry (6648.970375ms)
✔ T12 rollback takes the previous copy from the log (2541.277145ms)
✔ T13 a rollback across a migration needs --restore, then restores and goes live (3589.263326ms)
✔ T14 events written after the backup need --events N with the right N (3342.024838ms)
✔ T14b a rollback --restore whose gate fails stops both units and skips item 6 (6292.838156ms)
✔ T14c a rollback without --restore whose gate fails runs item 6 (6446.849123ms)
✔ T15 --plan names the migration and the restore and changes nothing (1895.703147ms)
✔ T26 --plan lists the commits an activate adds, after the migration lines (855.079889ms)
✔ T27 --plan names each migration step, or says the steps cannot be read (654.135105ms)
✔ T25 --plan refuses copies-unrelated when neither copy is an ancestor of the other (363.81159ms)
▶ T23 --plan reads the store without changing a file, stopped or held
✔ stopped store (466.507341ms)
✔ held store (449.905848ms)
✔ T23 --plan reads the store without changing a file, stopped or held (916.922098ms)
✔ T24 business-mismatch and capture-failed refuse with exit 3; a short id is a usage error (1060.156137ms)
✔ T16 status prints the pointer, the log, the check, bus status and drift, and writes nothing (735.52147ms)
✔ T19 a started line with no final line: status shows it and every changing verb refuses (974.128039ms)
✔ T20 a held run lock refuses run-locked without a log line; status and --plan still run (954.126012ms)
✔ T21 a bus unit with no MOSAIC_CLAUDE_BIN: adopt and activate refuse no-claude-pin (1266.249479ms)
▶ T22 the stopped store read: a -wal or -shm refuses store-unclean and is left alone; a clean read adds no file
✔ bus.sqlite-wal (1710.782106ms)
✔ bus.sqlite-shm (1859.932095ms)
✔ clean (1574.266875ms)
✔ T22 the stopped store read: a -wal or -shm refuses store-unclean and is left alone; a clean read adds no file (5145.708424ms)
ℹ tests 58
ℹ suites 0
ℹ pass 58
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 128039.528917
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
a97b359c89cdcc69c077518f93b8f5fa67b872722910803cf0d5c3da1789e284 docs/TOOLS.md
ee303cf4ad5fde793bdd248d7bc60503e83083cde17535dab2d539cb4d1dab74 packages/cli/README.md
73cdd6c2e2eceffa223d25c1af5f549c369d3cb68d906613ddd6f08c05db1bb0 packages/cli/tests/stack-release.test.mjs
19473e30b35c19cf2167fbf0d7aa0d3ed46562d50e44b738bb611d95cd123254 scripts/stack-copy.sh
6a2c828fefef0f467a1bea7c51f927790ee8990d58cfd26372c521ac64c53d7e scripts/stack-release.sh
@@ -0,0 +1,6 @@
2026-10-18T19:51:56Z pointer: hand-written, copy 52f35e7a0933e9fa845f59860a5de1709f9e5756, schema 3f
2026-10-18T19:51:56Z copy: (cd /home/jwoltje/darkwing-scratch/r162/wt && scripts/stack-copy.sh copy 8e7ea89a1e86447d26ec335e4b138acfd5130cbc --copies /home/jwoltje/darkwing-scratch/r162/plan-8e7/copies --allow-worktree)
2026-10-18T19:51:57Z copy: copied: /home/jwoltje/darkwing-scratch/r162/plan-8e7/copies/8e7ea89a1e86447d26ec335e4b138acfd5130cbc
2026-10-18T19:51:57Z plan: written to /home/jwoltje/darkwing-scratch/r162/out/scratch-plan.txt
2026-10-18T19:51:57Z files under the data root after the plan: . ./bus ./state ./state/stack-active.json
2026-10-18T19:51:58Z removed: chmod -R u+w /home/jwoltje/darkwing-scratch/r162/plan-8e7/copies/8e7ea89a1e86447d26ec335e4b138acfd5130cbc && rm -rf /home/jwoltje/darkwing-scratch/r162/plan-8e7/copies/8e7ea89a1e86447d26ec335e4b138acfd5130cbc; left: 0 entries
@@ -0,0 +1,114 @@
# SCRATCH ROOT, NOT LIVE. activate --plan run by darkwing at 2026-10-18T19:51:57Z in the row 162 slot.
# Data root: /home/jwoltje/darkwing-scratch/r162/plan-8e7/data (config /home/jwoltje/darkwing-scratch/r162/plan-8e7/cfg/config.json). There is no store file, so the store schema
# comes from the pointer. The pointer is HAND-WRITTEN: copy 52f35e7a0933e9fa845f59860a5de1709f9e5756 is the live bus unit's
# WorkingDirectory; schema 3f is that copy's SCHEMA_VERSION read with git show. pm, claude
# and console are placeholders, so the bus and Console lines below are not the live flags.
# Copies: /home/jwoltje/darkwing-scratch/r162/plan-8e7/copies (a scratch copy of 8e7ea89a1e86447d26ec335e4b138acfd5130cbc). The real --plan against the live pointer runs
# again in Mos's window before any adopt.
# Command: MOSAIC_CONFIG=/home/jwoltje/darkwing-scratch/r162/plan-8e7/cfg/config.json scripts/stack-release.sh activate 8e7ea89a1e86447d26ec335e4b138acfd5130cbc mosaic-stack --plan --copies /home/jwoltje/darkwing-scratch/r162/plan-8e7/copies --bin /home/jwoltje/darkwing-scratch/r162/plan-8e7/bin
plan: activate for mosaic-stack
from: 52f35e7a0933e9fa845f59860a5de1709f9e5756
to: 8e7ea89a1e86447d26ec335e4b138acfd5130cbc
store schema: 3f (from the pointer; there is no store file)
target schema: 3h
migration: 3f to 3h, after a backup of the stopped store
migration steps: 3f to 3g, 3g to 3h
needs: --migrate (not given; the run would roll back with migration-needs-flag)
needs: Jason present for this run (decision 96 condition 2)
commits: the run adds 92 (newest first, at most 200):
8e7ea89a1 sessions: Filbert row 129 r2 review (approve, comment 28950)
19fe85c3e queue: revs 1757-1790, row 124 done (Dewey r2 approve 1757), rows 125-127/129 re-pinned, 125 and 156 to dewey, 164 to rocko, row 149 done (Filbert r4 1771), 129 r1 changes (1780) and r2 in review (1787-1790), 154 phase 2 in review (Researcher 1782-1784, request 1785-1786)
81109fdd8 logs: Rocko row 129 r2 verification and row 164 accepted design picks (own lines)
c202d57a1 docs: Rocko channel design, cut-over and own log lines (sage-0766/0769)
537025145 sessions: Filbert row 129 r1 review (changes, comment 28933)
d2df4d934 logs: Rocko row 164 design handoff (own lines through temporary index)
c2f16240c docs: row 156 owner dewey, after row 125 (landing run brief)
03bbd2c98 roles, harness: security-reviewer and merge-gate roles, proactive contracts (row 149, #1643)
c682d5f7a docs: row 164 owner rocko, design packet before the claim (channel kind brief)
806ab89df sessions: Filbert row 149 r4 review (approve, comment 28929)
23f86feda logs: Rocko row 129 build, verification and review handoff (own lines via temporary index)
0f715a054 docs: lead decision 150, limit-check interface 2 with report (row 125, dewey-0593)
25eb9e976 sessions: Sage row 124 landing, rows 125-126 sequencing, row 162 rulings
5b203b614 docs: row 125 to dewey, row 126 after step 8 (PRD 1.2 build brief)
70504d23a bus, cli, webui: token accounting per run, schema 3h usage_records (row 124, #1618)
d52327229 sessions: Dewey row 124 round 2 review, approve (comment 28923, rev 1757)
7a13129be queue: revs 1731-1756, row 124 round 1 changes and round 2 in review (Rocko, Dewey 1738), row 129 claimed (1737), rows 157-161 re-pinned for the I4 checks, D1/D2 re-pinned, row 162 briefed to darkwing, row 164 channel kind added (decision 149)
cbe071125 logs: Rocko row 124 round 2 (own lines via temporary index)
810d1a191 sessions: Sage mos-0716 answers, D1 decision 96 conditions, decision 149 and row 164
c742ecec2 docs: lead decision 149 and the channel kind brief, after row 129 (Q156 item 4, Mos mos-0726, #1661)
cd9c071ae plans: D1 owner is darkwing
d503552ea plans: D1 brief carries decision 96's four conditions and records the claude pin and Console flags in the pointer (Mos mos-0716, Darkwing survey)
23533625d sessions: Dewey row 124 round 1 review, changes (comment 28920, rev 1738)
e51d8f204 plans: row 159 I4 tests add the container-address and network-membership checks (Mos, mos-0716)
5e0437659 logs: Rocko row 129 design and row 124 count correction (own lines)
1f3208d4e logs: Rocko row 124 build and verification (own lines via temp index)
640f4cef5 sessions: Sage row 155 ruling, D1/D2 rows 162-163, row 154 phase 1 to Mos, queue revs 1722-1730
6a2409f21 queue: revs 1722-1730, row 134 after 155, row 149 round 4 in review (Dewey 1723-1724), D1 and D2 rows 162-163 with P19 notes, row 157 after 163, row 154 brief re-pin
2b5c76eeb plans: row 154 brief owns two serve.mjs static routes and round-trips the goals review (Researcher phase 1)
0df05f2c5 plans: D1 and D2 brief, health-gated stack activate and rollback and the scratch-root rehearsal (#1659, #1660)
e27053867 sessions: Dewey row 149 round 4 in review (revs 1721-1724, comment 28916)
7dfda5182 sessions: Sage P19 334-335, queue revs 1713-1721, row 156 brief wake gap, row 149 round 4
9cc0ba0e1 plans: row 156 brief takes Filbert's three edits (task ref in landHolders, land and landing from one read, decision-consumed in flight)
957fbfd41 plans: row 156 brief wakes the landing run with a landing turn (Filbert, comment 28913)
a132b4605 queue: revisions 1713-1720 (rows 124, 154, 155 briefed and 124, 155 re-pinned by Sage; rocko 124 and darkwing 155 claims; filbert row 149 round 3 changes)
a3c56343a sessions: Filbert row 149 r3 review (changes, comment 28913)
9098c33f7 sessions: Sage row 87 rounds 5-8 and done, row 149 rulings, row 156, I2-I6, row 128 landing
6f85a5518 queue: revisions 1675-1712 (row 87 rounds 5-8 and done, I2-I6 as rows 157-161, row 156 added, row 128 round 1 and done, row 149 round 3 in review)
ade628c8b cli: mosaic makes projects and threads and talks in a named thread (row 128, #1622)
7de8b7270 sessions: Dewey row 149 round 3 build, slot runs and review request (comment 28902)
77b1a6e8a plans: row 87 follow-ups on the start limit, and decisions 147 (B1) and 148 (B2) (#1581)
db1519637 sessions: Filbert row 87 r8 and row 128 r1 reviews (approve, comments 28894 and 28895)
bb0766826 plans: brief for the host starting the landing run once a merge decision is approved (#1651)
917ef55e1 plans: row 87 round 8 counts a stop's no-such-container as gone, bounds each wait per container and gives I6 the unit comment (#1581)
3fd0b387f sessions: Filbert row 87 r7 review (approve, comment 28889)
4c1fee85a plans: row 87 round 7 makes the leftover rule wait by id until each core container is gone and names the start order (#1581)
e1f13a54d sessions: Filbert row 87 r6 review (approve, comment 28883)
80ebe2afc plans: row 87 round 6 gives the store one real-path spelling, starts each core fork on its first line and states the name-conflict refusal (#1581)
b04014e59 sessions: Filbert row 87 r5 review (approve, comment 28881)
75b4af0bc sessions: Filbert row 149 r2 review (changes, comment 28878)
232e05449 plans: row 87 round 5 keys the core container on the store, sets the stop timeout to cover both close waits, and defines connected and host.pids (#1581)
bf7d09c6c sessions: Sage row 87 rounds 3-4, row 128 brief, queue sweep
c95c437cb queue: revs 1659-1674, row 87 rounds 3 and 4 (round 4 approved by Filbert), row 128 assigned to Researcher, row 149 round 2
125531d5f sessions: Filbert row 87 r4 review (approve, comment 28876)
e5dbcf626 plans: row 87 round 4 adds the core container's lifecycle and bus status liveness, answering Darkwing's and Filbert's round 3 reviews (#1581)
d5025c8f2 sessions: Dewey row 149 round 2, rebase onto ac55e84fe
0f3eb25ea plans: refreshed row 128 brief, the bus project and thread verbs and talk --thread (#1622)
4af594fea sessions: Filbert row 87 r3 review (changes, comment 28870)
3e91d528f sessions: Researcher row 148 lines of 2026-10-16 (steps 1 and 1b, rounds 1 to 4)
2d6681a4d sessions: Researcher row 143 build, rounds 1 and 2, landing and worktree removal
ac55e84fe plans: row 87 round 3 adds the host channel and keeps bindLaunch, answering Darkwing's and Filbert's round 2 reviews (#1581)
e1964c854 docs: BUILD-LOG for the row 143 landing; Sage SESSIONS line
6e7fdf7ff queue: revisions 1645-1658 (row 143 done)
0278b9e0b bus, harness, business, roles: agents review Jason's spec commits, and the assistant summarizes (row 143, #1637)
970e88bf2 sessions: Filbert row 87 r2 review (changes, comment 28864)
80a2e442b sessions: Dewey row 143 r2 review (approve, comment 28862)
47b030d9b plans: row 87 round 2 answers Filbert's and Darkwing's reviews of the containerized install design (#1581)
8fcd0887e plans: row 143 round 2 adds the chief-of-staff relay bullet, and acceptance 314 needs row 128's thread (#1637)
c550efa27 queue: revisions 1632-1644 (re-pins of 149 and 143; row 143 in-review r1 for Researcher with the request 28849 and Dewey's changes 28853; row 87 in-review r1 with Filbert's 28851 and Darkwing's 28852 changes; row 149 in-review r1 for Dewey, request 28855)
8c0fdc807 sessions: Dewey row 149 in-review and row 143 r1 review (changes, comment 28853)
510382ea3 sessions: Filbert row 87 r1 review (changes, comment 28851)
203481725 plans: containerized install design and the I2 to I6 briefs (#1581)
c0af7f248 plans: no verb adds a role instance, so the 149 brief and acceptance 314 name Jason's business-file edit (#1643)
8517e5f45 queue: revisions 1629-1631 (rows 143 and 149 in progress for Researcher and Dewey, row 87 in progress for Sage)
8d47d6184 docs: acceptance 314 step 8 reads task trails, so mosaic trail gets its argument (#1524)
4b1795a8c queue: revisions 1627-1628 (briefs for 149 and 143 re-pinned to 3a16dfac8, row 149 carries the raise_decision domain parameter)
3a16dfac8 plans: row 149 carries Dewey's row 139 finding 1, the raise_decision domain parameter (#1643)
b9c1dcd82 docs: re-pin plan for rows 148 and 141 with the bus 3f to 3g migration (mos-0638)
d4c3b2838 docs: BUILD-LOG for the row 141 landing; Sage SESSIONS line; row 141 r2 packet; P19 follow-ups 321-322; row 155 carry line
28eeef8ea queue: revisions 1625-1626 (row 141 done)
de352dfaf bus, cli, business, harness: messages addressed by agent name (row 141, #1635)
2a85b41da docs: Filbert SESSIONS line for the row 141 round 2 rebase check
7e40fb83b docs: Filbert SESSIONS line for the row 141 round 1 review
d49da64a9 queue: revision 1624 (row 148 done)
625afe15f docs: BUILD-LOG for the row 148 landing; Sage SESSIONS line; P19 follow-ups 318-320 script and record
d4ea7e5a6 webui, bus, project: a Console editor for the project's spec and docs (row 148, #1642)
a4779d2a5 queue: revisions 1588-1623 (rows 137, 141, 148, 150)
fc5a8b74d docs: Filbert SESSIONS line for the row 148 round 3 review
e1e8dda0b docs: Dewey SESSIONS line for the row 148 round 3 review
862af3079 docs: BUILD-LOG for the 52f35e7a0 re-pin and the row 148 round 3 send-back; Sage SESSIONS line
dc12dc9c0 sage: window plan for the 52f35e7a0 re-pin, rows 135, 139, 150 and 137
22dbc2b28 dewey: row 137 live smoke plan packet (#1631)
bus: scripts/bus-service.sh install --copy 8e7ea89a1e86447d26ec335e4b138acfd5130cbc --copies /home/jwoltje/darkwing-scratch/r162/plan-8e7/copies --claude /hand-written/no-claude-pin-read
Console: scripts/webui-service.sh install --copy 8e7ea89a1e86447d26ec335e4b138acfd5130cbc --copies /home/jwoltje/darkwing-scratch/r162/plan-8e7/copies
CLI: scripts/stack-copy.sh wrapper 8e7ea89a1e86447d26ec335e4b138acfd5130cbc --copies /home/jwoltje/darkwing-scratch/r162/plan-8e7/copies --bin /home/jwoltje/darkwing-scratch/r162/plan-8e7/bin
# exit 0
@@ -0,0 +1,6 @@
2026-10-18T19:45:54Z pointer: hand-written, copy 52f35e7a0933e9fa845f59860a5de1709f9e5756, schema 3f
2026-10-18T19:45:54Z copy: (cd /home/jwoltje/darkwing-scratch/r162/wt && scripts/stack-copy.sh copy 606fafd43154ded993a503933681198e737987cd --copies /home/jwoltje/darkwing-scratch/r162/plan/copies --allow-worktree)
2026-10-18T19:45:56Z copy: copied: /home/jwoltje/darkwing-scratch/r162/plan/copies/606fafd43154ded993a503933681198e737987cd
2026-10-18T19:45:56Z plan: written to /home/jwoltje/darkwing-scratch/r162/out/scratch-plan.txt
2026-10-18T19:45:56Z files under the data root after the plan: . ./bus ./state ./state/stack-active.json
2026-10-18T19:45:56Z removed: chmod -R u+w /home/jwoltje/darkwing-scratch/r162/plan/copies/606fafd43154ded993a503933681198e737987cd && rm -rf /home/jwoltje/darkwing-scratch/r162/plan/copies/606fafd43154ded993a503933681198e737987cd; left: 0 entries
@@ -0,0 +1,134 @@
# SCRATCH ROOT, NOT LIVE. activate --plan run by darkwing at 2026-10-18T19:45:56Z in the row 162 slot.
# Data root: /home/jwoltje/darkwing-scratch/r162/plan/data (config /home/jwoltje/darkwing-scratch/r162/plan/cfg/config.json). There is no store file, so the store schema
# comes from the pointer. The pointer is HAND-WRITTEN: copy 52f35e7a0933e9fa845f59860a5de1709f9e5756 is the live bus unit's
# WorkingDirectory; schema 3f is that copy's SCHEMA_VERSION read with git show. pm, claude
# and console are placeholders, so the bus and Console lines below are not the live flags.
# Copies: /home/jwoltje/darkwing-scratch/r162/plan/copies (a scratch copy of 606fafd43154ded993a503933681198e737987cd). The real --plan against the live pointer runs
# again in Mos's window before any adopt.
# Command: MOSAIC_CONFIG=/home/jwoltje/darkwing-scratch/r162/plan/cfg/config.json scripts/stack-release.sh activate 606fafd43154ded993a503933681198e737987cd mosaic-stack --plan --copies /home/jwoltje/darkwing-scratch/r162/plan/copies --bin /home/jwoltje/darkwing-scratch/r162/plan/bin
plan: activate for mosaic-stack
from: 52f35e7a0933e9fa845f59860a5de1709f9e5756
to: 606fafd43154ded993a503933681198e737987cd
store schema: 3f (from the pointer; there is no store file)
target schema: 3h
migration: 3f to 3h, after a backup of the stopped store
migration steps: 3f to 3g, 3g to 3h
needs: --migrate (not given; the run would roll back with migration-needs-flag)
needs: Jason present for this run (decision 96 condition 2)
commits: the run adds 112 (newest first, at most 200):
606fafd43 plans: row 165 brief, Jason's Q4 confirmation relayed (mos-0805), hold lifted
cfde62f9f agents: Rocko row 164 r2 packet (#1661)
008746b6e plugins: channel kind for the CLI and Console chat paths (row 164, #1661)
e5b10d275 sessions: Filbert row 154 r3 review, approve
27f01be2e queue: revs 1805-1827, row 164 r1 in review (Rocko 1806-1807, request by Sage 1808-1809), r1 changes (Dewey 1814), r2 in review (Rocko 1816, 1820-1821, request by Sage 1824-1825) and approve (Dewey 1826); 125 r2 in review (Dewey 1805, 1811-1812) and approve (Filbert 1815); 154 r2 changes (Filbert 1810, Dewey 1813), r3 in review (Researcher 1817-1819, request by Sage 1822-1823) and Dewey approve (1827)
1dbf45610 sessions: Filbert row 125 r2 review, approve
6f963fd65 sessions: Filbert row 154 r2 review, changes
ed92e2e1e logs: correct Rocko row 164 resume stamp from recorded clock
3d527871c docs: Rocko row 164 candidate packet and own verification logs
370c4d897 sessions: Researcher row 154 phase 1, phase 2 and round 2 lines, row 128 mutants line (their own lines)
5ec790f14 queue: revs 1798-1804, row 165 added queued (Sage 1798, P19 332), 154 r2 in review (Researcher 1799-1801, request posted by Sage 1803-1804), 125 r1 changes (Filbert 1802)
7a7d7442d sessions: Filbert row 125 r1 review, changes
ab9f23d9e docs: row brief for adding and changing role instances (P19 332, #1662); I6 brief records Jason's P19 333 ruling
d62c61fbd logs: Rocko row 164 focused pass and full-gate fixture boundary (own lines)
fe7eef8fb queue: revs 1791-1797, row 129 r2 approve (Filbert 1791) and done (1792), 164 claimed (Rocko 1793), 154 r1 changes (Filbert 1794, Dewey 1797), 125 r2 in review (Dewey 1795-1796)
20b5030b5 sessions: Dewey row 125 in-review and row 154 r1 security review
fdb1d4b34 logs: Rocko row 164 test preparation and landed reference check (own lines)
e79ba6c94 sessions: Sage row 129 landing (850484a7d, #1623 closed)
3cbfbaac4 sessions: Filbert row 154 r1 review (changes, comment 28954)
850484a7d webui: Console chat area over the bus's project and thread verbs (row 129, #1623)
8e7ea89a1 sessions: Filbert row 129 r2 review (approve, comment 28950)
19fe85c3e queue: revs 1757-1790, row 124 done (Dewey r2 approve 1757), rows 125-127/129 re-pinned, 125 and 156 to dewey, 164 to rocko, row 149 done (Filbert r4 1771), 129 r1 changes (1780) and r2 in review (1787-1790), 154 phase 2 in review (Researcher 1782-1784, request 1785-1786)
81109fdd8 logs: Rocko row 129 r2 verification and row 164 accepted design picks (own lines)
c202d57a1 docs: Rocko channel design, cut-over and own log lines (sage-0766/0769)
537025145 sessions: Filbert row 129 r1 review (changes, comment 28933)
d2df4d934 logs: Rocko row 164 design handoff (own lines through temporary index)
c2f16240c docs: row 156 owner dewey, after row 125 (landing run brief)
03bbd2c98 roles, harness: security-reviewer and merge-gate roles, proactive contracts (row 149, #1643)
c682d5f7a docs: row 164 owner rocko, design packet before the claim (channel kind brief)
806ab89df sessions: Filbert row 149 r4 review (approve, comment 28929)
23f86feda logs: Rocko row 129 build, verification and review handoff (own lines via temporary index)
0f715a054 docs: lead decision 150, limit-check interface 2 with report (row 125, dewey-0593)
25eb9e976 sessions: Sage row 124 landing, rows 125-126 sequencing, row 162 rulings
5b203b614 docs: row 125 to dewey, row 126 after step 8 (PRD 1.2 build brief)
70504d23a bus, cli, webui: token accounting per run, schema 3h usage_records (row 124, #1618)
d52327229 sessions: Dewey row 124 round 2 review, approve (comment 28923, rev 1757)
7a13129be queue: revs 1731-1756, row 124 round 1 changes and round 2 in review (Rocko, Dewey 1738), row 129 claimed (1737), rows 157-161 re-pinned for the I4 checks, D1/D2 re-pinned, row 162 briefed to darkwing, row 164 channel kind added (decision 149)
cbe071125 logs: Rocko row 124 round 2 (own lines via temporary index)
810d1a191 sessions: Sage mos-0716 answers, D1 decision 96 conditions, decision 149 and row 164
c742ecec2 docs: lead decision 149 and the channel kind brief, after row 129 (Q156 item 4, Mos mos-0726, #1661)
cd9c071ae plans: D1 owner is darkwing
d503552ea plans: D1 brief carries decision 96's four conditions and records the claude pin and Console flags in the pointer (Mos mos-0716, Darkwing survey)
23533625d sessions: Dewey row 124 round 1 review, changes (comment 28920, rev 1738)
e51d8f204 plans: row 159 I4 tests add the container-address and network-membership checks (Mos, mos-0716)
5e0437659 logs: Rocko row 129 design and row 124 count correction (own lines)
1f3208d4e logs: Rocko row 124 build and verification (own lines via temp index)
640f4cef5 sessions: Sage row 155 ruling, D1/D2 rows 162-163, row 154 phase 1 to Mos, queue revs 1722-1730
6a2409f21 queue: revs 1722-1730, row 134 after 155, row 149 round 4 in review (Dewey 1723-1724), D1 and D2 rows 162-163 with P19 notes, row 157 after 163, row 154 brief re-pin
2b5c76eeb plans: row 154 brief owns two serve.mjs static routes and round-trips the goals review (Researcher phase 1)
0df05f2c5 plans: D1 and D2 brief, health-gated stack activate and rollback and the scratch-root rehearsal (#1659, #1660)
e27053867 sessions: Dewey row 149 round 4 in review (revs 1721-1724, comment 28916)
7dfda5182 sessions: Sage P19 334-335, queue revs 1713-1721, row 156 brief wake gap, row 149 round 4
9cc0ba0e1 plans: row 156 brief takes Filbert's three edits (task ref in landHolders, land and landing from one read, decision-consumed in flight)
957fbfd41 plans: row 156 brief wakes the landing run with a landing turn (Filbert, comment 28913)
a132b4605 queue: revisions 1713-1720 (rows 124, 154, 155 briefed and 124, 155 re-pinned by Sage; rocko 124 and darkwing 155 claims; filbert row 149 round 3 changes)
a3c56343a sessions: Filbert row 149 r3 review (changes, comment 28913)
9098c33f7 sessions: Sage row 87 rounds 5-8 and done, row 149 rulings, row 156, I2-I6, row 128 landing
6f85a5518 queue: revisions 1675-1712 (row 87 rounds 5-8 and done, I2-I6 as rows 157-161, row 156 added, row 128 round 1 and done, row 149 round 3 in review)
ade628c8b cli: mosaic makes projects and threads and talks in a named thread (row 128, #1622)
7de8b7270 sessions: Dewey row 149 round 3 build, slot runs and review request (comment 28902)
77b1a6e8a plans: row 87 follow-ups on the start limit, and decisions 147 (B1) and 148 (B2) (#1581)
db1519637 sessions: Filbert row 87 r8 and row 128 r1 reviews (approve, comments 28894 and 28895)
bb0766826 plans: brief for the host starting the landing run once a merge decision is approved (#1651)
917ef55e1 plans: row 87 round 8 counts a stop's no-such-container as gone, bounds each wait per container and gives I6 the unit comment (#1581)
3fd0b387f sessions: Filbert row 87 r7 review (approve, comment 28889)
4c1fee85a plans: row 87 round 7 makes the leftover rule wait by id until each core container is gone and names the start order (#1581)
e1f13a54d sessions: Filbert row 87 r6 review (approve, comment 28883)
80ebe2afc plans: row 87 round 6 gives the store one real-path spelling, starts each core fork on its first line and states the name-conflict refusal (#1581)
b04014e59 sessions: Filbert row 87 r5 review (approve, comment 28881)
75b4af0bc sessions: Filbert row 149 r2 review (changes, comment 28878)
232e05449 plans: row 87 round 5 keys the core container on the store, sets the stop timeout to cover both close waits, and defines connected and host.pids (#1581)
bf7d09c6c sessions: Sage row 87 rounds 3-4, row 128 brief, queue sweep
c95c437cb queue: revs 1659-1674, row 87 rounds 3 and 4 (round 4 approved by Filbert), row 128 assigned to Researcher, row 149 round 2
125531d5f sessions: Filbert row 87 r4 review (approve, comment 28876)
e5dbcf626 plans: row 87 round 4 adds the core container's lifecycle and bus status liveness, answering Darkwing's and Filbert's round 3 reviews (#1581)
d5025c8f2 sessions: Dewey row 149 round 2, rebase onto ac55e84fe
0f3eb25ea plans: refreshed row 128 brief, the bus project and thread verbs and talk --thread (#1622)
4af594fea sessions: Filbert row 87 r3 review (changes, comment 28870)
3e91d528f sessions: Researcher row 148 lines of 2026-10-16 (steps 1 and 1b, rounds 1 to 4)
2d6681a4d sessions: Researcher row 143 build, rounds 1 and 2, landing and worktree removal
ac55e84fe plans: row 87 round 3 adds the host channel and keeps bindLaunch, answering Darkwing's and Filbert's round 2 reviews (#1581)
e1964c854 docs: BUILD-LOG for the row 143 landing; Sage SESSIONS line
6e7fdf7ff queue: revisions 1645-1658 (row 143 done)
0278b9e0b bus, harness, business, roles: agents review Jason's spec commits, and the assistant summarizes (row 143, #1637)
970e88bf2 sessions: Filbert row 87 r2 review (changes, comment 28864)
80a2e442b sessions: Dewey row 143 r2 review (approve, comment 28862)
47b030d9b plans: row 87 round 2 answers Filbert's and Darkwing's reviews of the containerized install design (#1581)
8fcd0887e plans: row 143 round 2 adds the chief-of-staff relay bullet, and acceptance 314 needs row 128's thread (#1637)
c550efa27 queue: revisions 1632-1644 (re-pins of 149 and 143; row 143 in-review r1 for Researcher with the request 28849 and Dewey's changes 28853; row 87 in-review r1 with Filbert's 28851 and Darkwing's 28852 changes; row 149 in-review r1 for Dewey, request 28855)
8c0fdc807 sessions: Dewey row 149 in-review and row 143 r1 review (changes, comment 28853)
510382ea3 sessions: Filbert row 87 r1 review (changes, comment 28851)
203481725 plans: containerized install design and the I2 to I6 briefs (#1581)
c0af7f248 plans: no verb adds a role instance, so the 149 brief and acceptance 314 name Jason's business-file edit (#1643)
8517e5f45 queue: revisions 1629-1631 (rows 143 and 149 in progress for Researcher and Dewey, row 87 in progress for Sage)
8d47d6184 docs: acceptance 314 step 8 reads task trails, so mosaic trail gets its argument (#1524)
4b1795a8c queue: revisions 1627-1628 (briefs for 149 and 143 re-pinned to 3a16dfac8, row 149 carries the raise_decision domain parameter)
3a16dfac8 plans: row 149 carries Dewey's row 139 finding 1, the raise_decision domain parameter (#1643)
b9c1dcd82 docs: re-pin plan for rows 148 and 141 with the bus 3f to 3g migration (mos-0638)
d4c3b2838 docs: BUILD-LOG for the row 141 landing; Sage SESSIONS line; row 141 r2 packet; P19 follow-ups 321-322; row 155 carry line
28eeef8ea queue: revisions 1625-1626 (row 141 done)
de352dfaf bus, cli, business, harness: messages addressed by agent name (row 141, #1635)
2a85b41da docs: Filbert SESSIONS line for the row 141 round 2 rebase check
7e40fb83b docs: Filbert SESSIONS line for the row 141 round 1 review
d49da64a9 queue: revision 1624 (row 148 done)
625afe15f docs: BUILD-LOG for the row 148 landing; Sage SESSIONS line; P19 follow-ups 318-320 script and record
d4ea7e5a6 webui, bus, project: a Console editor for the project's spec and docs (row 148, #1642)
a4779d2a5 queue: revisions 1588-1623 (rows 137, 141, 148, 150)
fc5a8b74d docs: Filbert SESSIONS line for the row 148 round 3 review
e1e8dda0b docs: Dewey SESSIONS line for the row 148 round 3 review
862af3079 docs: BUILD-LOG for the 52f35e7a0 re-pin and the row 148 round 3 send-back; Sage SESSIONS line
dc12dc9c0 sage: window plan for the 52f35e7a0 re-pin, rows 135, 139, 150 and 137
22dbc2b28 dewey: row 137 live smoke plan packet (#1631)
bus: scripts/bus-service.sh install --copy 606fafd43154ded993a503933681198e737987cd --copies /home/jwoltje/darkwing-scratch/r162/plan/copies --claude /hand-written/no-claude-pin-read
Console: scripts/webui-service.sh install --copy 606fafd43154ded993a503933681198e737987cd --copies /home/jwoltje/darkwing-scratch/r162/plan/copies
CLI: scripts/stack-copy.sh wrapper 606fafd43154ded993a503933681198e737987cd --copies /home/jwoltje/darkwing-scratch/r162/plan/copies --bin /home/jwoltje/darkwing-scratch/r162/plan/bin
# exit 0