Compare commits

..
Author SHA1 Message Date
jason.woltje b7239bb66e agents: row 195 Case 1 install design r6 (#1693): Duckworth r5 B1 (confirm's result lines carry the activation's op, from, to and the rollback set's BACKUP, so from_log reads .53's log; sequence 7), B2 (MIGRATING recorded after backup_store and before migrate-schema.mjs, as TAKEN today; sequence 8), N1 marker step makes $RS, N2 gate --since, N3 apparmor.d word grep baseline, N4 item 4 under systemd-run with NoNewPrivileges, N5 plan --expect-from OLD 2026-10-20 16:53:53 -05:00
jason.woltje 0b6d29e909 agents: row 195 Case 1 install design r5 (#1693): Duckworth r4 B1 (one result line per activation, FAILED file and --cause, marker written first, refusals log no activation, real-sequence tests), B2 per mos-1098/mos-1101 (case1-pins.sh and case1-hostcheck.sh, seven-item drift stop with apt hook and boot unit, ops-01 owner, bwrap closed on drift), N1-N4; Console on 127.0.0.1:732 with CAP_NET_BIND_SERVICE, tester 7333 as P10; chains on row 190 r3 and row 230 (C2-13a), node under /opt, four factory profile items for Gosalyn 2026-10-20 16:46:21 -05:00
jason.woltje 0075be3581 agents: row 195 Case 1 install design r4 (#1693): Duckworth B1 (root holds only the go's values and the unit files; confirm and revert find the one open activation, AWAITING-UNITS; rollback --to checked against from_log previous; A2 failure path; root step log), B2 (confirm runs gate with --timeout; .53 runs the PM; external PM check reads launches list with ActiveEnterTimestamp, no journal; pm-on refuses), N1-N5; mos-1096 bwrap-userns-restrict from the pinned deb, dpkg-statoverride root:mosaic 0750, P9 accepted by Mos; gosalyn-0046 two units and args file; managed settings for DISABLE_AUTOUPDATER; sage-1481 C2-13a 2026-10-20 16:08:53 -05:00
jason.woltje 9a16d7795d agents: row 195 Case 1 install design r3 (#1693): Duckworth B1, one ordered .53 activate with a runner per step (case1-repin.sh, stack-release --units external with confirm and revert), stopped-store backup under $DATA/backups/stack; N1-N6; mos-1091 4a-4c with the bwrap AppArmor profile; gosalyn-0034 C2-13 items 2026-10-20 15:56:54 -05:00
jason.woltje 818b79696d agents: row 195 Case 1 install design r2 (#1693, brief blob 45be7f09af, Sage rulings sage-1451) 2026-10-20 15:38:55 -05:00
203 changed files with 1758 additions and 29367 deletions
+6 -11
View File
@@ -25,15 +25,10 @@ green at every step. Not production software — a proven foundation.
1. **Root is bootstrap-only.** First-class system configuration lives at the
repository root; everything else gets a dedicated directory (`roles/`,
`contracts/`, `missions/`, `tasks/`, `docs/`). Do not add new files to root.
2. **Configuration**: each stack instance has exactly one system config. The
default instance's is `~/.config/mosaic-dev/config.json`. Instance NAME's
is `~/.config/mosaic-NAME/config.json`, and its data root carries a marker
that names it. `~` is the home of the OS user that runs the instance. A
process reads only the config `MOSAIC_CONFIG` names, or the default's when
that is unset. Configs are created only by
`scripts/bootstrap.sh [--instance NAME]`, never overwritten, fail-closed on
any problem. Repo-scoped role authority lives in `roles/*.json`
(versioned, reviewed commits only).
2. **Configuration**: `~/.config/mosaic-dev/config.json` is the sole system
config — created only by `scripts/bootstrap.sh`, never overwritten,
fail-closed on any problem. Repo-scoped role authority lives in
`roles/*.json` (versioned, reviewed commits only).
3. **Secrets** never enter the repository or container images; auth is
runtime-only (read-only mount or environment variable).
4. **Contracts** (`contracts/`) are immutable and image-baked. Missions and
@@ -182,8 +177,8 @@ Full reference — usage, fields, exit codes, safety notes:
## Data map (canon)
- `~/.config/mosaic-dev/config.json`, or `~/.config/mosaic-NAME/config.json`
per named instance: system config (user-authored; never auto-written).
- `~/.config/mosaic-dev/config.json` — system config (user-authored; never
auto-written).
- `<dataRoot>` (from config; default `~/.mosaic-dev`):
- `runs/` — write-once run evidence (`result.json`, snapshots, `stderr.txt`)
- `sessions/` — pi JSONL session trees, one directory per named session
-1
View File
@@ -5174,4 +5174,3 @@ Before: managed project/thread verbs had no Console conversation area. After: fi
2026-10-20 04:44:54 UTC | Row 178 round 2 preparation | Before: Dewey C1 five render guards lacked killing tests. Added independent refusal fixtures, a temporary uid+1 id stub with cleanup, and empty loopback proof case; activation ordering recorded in r2 BUILD. Product unchanged; syntax/text preflight only, no test or mutant execution. Pending row 181 rebase and Sage slot.
2026-10-20 05:10:34 UTC | Row 178 #1676 round 2 verified | Before: C1 render refusals lacked killing tests and row 181 base was pending. Rebased onto 931651f2d, kept all COUNTED/count code and tests, appended T178 tail; added independent owner/collision/absolute/charset/header and empty-loopback proof tests. Four files singly 156/156, all 24 mutants killed, metadata and sandbox fixture probes pass; scratch sweep empty. Packet agents/rocko/work/queue-178/r2/, candidate 9300a670, inventory d0dcde7b (59/59), scan 58 files no findings; in-review revs 2166-2167, expected no-token request failure. No live actions or commits.
2026-10-21T01:28:05Z | Darkwing | row 232 #1734 installer I1 build, round 1 in review | Before: stack-release.sh managed only user units (systemctl --user), and stack-copy.sh had no bundle copy. After: --units external (adopt, activate, rollback --to, status, confirm, revert, gate --since, AWAITING-UNITS, stale-start, external-pm, --plan --expect-from) and stack-copy check --units external, copy --bundle FILE SHA, wrapper ID --create; user mode byte-identical to the base by test. First build 0af0c73c6 on 00d4cc1f6; rebased onto row 200 (cb5fdb064) as a0ffabc3f on darkwing/row232-r1; --sso refused under external (D9), row 200 config_checks run under external (D10). Gate on Sage's slot (sage-1618): cli 561/0, every scripts/test-*.sh green; 17/17 mutants killed. Results commit 84f1a91e6 (pushed, seat branch only). Manifest 7a3b4ea1 (9 paths), queue revs 2595-2596, request comment 29825, reviewer Filbert. Packet agents/darkwing/work/queue-232/BUILD.md.
+872
View File
@@ -0,0 +1,872 @@
# Row 195 design: the Case 1 install on 10.1.1.53 (r6)
Darkwing, 2026-10-20. Brief: `docs/plans/2026-10-20_hub-own-uid.md`, blob 45be7f09afc1 (f24f2cd40,
sage-1439). Gitea #1693. Reviewer Duckworth, gate owner Sage. Chained on row 190's candidate (round 3,
df65a3d4c on `darkwing/row190-r3`, rebased on row 192; the config canon) and on row 230's candidate
(C2-13a, #1732, Gosalyn: `scripts/case2-unit.sh` and `case2-precheck.sh`), Q177, sage-1499. Row 230's
brief is `docs/plans/2026-10-20_case2-rows.md` "C2-13a" at 5b8def2f1, from Gosalyn's design r11
(eac92d956 on `gosalyn/case2-design`, approved webby-0038). It has no candidate yet, so I read that
brief as the interface and expect a rebase. Section 2 lists what Case 1 needs from it that the brief
doesn't give.
Changes from r5 (Duckworth's REVIEW-r4.md at 62200362b on `duckworth/reviews`, duckworth-0054). This
round changes only B1's and B2's lines, their section 7 cases and the notes:
- B1. The result line `confirm` writes said op `confirm`, because `log_line` (:58) writes `$OP` and
`OP="$1"` (:1493). `from_log` (:169) reads only `adopt`, `activate` and `rollback`, so it would have
skipped every .53 activation: after adopt C0 and re-pins to C1 and C2, `from_log previous` returns
C0. Now A5's `ok` line and R4's line carry the op, from and to of the activation's `started` line,
and the backup that A3 records in the rollback set. Real sequence 7 and a mutant that writes op
`confirm`. Section 4.
- B2. r5 recorded the migration after the migrate, while today's run sets TAKEN before
`migrate-schema.mjs` (:1004-1005). A migrate that changed the store and then exited 1 would have
left no record, so R2 would skip the restore and R4 would gate OLD on a half-migrated store. A3 now
writes `MIGRATING` after `backup_store` returns and before the migrate. Real sequence 8 and a mutant
that records after the migrate. Section 4.
- N1. The marker step makes `$RS` with capture's exclusive `mkdir` (:416), and capture under
`--units external` makes none. N2. `gate` takes `--since T`, root's clock before its A2f start, as
its start floor. N3. Section 0 now has a read-only grep of `/etc/apparmor.d` for the word `bwrap`,
and item 3 matches the word against a list of exactly one file. N4. Item 4 runs under
`systemd-run` with the units' `NoNewPrivileges=yes`, as 2a' does. N5. A1's plan takes
`--expect-from OLD` and refuses `pointer-differs`.
- Not in this round: claude's path. gosalyn-0058 proposes that `--claude`, like `--node`, takes only a
root-owned path outside the home, and `/srv/mosaic/.local/bin/claude` fails that. I asked Sage to
rule and recommended moving claude under `/opt/mosaic-stack`. That change gets its own round.
Changes from r4 (Duckworth's REVIEW-r3.md at 9f4d5556b on `duckworth/reviews`; mos-1098; mos-1101;
gosalyn-0049):
- B1. An activation now has exactly one result line, written at the end. A5's gate failure and A3's
failure after its capture write no log line. Each writes a `FAILED` file next to `AWAITING-UNITS` and
exits 1, so the activation stays open for R2 and R4, and R4 writes the result. A3 writes
`AWAITING-UNITS` right after `started`. Refusals by `confirm`, `revert` and `gate` log no activation
id, so a refused `confirm` can't close one either. There are real-sequence tests on one log, and
mutants that put back r4's interim line, r4's marker order and a refusal that names the activation.
Section 4 and section 7.
- B2. The bwrap profile has an owner and a stop. `scripts/case1-hostcheck.sh` checks the profile file
against the pin in `scripts/case1-pins.sh`, the two loaded profiles, the live label of a process
bwrap runs, the statoverride and mode, that no other profile names bwrap, two sysctls and the port
floor. It has three callers:
1. A1 of every re-pin refuses `profile-drift`.
2. A root apt hook runs it after every package run, unattended ones included.
3. A boot unit runs it before the bus starts.
On drift, callers 2 and 3 unload the profile and close `/usr/bin/bwrap` to `mosaic`, so every role
session refuses to start. They write `DRIFT` and log it, and only root clears it. ops-01 owns the host
side under a Mos go: it reads the log in each STATE report and, before each re-pin window, compares
the upstream profile to the pin. A changed upstream profile becomes a new pin, which is its own review
round. Section 1 has the details. The facts behind it are in section 0: unattended-upgrades runs daily
on .53 with the security pocket, and the pinned bubblewrap is in that pocket.
- N1 and mos-1098 point 2. P9 has a seventh item: no `mosaic-bwrap` profile is loaded, and no profile
other than Ubuntu's attaches to `/usr/bin/bwrap`. r4 never installed `mosaic-bwrap`. F5 now compares
exact strings. P9 reads `accepted mos-1096` only while all seven items hold.
- N2: a role session gets no capabilities, so a nested bwrap fails. Section 1. N3: A3 refuses "before
it logs `started`". N4: the stale-start rule and its fixtures handle `ActiveEnterTimestamp`'s whole
seconds.
- The port squat (mos-1101 item 5, gosalyn-0049). It applies: role sessions share the host's network
namespace (`packages/seat/src/sandbox.mjs:67` unshares no net), so while the Console is down a
session can bind its loopback port. The Console moves to 127.0.0.1:732 with
`CAP_NET_BIND_SERVICE` on its unit (row 230's line), so no session can take it. The tester port stays
7333, because serve refuses a tester port below 1024. That residue is P10, accepted under Mos's
reading. Case 1 can't meet the preview trio or the proof pair, because .53 has no proxy. Section 2
names the factory serve set for row 230.
- Jason's Q9 (Vikunja #429) is still open. If install and upgrade become product commands, these
steps become that installer's configuration and row 195 rebases on its row (sage-1491). Section 6.
## 0. What I checked on the host
Read only, as `localadmin` over ssh, 2026-10-20 between 20:40Z and 21:00Z, and again at 21:27Z for the
last seven rows (the brief allows this):
| Item | 10.1.1.53 today | Needed |
|---|---|---|
| OS | Ubuntu 24.04.5, kernel 6.8.0-124, x86_64, 12 cores, 94 GiB, 217 GiB free on `/` | fits |
| `mosaic` user, group, `/srv/mosaic` | absent | step 2a creates them |
| node | `/usr/local/bin/node` v22.22.2, root-owned | 24 or later (every `packages/*/package.json` engines line); dragon-lin runs 26.8.1 |
| bwrap | absent, no statoverride; apt candidate `bubblewrap 0.9.0-1ubuntu0.3` | every role session runs under `/usr/bin/bwrap` (`packages/seat/src/sandbox.mjs:43`) |
| user namespaces | `kernel.apparmor_restrict_unprivileged_userns = 1`, apparmor 4.0.1, no bwrap profile loaded | bwrap needs a user namespace, which this setting refuses unless a profile allows it |
| `apparmor-profiles` | not installed; candidate `4.0.1really4.0.1-0ubuntu0.24.04.9`, whose `/usr/share/apparmor/extra-profiles/bwrap-userns-restrict` has sha256 `11d39094f044f0cda0febb3ad517b830301da6b2ce929664af09ee9e4dd264f9` (read from the deb on dragon-lin) | section 1 |
| `fs.protected_hardlinks`, `fs.protected_symlinks` | 1, 1 | mos-1096 item 2 |
| journald | `SplitMode=uid` (the default) | section 4, B2 |
| `useradd` | `USERGROUPS_ENAB yes` | 2a passes `--user-group` anyway |
| claude | not on localadmin's PATH | the factory's harness; dragon-lin runs 2.1.304 |
| ports 7330-7333 | free (no listener) | tester |
| `localadmin` | NOPASSWD sudo (#423 step 1, applied 20:05:47Z, mos-1066, mos-1077) | the root steps run through it |
| `jarvis` | groups sudo, docker, lxd, adm | #423 step 4 removes all four (section 5, P5) |
| unattended-upgrades | on: `APT::Periodic::Unattended-Upgrade "1"`, `apt-daily-upgrade.timer` enabled. Allowed origins `${distro_id}:${distro_codename}`, `-security`, ESM apps-security and ESM infra-security. No `Package-Blacklist` | B2: packages change daily without a window |
| bubblewrap's pocket | `0.9.0-1ubuntu0.3` is in both `noble-updates` and `noble-security`, so its later security updates install unattended | B2 |
| apparmor | installed `4.0.1really4.0.1-0ubuntu0.24.04.8`. The `.9` candidate is in `noble-updates` only, which unattended-upgrades doesn't take. `aa-status --json` works | B2 |
| profiles that name bwrap | at 21:51:46Z `grep -rlw bwrap /etc/apparmor.d` and `grep -rlF /usr/bin/bwrap /etc/apparmor.d` both find nothing (exit 1). After 2a' the word grep lists only `bwrap-userns-restrict` | N1, P9 item 7, hostcheck item 3 |
| `net.ipv4.ip_unprivileged_port_start` | 1024 | the port squat |
| listeners below 1024 | 22 (sshd) and 53 (the resolver) only; 732 is free | the Console's port (section 1) |
| other listeners | 4003, 6052, 6055, 8123, 8765, 14242, 18555, and on loopback 1933, 5905, 6085, 6379, 6380, 9119, 9225, 18554, 18789, 38147 | none in 7330-7333 |
## 1. The install as inputs to the Case 2 scripts
Instance. Row 190's canon with the default name, `dev`, as Case 2 uses it (Gosalyn design 9). `~` is
`mosaic`'s home, `/srv/mosaic`, so:
| Input | Value |
|---|---|
| user | `mosaic`, primary group `mosaic` with no other member |
| home | `/srv/mosaic`, 0700, owner `mosaic` |
| config dir | `/srv/mosaic/.config/mosaic-dev` |
| data root | `/srv/mosaic/.mosaic-dev`, marker `instance.json` `{"instanceVersion":1,"name":"dev"}` |
| business | `mosaic-stack`, file `<configDir>/businesses/mosaic-stack.json` (section 2) |
| profile | `--profile factory` (row 230, C2-13b) |
| roles | the factory set in section 2 |
| human id | Jason's id, given in the window's go. Not `tester` (`broker.mjs:245`) |
| model credential | `<configDir>/secrets/claude-session.env`, 0600 (row 190's `SESSION_ENV` for the default instance) |
| token slot path | none for this row (section 2) |
| Console port | 127.0.0.1:732, below the port floor, bound through the Console unit's `CAP_NET_BIND_SERVICE` (row 230). The port squat below says why |
| tester port | 127.0.0.1:7333 (P10) |
| board | none. UNITS has no board unit, and the Console gets no `--board` |
| harness | claude at `/srv/mosaic/.local/bin/claude`, pinned through `MOSAIC_CLAUDE_BIN` (decision 91), which the bus unit's `@CLAUDE_ENV@` line sets (section 2) |
| node | `/opt/mosaic-stack/node-<version>/bin/node`, a pinned 26.x release, `root:root`. Row 230 refuses a `--node` under the home or not owned by root |
| PM | on: the bus unit's `@PM_FLAGS@` renders ` --pm` (section 2, section 4) |
| source | `/srv/mosaic/src/mosaic-stack` (`mosaic`'s checkout, section 4) and `/root/mosaic-stack` (root's) |
Why `dev` and not a named instance: one instance per OS user, and Case 2's rows already assume
`/srv/mosaic/.config/mosaic-dev`. A second instance under `mosaic` on .53 would need a name, and row 190
makes that a flag, not a move.
Why node lives under `/opt/mosaic-stack`: changing `/usr/local/bin/node` would change what Hermes and
anything else on the host runs, and Q181 keeps Hermes running. r4 put node under `/srv/mosaic`, but row
230 requires a root-owned `--node` outside the home. Nothing else on the host puts `/opt/mosaic-stack`
on its PATH.
PATH for `mosaic`'s steps. `sudo -u mosaic -H` resets PATH to sudo's `secure_path`, which finds
`/usr/local/bin/node`, v22. The precheck would then refuse, and `scripts/mosaic` would run v22 (it runs
`exec node` from PATH, `scripts/mosaic:22-40`). So every `mosaic` step in the run sheet and in
`case1-repin.sh` runs as `sudo -u mosaic -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin ...`, the
list row 230 renders as the units' `@PATH@`. `case1-repin.sh` builds it from the `--node` in
`case1-unit.args`.
The units (row 230, design r11). Two system units, from the existing templates, in start order:
`[email protected]` from `packages/cli/systemd/mosaic-bus.service.in`, then
`mosaic-webui.service` from `packages/webui/systemd/mosaic-webui.service.in`. Root stops them in the
reverse order. `@REPO@` is the copy dir, and `%h` becomes the literal home. Row 230 adds
`ProtectSystem=strict`, `ReadWritePaths=/srv/mosaic`, `ProtectHome=yes` and `PrivateTmp=yes` to both, and
`AmbientCapabilities=CAP_NET_BIND_SERVICE` with `CapabilityBoundingSet=CAP_NET_BIND_SERVICE` to the
Console only. The Console unit has no `[Install]`, so root starts it by hand after a reboot until Jason
rules (mos-1097). The install call is:
```
case2-unit.sh --profile factory --user mosaic --home /srv/mosaic --business mosaic-stack --copy ID \
--node /opt/mosaic-stack/node-<version>/bin/node --pm --claude /srv/mosaic/.local/bin/claude \
-- --port 732 --business mosaic-stack --tester-port 7333
```
`--pm` and `--claude` are the two inputs row 230's brief doesn't have yet (section 2). A re-install
must pass the same `--node`, flags and serve flags, and only the copy id may change (row 230's
re-install check). So 2c writes the whole argument list, without `--copy`, to
`/etc/mosaic-stack/case1-unit.args`, `root:root` 0644, one argument per line. Every later
`case2-unit.sh` call in section 4 reads that file and adds `--copy`. `mosaic` can't write it. Changing
a serve flag is a re-install with its own go, not a re-pin.
The port squat (mos-1101 item 5). Role sessions share the host's network namespace:
`packages/seat/src/sandbox.mjs:67` unshares the user, PID and IPC namespaces and not the network. So
while the Console is down, which A2 to A4 of every re-pin and any crash before `Restart=on-failure`
brings it back both cause, a session can bind 127.0.0.1:7332. It would then receive Jason's `ssh -L`
traffic, and it would answer the gate's `/healthz` (`stack-release.sh:485-487`) in the Console's place.
At 732 that can't happen. The floor is 1024 (section 0), a session has no capabilities (below), and
`mosaic`'s own processes outside the sandbox have none either. Only the Console unit holds
`CAP_NET_BIND_SERVICE`. Its children, the human-cli and read scripts it spawns
(`packages/webui/src/bus.mjs:31`, `reads.mjs:79`), inherit the ambient capability. No role session
descends from the Console: sessions are children of the bus unit, which has no capability line. The
gate needs no change, because `console_port` (`stack-release.sh:213-215`) reads the Console's `--port`
from the pointer. Row 230's precheck refuses when the floor is at or below the Console's port, and
`case1-hostcheck.sh` checks it after every package run (B2). The tester port can't go below 1024,
because serve refuses that (`packages/webui/src/cli.mjs:33`), so P10 records it.
Node download (N4). The run sheet pins one release, its tarball name and its sha256. As root, in a
root-owned temp dir: fetch the tarball, `SHASUMS256.txt` and `SHASUMS256.txt.asc`; `gpg --verify` the
`.asc` against the Node release keys listed in the nodejs/release-keys repository, imported into a
keyring under `/etc/mosaic-stack/node-keys`; check that the tarball's line in the verified file is the
pinned sha256; then `sha256sum -c` the tarball and unpack it to `/opt/mosaic-stack/node-<version>`,
`root:root`, no file writable by group or other. Any mismatch stops the step. Rollback: `rm -r` that
dir.
Claude (N5). The run sheet pins the version, 2.1.304 unless the window's plan names a newer one. As
`mosaic`: `npm install --prefix /srv/mosaic/.local @anthropic-ai/claude-code@<pin>`, with `@<pin>`
exact. `/srv/mosaic/.local/bin/claude` is the path `MOSAIC_CLAUDE_BIN` names. The autoupdater stays off
through `/etc/claude-code/managed-settings.json`, `root:root` 0644, holding
`{"env":{"DISABLE_AUTOUPDATER":"1"}}`. Root writes it in 2a'. Why there:
- `ENV_ALLOW` (`packages/seat/src/session.mjs:83-88`) doesn't pass `DISABLE_AUTOUPDATER`, so a unit
`Environment=` line never reaches a role session (gosalyn-0046 item 3).
- A role session's HOME is the run's own `home/`, and the sandbox doesn't show `~/.claude`
(`session.mjs:92-93`, `sandbox.mjs:16`). So `mosaic`'s claude settings file doesn't reach one either.
- The sandbox mounts `/etc` read-only (`sandbox.mjs:85`), and Claude Code reads managed settings from
`/etc/claude-code/` whatever HOME is. `mosaic`'s own CLI runs, like the precheck's marker prompt
and F6, read it too.
The pinned binary is a read-only bind inside the sandbox (`sandbox.mjs:9-10`), so a session can't
rewrite it anyway. The managed file covers `mosaic`'s runs outside the sandbox. If the installed
version doesn't honour `env` there, F6 catches the drift. Rollback: `rm` the file.
bwrap and the user namespace (mos-1091 4a, 4b; mos-1096 item 2; N1). Three root steps in 2a', in this
order, each with its rollback. The `mosaic` group must exist first (2a).
1. `apt-get install bubblewrap=0.9.0-1ubuntu0.3`, the version the run sheet pins. Then record
`stat -c '%U:%G %a' /usr/bin/bwrap` to `/etc/mosaic-stack/bwrap.orig` (I expect `root:root 755`).
Rollback: `apt-get remove bubblewrap`.
2. Ubuntu's own `bwrap-userns-restrict`, one file, not the package. `apt-get download
apparmor-profiles=4.0.1really4.0.1-0ubuntu0.24.04.9` into a root-owned temp dir, extract
`./usr/share/apparmor/extra-profiles/bwrap-userns-restrict` with `dpkg-deb --fsys-tarfile`, check
its sha256 is `11d39094f044f0cda0febb3ad517b830301da6b2ce929664af09ee9e4dd264f9`, and install it as
`/etc/apparmor.d/bwrap-userns-restrict`, `root:root` 0644. Load it with `apparmor_parser -r
/etc/apparmor.d/bwrap-userns-restrict`. `aa-status` then lists `bwrap` and `unpriv_bwrap` in
enforce mode. Why one file: the package also installs 20 profiles under `/etc/apparmor.d`
(`usr.sbin.dnsmasq`, `bin.ping`, `usr.sbin.smbd` and more), which would load at the next boot and
could change services on the host that have nothing to do with Mosaic.
What the profile does: `bwrap` gets `userns`, `capability` and `mount` for its own setup, and
every program it runs is stacked under `unpriv_bwrap`, which has the same file and network rules
with `audit deny capability`. So a role session gets a user namespace and no capabilities in it.
The sysctl stays at 1, so nothing else on the host gains user namespaces.
One consequence (N2): a bwrap that a session starts inside its own sandbox fails, because it needs
capabilities in the namespace. Claude Code's own sandbox is such a bwrap, and it's off by default,
so a role session doesn't use it. F3 shows a session still answers under the profile.
Rollback: `apparmor_parser -R /etc/apparmor.d/bwrap-userns-restrict`, then `rm` it.
3. `dpkg-statoverride --update --add root mosaic 0750 /usr/bin/bwrap` (Mos). Only root and the
`mosaic` group can read or run bwrap, and package upgrades keep the mode. Rollback:
`dpkg-statoverride --remove /usr/bin/bwrap`, then `chown` and `chmod` to the values in
`bwrap.orig`.
4. The drift stop (B2, below): `case1-hostcheck.sh` and `case1-pins.sh` from `/root/mosaic-stack` to
`/usr/local/lib/mosaic-stack/`, `root:root` 0755 and 0644; the apt hook
`/etc/apt/apt.conf.d/80mosaic-hostcheck`; the boot unit `mosaic-hostcheck.service`, enabled. Then
`case1-hostcheck.sh --check` exits 0. Rollback: disable and remove the unit, `rm` the hook file and
the dir.
Full rollback runs 4, 3, 2, 1. Tests after 2a' and again in section 5: as `mosaic`, `bwrap --unshare-all
--ro-bind / / true` exits 0 (row 230's precheck line, F4); as `jarvis`, `/usr/bin/bwrap --version`
fails with "Permission denied" (P9); `getent group mosaic` lists no member and no other account has
gid `mosaic` (P9); `fs.protected_hardlinks` is 1 (P9). A role session running under the profile is F5.
One more test runs in 2a', because I can't settle it from here. Row 230's units keep the templates'
`NoNewPrivileges=yes`, and AppArmor limits profile changes for a task with no-new-privs set. The
`bwrap` attach happens on exec from an unconfined task, and the child's change is a stack
(`bwrap//&unpriv_bwrap`), which only narrows. I expect both to pass under no-new-privs, since bwrap
sets no-new-privs on itself before it runs the child, and Ubuntu's profile works with that. But I
haven't seen it run, and .53 is read-only to me. So as root, after step 4: `systemd-run --wait --pipe
--uid=mosaic --gid=mosaic -p NoNewPrivileges=yes -p ProtectSystem=strict -p ProtectHome=yes -p
PrivateTmp=yes /usr/bin/bwrap --unshare-user --unshare-pid --ro-bind / / cat /proc/self/attr/current`
prints `bwrap//&unpriv_bwrap (enforce)`. If it fails, the window stops at 2a' and rolls back 4 to 1,
before any `mosaic` step. That beats finding it at F5.
The drift stop (B2, Duckworth's REVIEW-r3 B2, mos-1098 point 1). r4 installed a profile file that no
package owns, from a pinned deb, onto a host where unattended-upgrades changes packages every day
(section 0). Three things can move under it: bubblewrap (its security updates install unattended), the
`apparmor` package and its parser, and any new profile a package ships that attaches to
`/usr/bin/bwrap`. Two more host values matter to this design: the user-namespace sysctl and the port
floor. So:
- `scripts/case1-pins.sh` holds the pins, sourced by the scripts below: the `apparmor-profiles` version
the file came from, the file's sha256, and the bubblewrap version 2a' installs. Changing a pin is a
reviewed commit and its own round. The bubblewrap version is an install pin, not a drift item:
a security update to bubblewrap keeps the statoverride, and the profile attaches by path.
- `scripts/case1-hostcheck.sh` checks seven items and prints one line each:
1. `/etc/apparmor.d/bwrap-userns-restrict` has the pinned sha256, and `dpkg -S` finds no package
that owns it.
2. `aa-status --json` lists `bwrap` and `unpriv_bwrap` in enforce mode, and no loaded profile named
`mosaic-bwrap`.
3. `grep -rlw bwrap /etc/apparmor.d` lists exactly `/etc/apparmor.d/bwrap-userns-restrict`. The word
match also catches a path written as `/{usr/,}bin/bwrap`. Section 0's read found no file today.
4. The live label: 2a' line `systemd-run --wait --pipe --uid=mosaic --gid=mosaic -p NoNewPrivileges=yes
-p ProtectSystem=strict -p ProtectHome=yes -p PrivateTmp=yes /usr/bin/bwrap --unshare-user
--unshare-pid --ro-bind / / cat /proc/self/attr/current` prints exactly `bwrap//&unpriv_bwrap
(enforce)`. It runs under the units' no-new-privs, so it reads the label a role session gets.
This catches a profile that attaches by a glob that item 3's search misses.
5. `dpkg-statoverride --list /usr/bin/bwrap` reads `root mosaic 0750`, and `stat` agrees.
6. `kernel.apparmor_restrict_unprivileged_userns` is 1 and `fs.protected_hardlinks` is 1.
7. `net.ipv4.ip_unprivileged_port_start` is above the Console's `--port` in the installed unit.
`--check` exits 0 when all hold and 1 otherwise. `--enforce` runs the same check and always exits 0.
On a failure, or while `/etc/mosaic-stack/DRIFT` exists, it applies the stop:
`apparmor_parser -R` on the profile if loaded, then `chown root:root` and `chmod 0700` on
`/usr/bin/bwrap`. It writes the failed items to `DRIFT` and appends a line to
`/var/log/mosaic-stack/hostcheck.log`. With bwrap closed to `mosaic`, every role session refuses
to start. That's the fail-closed state, and the stack needs no change for it. The chmod is undone by
the next bubblewrap upgrade, which reapplies the statoverride. But the apt hook runs again right
after that upgrade and finds `DRIFT`, so it closes bwrap again. Only root removes `DRIFT`, in a
reviewed step with a new pin or a Mos go. I don't call `dpkg-statoverride` from inside the hook,
because I haven't confirmed it can take dpkg's lock while apt holds the frontend lock.
- Three callers. A1 of every re-pin runs `--check` and refuses `profile-drift` on exit 1. The apt hook
is `DPkg::Post-Invoke { "/usr/local/lib/mosaic-stack/case1-hostcheck.sh --enforce || true"; };`, so it
runs after every package run, unattended ones included, and can't fail one. The boot unit,
`mosaic-hostcheck.service` (`Type=oneshot`, `After=apparmor.service`,
`Before=mosaic-bus@mosaic-stack.service`, `WantedBy=multi-user.target`), runs `--enforce` before the
bus starts, which covers a kernel or parser change that takes effect at boot.
- Owner. ops-01 owns the host side under a Mos go. It reads `hostcheck.log` in each STATE report and
sends any `DRIFT` to Mos. Before each re-pin window it also runs `--upstream`, which downloads the
current `apparmor-profiles` candidate into a temp dir and compares its `bwrap-userns-restrict` to the
pin. A difference goes to Mos as a new-pin request and refuses nothing. The pins and the scripts are
this row's, and later changes go through review like any other.
Who runs what:
| Step | Runs as | Through |
|---|---|---|
| 2a: `useradd --system --user-group --home-dir /srv/mosaic --shell /usr/sbin/nologin mosaic`, `/srv/mosaic` 0700, `/etc/mosaic-stack` `root:root` 0755, `/var/log/mosaic-stack` `root:root` 0750 | root | ops-01, `ssh localadmin@10.1.1.53`, `sudo` (#423 step 1) |
| 2a': node under `/opt/mosaic-stack`; bubblewrap, the AppArmor profile, the statoverride, the drift stop and the managed settings file, each with the test and rollback above; the no-new-privs test | root | ops-01, same path, R27 (mos-1091) |
| 2a'': a `git bundle` of the reviewed commit copied to .53; root clones it to `/root/mosaic-stack` and checks HEAD is the commit the go names; it installs the bundle as `/srv/mosaic/incoming/<sha>.bundle`, owner `mosaic`, 0600 | root | ops-01, same path |
| 2b: claude under `~/.local` (above); `git clone` of the bundle to `/srv/mosaic/src/mosaic-stack`; `scripts/bootstrap.sh`; `stack-copy.sh copy`; the install with the inputs above (C2-13b's `case2-install.sh --profile factory` if it has landed, else the run sheet's steps) | `mosaic` | `sudo -u mosaic -H env PATH=...` from `localadmin` (PATH above). `mosaic` gets no key and no login shell |
| 2b': place `claude-session.env` | Jason, or who he names (Q173 secrets rule) | section 3 |
| 2c: write `case1-unit.args`; `case2-unit.sh` with those arguments and `--copy <id>`, from `/root/mosaic-stack`; then `case2-precheck.sh --profile factory` as `mosaic`, which reads the Console's `--port` from the installed unit for its port-floor line, so it runs after the render; `systemctl enable` the bus unit; the first start of UNITS. The Console unit has no `[Install]` until Jason rules (C2-13a), so it isn't enabled, and after a host reboot root starts it by hand until then. The run sheet says so | root | ops-01, same path |
| 2c': `stack-release.sh adopt mosaic-stack --units external`, which records the running copy as the pointer (`do_adopt`, `scripts/stack-release.sh:856`) | `mosaic` | `sudo -u mosaic -H` |
| every later re-pin or rollback | root, with its `mosaic` steps through `sudo -u mosaic -H` | ops-01 runs `case1-repin.sh` (section 4) |
| acceptance (section 5) | `localadmin`, with `sudo -u mosaic -H` for the in-instance checks | the agent the window names |
No git credential lands on .53. Both checkouts come from bundles that ops-01 copies in, and a re-pin
brings a new bundle that `mosaic` fetches into its checkout.
## 2. Where Case 1 differs from Case 2
| Case 2 item (row 230 brief) | Case 1 | Reason |
|---|---|---|
| business file with the library mapping | business `mosaic-stack` with no `library` key; `--profile factory` refuses a `kind: "library"` project (`profile-library-business`) | the factory has no books repo |
| `bean`, `chet`, persona vars and files, specialists | factory roles from `roles/`, with a chief that has a harness var | Jason names the set in the go. I haven't read his live business file and won't. The acceptance needs only the chief (F3), since a chief with no harness var answers `no-route`. Others are added later by the human with `instances.set`, no new row |
| token slot `library-gitea` | none | the factory's per-agent git identity is row 199's credential store (Megavolt, #1697). Until it lands, git work stays on dragon-lin (Q182) |
| precheck: `bus.tester` is not on | `bus.tester` is on; `factory` requires it | the acceptance and the Q173 test system need the tester (agents act as tester, never Jason) |
| precheck: persona file modes and digests | skipped under `factory` | no persona files |
| precheck: `library.sync`, `library.status` | skipped under `factory` | no library |
| precheck: `ZAI_API_KEY` in the key file | doesn't run | no instance has provider `zai` |
| precheck: no Discord binding | kept | the factory's Discord connector stays on dragon-lin. Two connectors on one bot token would split its events |
| precheck: claude marker prompt with `--system-prompt-file` | runs, since the chief's harness is claude-code | |
| precheck: `sudo -n true` fails, `id -G` one entry, run root 0700, config validates, node 24 or later, `bwrap` exits 0, free memory, HOME | kept | node and bwrap are in row 230 (gosalyn-0034) |
| Console serve flags: `--port` 1..1023, `--business`, the preview trio, `--send`, the proof pair, no `--tester-port` | `--port 732`, `--business mosaic-stack`, `--tester-port 7333`. No preview trio, no `--send`, no proof pair | below |
| bus: `@PM_FLAGS@` and `@CLAUDE_ENV@` render empty | ` --pm`, and `Environment=MOSAIC_CLAUDE_BIN=/srv/mosaic/.local/bin/claude` | below |
All of these are inputs. Nothing in the Case 2 scripts hardcodes a Case 1 value (gosalyn-0034 item 4).
Row 230's brief says the factory profile takes the library serve set plus an optional `--tester-port`,
and that row 195 names any other difference (gosalyn-0049). .53 can't meet the library set, and the bus
needs two lines the brief renders empty. The factory profile I need from row 230:
1. Serve flags. `--port N` with N in 1..1023 and `--business ID` are required, and `--tester-port N` in
1024..65535, not equal to `--port`, is optional. The preview trio (`--public-host`, `--auth-header`,
`--allow-user`) and the proof pair (`--proof-header`, `--proof-file`) are refused. Both exist for a
proxy in front of the Console: the proxy signs the user in and adds the header, and the proof shows
the request came through it. .53 has no proxy. Jason reaches the Console with `ssh -L` (sage-1451
Q3), so there is no header to trust and no proxy to hold the proof. `--send` is refused too, since
serve takes it only with the trio or `--sso` (`packages/webui/src/cli.mjs:15`). When Q175 SSO reaches
.53, `--sso` with an optional `--send` (row 192) would be the factory's sign-in, and adding it is a
re-install with its own go. Row 230 can accept `--sso [--send]` under factory now or later. Either
way it needs a `console.sso` block in `mosaic`'s config, which this row doesn't write. `--checkout`
and `--board` stay refused, as in the library set.
2. `--pm` (factory only) renders the bus unit's `@PM_FLAGS@` as ` --pm`. The factory's PM dispatches its
work on .53 as on dragon-lin (section 4). With the token empty the bus never launches the PM, and
A5's PM check fails every re-pin.
3. `--claude PATH` (factory only) renders `@CLAUDE_ENV@` as `Environment=MOSAIC_CLAUDE_BIN=PATH`, the
line `scripts/bus-service.sh:79` renders today. PATH must be absolute. Without it the bus runs
`claude` from the unit's PATH, which is the node dir, `/usr/local/bin`, `/usr/bin` and `/bin`, and
holds no claude on .53. The chief's first launch would then fail. The precheck's claude marker
prompt should run the same binary, so it reads the path from the installed bus unit.
4. Row 190's two template tokens, if row 190 lands before row 230. Row 190 adds `@CONFIG_ENV@` to both
templates and `@SESSION_ENV@` to the bus template's `EnvironmentFile=` line. For the default
instance, `@CONFIG_ENV@` is deleted (`scripts/instance.sh`) and `@SESSION_ENV@` is
`<home>/.config/mosaic-dev/secrets/claude-session.env`. Row 230's "every token has one value" list
and its `EnvironmentFile=` check need both. This applies to the library profile too.
I've sent these to Gosalyn. If row 230's candidate lacks any of them, row 195's manifest names the
change to `case2-unit.sh` or `case2-precheck.sh`, reviewed by Duckworth with Webby told.
## 3. Model access
Q185: Case 1 uses the account the factory uses today, the Claude subscription behind dragon-lin's
`claude-session.env`. The install never copies that file from dragon-lin. In the window, Jason (or the
person he names under the Q173 secrets rule) makes a credential for .53 from that account and writes it
to `/srv/mosaic/.config/mosaic-dev/secrets/claude-session.env` as `mosaic`, mode 0600. Nobody prints it,
and no script reads its value. The precheck checks only its mode, owner and that the variable name is
present (a `grep -c` on the name, output a count).
Quota: the factory on dragon-lin and on .53 share one subscription. Q185's "use existing" accepts that.
Case 2 shares nothing with it (Mos's reading of Q185).
The pi harness stays off on .53 until P19 #383 settles (mos-0865). The business file names claude only.
## 4. Re-pin, rollback and backup on .53
Unit form (ruled, sage-1451 Q1): system units that root writes, with `User=mosaic`. `mosaic` can't
start or stop them, and root never runs code that `mosaic` can write. `stack-release.sh activate` today
does everything in one run under `systemctl --user` and puts the old stack back inside that run
(`rollback_in_run`, `scripts/stack-release.sh:561`). On .53 the same run needs two users. So one
re-pin is one ordered list, and one root command drives all of it: `scripts/case1-repin.sh`, run by
ops-01 from `/root/mosaic-stack`. Each `mosaic` step is `sudo -u mosaic -H` running
`/srv/mosaic/src/mosaic-stack/scripts/stack-release.sh` with `--units external`. Copies carry no
`stack-release.sh` (`COPY_PATHS`, `scripts/stack-copy.sh:23-30`), so the `mosaic` steps run from that
checkout, as dragon-lin's run from the canonical one.
What root holds, and where each value comes from (r4's B1):
| Value | Source |
|---|---|
| ID, the target copy | the go |
| TO, a rollback's target | the go, as `--to ID` |
| `--restore ACT --events N` | the go, when the rollback restores |
| OLD, the running copy | the `ExecStart=` of the two unit files root installed. If the bus and Console units name different copies, A1 refuses `units-disagree` |
| the `case2-unit.sh` arguments | `/etc/mosaic-stack/case1-unit.args` (section 1) |
| each `mosaic` step's result | its exit code |
Root parses nothing `mosaic` wrote and runs nothing under `/srv/mosaic` except through `sudo -u mosaic
-H`. It never needs the activation id: the `mosaic` steps find it themselves.
One result line per activation (B1). The stack log closes an activation on the first line after
`started` that names it, whatever the result (`interrupted`, :146-156). r4 wrote an interim
`... rollback pending` line at A3's and A5's failures. That line closed the activation, so R2 and R4
then refused `open-activation` against an activation the log no longer held open. The rollback could
never run as designed, and nothing tested the steps in sequence. r5 keeps an activation open from A3's
`started` line until its single result line, and A5 or R4 writes that line:
| Activation ends at | Result line | Written by |
|---|---|---|
| A3 fails before its rollback set exists (marker or capture) | `refused code marker-failed` or `refused code capture-failed`, as `capture || refuse` does today (:987). Nothing has changed, so there is nothing to roll back | A3 itself, exit 3, after it removes the marker. Root runs A2f |
| A5 passes | `ok` | A5 |
| A3 (after capture), A4 or A5 fails, and the rollback passes | `failed code <c> rollback ok` for A3 and A4, `gate-failed code <c> rollback ok` for A5 | R4 |
| the rollback fails at R4's gate | `rollback-failed code <c> rollbackCode <r>` | R4 |
| R1, R2 or R3 fails, or the run dies | none. The activation stays open, and the next activate refuses `interrupted-run` (:603-606), which Jason or Mos settles as on dragon-lin | |
What the result line carries (r5's B1). `log_line` (:58) writes `op "$OP"`, and `from_log` (:169)
reads only lines whose op is `adopt`, `activate` or `rollback`. So `confirm` doesn't log under its own
op. Before A5's `ok` line or any line R4 writes, it sets OP, FROM and TO from the open activation's
`started` line, ACT to that activation, and BACKUP from the rollback set's `BACKUP` file, which A3
writes when `backup_store` returns. A re-pin's lines then read `op activate`, a rollback's `op
rollback`, each with the from, to and backup a dragon-lin run would log. `from_log previous` and
`from_log backup` read .53's log as they read dragon-lin's. `confirm` reads only the line A3 wrote for
that activation, which `interrupted` already names.
A failing `mosaic` step after `started` writes no log line. It writes its code to `FAILED` beside
`AWAITING-UNITS` and exits 1. R4 reads that code for the result line. Root passes R4 `--cause
A3|A4|A5`, the step it saw fail, from its own exit codes. A4 is root's own step and has no `FAILED`
file, so R4 writes `failed code units-start` for it. A cause with no `FAILED` file where one should be
writes code `unrecorded`.
The open activation. A3 writes `$STATE/stack-rollback/<ACT>/AWAITING-UNITS` holding the target copy
(renamed from r3's `PENDING`, which read like the pointer's `pmPending`, `P_PENDING` at :128; N3).
`confirm` and `revert` take the copy root passed, not an activation id. Each lists the
`AWAITING-UNITS` files and the `interrupted` set (:146), and goes on only when there is exactly one
file, its activation is the one `interrupted` names, and the copy in it is the copy root passed.
Anything else refuses `open-activation` with exit 3 and the list it found, and changes nothing.
`confirm` without `--rollback` also refuses `activation-failed` when `FAILED` exists, so a second
confirm can't pass an activation that already failed. Whichever `confirm` writes the result line
removes `AWAITING-UNITS` and `FAILED` after it.
Refusals of the three external verbs. `confirm`, `revert` and `gate` act on an open activation by
design, so they skip the `interrupted-run` refusal (:605). Their refusals log `result refused` with no
activation id: they set ACT only after every check has passed. So a refused `confirm`, a wrong copy
say, leaves the open activation open. Today's `refuse` (:62-67) adds the activation id whenever ACT is
set, and the external verbs keep that order. A test and a mutant hold it (section 7).
Root's step log. Every step `case1-repin.sh` runs appends one line to `/var/log/mosaic-stack/case1-repin.log`,
`root:root` 0640: the time from `date -u`, the step, its argv, its exit code. `mosaic` can't write it,
so it stays readable after a run that dies before `mosaic` logs anything.
### Activate: `case1-repin.sh activate ID [--migrate] [--timeout S]`
| # | Step | Runner | On failure |
|---|---|---|---|
| A1 | `/root/mosaic-stack` HEAD is the commit the go names. ID is 40 lowercase hex and names a copy under `/srv/mosaic/.local/share/mosaic-stack/copies` that holds `scripts/mosaic` (row 230's copy-id rule). `case1-hostcheck.sh --check` exits 0, else `profile-drift` (B2). Root reads OLD from the unit files. `stack-release.sh activate ID mosaic-stack --units external --plan --expect-from OLD` exits 0. The plan refuses `pointer-differs` when the pointer's copy isn't OLD (N5), so a pointer and units that disagree stop before any stop | root; the plan as `mosaic` | refuse, exit 3. Nothing has changed |
| A2 | `systemctl stop` UNITS in reverse order, then wait until each is inactive, within `--timeout` | root | A2f below |
| A3 | `stack-release.sh activate ID mosaic-stack --units external [--migrate]`. Before it logs `started` (N3), it refuses `units-running` unless each of UNITS is inactive (system-manager reads, which need no privilege) and the store is stopped (`stopped`, :359), and `interrupted-run` while any activation is open. Then, in this order: logs `result started`; makes `$RS` with capture's `mkdir -p` of `stack-rollback` and exclusive `mkdir -m 0700` (:415-416), and writes `AWAITING-UNITS` in it at once (N1); captures the rollback set into that directory, making none (pointer, wrapper, and the sha256 and `ExecStart=` of each installed unit, read only); takes `backup_store` (:372) into `$DATA/backups/stack/<ACT>` and writes its name to `BACKUP` in the rollback set; under `--migrate`, writes `MIGRATING` in the rollback set and sets TAKEN, both before `migrate-schema.mjs` runs, where today's run sets TAKEN (:1004-1005, r5's B2), then migrates; installs the wrapper for ID. It writes no pointer | `mosaic` | a refusal before `started` exits 3 with nothing changed. A marker or capture failure, an existing `$RS` included, removes the marker if it wrote one, logs its `refused` line and exits 3. Root runs A2f for both. Any later failure writes `FAILED` with its code, logs nothing, cleans up nothing and exits 1. Root runs R1-R4 with `--cause A3` |
| A4 | `case2-unit.sh` with `case1-unit.args` and `--copy ID`, `systemctl daemon-reload`, `systemctl start` UNITS in order | root | R1-R4 with `--cause A4` |
| A5 | `stack-release.sh confirm ID mosaic-stack --units external --timeout S`. This is `gate` (:522), which polls `gate_once` (:471) every second until `--timeout`, with the external checks below. On a pass it writes the pointer, logs `result ok`, removes `AWAITING-UNITS`, exit 0. When the timeout runs out it writes `FAILED` with the last gate code, logs nothing and exits 1 | `mosaic` | R1-R4 with `--cause A5` |
Why A3 writes the marker first and does no cleanup of its own. With the marker written last, as in r4,
an A3 failure after `started` left an open activation with no marker, and R2 and R4 refused it. With
the marker first, every A3 failure after the marker leaves exactly what R2 needs: the open activation,
its marker and whatever part of the rollback set exists. R2 is then the one cleanup path, for A3, A4
and A5 alike. Two cleanup paths, A3's own and R2, would each need their own tests for each failure point.
A2f, a stop that fails or times out, or an A3 that exits 3. This run has no open activation: A2 logs nothing, and
A3's exit 3 comes either before `started` or after its own `refused` line. Nothing of `mosaic`'s has
changed. Root reads `date -u +%s` as T, runs `systemctl start` UNITS in order, then `stack-release.sh gate
OLD mosaic-stack --units external --since T --timeout S`, a new verb that runs `gate` on OLD and writes no
log line, no pointer and no file. A2f has no `started` line to take a start floor from (the first
re-pin after adopt has none at all), so `--since` is the floor, and `gate` refuses `usage` without it
(r5's N2). T is root's own clock, so root still parses nothing `mosaic` wrote. A unit that never
stopped keeps its old `ActiveEnterTimestamp`, fails `stale-start` and takes the exit 2 path below.
If that passes, root logs `A2 failed, OLD running` (or `A3 refused`) in its step log and exits
1. If it fails, root stops UNITS, logs `OLD gate failed <code>` and exits 2. The next run's
`interrupted` check finds nothing open either way.
The external gate. `gate_once` keeps its order and codes, with these changes under `--units external`:
- `systemctl` without `--user` for `is-active` and `show`. Both work unprivileged against the system
manager.
- This start's time is the bus unit's `ActiveEnterTimestamp`, read with `systemctl show --timestamp=unix
-p ActiveEnterTimestamp`, in place of the `START_MS` that `start_both` (:451) sets on dragon-lin.
It is whole seconds (`@1792540800`), and the `started` line has milliseconds (N4). So the rule
compares seconds: the start passes when its second is at or after the floor of `started`'s second.
A unit that was active across A3 can't reach this check, because A3 refuses `units-running`, so the
equal second can only be a start after A3. An earlier second fails `gate_once` with the gate code
`stale-start`. It's a gate failure like any other, not a refusal (r4 had it as a refusal, which
would have logged against the open activation): `gate` keeps polling, since `Restart=on-failure`
can bring a fresh start, and at the timeout `FAILED` holds `stale-start`. The PM check's
`startedAt` bound uses the same second.
- `copy-check` runs `stack-copy.sh check --units external`, which reads the system units.
- The PM check is below.
The PM (r4's B2). .53 runs the PM: the factory's PM dispatches its work, as it does on dragon-lin, and the
serve flags carry `--pm`. The pointer's `pm` comes from the installed bus unit's `ExecStart=`, as the
unit parse at :195 already reads it. Today's `pm_launched` (:505) takes the run id from the
`launched ...` line in `journalctl --user -u BUS_UNIT`. On .53 that read fails twice over: the unit is
a system unit, and under `SplitMode=uid` the logs of a system-range uid land in the system journal,
which `mosaic` (no `adm` or `systemd-journal` group) can't read. Under `--units external` the check
reads `launches list --json` alone. It passes when exactly one entry has business `mosaic-stack`,
instance equal to the business file's `launch.by` (the PM's instance, the one `bus start --pm` launches
at `packages/cli/src/launcher.mjs:675`), `live` true, and `startedAt` at or after this start's time.
Zero entries, two, a stale one or one for another business fail with `pm-launch-unconfirmed` and what
it saw, as today. A run the PM starts for another role has another instance, so it doesn't count.
Why not the other reads. A regular uid for `mosaic` would put its logs in a per-user journal it can
read, but it changes the account Case 2's install creates. Root reading the journal and passing the run id in
would make root parse output from `mosaic`'s process, which section 4's rule forbids.
Under `--units external`, `pm-on` (:1137) and `activate --pm-after-harness` refuse `external-pm`. Both
flip the PM on dragon-lin by restarting the user bus with new arguments. On .53 the PM's state is a
serve flag in a root-owned unit, so changing it is a re-install with its own go.
### Rollback inside the run (R1-R4)
| # | Step | Runner | On failure |
|---|---|---|---|
| R1 | `systemctl stop` UNITS, then wait as in A2 | root | rollback-failed |
| R2 | `stack-release.sh revert ID mosaic-stack --units external`. It finds the open activation as above, puts the wrapper back from the rollback set and checks its sha256, and restores TAKEN (`restore_store`, :396) only if the rollback set holds `MIGRATING`. Each part is idempotent, so it works for an A3 that stopped partway: no `MIGRATING` means no restore, a migrate that failed partway is restored from the backup taken before it, and copying the saved wrapper over an unchanged one changes nothing. It writes no log line | `mosaic` | rollback-failed, activation left open |
| R3 | `case2-unit.sh` with `case1-unit.args` and `--copy OLD`, `systemctl daemon-reload`, `systemctl start` UNITS | root | rollback-failed, activation left open |
| R4 | `stack-release.sh confirm ID mosaic-stack --units external --rollback OLD --cause A3\|A4\|A5 --timeout S`. It finds the open activation, checks OLD is its `from`, gates OLD with `gate`, and writes the activation's one result line from the cause and the `FAILED` code: `failed code <c> rollback ok` (A3, A4), `gate-failed code <c> rollback ok` (A5), or `rollback-failed code <c> rollbackCode <gate>` when OLD's gate fails | `mosaic` | rollback-failed, logged |
rollback-failed: root stops UNITS and exits 2, as `rollback_failed` (:587) does. When R4's gate fails,
the log has the `rollback-failed` line and the activation is closed. When R1, R2 or R3 fails, no
`mosaic` step writes a result, so the activation stays open with its marker. Root's step log has the
codes, the next activate refuses `interrupted-run` until Jason or Mos settles it (:605), and status
shows it. I chose that over a log-only verb for root to call after an R2 failure: nothing could gate
the old stack at that point, and a closed activation would let the next activate start against a
wrapper and store in an unknown state. Exit codes match `stack-release.sh`: 0 ok, 1 failed with the old
stack running (rolled back, or A2f), 2 rollback-failed with UNITS stopped, 3 refused with nothing
changed.
### Rollback to the previous copy: `case1-repin.sh rollback --to ID [--restore ACT --events N]`
The same list with TO in place of ID. The go names TO, from Mos's or Sage's read of `stack-release.sh
status` as `mosaic`. Root passes it. A1's plan, `stack-release.sh rollback mosaic-stack --units external
--to ID --plan --expect-from OLD`, refuses `pointer-differs` as in activate. It also exits 3 unless ID
is `from_log previous` (:162), the previous distinct copy, so a
stale or mistyped go refuses before anything stops. With `--restore`, the plan also refuses unless
`$DATA/backups/stack/ACT` exists. `--to` is a new flag, accepted only with `--units external`;
dragon-lin's rollback keeps reading TO from the log.
The post-backup event count runs in A1's plan as `mosaic` while the bus still holds the store (the
`heldcount` read, :1074), and again in A3 on the stopped store. A difference refuses
`post-backup-events`, as `run_steps` does (:1013).
### Migrations (N4)
`--migrate` under `--units external` runs only inside A3, and A3 refuses `units-running` before it logs
or migrates anything while any of UNITS is active. So a migration never runs against a store a live bus
holds. The test is in section 7.
### Backup
A3's `backup_store` is today's. It copies the stopped store into `$DATA/backups/stack/<ACT>`, so
`rollback --restore <ACT>` and row 181's guard find it. As on dragon-lin, the config dir isn't in the
backup, and the credential isn't either. PBS backups of VM 1008 cover the disk (P4). A restore swaps
only `bus/` and leaves `secrets/claude-session.env` in place (N6, in the run sheet).
The install's adopt (2c') takes no backup. The store holds only what the acceptance wrote, and the first
re-pin's A3 takes the first backup.
### The log
Every `mosaic` line goes to `$DATA/state/stack-activation-log.jsonl`, as today, with `units external`
added. So `from_log previous` reads .53's history the same way it reads dragon-lin's. A3 writes
`started`. A5 or R4 writes the one result line, with the op, from, to and backup of that activation,
or A3 its `refused` line before any change. Refusals of
`confirm`, `revert` and `gate` log no activation id. If a run dies between `started` and the result,
the log keeps `started` with no result, the next run's `interrupted` check reports it, and root's step
log shows the last step that ran.
### Who
Mos or Sage plans each re-pin. ops-01 runs `case1-repin.sh` on Jason's or Mos's go, and a STATE report
follows. The gate and the in-run rollback have one owner, the root command, as they do on dragon-lin.
### dragon-lin's path
`--units external` is a new value. Without it, `stack-release.sh` and `stack-copy.sh check` run as they
do today, `pm_launched` still reads the user journal, and rollback still takes TO from the log.
`confirm`, `revert`, `gate` and `--to` refuse without it. Section 7 names the tests that show this.
### Not taken
- Row 190's user units under `mosaic`, with root running `loginctl enable-linger mosaic` once. Every
existing re-pin tool would work unchanged and root would be needed only once. But the unit form
would differ from Case 2, and a user manager would run as `mosaic`.
- A sudoers rule that lets `mosaic` start and stop the units. P1 and row 230's precheck require that
`mosaic` has no sudo.
- `confirm` and `revert` taking ACT from root. Root would have to read it from `mosaic`'s output or
log, which the rule above forbids (r4's B1).
## 5. Acceptance checks
A new script, `scripts/case1-accept.sh`, in this row's manifest. It runs on .53 as `localadmin` and prints
one line per check: `<id> <path> <mark> <evidence>`. The marks are `closed`, `accepted Q182`,
`accepted Q183`, `accepted mos-1096`, and `unnamed`, which means "not named in Q182, Q183 or a Mos
ruling". The script ends with a count of each mark. It exits non-zero only when a functional check (F)
fails. A P check never changes the exit status. Each `unnamed` line goes to Mos, who takes it to Jason
(R8 N1).
Functional checks. Any failure fails the row:
| Id | Check | How |
|---|---|---|
| F1 | the units run as `mosaic` | `systemctl show <unit> -p User -p MainPID` for each of UNITS; each MainPID's uid in `/proc/<pid>/status` is `mosaic`'s, and each unit is active |
| F2 | the Console listens on 127.0.0.1:732 on .53 and answers there | `curl -fsS http://127.0.0.1:732/healthz` on .53 succeeds, and the listener table shows 732 bound to 127.0.0.1 only, by the Console unit's MainPID. Jason reaches it with `ssh -L 7332:127.0.0.1:732 localadmin@10.1.1.53` and his browser at 127.0.0.1:7332 as today. A LAN listener waits for Q175 SSO (sage-1451 Q3) |
| F3 | one tester message gets a reply | as `mosaic`: `mosaic talk chief <marker> --tester`, then `mosaic follow --tester`, with a 300 s limit. A stored reply to the marked message passes. The inbox alone shows decisions only, so it isn't used (N3) |
| F4 | the precheck passes | `case2-precheck.sh --profile factory` exits 0 as `mosaic` |
| F5 | a role session runs under bwrap with the profile | while F3's reply is pending: a `/usr/bin/bwrap` process owned by `mosaic` has a claude process under it. `/proc/<bwrap pid>/attr/current` equals `bwrap (enforce)` and the claude process's equals `bwrap//&unpriv_bwrap (enforce)`, each compared as an exact string after the trailing newline is cut (N1). If `launches list --json` records the sandbox, the build reads that for the first part, since a short session can end before `ps` sees it |
| F6 | claude is the pinned version | `/srv/mosaic/.local/bin/claude --version` as `mosaic`, after F3, equals the run sheet's pin |
| F7 | the PM run is live | `launches list --json` as `mosaic` has exactly one live entry for the PM's instance, the check `confirm` runs (section 4) |
Privilege and host paths. These report and never fail the row:
| Id | Path | Mark | Evidence the script prints |
|---|---|---|---|
| P1 | `mosaic` has sudo | closed only when `sudo -l -U mosaic` prints "is not allowed to run sudo" (N2); any other output is unnamed | the `sudo -l` result |
| P2 | `mosaic` has supplementary groups or a key | closed when `id -G mosaic` is one entry and `~mosaic/.ssh/authorized_keys` is absent or empty | `id`, file state |
| P3 | `localadmin` NOPASSWD sudo, reached with the dragon-lin key | accepted Q183 (agents keep ssh to .53; localadmin from #423 step 1) | `sudo -l -U localadmin` |
| P4 | root on pve over VM 1008, and PBS backups of VM 1008, which hold `/srv/mosaic` including the credential | accepted Q182 and Q183 (agents keep root on pve and PBS) | a fixed line; the script can't see pve from inside |
| P5 | `jarvis`, which runs Hermes, can reach `/srv/mosaic` through sudo, or through the docker or lxd groups, which are root-equivalent | closed when `jarvis` is in none of sudo, docker and lxd; unnamed otherwise. #423 step 4 lands before the install (mos-1091 4c), and its `step4.sh` removes `jarvis` from sudo, docker, lxd and adm, so after it this reads closed (Duckworth N2) | `id jarvis` |
| P6 | other members of sudo, docker, lxd | closed when sudo holds only `localadmin` and docker and lxd are empty. Every other member is its own unnamed line, `jarvis` included (N1) | `getent group` lines |
| P7 | a file under `/etc/sudoers.d` naming `mosaic` | closed when none | the file names |
| P8 | the Console or tester reachable from the LAN | closed when 732 and 7333 are bound to 127.0.0.1 only, each by the Console's MainPID, and nothing listens on 7331 | listener table |
| P9 | a user namespace through `/usr/bin/bwrap` | accepted mos-1096 only while all seven hold, unnamed otherwise: (1) `dpkg-statoverride --list /usr/bin/bwrap` reads `root mosaic 0750`; (2) `aa-status --json` lists `bwrap` and `unpriv_bwrap` in enforce mode; (3) `/etc/apparmor.d/bwrap-userns-restrict` has the pinned sha256 and no package owns it; (4) `sudo -u jarvis /usr/bin/bwrap --version` fails with "Permission denied"; (5) `getent group mosaic` lists no member and no other account in `/etc/passwd` has its gid; (6) `fs.protected_hardlinks` is 1; (7) no `mosaic-bwrap` profile is loaded, no other file under `/etc/apparmor.d` names `bwrap`, and the live label of a process bwrap runs is exactly `bwrap//&unpriv_bwrap (enforce)` (N1, mos-1098 point 2). Items 1-3, 6 and 7 are `case1-hostcheck.sh --check`'s, so P9 reuses its lines | each item's line, and Mos's reason: "Case 1's agents are the factory's agents. In Q182 and Q183 Jason accepted that the factory reaches root on pve and, through it, every VM. So a user-namespace path to root on .53 gives that class no new reach." With the profile, what's left is bwrap's own setup, run by `mosaic` |
| P10 | a session binds the tester port, 7333, while the Console is down | accepted mos-1101 while `ip_unprivileged_port_start` is above 732 and 7333 is held by the Console's MainPID at the check. Serve refuses a tester port below 1024 (`cli.mjs:33`), so the Console-port fix can't cover it. A squatter would get the tester's writes and could answer them. The tester's users are the factory's agents, the class Mos's reading covers (mos-1101 item 5). Mos can object, and then this line reads unnamed | the floor, and the owner pid of 7333 |
Tests, in `packages/cli/tests/case1-accept.test.mjs`. The script takes a fake root (`--root DIR`) for
`/proc`, `/etc` and `getent` output, and fake `systemctl`, `curl`, `ss`, `sudo`, `bwrap`, `aa-status`,
`dpkg-statoverride`, `sysctl` and `claude` on PATH. Cases:
- each F check failing gives a non-zero exit with its line;
- each P check in each of its marks, including P1 with output that grants nothing but lacks the refusal
text (unnamed), P9 unnamed once for each of its seven items missing, and P10 unnamed with a floor of
700 and with 7333 held by another pid;
- F5 fails on a label of `unpriv_bwrap (enforce)`, `bwrap//&unpriv_bwrap (complain)` and
`bwrap (enforce)` with a trailing space;
- a run with every P check `unnamed` still exits 0;
- the summary counts match the lines.
Mutants: one per F check (the check made to pass on failure), one that maps P5 to `closed`, one that
closes P1 on any output without a rule line, one that drops P9's jarvis item, one that drops P9's item
7, and one that makes F5 a substring match.
Nothing in the tests reaches .53 or dragon-lin's live state.
## 6. Rulings
sage-1451:
- Q1, unit form: the brief's system unit. C2-13's `ExecStart=` check accepts a varying copy id, and
C2-13 took that (gosalyn-0034 item 3).
- Q3, the Console: it listens on 127.0.0.1 on .53 (`packages/webui/src/serve.mjs:466`; `cli.mjs` passes no
host), and Jason reaches it with `ssh -L`. F2 says so. A LAN listener waits for Q175 SSO.
- Q4, the date: if C2-13's candidate is late, I build on C2-13's latest commit and rebase (Q177). Under
the #426(a) push rules (mos-1086) a rebase goes to a new branch name.
mos-1091 (none goes to Jason):
- Q2 4a: installing bubblewrap is an install step, run by ops-01 as root under R27. Rollback is
`apt-get remove`.
- Q2 4b: a per-binary AppArmor profile for `/usr/bin/bwrap`, the sysctl left at 1. r4 takes Ubuntu's
`bwrap-userns-restrict` for it (mos-1096). Section 1 has the steps.
- 4c: P5 ends at #423 step 4, which Mos sent to Duckworth so it lands in Jason's VM 1008 boot session,
before the install. The P5 check stays.
mos-1096:
- B1 and B2 are mine, and so is the PM answer: .53 runs the PM (section 4).
- The `dpkg-statoverride` on `/usr/bin/bwrap`, the hardlink and group checks, the jarvis refusal and F5
under the profile (sections 1 and 5).
- P9 is Mos's acceptance, with the reason quoted in section 5.
- Jason's Q9 on install and upgrade is open. r4 goes ahead, since B1's re-pin and rollback are needed
whatever he answers. If his answer changes how upgrades reach .53, the change lands in section 4 and
the run sheet as a new round.
mos-1098 and mos-1101 (on Duckworth's REVIEW-r3):
- Duckworth's CHANGES stand. B1 and B2 are fixed in this round, with B1's real-sequence test, and all
three of B2's asks: an owner across upgrades, what fails closed, and the pins as reviewed values.
- r4's PM, statoverride and P9 stand. mos-1098's two questions are answered in section 1: the profile's
owner across upgrades is ops-01 with `case1-hostcheck.sh`, and the stop is bwrap closed to `mosaic`.
Both profiles attach to `/usr/bin/bwrap`: `bwrap` to the binary and `unpriv_bwrap` stacked on what it
runs. P9 item 7 checks that no third one does.
- P9 has seven items and F5 compares exact strings (N1).
- The port squat applies (section 1). The Console takes Case 2's fix, a port below 1024 with
`CAP_NET_BIND_SERVICE` and the floor checked. Only the tester port can't take it, and P10 records it
under Mos's reading that Case 1's sessions are the factory class. I've told Mos, who can object.
- Work on r5 goes ahead while Jason's Q9 (Vikunja #429) is open. If install and upgrade become
product commands, these steps become that installer's configuration and row 195 rebases on its row
(sage-1481, sage-1491).
sage-1499: C2-13a is row 230 (#1732, Gosalyn), and row 195 chains on row 230's candidate.
gosalyn-0049: the factory profile is the library set plus an optional `--tester-port`, and row 195 names
any other difference. Section 2 names four, with the reason .53 can't meet the preview trio, `--send` or
the proof pair. The managed-settings route needs no Case 2 change.
## 7. Manifest (planned)
- `scripts/case1-repin.sh` (new) and `packages/cli/tests/case1-repin.test.mjs` (new). Fake `systemctl`,
`sudo` and `case2-unit.sh` on PATH record their argv, with a scratch home standing in for `/srv/mosaic`,
a scratch dir for `/etc/mosaic-stack` and one for `/var/log/mosaic-stack`. Cases:
- a pass runs A1-A5 in order;
- a failure at A3, A4 or A5 runs R1-R4 with `--cause` set to that step, and exits 1;
- an A3 exit 3 runs A2f, not R1-R4;
- a failure at R2 or R4 exits 2 with UNITS stopped;
- A1 refuses `profile-drift` when a fake `case1-hostcheck.sh --check` exits 1, before any stop;
- every `mosaic` step's argv carries `env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin`, with the node
dir from `case1-unit.args`;
- a HEAD mismatch, an ID that fails the copy-id rule, or unit files that name two copies refuses with
exit 3 before any stop;
- A1's plan argv carries `--expect-from` with OLD from the unit files;
- A2f passes `gate OLD --since T`, with T from a fake `date` that root calls before its start;
- A2f: a fake `systemctl` that never reports inactive. The script starts UNITS, runs `gate OLD`, exits
1 with OLD's gate passing and 2 with it failing, and the stack log is byte-identical before and
after;
- `rollback --to` with an ID that isn't `from_log previous` exits 3 before any stop;
- every exec of a path under the home goes through `sudo -u mosaic -H` (from the recorded argv);
- no argv root passes holds an activation id, and OLD comes from the unit files, not from anything
the `mosaic` steps print;
- every `case2-unit.sh` call carries the arguments in `case1-unit.args` and differs only in `--copy`;
- the step log has one line per step with its exit code.
- `scripts/stack-release.sh`: `--units external` for `adopt`, `activate`, `rollback` and `status`, the
`--to` flag for an external rollback, and the new `confirm`, `revert` and `gate` verbs, which refuse
without it. `scripts/stack-copy.sh`: `check --units external`, which reads the system units. Tests in
`packages/cli/tests/stack-release-external.test.mjs` (new), against a fake system-scope `systemctl`
and a fake `mosaic` for `bus status` and `launches list`:
- `units-running` refuses while any unit is active, and `activate --migrate` with a unit active
leaves the store, the log and TAKEN as they were (N4);
- activate leaves the pointer as it was, writes `AWAITING-UNITS`, and writes a backup of the stopped
store under `$DATA/backups/stack/<ACT>`;
- `confirm` and `revert` refuse `open-activation` with zero `AWAITING-UNITS` files, with two, with
one whose copy isn't the one passed, and with one whose activation isn't in the `interrupted` set;
- `confirm` polls: a fake bus whose status turns good after 3 s passes with `--timeout 10` and fails
with `--timeout 1`;
- `stale-start` (N4): with `started` at 1792540800.900, an `ActiveEnterTimestamp` of `@1792540800`
passes and `@1792540799` fails `gate_once` with `stale-start`. A fake that turns to `@1792540805`
after 2 s passes with `--timeout 10`, which shows it's a gate code that polling can clear, not a
refusal;
- `confirm` on a pass writes the pointer, logs `ok` and removes `AWAITING-UNITS`. On a timeout it
writes `FAILED` with the gate code, leaves the pointer, the log and the marker as they were, and
exits 1;
- `confirm` without `--rollback` refuses `activation-failed` when `FAILED` exists;
- every line `confirm` writes has op `activate` or `rollback`, never `confirm`, and the from, to and
activation of its `started` line, and backup equal to the rollback set's `BACKUP`;
- `gate` (N2): with `--since 1792540800`, `@1792540800` passes and `@1792540799` fails `stale-start`;
without `--since` it refuses `usage`;
- `--plan --expect-from` (N5) refuses `pointer-differs` when the pointer names another copy, for
activate and for `rollback --to`;
- A3's order: a fake that fails the capture leaves the log with `started` and `refused code
capture-failed` for that activation, no marker and no `FAILED`, and exit 3. An existing `$RS`
gives `refused code marker-failed` and exit 3, and a capture under `--units external` makes no
directory of its own. A fake that fails the
wrapper install after a migration leaves `started` only, the marker, `FAILED`, TAKEN as migrated,
and exit 1;
- refusals by `confirm`, `revert` and `gate` log with no activation id, and the `interrupted` set
is the same before and after each one;
- the PM check passes with one live entry for the PM's instance since the start, and fails
`pm-launch-unconfirmed` with none, with two, with one whose `startedAt` is before the start, with one
not live, and with one for another business. A second live entry for another instance doesn't
count. A fake `journalctl` on PATH records any call, and no external run makes one;
- `pm-on` and `activate --pm-after-harness` refuse `external-pm`;
- `revert` puts the wrapper back, and restores TAKEN only after a migration;
- `confirm --rollback OLD` writes the line for each `--cause` (A3 and A4 `failed`, A5 `gate-failed`,
with the `FAILED` code, `units-start` for A4, `unrecorded` when the file is missing), or
`rollback-failed` when OLD's gate fails. It removes the marker and `FAILED` after the line, and
refuses when OLD isn't the open activation's `from`;
- real sequences on one log (B1). The fake `sudo` execs the real `stack-release.sh` as the test's
user, so each step reads the log and state the last one wrote. Fakes stand in only for
`systemctl`, `mosaic` and `case2-unit.sh`. Each sequence ends with the log holding exactly one
`started` and one result line for its activation, an empty `interrupted` set, no marker or
`FAILED`, and a following `activate --plan` that doesn't refuse `interrupted-run`:
1. activate, confirm times out, revert, `confirm --rollback --cause A5` passes:
`gate-failed code <c> rollback ok`;
2. activate with `--migrate`, the wrapper install fails, revert restores TAKEN, `confirm --rollback
--cause A3`: `failed code <c> rollback ok`;
3. activate, root's A4 fails, revert, `confirm --rollback --cause A4`: `failed code units-start
rollback ok`;
4. activate whose capture fails: `refused code capture-failed` and nothing open, then A2f's
`gate OLD` writes nothing;
5. activate, then `confirm` with the wrong copy is refused, then revert and `confirm --rollback
--cause A5` still close the activation, because the refusal didn't;
6. activate, revert fails (a wrapper whose saved copy is missing): the activation stays open, and
the next `activate --plan` refuses `interrupted-run`;
7. adopt C0, activate C1 and confirm, activate C2 and confirm (r5's B1): both `ok` lines read
`op activate` with their `started` line's from and to and the backup each took. `rollback --to C1
--plan` exits 0 and `--to C0 --plan` refuses. A rollback to C1, confirmed, logs `op rollback`, and
`from_log previous` then names C2;
8. activate with `--migrate`, where the copy's `migrate-schema.mjs` is a fake that writes to the
store and exits 1 (r5's B2): `FAILED` holds `migrate-failed` and `MIGRATING` exists. revert
restores the store byte-identical to the backup, and `confirm --rollback --cause A3` gates OLD on
it: `failed code migrate-failed rollback ok`;
- `rollback --to` refuses when ID isn't `from_log previous`, and `rollback --restore` finds a backup
that an external activate took;
- `gate` writes nothing: the log, the pointer and the state dir are byte-identical before and after;
- `confirm`, `revert`, `gate` and `--to` refuse without `--units external`.
- dragon-lin unchanged. `stack-release.test.mjs` and `stack-copy.test.mjs` run unchanged and green. A
new case in `stack-release-external.test.mjs` runs a default activate, rollback and check against a
fake `systemctl` that records argv, and every call carries `--user`. A default `activate --plan`
prints the same bytes on the candidate as on its base, with the base output as a fixture.
- Mutants for the re-pin:
- `confirm` writes the pointer on a gate failure;
- `confirm` calls `gate_once` once in place of `gate`;
- the external PM check counts entries of any instance;
- the external PM check drops the `startedAt` bound;
- `confirm` takes the newest `AWAITING-UNITS` instead of refusing two;
- `rollback --to` skips the `from_log previous` comparison;
- `case1-repin.sh` skips R1-R4;
- `case1-repin.sh` exits 0 after A2f;
- `case1-repin.sh` runs `mosaic`'s `stack-release.sh` without `sudo -u mosaic`;
- the default path drops `--user` from one `systemctl` call;
- `confirm`'s timeout writes r4's interim `gate-failed ... rollback pending` line (B1; killed by
sequences 1 and 5);
- A3 writes `AWAITING-UNITS` last, after the wrapper (killed by sequence 2);
- an external verb's refusal logs with the open activation id (ACT set before the checks; killed by
sequence 5 and the refusal test);
- `stale-start` is a refusal again (killed by the polling case);
- `revert` restores TAKEN without `MIGRATING` (killed by the revert case);
- `confirm` logs with its own op, `confirm` (killed by sequence 7 and the op case);
- A3 writes `MIGRATING` after `migrate-schema.mjs` (killed by sequence 8);
- `gate` takes its floor from the newest `started` line when `--since` is missing (killed by the
`gate` case).
- `scripts/case1-pins.sh` and `scripts/case1-hostcheck.sh` (new),
`packages/cli/systemd/mosaic-hostcheck.service` (new) and `packages/cli/tests/case1-hostcheck.test.mjs`
(new). The script takes `--root DIR` for `/etc`, `/proc/sys` and `/usr/bin/bwrap`'s stat, and fake
`aa-status`, `dpkg`, `dpkg-statoverride`, `apparmor_parser`, `systemd-run`, `chown` and `chmod`
on PATH record their argv. Cases:
- `--check` exits 0 with all seven items and 1 with each one failing alone, its line naming it;
item 3 fails on a second file naming `bwrap` as a word, one that writes `/{usr/,}bin/bwrap` included,
and passes with `bwrap-userns-restrict` alone. Item 4 fails on a label of `unpriv_bwrap (enforce)`
or one with a trailing space, and its `systemd-run` argv carries `-p NoNewPrivileges=yes` and the
other 2a' properties. Item 7 fails with the floor at 732;
- `--enforce` exits 0 on a pass and on every failure. On a failure it unloads the profile, closes
bwrap to `root:root` 0700, writes `DRIFT` and one log line. With `DRIFT` present and every item
passing, it still closes bwrap;
- `--enforce` never calls `dpkg-statoverride` with `--update`, `--add` or `--remove`;
- a bubblewrap version other than the install pin is a printed line, not a failure;
- `--upstream` against a fake deb whose file differs prints the difference and exits 0;
- the service unit has `Type=oneshot`, `After=apparmor.service`,
`Before=mosaic-bus@mosaic-stack.service` and runs `--enforce`.
Mutants: `--enforce` exits 1 on a drift; `--enforce` ignores `DRIFT` when the items pass; item 1
drops the `dpkg -S` owner check; item 3 matches only the `/usr/bin/bwrap` path; item 4 matches a
substring; item 4 drops `-p NoNewPrivileges=yes`; item 7 compares with `>=`.
- `scripts/case1-accept.sh` (new) and `packages/cli/tests/case1-accept.test.mjs` (new), section 5.
- Row 230 (C2-13a). `--profile`, the node, bwrap and port-floor precheck lines, the copy-id rule, the
two units and the Console's capability lines are in its brief at 5b8def2f1. Section 2's four items
are not. Any of them that row 230's candidate lacks becomes a named change here, reviewed by
Duckworth with Webby told. I told Gosalyn earlier that the settings-file route for
`DISABLE_AUTOUPDATER` doesn't reach a role session, and that Case 1 uses the managed settings file.
- Docs: the Case 1 run sheet `docs/guides/case1-install.md` (new). It lists the window's steps in order,
each with who runs it and its go, the pinned node sha256 and claude version, a pointer to
`case1-pins.sh` for the bubblewrap and profile pins, the statoverride, the drift stop and the managed
settings file, each with its rollback, how ops-01 clears a `DRIFT`, the `ssh -L` line for port 732,
and a note that a restore leaves the credential in place.
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, dragon-lin's units or `~/.mosaic`.
-287
View File
@@ -1,287 +0,0 @@
# Row 232 (I1): stack-release external units and bundle copies
Brief: `docs/plans/2026-10-20_installer-rows.md` section I1, blob `f35e7e57daee` (commit ad6f0be7a). Gitea
#1734, Vikunja P19 #432. Owner Darkwing, reviewer Filbert, gate owner Sage. Design input: row 195 DESIGN r6,
`agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66 (approved in duckworth-0055). Where the brief and
r6 differ, the brief wins. sage-1561 ruled where the stack user's `stack-release.sh` runs: from its own
clone of the release bundle, so `check_target`, the plan's commit list and status drift are unchanged and
nothing adds `safe.directory`.
## Files
Base: cb5fdb064 (row 199's landing, on top of row 200's 9e1085816). Branch `darkwing/row232-r1`. The first
build, 0af0c73c6 on `darkwing/row232`, sat on 00d4cc1f6 (row 190); row 200 landed first, so this one is
rebased onto it (see D9 and D10).
- `scripts/stack-release.sh`: `--units user|external`, the `sc` and `jctl` backend, `confirm`, `revert`,
`gate --since`, `rollback --to`, `--plan --expect-from`, the external gate (start floor, PM check from
`launches list`), `external-pm`, and the `awaiting units:` status line.
- `scripts/stack-copy.sh`: `check --units external`, `copy --bundle FILE SHA`, `wrapper ID --create`.
- `packages/cli/tests/stack-release-external.test.mjs` (new, 25 tests).
- `packages/cli/tests/stack-release-fixture.mjs` (new): the helpers `stack-release.test.mjs` defined, moved
so both files share them. See deviation D1.
- `packages/cli/tests/stack-release.test.mjs`: the helpers leave and one import line comes in. The 74 tests
have the same names and the same bodies as on the base.
- `packages/cli/tests/stack-copy.test.mjs`: three new `row 232:` tests and a wider `node:fs` import.
- `docs/TOOLS.md`: the stack-copy usage block and three paragraphs, the stack-release usage block, and a new
subsection "Root-owned units (`--units external`, row 232)".
- `packages/cli/README.md`: one paragraph.
- This file.
The worktree's `node_modules` symlink is not part of the candidate.
## User mode is unchanged
Every `systemctl` call goes through `sc` and every `journalctl` call through `jctl`. Under `user` they
are `systemctl --user "$@"` and `journalctl --user "$@"`. Under `external`, `sc` allows only `show`,
`is-active` and `list-units`, and `jctl` returns 1, so an external run can't start, stop, install or read
the journal even through a path the tests miss.
Two tests hold the brief's "every command line and every byte of output":
- `user mode: every systemctl call carries --user, and activate, rollback and check make the base's
calls`. A fixture repo gets the base's two scripts from `git show cb5fdb064:`. The same activate,
rollback and `stack-copy.sh check` run from both trees, and the recorded argv (with the fixture root and
port replaced by placeholders) must match line for line. Every call carries `--user`.
- `user mode: a default activate --plan prints the base's bytes`, for four plans.
The base output is produced live from the base blobs, not stored as a fixture file. See deviation D3.
## r6 section 7, test by test
All in `stack-release-external.test.mjs` unless named.
| r6 :742-812 item | Test |
|---|---|
| `units-running` (N4), store, log and TAKEN left | `units-running (N4) refuses while a unit is active...`; also checks that `failed` counts as down and that the Console is checked first |
| activate leaves the pointer, writes `AWAITING-UNITS` and a backup | `activate stages: ...`, which also checks the set's file list, `UNITS.sha256`, no `--user`, no journal call and `status` |
| `open-activation` with zero, two, the wrong copy, one not open | `confirm and revert refuse open-activation ...` |
| `confirm` polls: 3 s fails at `--timeout 1`, passes at `--timeout 10` | `confirm polls: bus status good after 3 s fails ...` and `confirm polls to a pass ...` |
| `stale-start` floor with `started` at .900 | `stale-start: @1792540799 fails ...` and `stale-start is a gate code ...` |
| `confirm` pass writes pointer, ok, removes marker; timeout writes `FAILED` and exits 1 | the two polling tests |
| `activation-failed` when `FAILED` exists | `confirm polls: ... FAILED then refuses activation-failed` |
| lines carry the started op, from, to, activation and backup | `sameAsStarted` in every confirm test, and sequence 7 |
| `gate` (N2) with and without `--since` | `gate (N2, A2f): ...` and the usage test |
| `--plan --expect-from` (N5), activate and `rollback --to` | sequence 7 |
| A3 order: capture-failed, marker-failed, wrapper failure after migrate | `A3 order: ...` and sequence 2 |
| refusals log no activation; `interrupted` unchanged | `open-activation` test and sequence 5 |
| PM check: one, none, two, early, not live, other business, other instance; no journal call | `the external PM check reads launches list ...` (modes '', coder, none, twolive, early, dead, other) and `journalCalls` in every external test |
| `external-pm` | `pm-on and activate --pm-after-harness refuse external-pm` |
| `revert` restores TAKEN only after a migration | `revert puts the wrapper back and leaves the store ...` and sequences 2 and 8 |
| `confirm --rollback` per cause, `unrecorded`, `rollback-failed`, mismatch | sequences 1, 2, 3, 5 and 8, and `confirm --rollback refuses rollback-mismatch, and logs rollback-failed ...` |
| sequences 1-8 | `sequence 1:` to `sequence 8:`, each ending with the `closed` check (one started and one result line, empty `interrupted`, no marker or `FAILED`, a following `--plan` that doesn't refuse `interrupted-run`) |
| `rollback --to` refuses not-previous; `rollback --restore` finds an external backup | sequence 7, and `confirm --rollback after a --restore swap ...` |
| `gate` writes nothing | the `gate (N2, A2f)` test and sequence 4 compare the log, the pointer and the state tree |
| `confirm`, `revert`, `gate`, `--to` refuse without `--units external` | `confirm, revert, gate, --to and --expect-from need --units external ...` |
| default path: every call carries `--user`; default `--plan` bytes | the two user-mode tests above |
The brief's additions, in `stack-copy.test.mjs`:
- `row 232: copy --bundle copies a SHA the bundle's refactor reaches, byte-identical to copy SHA, and
refuses the rest`: a short SHA, a missing file, a symlink, a bundle that fails `git bundle verify`, a
bundle with no `refs/heads/refactor`, a SHA not in the bundle, and a SHA the bundle carries on another
branch that refactor doesn't reach. The bundle copy's tree matches `copy SHA`'s byte for byte, and the
`release:` and `bundle sha256:` lines are checked.
- `row 232: wrapper --create writes the first wrapper only, the same bytes wrapper ID writes, and refuses
over any file`, a symlink included. Without `--create` a missing wrapper still refuses.
- `row 232: check --units external reads the system units, the default reads the user units`.
## Mutants
`mutants.sh` in my scratch (not committed, it edits the worktree), 17 mutants: r6's re-pin list (:817-838)
minus the three `case1-repin.sh` ones, plus the brief's two. Each one applies exact replacements after a
checked backup, runs `bash -n`, runs its named killers, restores the file and checks its sha256. Two
controls run first with no change: the whole external file, and stack-copy's `row 232:` tests.
| Mutant | Killers |
|---|---|
| m01 `confirm` writes the pointer on a gate failure | `confirm polls: bus status` |
| m02 `gate_once` in place of `gate` | `confirm polls to a pass`, `stale-start is a gate code` |
| m03 PM check counts any instance | `external PM check` |
| m04 PM check drops the `startedAt` bound | `external PM check` |
| m05 `confirm` takes the newest `AWAITING-UNITS` | `open-activation` |
| m06 `rollback --to` skips the comparison | `sequence 7:` |
| m07 the default path drops `--user` from one call (`is-active`) | `user mode: every systemctl` |
| m08 the timeout writes r4's interim `rollback pending` line | `confirm polls: bus status`, `sequence 1:`, `sequence 5:` |
| m09 `AWAITING-UNITS` last, after the wrapper | `sequence 2:` |
| m10 a refusal names the open activation | `sequence 5:`, `open-activation` |
| m11 `stale-start` is a refusal | `stale-start is a gate code` |
| m12 `revert` restores without `MIGRATING` | `revert puts the wrapper back` |
| m13 `confirm` logs op `confirm` | `sequence 7:`, `confirm polls to a pass` |
| m14 `MIGRATING` after the migration | `sequence 8:` |
| m15 `gate` takes its floor from the log without `--since` | `gate (N2` |
| m16 `copy --bundle` skips the reachability check | stack-copy `row 232:` |
| m17 `wrapper --create` overwrites | stack-copy `row 232:` |
## Deviations and choices
D1. The shared helpers moved to a new file, `packages/cli/tests/stack-release-fixture.mjs`. The brief's
files list names `stack-release.test.mjs` "where a shared helper moves" but not a destination. Importing
from the other test file would run its 74 tests twice, so the helpers need a module of their own.
D2. The fixture's T17 check (what a run may never remove) now exempts a rollback set's `AWAITING-UNITS` and
`FAILED`. `confirm` removes both when it closes the activation (r6 A5 and R4). User mode writes neither, so
user-mode runs check exactly what they did.
D3. The user-mode argv and `--plan` tests read the base's scripts with `git show cb5fdb064:` rather than
storing their output as a fixture file. A later row that changes user-mode output on purpose moves the
`BASE` constant in the test to its own base, or the test fails. I chose this over a stored fixture because
a stored copy can drift from the base without anyone noticing.
D4. `marker-failed` is reached by making `state/stack-rollback` unwritable. r6 says "an existing `$RS`", but
`next_activation` never picks a taken name, so no test can make the run meet an existing `$RS`. The
exclusive `mkdir` is still what refuses.
D5. The stale-start polling case turns the enter time to `@1792540800`, not r6's `@1792540805`, with the
started line at 1792540800.900. That also checks that the floor is the whole second, so .900 doesn't push it
to 1792540801.
D6. No fake `sudo` or `case2-unit.sh`. r6's sequences ran under them because r6 had `case1-repin.sh` drive
the steps, and that script is I3's. Here the test plays root: it stops and starts the fake system units and
installs with the existing `bus-service.sh` and `webui-service.sh` stand-ins, between the real
`stack-release.sh` calls, on one log.
D7. The fake marker in the `open-activation` test is named `20000101T000000Z-<C1 prefix>` so it sorts
before the real one. A marker that sorted after it would leave m05 (newest marker) alive.
D8. m10 sets `ACT` inside `open_activation`, where r6's wording ("ACT set before the checks") lands in this
code. `ACT` is set only after every check in `confirm`.
D9. Row 200's `--sso` is a usage error under `--units external`, with the proof flags, `--no-proof` and
`--no-send`. It is a Console flag, and root's Console unit carries the Console flags; the script under
external renders no Console line and never calls `console_for_target`, so `sso_checks` doesn't run there
either. The usage text, the TOOLS.md bullet and the external usage test name it.
D10. Row 200's `config_checks` runs under `--units external` too, after `plugin_checks`, in both the
`--plan` and run paths of `activate` and `rollback --to`. It checks the target's loader against the stack
user's `config.json`, which the bus on a VM still reads, so a `console` key the target refuses would break
root's bus start the same way. The cost is one more refusal (`config-console-key`) an external run can
print; I took it over a bus that fails after root's start. The rebase changed no user-mode line: the
conflicts were the `SSO=0` globals line, the `plan` body (row 200's `SSO_PLAN` line sits in the user
branch) and the two `--plan` lines that gained `config_checks`. In the tests, row 200's fixture lines
(the loaders, `sso-config.mjs` and the `--sso` cli.mjs line for C1 and C) moved into
`stack-release-fixture.mjs` with the rest of the copy setup, and the `ALGS` import stays in
`stack-release.test.mjs`, which T57 uses.
## duckworth-0055's notes on r6 (brief line 169)
Duckworth's r6 approval (REVIEW-r5.md at 23f5520b5) carried two notes.
- N1, A2's stop timeout under `gate --since`. A unit that never stopped keeps its old `ActiveEnterTimestamp`,
so `gate --since` fails it with `stale-start`, and the run ends at exit 2 with root stopping the units.
That is intended: a unit that won't stop is a fair reason to stop. In I1 this is the gate code and its
polling (`stale-start: @1792540799 fails ...`, `gate (N2, A2f)`, m11). The run sheet's A2f line and the
re-pin test that points "exits 1 with OLD's gate passing" at an A3 exit 3 belong to I3, which owns
`scripts/stack-upgrade.sh` and `docs/guides/host-run-sheet.md`. I'll name both in the I3 build.
- N2, the claude path under `/opt/mosaic-stack` (gosalyn-0058, sage-1522). I1 doesn't touch it. Under
`--units external` the script checks no claude pin of its own (`no-claude-pin` applies only to user
mode), because root's unit carries it. The `--claude` value, the managed-settings reasoning, F6 and the
run sheet's claude step go to I2 and I3.
## Run environment
Worktree `/mnt/storage/scratch/darkwing-r232`, `TMPDIR=/mnt/storage/scratch/tmp/r232`, node from PATH.
One external file run takes about 94 s.
## Results
Singles on the rebased candidate, before the gate:
| File | Tests | Pass | Fail |
|---|---|---|---|
| `packages/cli/tests/stack-release-external.test.mjs` | 25 | 25 | 0 |
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 16 | 0 |
| `packages/cli/tests/stack-release.test.mjs` (row 200's T57 included) | 192 | 192 | 0 |
| `packages/cli/tests/stack-instances.test.mjs` | 23 | 23 | 0 |
| `packages/webui/tests/pinned-copy.test.mjs` | 10 | 10 | 0 |
| `packages/cli/tests/channel-recovery.test.mjs` (imports `channel-fixture.mjs`, which names the scripts) | 1 | 1 | 0 |
| `packages/cli/tests/host.test.mjs` (reads `packages/cli/README.md`) | 42 | 42 | 0 |
These are every test file that reads `stack-release.sh` or `stack-copy.sh` (a grep of `packages/*/tests` and
`scripts/`). None of them pins a sha256 of a changed file. `packages/cli/tests/host.test.mjs` reads
`packages/cli/README.md` for a unit file name; this row only adds a paragraph there. The grep and the runs
were repeated on the rebased tree on 2026-10-21; the reader set is the same. A dry run of `mutants.sh`
matches each of the 17 replacements once on the rebased scripts.
### Gate, 2026-10-21, Sage's slot (sage-1618)
Candidate a0ffabc3f on `darkwing/row232-r1`, run by `~/darkwing-scratch/r232/gate.sh`: the cli suite, then
every `scripts/test-*.sh`, one after another, stopping at the first failure. busy.sh passed at 01:07:49Z
after Launchpad's single `stack-release.test.mjs` run ended. The gate stopped at the first one, so the
chain waited for that PID to exit and checked again. The EXIT trap removed the compose network; no
container was left.
| Step | Time (UTC) | Result |
|---|---|---|
| `packages/cli`, `node --test tests/*.test.mjs` | 01:07:50 to 01:17:53 | 561 pass, 0 fail, 0 skipped |
| `scripts/test-auth.sh` | 01:17:53 to 01:17:54 | 15 passed, 0 failed |
| `scripts/test-conductor.sh` | 01:17:54 to 01:18:00 | 17 passed, 0 failed |
| `scripts/test-config.sh` | 01:18:00 to 01:18:02 | 31 passed, 0 failed |
| `scripts/test-discord.sh` | 01:18:02 to 01:18:12 | 78 passed, 0 failed |
| `scripts/test-extension-package.sh` | 01:18:12 to 01:18:13 | 18 passed, 0 failed |
| `scripts/test-foundation.sh` | 01:18:13 to 01:19:01 | 44 passed, 0 failed |
| `scripts/test-land-work.sh` | 01:19:01 to 01:19:14 | 86 passed, 0 failed |
| `scripts/test-queue.sh` | 01:19:14 to 01:19:37 | 156 node tests pass, queue suite 28 passed, 0 failed |
| `scripts/test-release.sh` | 01:19:37 to 01:19:51 | 14 passed, 0 failed |
| `scripts/test-task.sh` | 01:19:51 to 01:20:44 | 98 passed, 0 failed |
### Mutants, 2026-10-21, same slot
`~/darkwing-scratch/r232/mutants.sh`, right after the gate. Both controls (the external file whole, and
stack-copy's `^row 232: ` tests) exited 0 on the unmutated tree. Each mutant was applied after a checked
backup and a syntax check, ran its killers from the table above, and was put back with its sha256
checked. The script stops on any restore mismatch, and it ran to its end.
| Mutant | Killer exit | Result |
|---|---|---|
| m01 pointer on gate failure | 1 | killed |
| m02 gate once | 1 | killed |
| m03 PM from any instance | 1 | killed |
| m04 no startedAt check | 1 | killed |
| m05 newest marker | 1 | killed |
| m06 --to not compared | 1 | killed |
| m07 one call without --user | 1 | killed |
| m08 interim pending line | 1 | killed |
| m09 marker written last | 1 | killed |
| m10 refusal names the open activation | 1 | killed |
| m11 stale-start refuses | 1 | killed |
| m12 revert always restores | 1 | killed |
| m13 confirm op | 1 | killed |
| m14 MIGRATING after migrate | 1 | killed |
| m15 gate floor from the log | 1 | killed |
| m16 bundle reachability unchecked | 1 | killed |
| m17 --create overwrites | 1 | killed |
17 of 17 killed, 01:22:16 to 01:24:32Z. I read the logs for m01 and m09, two of the quick kills: each failed on
its named test's assertion (m09 on `AWAITING-UNITS` in sequence 2), not on a load error.
## Round 2: rebase onto 8fede5fc6 (sage-1673)
Rebase only. Before the landing gate, a check of BASE..HEAD on the manifest paths found that row 230
(c8bc5cf4c) had changed one of them after the round 1 base, cb5fdb064. Sage ruled a re-round with a
scoped review, the same shape as row 210 round 3.
Branch `darkwing/row232-r2`: the two round 1 commits, a0ffabc3f and 84f1a91e6, cherry-picked onto
8fede5fc6 with no conflict, then this section. No code or test changed.
- `git merge-tree --write-tree origin/refactor darkwing/row232-r1` (refactor at 8fede5fc6) gives tree
837bde79236c with no conflict. The cherry-picked tip before this section has the same tree.
- `packages/cli/README.md` is a clean 3-way merge. Row 230's hunk is at lines 710-717 (the session credential
paragraph); this row's is a new paragraph at 773. With `git patch-id --stable`, the README diff from r1 to
r2 is 02b679e2, the same as row 230's README diff `cb5fdb064..8fede5fc6`. This row's README diff is
c395ecd2 on both bases (`cb5fdb064..r1` and `8fede5fc6..r2`).
- The seven code, test and doc paths other than the README hash the same as in manifest 7a3b4ea1, which
Filbert approved in round 1: `docs/TOOLS.md`, `packages/cli/tests/stack-copy.test.mjs`,
`packages/cli/tests/stack-release-external.test.mjs`, `packages/cli/tests/stack-release-fixture.mjs`,
`packages/cli/tests/stack-release.test.mjs`, `scripts/stack-copy.sh`, `scripts/stack-release.sh`.
This file also matched until this section was appended. Its other lines are unchanged.
- The one test that reads the README, `host.test.mjs:855`, looks for the unit's `EnvironmentFile=` line.
That line is unchanged context in row 230's hunk.
- No row 230 test reads `stack-release.sh` or `stack-copy.sh`, and no test of this row reads a row 230 file.
Nothing was run for this round. The landing gate runs on the r2 tip on Sage's slot.
Follow-up, doc-only (sage-1673): `scripts/case2-unit.sh:108` and `scripts/case2-precheck.sh:157` cite
`scripts/stack-copy.sh:8` and `:22` for `COPIES` and `copy_root`. With this row those lines are `:10` and
`:26`. Only comments are affected and no test reads them. Row 233 doesn't touch those scripts, so the fix
goes on the #1732 carry list.
-11
View File
@@ -1,11 +0,0 @@
# Gosalyn
Worker seat for Mosaic Stack, MVP track T2 (Case 2, the creative writing hub, Vikunja #416).
- T3 thread: 7604438a-0ded-41c8-bd3f-26f57fdb5cd2
- Model: Opus 5.5 (Claude Code, T3)
- Launched by Mos (Jason's main agent, T3 38bc4c81-6270-4021-b00c-6a15c43b9b0e), who briefs the seat
- Repository lead and queue: Sage (T3 1ef1e4f8-3ead-4208-beca-38f9f1add079)
- Branch: gosalyn/case2-design, pushed only by this seat, landed on refactor by Sage
Work lives in `work/`. The Case 2 design is `work/case2/DESIGN.md`.
-104
View File
@@ -1,104 +0,0 @@
# Row 230: C2-13a unit render and host precheck
Gitea #1732. Vikunja #416 (track T2). Brief: docs/plans/2026-10-20_case2-rows.md, section "C2-13a",
blob f6b4933f0f47 (01b40d357, sage-1562). Base: 00d4cc1f6 (row 190). Reviewer: Webby. Gate: the cli
suite plus every scripts/test-*.sh.
## What ships
`scripts/case2-unit.sh` renders `[email protected]` and `mosaic-webui.service` from the two existing
templates into the system unit directory, for the default instance only. It writes each unit through
a temp file at mode 0644, runs `daemon-reload`, enables both units, starts the bus and then the
Console. If the bus fails to start, the script refuses and leaves the Console unstarted. Both units
get `WantedBy=multi-user.target`, and neither gets `After=` or `Wants=`. The script never links a unit
file into place. A re-install refuses unless each installed unit passes the same checks, both units
name one copy, and each equals the render apart from the copy id.
`--profile library` (the default) refuses `--pm`, `--claude`, `--tester-port` and `--sso`, and needs
the preview proxy flags plus the proof pair. `--profile factory` refuses the six proxy flags and takes
`--pm` and `--claude`. Both refuse `--checkout` and `--board`.
`scripts/case2-precheck.sh --business ID [--profile factory|library]` runs as `<user>`. It prints one
`ok` or `REFUSED` line per check and exits 1 if any check refused. It prints variable names only,
never values.
`packages/cli/README.md:713` now says nothing creates the session env file, only the Case 2 precheck
reads it, and the precheck prints only variable names (webby-0034 N2, webby-0035).
## Deviations for review
Sage accepted all six for the build (sage-1570). Webby rules on each.
1. The precheck takes `--business ID`. The brief names no other way to find the business file.
2. A run root `<dataRoot>/launches/<business>` that doesn't exist passes, with a note that the first
launch creates it at 0700 (session.mjs:47 and newRun). A symlink refuses, and so does an existing
entry that isn't a 0700 directory.
3. The claude marker check passes when the reply holds the marker and input plus cache tokens are at
most 2000. **The 2000 ceiling is unmeasured.** The tests use fakes at 40 and 9000 tokens. Measuring
it on a real claude belongs to row 195's install run, not this row.
4. A non-root test can't create a root-owned proof file. For the "proof not owned by the user" case,
the fake getent reports another uid for the user, so the user's own file reads as someone else's.
5. The unit script refuses `%h` left in any unit, rendered or installed. A system unit expands `%h`
to root's home. The render replaces every `%h`, so the test edits an installed unit to reach it.
6. With `--auth-header X-Authentik-Uid`, the allowed user must be 64 lower-case hex characters (lead
decision 101). The test covers both the refusal and the pass.
## Tests (packages/cli/tests/case2-unit.test.mjs, 30 cases after round 2)
The tests use a scratch unit directory and a scratch home, plus fake systemctl, getent, sudo, id,
node, bwrap and claude. They never touch /etc/systemd, ~/.mosaic-dev, ~/.config/mosaic-dev or a
live unit. Every precheck case asserts its output doesn't hold the fixture key.
- Unit: the clean library render, PATH for a node outside the default list and for /usr/bin/node, the
systemctl order, a failed bus start, no `systemctl link`, factory `--pm` and `--claude` rendered and
absent, re-install to another copy, edits to an installed unit, template restrictions,
EnvironmentFile placement, unknown tokens, PermissionsStartOnly and `+`/`-+` Exec prefixes,
symlinked unit paths, bad `--node`, re-install drift, a split copy pair, bad copy ids, every library
and factory flag refusal, and bad `--claude`.
- Precheck: a good host, node older than 24, more than one group, sudo succeeding, XDG_DATA_HOME,
HOME mismatch, a run root mode, a bad config, bwrap failing, the port floor at or below the Console
port, a missing Console unit, bus.tester per profile, profile-library-business, the claude check
(no route, replaces, appends, missing, the unit's MOSAIC_CLAUDE_BIN beating PATH), and the session
env file (missing, 0644, symlink, empty, emptied by a later line, quoted).
- Bind (mos-1100): inside `binds()` plus `sandboxArgv()`, a bind on port 980 gets EACCES, and a bind
on a free port of 1024 or above succeeds. The test fails if the host's floor is at or below 980.
## Verification
Round 1 singles, run before the gate: case2-unit.test.mjs passes 28/28 with no skips, and host.test.mjs (it
reads the cli README) passes 42/42. No test pins a hash of the README. No other test reads the two
scripts.
## Round 2 (webby-0046, sage-1589, sage-1590, sage-1591)
Base: origin/refactor (rebased from 00d4cc1f6; the 15 commits between touch none of the five paths
or the two unit templates). Branch gosalyn/c2-13a-r2.
B1. The precheck now has a case for each refusal Webby named: the user missing from passwd (:77),
a business file that is unparsable, missing or fails the schema (:137), a run root that is a symlink
to a 0700 directory (:151), a Console ExecStart with no --port (:179), an unreadable port floor
(:180), a claude reply without the marker (:223) and a claude reply that isn't JSON (:224). The fake
claude gained `nomarker` and `text` kinds. The no-claude-code case and the missing-claude case share
a PATH without `/usr/bin/claude` (`pathWithoutClaude`), so the no-claude-code case now shows a host
with no claude passes.
N1 (ruled in for this round). Before writing, `case2-unit.sh` asks `systemctl is-active` for both
units. For each unit that was running, it prints that the unit is still on its old copy, never
"started", and then that the bus and Console must restart together to reach the new copy. A unit
that wasn't running gets "started ... on copy". A fresh install prints the round 1 line. The script
still restarts nothing. The new test covers both units running and only the Console running.
N4. The README line at :713 is rewrapped.
Mutants, run in Sage's slot on a scratch worktree, one branch each: :77, :137, :151, :179, :180,
:223, :224, a precheck that refuses with no claude on PATH (for the no-claude-code case), and an
install that prints "started" for a running unit (sage-1591).
## Carry items for row 195
- N1's restart. Row 195 restarts the bus and Console together after a re-install over running
units, as the install line now asks.
- N2. A system drop-in check like `bus-service.sh:122-145`, so a drop-in under
`[email protected]/` or `mosaic-webui.service.d/` can't change what the checked unit runs.
- N3. The precheck finds claude on the shell PATH. The bus unit's `PATH=` is what a session uses
when `MOSAIC_CLAUDE_BIN` is unset, so the check should search that PATH.
-216
View File
@@ -1,216 +0,0 @@
# Row 199 (#1697): per-agent credential store, each agent's own git identity, publish to its fork
Builder: Megavolt. Brief: `docs/plans/2026-10-20_agent-credentials.md`. Design: `DESIGN.md` r6 at
4d3007ec9, approved by Duckworth in duckworth-0027 (REVIEW-r6.md at 640011518). Authority model ruled
by Mos in mos-1027. Branch: megavolt/row199. Reviewer: Duckworth.
Status: candidate on megavolt/row199-on-190, rebased on origin/refactor 00d4cc1f6 (sage-1555). The
full gate and the section 11 mutants ran in Sage's slot; results under "Runs".
## Commits so far
- 286bf598c business: agent identity, the publish grant, the project upstream with the D9c parse, store
credential references; git.push.remote as a hard wait.
- a2c387dba bus: broker publish authority (checkPublish, recordPublish, publishRefused, pushCommit on
the raise); the D9c parse moves to `business/src/vocabulary.mjs`; pm.md and chief-of-staff.md list
git.push.remote among the hard waits.
- 609dc9364 bus: the per-agent credential store in `credentials.mjs`.
- 504336797 bus: runtime wiring, the store root and the four publish entries in the op table.
- a18fc6cf8 tasks: request calls use `useFor` keyed from the session; the start probe keeps `use`. The
D5 test in `credential-store.test.mjs` pins the `use(` call sites to `credentials.mjs` (useFor's own
call) and `tasks/src/adapter.mjs` (the host's @sync and start probe).
- aa740cc71 bus: the lifecycle verbs `credentials.issue`, `credentials.revoke` and `credentials.delete`
in `credential-verbs.mjs`, the broker branch, the runtime wiring; `organizationWriter` gets a
`detail` on `write` and a receipt-only `record`.
- 0cdace415 cli: `mosaic credentials status|issue|rotate|revoke|delete`, the scripts/mosaic route, docs.
## Deviations from DESIGN.md r6
1. **Identity is optional on an agent.** D8 reads as identity on every agent. Today's business files
carry none, so identity stays optional and `publish` requires it (N10's load refusal). An agent
without identity commits as E7 (T6).
2. **Revoke of a `{file}` or `{env}` reference.** Section 6 describes revoke for store references.
For a reference outside the store, revoke forgets the token in memory and never unlinks the file;
the lifecycle verb then turns the business file's reference into `store`, so a restart finds
nothing. The test: `credential-store.test.mjs`, "revoke of a {file} reference".
3. **Issue order.** Issue writes the token file (tmp, fsync, rename, dir fsync), then runs the
caller's commit (the business file's reference becomes `store`), then sets memory. A failed commit
leaves the file on disk and the token out of memory, so the agent stays `not-issued` until the
issue is run again. The test: "a failed commit leaves the token on disk but not in memory".
4. **The D9c parse lives in `business/src/vocabulary.mjs`, not `business.mjs`.** The broker reads
`forkProblem` and `forkUrl`, and `project-runtime.test.mjs` pins the broker's import closure to
files that import nothing. `business.mjs` wraps `parseGiteaUrl` with the loader's refusal
messages. The pin test names the two new imports.
5. **`withGitea` takes the publish ticket, not a cap (D9b).** The key is the session checkPublish
checked with `#agent` (a run holding its claim; never a tester, reader or human), read through
`broker.publishSession(ticket)`. The token is usable only between checkPublish and recordPublish
or publishRefused. Its refusals are filed once, by publishRefused on the task's trail, and not
also by `refusing()`. The test: `publish.test.mjs`, "runtime: withGitea keys from the open ticket
only".
6. **Role contracts.** `workflow-roles.test.mjs` requires the hard waits listed in `roles/pm.md` and
`roles/chief-of-staff.md` to equal GATED_ONLY. Adding git.push.remote to the vocabulary made it
fail, so both lists now name git.push.remote. Prose only; no hash pin reads either file. Duckworth
should review this hunk because `roles/` is reviewed authority.
7. **Delete takes a reason.** Section 6 lists `delete AGENT` with no reason. The broker requires
`reason` on delete as on revoke, so every receipt that ends a credential says why.
8. **Rotate is `credentials.issue`.** There are three bus verbs, not four. An issue over an entry the
bus holds is recorded as `rotate` in the receipt and the answer. The CLI's `rotate` reads the
status first and refuses before reading stdin when nothing is issued.
9. **Issue covers entries the bus loaded.** The business file must already name the kind for the
agent, and the bus must have loaded that reference at start; otherwise `credential-unknown`. An
agent added after the bus started needs the restart row 165 already asks for. Row 194's human
`create` is where a new agent's `store` references come from.
10. **No Console page yet.** Section 6 puts the verbs in the Console's agent page too. This branch has
the bus verbs and the CLI. The Console page is a separate piece (packages/webui, its `BUS_VERBS`
and `STATUS` pins); I raise the split with Sage before the manifest. Ruled in sage-1421: the
Console page is out of row 199 and becomes a follow-up row with Webby as reviewer. The manifest
holds bus, business, tasks, cli and seat.
11. **The push runs in the bus child, and the git socket polls for it (D9b).** The git socket runs in
the host process. The broker, the runtime and the token are in the bus child that
`packages/cli/src/host.mjs` forks, and host ops cross that IPC one at a time with a 10 s answer
limit. ls-remote and the push can run longer than that, so `withGitea(ticket, fn)` can't wrap a
host-side git. The split: the socket runs D9 steps 3 and 4 (tip and identity under the lock,
`working.publish`), then op `publishPush`, which starts `publishRemote` in the bus child under
`withGitea` and answers `{state: 'running'}`. The socket polls op `publishResult` every 200 ms
until it reads `pushed` or `refused`. Fork, user, source and destination come from the ticket
(`broker.publishTarget`), never from the host's message. `recordPublish` also refuses `ref-moved`
when the pushed ref isn't the ticket's destination. A second `publishPush` on a running ticket
refuses `publish-busy`. The token reaches git on fd 3 only (`runGit` option `pass`), read once
by a one-shot credential helper that answers `get` for https on the fork's exact host and port.
On close, a running publish's git isn't awaited; git's own time limit ends it, and the ticket
goes with the process. The 10 s IPC limit stays as it is (sage-1440). The poll has a bound
(sage-1440): `publishDeadlineMs`, by default four times git's limit and grace (publishRemote runs
at most four git commands) plus 30 s. Past it the socket refuses `publish-timeout`, filed once by
publishRefused, and logs that the bus process's push may still reach the fork. A failure that is not a refusal is filed as `git-failed` and logged on
the bus's stderr by error name and code only. Tests: `seat/tests/publish.test.mjs` (11),
`bus/tests/publish.test.mjs` (the process ops), `cli/tests/publish-socket.test.mjs` (7, end to
end through the socket, the in-process runtime, the store and a TLS fake Gitea; one is the
publish-timeout outcome).
12. **No harness tool for publish.** `packages/harness/src/git-tools.mjs` has push, fetch and land
clients. A runner tool that calls the socket's `publish` op is in packages/harness, outside the
manifest scope sage-1421 set. Today a publish needs a socket client; the tool is a follow-up.
13. **The bind rule's `git.push.remote` clause has no test of its own.** D11 adds it and the
launcher has it. A role can publish only a push its own role made (`work-not-yours`), and
`recordPush` needs `git.push.working`, which already binds the socket. No role reaches the
clause alone, so dropping it changes no outcome today. It matters once a role can publish work
it didn't push. Ruled in sage-1440: keep it; its mutant goes in the section 11 list as
equivalent, with this reasoning, and Duckworth judges it.
14. **A push between checkPublish and the git push refuses; it never lands a different commit.**
T3 says such a push "doesn't change the commit that lands". Under the hard wait it refuses:
`working.publish` checks the work ref's tip against the ticket's source under the lock
(`ref-moved`, `seat/tests/publish.test.mjs`), and `recordPublish` checks `pushCommit` again
(`decision-mismatch`, `bus/tests/publish.test.mjs` "r4 B4"). The fork gets only the ticket's
oid either way. N9's routing-commit test still applies. Ruled in sage-1440: T3's wording in
DESIGN.md now reads "refuses with `ref-moved` or `decision-mismatch`", and an end-to-end test
asserts each code mid-flight in `cli/tests/publish-socket.test.mjs`: a push after checkPublish
refuses `ref-moved` with no request to the fork; a push recorded while the bus process pushes
refuses `decision-mismatch`, filed once, and the fork holds only the ticket's oid.
15. **Fix: rotate on a due gitea key (gizmoduck-0050, sage-1494).** The CLI's `ISSUED` set
(`packages/cli/src/credentials.mjs`) listed valid, expiring and expired, but the bus's `status()`
gives a gitea key past its rotate-by date `rotation-due`. So `credentials rotate` refused the one
case it exists for. `ISSUED` now names every state status() gives a held token. The test:
`cli/tests/credentials.test.mjs`, "rotate goes ahead on each held state". Its mutant (the old set)
fails that test and goes in the section 11 list.
16. **The bus process refuses a boot config carrying `publishTls` (M26, sage-1563).** process.mjs
used to drop `publishTls` from the boot config before `startBroker`, and no test could see the
drop. The host's boot config (`packages/cli/src/config.mjs`) never carries it; it is the tests'
scratch CA for an in-process runtime only. A boot that carries it now refuses
`invalid-host-request` and the process exits 2, rather than trusting it or dropping it quietly.
The test: `bus/tests/publish.test.mjs`, "the bus process answers the publish ops from their own
lines". Every test file that reads process.mjs ran singly, green (see "Runs").
17. **M17 is equivalent: `#authorityFor`'s pushCommit comparison is also made by `#publishFacts`.**
A git.push.remote raise with no recorded push refuses `no-push` (`broker.mjs` raise,
`pushCommit = ... ?? fail('no-push')`), so every publish decision carries a pushCommit.
`#publishFacts` takes `source = result.pushCommit` and refuses `decision-mismatch` when it isn't
`#latestPush(task_ref).commit`, the same two values `#authorityFor` compares. The check stays as
defense in depth, for a decision whose context lacks pushCommit, which no code path writes today.
Duckworth judges it, as with M27.
## N9, for the routing commit (sage-1388)
Today git.push.remote is a hard wait, so every publish carries a decision, and the decision's
`pushCommit` fixes the commit. If a later commit routes git.push.remote within role, a publish has
no decision and no `pushCommit`. `#publishFacts` then takes the commit from `#latestPush`, and the
git socket's tip check under the lock is the only thing that holds it. That commit needs a test
that a push between checkPublish and the git push doesn't change what lands.
## R1, the backup set
Checked at 504336797. Row 185's `backup_store` (`scripts/stack-release.sh:372-392`) copies only
`<dataRoot>/bus/bus.sqlite`, plus its sums, modes, listing and stamps, into
`<dataRoot>/backups/stack/<activation>/`. Nothing under `<configDir>` is copied, so
`<configDir>/secrets/agents` isn't in the backup set. `rehearse` copies the live business file
through `R_BUSINESS_JS` (`:1268-1288`). Its `swap` replaces every credential reference, a `store`
one included, with a placeholder file in the rehearsal's own dir. The guard then refuses
`credential-ref-live` unless every reference is such a placeholder (`:1329-1337`), so a rehearsal
never reads a store token. A backup tool outside this repository that copies `<configDir>` would
copy tokens. That is R1's open half, and it is Jason's to check on each VM.
## T1 limit
A wrong owner on a store dir or file can't be set up without root, so T1 covers modes, symlinks and
dirs. The owner check (`st.uid !== process.getuid()`) is in `privateDir` and `#read`, and is untested.
## Runs
Single files, each green on its commit: bus broker, decision-close, decision-route, decisions,
hard-waits, land, project-runtime, launch-notice, review, publish, credential-store, credentials,
credential-status, limits, tester, plugins-verb, routes-verb, limits-runtime, instances-verb, web,
usage-runtime, limit-checks, daily-quota-runtime, chief-of-staff, tasks, tester-writes; every
business test file. Since: credential-verbs (2/2), credential-store (11/11, with D5), each tasks file,
tester, credential-status, plugins-verb, routes-verb, instances-verb; business launch-settings,
plugin-settings, instance-settings, token-limit-settings; cli credentials (3/3), cli, app,
tester-reads, tester, other-tree, transport, instances, limits, verbs. Hash pins: none on a changed
file (`thread-migration.test.mjs:46` pins DDL only).
Publish, single files, each green before its commit: seat
checkout (77), working (26), publish (11), landing (8); bus process (8), land (17), publish (12),
credential-verbs (2); cli publish-socket (4), git-socket (24), launcher (29); harness tools (22).
Hash pins: none on a changed file.
sage-1440 changes (the poll bound, the mid-flight tests), single files, green before the commit: cli
publish-socket (7), git-socket (24), launcher (29). Hash pins: none on a changed file.
The rotation-due fix, single files: cli credentials (4), green; the old-set mutant fails it 3/1.
Hash pins: none on a changed file.
Slip, 2026-10-20 about 19:51Z: I ran `node --test packages/tasks/tests/*.test.mjs` in one command,
which is a package suite, without asking Sage for the slot. It passed, 97 pass and 0 fail, in under a
minute. Reported to Sage in megavolt-0017.
Rebase (sage-1555): the 21 commits from megavolt/row199 sit on origin/refactor 00d4cc1f6 as the new
branch megavolt/row199-on-190 (no force-push). `git range-diff` shows all 21 patches as `=`; the
merge of 12c85ada1 flattened with an empty remerge-diff.
Full gate, Sage's slot, on c6868ba41 (base 00d4cc1f6), logs started 2026-10-20T22:55:44Z: bus 476
pass 0 fail; business 191/0; tasks 97/0; cli 522/0 (472 s); seat 163/0; scripts/test-queue.sh
156/0; test-auth, test-conductor, test-config, test-discord, test-extension-package,
test-foundation, test-land-work, test-release and test-task each exit 0. The test-task compose run
left the network megavolt-row199_default with 0 containers; I removed it.
Section 11 mutants, batch 1, on c6868ba41, 2026-10-20T23:09:03Z to 23:09:56Z, restores verified and
packages/ clean: 22 of 28 killed; M27 survived, expected equivalent (deviation 13). Five expected
kills survived, each a test gap: M05 (the token in the push URL), M06 (revoke unlinks before it drops
memory), M12 (http.followRedirects=false dropped), M17 (#authorityFor's pushCommit check dropped)
and M26 (publishTls reaches startBroker). Launchpad's stack-release single file ran alongside from
23:09:28Z, so batch 2 reruns all 28 (sage-1563).
Fixes, each checked by its mutant alone: 5fa7b2553 seat publish, a test reads git's argv through a
PATH shim and finds no token and no userinfo (M05 killed); the 302 test's target shares the CA git
trusts, so only http.followRedirects=false keeps git off it (M12 killed). 116eeec48 bus,
credential-store: revoke's unlink fails (dir 0500) and the token is already out of memory (M06
killed); process.mjs refuses publishTls, deviation 16 (M26 killed, its anchor is now the refusal
line). M17 is equivalent, deviation 17. Single files after the fixes, each green: seat publish (12),
bus credential-store (12), bus publish (12), bus process (8), and every other file that reads
process.mjs: bus credential-status (4), limit-checks (16), land (17), tasks (9), end-launch (7); cli
trackers-boot (1), thread-launch (11), publish-socket (7). Hash pins: none on a changed file.
Section 11 mutants, batch 2, all 28, on 7c3cdd940 in Sage's slot (sage-1565), 2026-10-20T23:17:59Z to
23:18:48Z, restores verified and packages/ clean: 26 killed; M17 and M27 survived, both expected
equivalent (deviations 17 and 13). Each of the five batch 1 survivors but M17 now fails its test. Then
pkg-bus 477 pass 0 fail, pkg-seat 164/0, pkg-cli 522/0 (461 s), ending 23:26:40Z. No other seat's run
was active: the busy check said CLEAR before the batch started. Logs:
/mnt/storage/scratch/megavolt-r199/batch2-20261020T231650Z/.
-416
View File
@@ -1,416 +0,0 @@
# Row 199 design: a credential store per agent, own git identity, pushes to a remote
Megavolt, 2026-10-20. Gitea #1697, MVP T1 item 6. Brief: `docs/plans/2026-10-20_agent-credentials.md`.
Reviewer Duckworth. Mos rules on the authority model (section 9) before any code. Base: refactor at 992650232.
No credential value was read to write this. Evidence is code and docs only.
r2 answers duckworth-0013 (review at 6ab9cd927 on duckworth/reviews): B1 in D3a, T1 and T5, B2 in D9 step 5
and T3, N1 in section 6 and Q-M3, N2 in section 6, N3 in D9 step 3, N4 in D9a and T3, N5 in E5 and D9b.
r3 records Mos's rulings (mos-1027) in section 9. The Q-M2 hard wait changes D9 step 1, D9b and T3. Q-M1
drops O2 (D10, A3, R2).
r4 answers duckworth-0017 (REVIEW-r3.md at 4065ed59f): B3 in T3 and D9 step 5, B4 in D9 steps 1, 3 and 5, N6
and N7 in D9a, N8 in D9 step 1.
r5 takes webby-0011 N1 (row 194 review, through honker-0009): the host checks at each push that the
remote's owner is the agent's own Gitea user. r4 named one `remote` per project, which can't be a fork per
agent (O1) once two agents publish the same project. So `projects.<p>.remote` now names the upstream
repository, and the host builds each agent's fork URL from it (D9 step 2, D10, T3). It also fixes a conflict
found while building: D8 made `identity` required for every agent whose definition declares `gitea`, while D9
step 2 says today's files load unchanged. Coder declares `gitea`, so r5 makes `identity` optional and
`publish` require it (D8).
r6 answers duckworth-0026 (REVIEW-r5.md at 3cb6888e1), with Mos's acceptance (mos-1043). B5: the derived URL
has one name, `fork`, and D9a's `ls-remote`, step 5's push, the helper's host match, `checkPublish`'s result
and step 6's event all use it (D9, D9a, D9b, T3). B6: the URL parse is written down (D9c) and tested (T3).
N10 in D8 and T3. N11 in D9a and A3. N9 is carried to BUILD.md for the routing commit.
## 0. Terms
- **Stack instance**: one Mosaic Stack deployment (row 190). It has one OS user, one config dir
`~/.config/mosaic-NAME` (here `<configDir>`), one data root and one bus. Case 1 and Case 2 are two stack
instances on two VMs (DECISIONS Q180).
- **Agent**: a role instance in a business file (`roles.<agent>`). Today's code calls it "instance". This
design says agent.
- **Kind**: a credential type: `gitea`, `vikunja`, `model`.
- **Host**: the bus process and its git socket, running as the stack instance's OS user, outside any sandbox.
## 1. What exists (measured at 992650232)
| Code | Fact | Where |
|---|---|---|
| E1 | The business file references credentials per agent: `roles.<agent>.credentials.<kind> = {file or env, rotateBy or expires}`. The kinds an agent may hold are the ones its role definition lists | `business/src/business.mjs:89-111`, `credentials.mjs:18-38`, `role.mjs:105-118` |
| E2 | The bus reads each token once at start (O_NOFOLLOW, own uid, 0600, at most 16 KiB, outside the repos and data root) into a private map keyed `<business>/<agent>:<kind>` | `bus/src/credentials.mjs:18-89`, `bus/src/runtime.mjs:78-89` |
| E3 | Only Vikunja uses a token: `credentials.use("<business>/<agent>", "vikunja", ...)`. Gitea tokens are loaded and never used. `assertClean` refuses any event that contains a token | `tasks/src/adapter.mjs:70`, `runtime.mjs:134`, `broker.mjs:560` |
| E4 | Agents run in bwrap as the bus's own uid. `~/.config`, `~/.mosaic` and `~/.ssh` are not bound. The git socket is bound only for `git.push.working`, `review.verdict` or a landing | `seat/src/sandbox.mjs:21-30, 104-124`, `cli/src/launcher.mjs:401` |
| E5 | A session proves who it is with a 64-hex cap the broker mints and maps to a frozen session record. The git socket resolves the cap to (business, role) before anything else | `broker.mjs:280, 379-383`, `packages/cli/src/git-socket.mjs:398-407` |
| E6 | The model credential is one env file per host (`EnvironmentFile=-%h/.config/mosaic-dev/secrets/claude-session.env`). `ENV_ALLOW` passes it into every sandbox, so every agent holds the same model token | `cli/systemd/mosaic-bus.service.in:24`, `seat/src/session.mjs:83-94` |
| E7 | Commits in a session's clone use `<agent>@<business>.mosaic.invalid`. Landed squashes use "Mosaic bus host" | `sandbox.mjs:151-157`, `working.mjs:280-281` |
| E8 | No code pushes to a remote. The git socket pushes bundles into a local bare working repo with no remote and no credential | `working.mjs:1-14, 198-238` |
| E9 | Adding or changing an agent refuses a definition that declares credentials (`organization-credentials-out-of-scope`). Binding them is a hand edit. Row 194's design keeps that refusal for create and hire "until the credentials row" | `business/src/instance-settings.mjs:20-29`, row 194 DESIGN.md D1 |
| E10 | The development repository's helper pushes as one identity from one JSON file | `scripts/git-credential-mosaic.mjs` |
So per-agent references and a host-only reader already exist for two kinds. What's missing: a fixed place for
the files, an agent identity, any remote push, lifecycle verbs, and a model credential that agents don't
share.
## 2. The store (brief item 1)
D1. **Layout.** Each stack instance keeps its agents' secrets under its own config dir:
```
<configDir>/secrets/agents/ 0700, owner = the stack instance's OS user
<configDir>/secrets/agents/<business>/<agent>/ 0700
<configDir>/secrets/agents/<business>/<agent>/<kind> 0600, the token only, one line
```
This is row 190's secrets root (`<configDir>/secrets/...`, as Q175's slot uses). The token file keeps E2's
format, so the bus reader doesn't change. Nothing in a token file is a name, an email or a URL. Those are not
secret and go in the business file (D6), where review sees them.
D2. **The business file reference.** A new reference form, `{"store": true, "rotateBy" | "expires": DATE}`,
means "the file at the layout path for this business, agent and kind". The path is derived, never written,
so an agent's entry can't name another agent's file. The old `{file}` and `{env}` forms stay valid for
migration (section 7). A `{file}` reference that points inside `<configDir>/secrets/agents/` but not at its
own agent's path refuses `credential-not-yours` when the business file loads.
D3. **Read path.** Unchanged in shape: the bus reads every entry once at start (E2) and on a store change
(section 6), and holds the values in memory. No sandbox binds `<configDir>` (E4 already holds). No token goes
into a sandbox for `gitea` or `vikunja`. The agent asks the bus to act, and the bus uses the token.
D3a. **A `store` reference with no file.** ENOENT on a `store` path loads as an absent entry. `status` shows
`not-issued`, and `useFor` refuses `credential-unavailable`. The bus still starts. This is the state after a
create without `issue` (D13) and after `revoke` (section 6). Every other defect still refuses start: wrong
owner or mode on the file or a dir, a symlink, a non-regular file, bad format, a missing or wrong-mode
`secrets/agents/` dir. `{file}` and `{env}` keep E2's behavior, where a missing file refuses start.
## 3. The "RLS equivalent" (brief item 1)
Three layers. Each one is enforced by code or the OS, not by prose.
D4. **OS, between stack instances.** Files are 0600 and dirs 0700, owned by the stack instance's own OS user.
Case 1's user can't read Case 2's store (Q180: each instance under its own user without sudo). The bus refuses
to start when any file or dir under `secrets/agents/` has the wrong owner or mode (`credential-file`, as E2
does for files today, extended to the dirs).
D5. **Bus, between agents of one stack instance.** The principal for every credential use comes only from the
verified cap (E5). The agent-facing API is `credentials.useFor(session, kind, op)`: `session` is the broker's
frozen record, and the key is built from `session.business` and `session.role`. It has no agent argument, so
there is nothing in a request to point at another agent. The existing `use(name, kind, op)` stays for the
host's own `@sync` entry and is not reachable from a request. The tasks adapter's per-role calls (`ctx.as`,
`verbs.mjs:126`) move to `useFor`. Any request field that names an agent other than the caller (`as`,
`agent`, `instance`, `role` on a credential path) is refused `credential-not-yours` and recorded through
`broker.refuse()` as `action.refused`, with the caller's cap and the task ref. A build test greps every
`credentials.use(` call site and fails on a new one outside the allowlist.
D6. **What stays open.** A process of the same OS user outside the sandbox can read every agent's file. That
is the same limit as the human proof (P19 333). Q180's per-user VMs move the boundary to "one stack instance
per OS user". Agents of one instance are kept apart by the sandbox and D5, not by the OS.
## 4. Kinds (brief item 2)
| Kind | 10-27 | Use |
|---|---|---|
| `gitea` | yes | Git push to a granted remote (section 5). Gitea API verbs (issues, pull requests) wait. The factory's review flow runs in the working repo today, and no 10-27 case needs an agent to open a pull request |
| `vikunja` | yes, exists | Moves to `store` references and `useFor`. No behavior change |
| `model` | per stack instance only | See D7 |
D7. **Model provider.** A model token has to sit inside the sandbox today, because the harness calls the
provider itself (E6). Per-agent model tokens need a proxy that holds the real token and gives each run its own
short-lived one, which is the CLIProxyAPI line in TARGET.md. That waits for its own row. For 10-27 the model
credential is per stack instance: row 190's `<configDir>/secrets/claude-session.env`, owned by that
instance's OS user. It is never a copy of jwoltje's file. Jason signs in on each VM user himself (A6). Whether
Case 1 and Case 2 run on Jason's existing subscription or need a new account is Jason's call (Q-J1). This row
doesn't pick. A pi model credential also waits for P19 #383.
## 5. Git identity and push (brief item 3)
D8. **Identity.** A new business-file key, `roles.<agent>.identity = {name, email, gitea: {url, user}}`. It is
optional, because today's business files with a `gitea` agent (coder) must load unchanged. `publish` (D9
step 2) requires it, and requires that the definition declare `gitea`. A business file with a `publish`
grant on an agent that lacks either refuses at load, and the error names `roles.<agent>.publish` and says
"needs identity and a gitea definition" (N10). Load is the only place this is checked: a business file that
fails it never reaches the bus. The sandbox sets `GIT_AUTHOR_*` and `GIT_COMMITTER_*` from
it in place of E7's invalid domain. The email is the one Jason gives the agent's Gitea user, so Gitea links the
commits to that user. Agents without `gitea` keep E7.
D9. **Push to a remote: a new git socket op, `publish`.** No new socket, no bundle, and no token in the sandbox.
```
publish {"cap", "op": "publish", "task_ref", "project", "branch", "decision"}
-> {"ok": true, "result": {"fork", "ref", "commit", "previous"}} or a refusal
```
The host checks, in order, and each refusal goes through `broker.refuse()`:
1. Identity from the cap (E5), then the broker's `#checkAuthority(s, "git.push.remote", {decision, target:
task_ref})`, as `checkLand` does for a merge (`broker.mjs:907-913`). `git.push.remote` is a new action in
ACTIONS and is not in ROUTED (REQ-WAIT-3, `packages/business/src/vocabulary.mjs:52-58`,
`broker.mjs:42-69`), so it is a hard wait and classifies `gated` for every role. Every publish therefore
needs a decision the human resolved for this task ref. A decision is spent once (`decision-consumed`).
**The decision binds one commit.** `#authorityFor` binds action, role, thread and target only
(`broker.mjs:769-798`), so the commit needs its own carrier. The raise records it the same way it records
`holderRun` for `role.revoke` (`broker.mjs:1733`). When the action is `git.push.remote`, the
`decision.raise` event body gets `pushCommit`. That is the commit of `#latestPush(business, target)` at
raise time, read by the broker and never taken from the agent. A raise with no recorded push for the task
refuses `no-push`. The decision's question shown to the human ends with that commit. `#authorityFor` then
refuses `decision-mismatch` when `context.pushCommit` differs from the commit of `#latestPush` now, which
is the same check `holderRun` gets at `broker.mjs:796`. So the sequence approve at A, push B, publish B
refuses.
No role definition grants the action yet. When Jason's PRD version routes it (Sage, Vikunja #420), a
reviewed commit adds it to both ROUTED lists and to coder's `withinRole`. From then on a coder publishes
without a decision, and `decision` is optional for a within-role class only.
2. The project is in the agent's grant, `roles.<agent>.publish: [project]`, and the business file names the
project's upstream repository, `projects.<project>.remote: "https://HOST/OWNER/REPO"`
(`remote-not-granted`). Both URLs pass D9c's parse at load. The upstream's host and port equal
`identity.gitea.url`'s (`remote-host-mismatch`). The host never contacts that URL. It builds the agent's
fork, `fork = https://HOST[:PORT]/<gitea.user>/REPO`, from the upstream's origin and repository name and
the agent's own `gitea.user`. `fork` is the only URL the rest of `publish` uses: D9a's `ls-remote`, step
5's push, the helper's host match, `checkPublish`'s result and step 6's event. So the owner of the
repository that receives a push is the agent's own Gitea user on every push, by construction, and the
agent can't name it (webby-0011 N1). D9c is what makes "by construction" hold. Gitea's refusal of a
push to a repository the user doesn't own or collaborate on is the second layer. An upstream whose owner
equals the agent's `gitea.user`, compared without case, refuses `remote-not-fork`, at load and at push. `remote` is a new key
beside `root` and `repo` (`business.mjs:186`), and `identity` and `publish` are new keys on a role entry
(`business.mjs:89`). All three are optional, so today's business files load unchanged.
3. The source is one commit, `C = context.pushCommit`. It is checked three ways, so all three are equal:
the decision's commit, the commit of `#latestPush`, and the work ref's tip in the working repo,
`work/vikunja-<project>-<task>` (`working.mjs:110-114`). The tip is read under the repository's lock, and
if they differ the push refuses `ref-moved`. The work ref is per task, not per agent. Two checks make it the
caller's: `#latestPush` was recorded by the caller's role (`work-not-yours`), and step 4. The destination is always `refs/heads/<gitea.user>/<branch>`, built by the host.
`validBranch(branch)` holds (`invalid-request`). The agent never names the destination ref.
4. Every commit in `main..<work ref>` has author and committer email equal to `identity.email`
(`identity-mismatch`).
5. The push runs through `credentials.useFor(session, "gitea", ...)`: `git push <fork> <C>:<dst>`, by oid,
with no `+` and no `--force`. It never pushes by ref name, so a push to the working repo after
`checkPublish` can't change what goes out (`non-fast-forward` from git stays a refusal). Git runs with E8's SAFE flags
(`working.mjs:92-98`), which set `protocol.allow=never`. `publish` adds exactly two flags,
`protocol.https.allow=always` and `http.followRedirects=false`, plus `GIT_TERMINAL_PROMPT=0` and no global
or system config. The token reaches git through a one-shot credential helper that reads it from an
inherited pipe, never through argv, env, the URL or a file. Seat `gitEnv` (`checkout.mjs:79-89`) takes only
PATH from the host and sets the rest itself, so no proxy or `GIT_SSL_*` variable reaches git. `publish` takes a `tls` parameter that
defaults to `[]`, the same way `push` takes `fsck = FSCK` (`working.mjs:100`). Tests pass
`["-c", "http.sslCAInfo=<scratch CA>"]`. The host never sets it, and nothing in config or env reaches it. The helper answers `get` only when `protocol`
is `https` and `host`, port included, equals `fork`'s. It writes nothing for any other `get` and for
`store` and `erase`. `assertClean` checks git's output, so a fork that echoes the token gets
`credential-leak`.
6. The bus records `action.allowed` for `git.push.remote` with fork, ref and commit.
D9a. **The agent's fork must share root's history.** A push sends every object the fork lacks. The working
repo's `main` is the project root's history (`working.mjs`, seed), so a fork that doesn't share it would
receive all of root, landing squashes included. The check runs on `fork`, never on the upstream: a related
upstream says nothing about what the agent's `<user>/REPO` holds.
`publish` runs `seed` first (`working.mjs:164-176`), under the repository's lock as `push` does, so `main`
catches up with root before the check below. That means a fork also receives any landed root commits that
upstream doesn't have yet, because they sit under the agent's work. That is expected: the agent's branch is
built on them.
Before the push, the host runs `git ls-remote --symref <fork> HEAD` (same flags and helper). It refuses
`remote-unrelated` when the fork is empty or when that tip isn't a commit the working repo already has and an
ancestor of `main`. When `ls-remote` exits non-zero, for a fork that doesn't exist, a refused token or a
network failure, `publish` refuses `fork-unavailable` and the push doesn't run (N11). Git's stderr isn't
parsed to tell those apart and isn't returned, since it can carry the URL. Only `ls-remote` crosses the
network for this, so nothing is fetched from the fork. If the fork has moved past root, the refusal stands
until root catches up. That is a human pull into root, not an agent's.
D9c. **The URL parse (B6).** `projects.<p>.remote` and `identity.gitea.url` are parsed with `new URL()` at load,
and the business file refuses to load unless all of these hold:
- The scheme is `https:`. There is no username, password, query or fragment.
- The string equals its own normalized form: for the upstream, `url.origin + url.pathname`; for
`gitea.url`, `url.origin + "/"` or `url.origin`. So a dot segment (`/<user>/../OWNER/REPO`), a
percent-encoded dot, an upper-case host, an explicit `:443`, a backslash, userinfo, a query and a fragment
each refuse, because `new URL()` would change them (checked with node: each differs from its normalized
form). A trailing slash survives normalization and is refused by the two-segment rule below. The checks below run on that path, so the owner that is checked is
the owner that is used.
- The upstream's path is exactly `/OWNER/REPO`: two non-empty segments, no more and no fewer. One trailing `.git` on
REPO is allowed and stripped. `fork` is built from the stripped REPO.
- `OWNER`, `REPO` and `gitea.user` each match Gitea's name rule, `^[A-Za-z0-9_.-]{1,100}$` and not `.` or
`..`. REPO after stripping must still match.
- `gitea.url`'s path is `/`. A Gitea served under a sub-path isn't supported by this row and refuses at load.
- Host and port are compared after `new URL()` normalized them, and an omitted port counts as 443 on both
sides.
- Gitea compares user and repository names without case (`lower_name`), so `remote-not-fork` compares the
upstream's OWNER with `gitea.user` without case. An upstream owned by `Coder` and a `gitea.user` of `coder`
would otherwise build a "fork" that is the upstream itself.
D9b. **How `publish` reaches the broker and the store.** The git socket runs in the bus process
(`cli.mjs:504-506`) and reaches the broker only through the host's op table (`bus/src/runtime.mjs:222-246`).
`publish` adds three entries there, each wrapped in `refusing()`:
- `checkPublish(cap, args)`: steps 1 to 3, including the decision check. It returns `{ticket, fork, user,
email, source}` and no token. The ticket holds the session and the decision until `recordPublish`, the
same way `checkLand` holds a landing.
- `withGitea(cap, fn)`: `credentials.useFor(broker.session(cap), "gitea", fn)`. `fn` gets the token and runs
step 5. The token never leaves that call.
- `recordPublish(ticket, result)`: step 6, and it spends the decision.
The op table gets no entry that returns a token.
D10. **Which refs.** Our side: only `refs/heads/<gitea.user>/*`, set by the host (D9 step 3). The default
branch and other agents' prefixes can't be named. Gitea side, two options (Q-M1):
- **O1. A fork per agent. Ruled by Mos (mos-1027).** The agent's Gitea user has read on the upstream repo and owns a fork
named like it. The host derives the fork's URL from the upstream and the agent's user (D9 step 2).
Its token can write only repos the user owns or collaborates on, so Gitea itself stops any push to upstream
or to another agent's fork. No branch rules are needed per agent, so none can drift. The cost: work reaches
upstream as a pull request from the fork, opened by a human or a later API verb.
- **O2. One shared repo. Not chosen.** The agent is a Write collaborator. Upstream's default and release branches get
branch protection with no agent on the push list, plus a rule `<user>/*` per agent. Gitea doesn't stop agent
A from creating an unprotected name outside `A/*` unless a catch-all rule outranks the per-agent ones. That
depends on Gitea's rule priority, which I haven't verified on our version (probe P1). Every hire adds a rule.
Token scopes for either: `write:repository` and `read:user`, nothing else, per vocabulary.mjs rules (no
`all`, no admin).
D11. **The host git socket.** `publish` is added to the socket. `push`, `fetch` and `land` don't change. The
socket's bind rule (E4) adds `git.push.remote`. The working repo stays credential-free and remote-free. The
`fork` URL is passed per call and never written into its config. A landing still squashes into the project
root as "Mosaic bus host". Landings don't push anywhere.
## 6. Lifecycle (brief item 4)
All verbs are on the bus, in the CLI (`mosaic credentials ...`) and in the Console's agent page. Every verb is
human-only. Under Q173 the tester may run `status` and `revoke` on the dev stack instance. `issue` and
`rotate` stay with Jason, because the tester is an agent and the token would pass through its session
(duckworth N1). Each one appends to the organization
writer's `changes.jsonl` (actor, agent, kind, verb, date; never a value). No schema migration, no new event
kind. A refusal uses `action.refused`.
| Verb | What it does |
|---|---|
| `issue AGENT KIND --rotate-by DATE` (gitea) or `--expires DATE` (vikunja) | Reads the token on stdin (never argv), checks E2's format, writes `<kind>.tmp` 0600, fsyncs, renames over `<kind>`, and sets the business file reference to `{"store": true, date}`. The bus reloads that one entry, with no restart |
| `rotate` | Same as `issue` over an existing entry. The old value is gone from disk and memory when the call returns. Jason then deletes the old token in Gitea (A5) |
| `revoke AGENT [KIND] --reason` | Drops the entry from the bus's memory first, then unlinks the file. A use in flight finishes. Every later use refuses `credential-unavailable` |
| `delete AGENT` | `revoke` for every kind, then removes the agent's dir. Refused while the agent has a running claim |
| `status [AGENT]` | Kind, date and state per entry (E3's `status()`), no values |
D12. **"Stops working at once."** On our side this holds without a timing window. A fired agent never holds a
`gitea` or `vikunja` token (D3), so revoking means the bus no longer has it. On the Gitea and Vikunja side the
token still exists until Jason deletes it (A5). The status line for a revoked entry says so until Jason
confirms. The model token is the exception: it sits in the running sandbox (D7), so a fired agent's runs must
end. Row 194's fire ends them.
D13. **What row 194 calls.**
- `credentials.issue` is never called by the chief. Row 194's hire stays uncredentialed (its D1).
- A human `create` of a credentialed definition: row 194 lifts E9's refusal for the human path only. The agent
is created with `store` references and no files. `launch` refuses `credential-unavailable` until Jason runs
`issue`.
- `fire` and `delete`: row 194 calls `credentials.revoke(business, agent, {reason, actor})` before it changes
the business file. A failed revoke stops the fire (`credential-revoke-failed`), so an agent can't be fired
with its credentials still live on our side.
- `regenerate`: no credential change.
## 7. Who does what (brief item 5)
Agents never mint accounts or tokens. Jason's steps, per agent that needs `gitea`:
- A1. Create the Gitea user: Site Administration, User Accounts, Create User Account. The name is the agent's
`gitea.user`. The email is the agent's `identity.email`.
- A2. Create its token: sign in as that user, Settings, Applications, Generate New Token, scopes
`write:repository` and `read:user`, nothing else.
- A3. Create the fork of each granted repo as that user (O1). Gitea's push-to-create
(`ENABLE_PUSH_CREATE_USER`, `ENABLE_PUSH_CREATE_ORG`) stays off, so a push to a missing `<user>/REPO`
fails rather than creating a plain repository (N11). `publish` refuses `fork-unavailable` before that.
- A4. On the stack instance's VM, as its OS user: `mosaic credentials issue AGENT gitea --rotate-by DATE`, with
the token pasted into stdin.
- A5. On fire or rotate: delete the token in Gitea (Settings, Applications), or disable the user.
- A6. Model: sign in to the provider on each VM user to write `<configDir>/secrets/claude-session.env`.
Vikunja bot accounts and tokens are already Jason's (tracker bots). Only A4 changes for them.
## 8. Migration (brief item 6)
- M1. Existing `{file}` and `{env}` references keep working. A business moves one agent at a time with
`issue`, which rewrites that agent's reference to `store`.
- M2. Nothing here touches the dev repository's helper, its `.git/config`, `~/.mosaic` or `~/.mosaic-dev`.
Dev seats (jarvis, seat tokens) keep pushing as today. The mos-0981 rule ends only when Mos says so.
- M3. The live dev stack instance needs no change at re-pin: no reference uses `store` until someone issues
one.
## 9. Questions
Mos ruled on the authority model in mos-1027 (2026-10-20T19:27Z):
- Q-M1. O1, one fork per agent.
- Q-M2. `git.push.remote` and its bounds in D9 to D11 are approved. It lands as a hard wait: out of ROUTED,
and no role grants it within-role (D9 step 1). Sage puts its routing in the PRD version draft going to
Jason with `role.hire` and `role.fire` (Vikunja #420). After his approval, a reviewed commit routes it and
grants it to coder.
- Q-M3. Issue, rotate and delete are human-only, and the chief gets none of them. Under Q173 the dev
instance's tester gets `status` and `revoke` only.
- Q-M4. The model credential stays per stack instance for 10-27. Per-agent model tokens wait for a proxy
row.
Open for Jason. Mos takes these to him one at a time:
- Q-J1. The model account for Case 1 and Case 2: existing subscription or a new account.
- Q-J2. Gitea users and tokens for the agents that need them (A1 to A3).
## 10. What this closes (brief item 7)
- G11: closes the store (D1 to D6) and lifecycle (section 6) for `gitea` and `vikunja`. Still open: per-agent
model credentials (D7), and the same-uid limit inside one stack instance (D6).
- G13: closes "own credentials for git" (D8 to D10). Still open: Gitea API verbs (section 4).
- C2.5 (git remote): Case 2's books repo can use `publish` once its project names a remote.
## 11. Tests and mutants
All tests use scratch roots, fake tokens and a fake Gitea: `git http-backend` behind a node http server on
127.0.0.1 that checks basic auth against a fake token and records each ref update. No request leaves the host.
- T1. Layout: wrong mode or owner on a dir or file refuses start. A `{file}` reference into another agent's
path refuses `credential-not-yours`. A bus restart after a create without `issue` starts, and the agent's
status is `not-issued`. A missing `{file}` still refuses start.
- T2. `useFor` keys from the cap only. A request naming another agent is refused and leaves one
`action.refused`.
- T3. The fake Gitea serves https on 127.0.0.1 with a cert from a scratch CA made by the test. It reaches git
only through the `tls` parameter (D9 step 5). Each refusal test that should reach the network asserts that
the first port received the request, so no test passes because git refused http first. `publish`: the
happy path lands `refs/heads/<user>/<branch>` on the agent's fork in the fake, which shares root's history, at
exactly the decision's commit. Refusals for each step in D9, one test each, including `work-not-yours`. Until routing, a publish with
no decision refuses, one with a decision for another task or commit refuses, and a second publish on a
spent decision refuses `decision-consumed`. Approve while the latest push is A, push B, publish: refuses
`decision-mismatch`, and the fork receives nothing. A push to the working repo between `checkPublish` and
the git push refuses with `ref-moved` or `decision-mismatch` (sage-1440, BUILD.md deviation 14). A raise with no recorded push refuses `no-push`. A test pins that `git.push.remote` is in neither ROUTED list
and that no `roles/*.json` grants it. A force-push attempt is
refused by git's non-fast-forward. A fake fork that answers 302 to a second 127.0.0.1 port: the push
fails, and the second port receives no request with an Authorization header. A remote on the right host
and a different port is refused at step 2. Two agents granted one project each land on their own fork:
agent A's push reaches `/<A's user>/REPO` only and B's reaches `/<B's user>/REPO` only, and the fake's
upstream repository receives nothing. An upstream owned by the agent's own user refuses `remote-not-fork`,
and so does one whose owner differs from `gitea.user` only in case. With an upstream that shares root's
history, a first publish to an empty fork and to an unrelated fork each refuse `remote-unrelated`, and
neither fork nor upstream receives objects. A fork that doesn't exist refuses `fork-unavailable`, and the
fake records no push. `ls-remote` and the push each reach the fork's path only (the fake records every
path). Load refusals (D9c), one test each: a dot segment, an extra path segment, a missing segment,
a trailing slash, userinfo, a query, a fragment, an explicit `:443`, a name outside Gitea's rule, and a `gitea.url` with a
sub-path. A `.git` suffix loads and builds the fork without it. A `publish` grant on an agent without
`identity`, and on one whose definition lacks `gitea`, each refuse at load with the N10 message.
- T4. Token handoff: the token never appears in argv, env, the fork URL, the working repo config,
`changes.jsonl`, events or logs (scan every file under the scratch roots).
- T5. Lifecycle: issue, rotate, revoke and delete, each live with no restart. After revoke, `publish` and the
Vikunja path refuse `credential-unavailable`, and so they do after a bus restart that follows the revoke.
The tester can run `status` and `revoke`, and `issue` and `rotate` refuse it.
- T6. Default: an agent without `identity` commits as E7. Existing `{file}` references load unchanged.
Mutants to kill: drop the cap-derived key in `useFor`, let the request name the destination ref, add `+` to
the refspec, skip the identity check, put the token in the URL, revoke file before memory, skip the dir mode
check, allow a `{file}` reference into another agent's dir, make ENOENT on a `store` path fatal again, make
ENOENT on a `{file}` path silent, a helper that answers any host, drop `http.followRedirects=false`, drop the
`work-not-yours` check, drop the `remote-unrelated` check, add `git.push.remote` to ROUTED, skip the spent
check on the decision, drop the `pushCommit` comparison in `#authorityFor`, push by ref name in place of the
oid, push to the upstream URL in place of the derived fork, run `ls-remote` against the upstream in place of
the fork, take the fork's owner from anywhere but the agent's `gitea.user`, compare the owner with case, run the
D9c checks on the raw path in place of the normalized one, read a failed `ls-remote` as empty, skip the tip
check under the lock, and pass a CA flag in production.
## 12. Risks
- R1. Backups: if a backup tool copies `<configDir>`, it copies tokens. The build checks row 185's backup set
and says what it includes.
- R2. Withdrawn. O1 was ruled, so probe P1 isn't needed.
- R3. Outbound HTTPS from the bus host to Gitea is new network reach for the host process. On the VMs this is
Jason's network to confirm.
-75
View File
@@ -1,75 +0,0 @@
# Row 237: one decision publishes once (#1743)
Megavolt, 2026-10-21 (from `date -u`). Brief: `docs/plans/2026-10-20_agent-credentials.md`
§ "row 199 follow-up N1", blob dca67d310e1b. Source: Duckworth's row 199 round 1 note N1
(duckworth-0068). Base: `cb5fdb064` (row 199's landing). Reviewer Duckworth, gate owner Sage.
## The defect
`checkPublish` opens a ticket after checking the decision is unspent. The decision is spent only at
`recordPublish`, which runs after the bus process has pushed. Two `checkPublish` calls on one decision
therefore both get tickets, and both push. With different branches, the commit lands on two fork refs:
`refs/heads/<user>/feat` and `refs/heads/<user>/other`. The second `recordPublish` then refuses
`decision-consumed`, but by then its push has already reached the fork.
## The fix, and why this option
`checkPublish` refuses `publish-busy` when an open ticket in `#publishes` holds the same business and
decision (`packages/bus/src/broker.mjs:1038-1040`). It runs after the authority and fact checks, so a
spent decision still refuses `decision-consumed` first.
The brief's other option, rechecking `#spent` in `publishTarget`, doesn't close the race. Both tickets
reach `publishTarget` before either push ends, and nothing is spent until `recordPublish`. So both
rechecks pass and both pushes run. The only state that exists while the first push runs is the open
ticket, so the check reads that.
A refusal (`publishRefused`) drops the ticket and leaves the decision unspent, so a retry on the same
decision gets a new ticket. That is row 199's behaviour, unchanged. A within-role publish has no
decision and isn't affected.
## Tests
- `packages/bus/tests/publish.test.mjs:186`. A second ticket refuses `publish-busy`, on the same branch
and on another. After `publishRefused` the retry gets a ticket and a third refuses busy. After
`recordPublish` there is one `action.allowed` for git.push.remote and the next call refuses
`decision-consumed`. Another agent's decision on the same task isn't held.
- `packages/cli/tests/publish-socket.test.mjs:269`. End to end through the git socket, the bus runtime
and the TLS fake Gitea. While the first publish holds its ticket, a second publish on the same
decision to branch `other` gets `publish-busy`. The fake Gitea gets exactly one `git-receive-pack`,
the fork has only `refs/heads/coder/feat`, and there is one `action.allowed`.
Runs on this branch, single files:
- publish.test.mjs: 13/13.
- publish-socket.test.mjs: 8/8.
## Mutant
M1 removes the `publish-busy` line. Both tests fail:
- The bus test fails at the first busy assert.
- The cli test fails because the second publish answers `ok: true` with `ref: refs/heads/coder/other`.
That is the defect: two refs on the fork.
Logs are in `/mnt/storage/scratch/megavolt-r237/` (m1-bus.log, m1b-cli.log). `cmp` confirmed
broker.mjs matched the saved copy after the restore. The first cli mutant run (m1-cli.log) was against
a hook that recursed on every checkPublish. I fixed the hook to fire only for branch `feat` and reran
it (m1b-cli.log).
## Residuals, not fixed here
1. **A ticket that never ends holds its decision until the bus restarts.** `#publishes` is in memory.
That happens when the host dies between checkPublish and recordPublish or publishRefused. It fails
closed (`publish-busy`), and a restart clears it.
2. **publish-timeout (sage-1440).** The socket files `publishRefused` and drops the ticket, but the bus
process's push may still be running. A retry on the same decision can then open a ticket while that
push is live. This is the same residual the socket already logs ("the bus process's push may still
reach the fork"). Closing it means the runtime's `remotes` must keep the decision busy until its push
settles, which reaches into runtime.mjs. That is beyond the brief's two options.
3. **checkLand has the same shape.** `#landings` takes a ticket per call and the decision is spent at
`recordLand`. Whether two landings on one decision can both reach main isn't checked here. That's a
follow-up for Sage to row or dismiss.
## Gate
Per the brief: Duckworth approves the manifest, the bus suite and every `scripts/test-*.sh` are green
on Sage's gate, and M1 is killed. This row also changes a cli test file, so the cli suite (or that file
alone) should run on the gate too.
-71
View File
@@ -1,71 +0,0 @@
# Row 241: one decision lands once (#1747, P19 441)
Megavolt, 2026-10-21 (from `date -u`). Brief: `docs/plans/2026-10-20_agent-credentials.md`
§ "Brief: row 237 follow-up, one decision lands once, with tests at the broker", c25785d3d. Source:
row 237 residual 3, megavolt-0068, megavolt-0069 and sage-1651. Base: origin/refactor f5753c0e8, row 210's landing.
Reviewer Duckworth, gate owner Sage. Tests only; no source changes.
## Why landing has no row 237 race
Row 237 fixed publish: two tickets on one decision both pushed, because the decision is spent only
after the push, and a push to a remote fork can't be taken back. Landing has the same ticket shape
(`checkLand` opens a ticket, `recordLand` spends the decision), but three guards keep one decision
from landing twice:
1. **land-busy, the outer guard.** `doLand` in `packages/cli/src/git-socket.mjs:351` refuses
`land-busy` while another landing runs. The slot is taken before the first await, and freed only
when the landing ends. There is one git socket per launcher, and one host per dataRoot (host.lock).
So through the socket two landings never overlap, and a later one stops at `checkLand` with
`decision-consumed`. Even past that, applying the same reviewed patch to the new main fails
`base-conflict` in `prepareLand`.
2. **The CAS on main.** `finishLand` (`packages/seat/src/working.mjs:391`) moves main with
`update-ref main <commit> <old>`, so a landing prepared on a tip that has since moved refuses
`base-moved` and writes nothing.
3. **The recordLand recheck.** `finishLand` calls `record()`, which is `recordLand`, under the same
repository lock. `recordLand` runs `#landFacts` again (`packages/bus/src/broker.mjs:963`), which
refuses `decision-consumed` once the decision is spent. When `record()` throws, `finishLand` moves
main back.
Guards 2 and 3 can't be reached end to end, because guard 1 stops a second landing first. So they are
pinned below the socket, and no test bypasses land-busy.
### The unrecorded-commit window
Between the `update-ref` and the end of `record()`, main names the squash, which isn't recorded yet.
That window is under the repository's lock, so no other push, review or landing in this host sees
it. If `record()` refuses, main moves back. If moving back fails, `finishLand` refuses
`rollback-failed` and main keeps naming an unrecorded commit (tested at `working.test.mjs:555`, the
main.lock case). That is existing behaviour, unchanged here.
## The test
`packages/bus/tests/land.test.mjs:312`. Two tickets pass `checkLand` on one decision before either
records. `recordLand` on the first lands. `recordLand` on the second refuses `decision-consumed`
(:319), and leaves one merge `action.allowed`, one RESULT and no `action.refused`. `landRefused`, as
the socket would call it, then files the refusal and ends the ticket.
**The assert that pins the recordLand recheck is :319.** The `#spent` line is shared with
`checkLand`, so dropping it also fails :144 (a `checkLand` after the decision is spent). Only :319
shows the recheck at record time.
Single file on this branch: land.test.mjs 18/18.
Whole suites on this branch before in-review: bus 500/500, seat 164/164, cli 565/565 (02:52 to 03:01Z).
## Mutants
| Mutant | Change | Killed by |
|---|---|---|
| M1 | drop the `#spent` line in `#landFacts` (broker.mjs:963) | land.test.mjs: :312 at :319, and :94 at :144 (16/2) |
| M2 | drop `land.old` from the `update-ref` in `finishLand` (working.mjs:391) | working.test.mjs:555 at :567, the `base-moved` assert (25/1) |
| M3 | drop the `land-busy` throw (git-socket.mjs:351) | git-socket.test.mjs:812 at :825, the `land-busy` assert (23/1) |
Each run was one test file on base f5753c0e8 (row 210's landing, which moved the `#spent` line from
:945 to :963), 2026-10-21 02:51Z. Each assert failed with "Missing expected exception" or "rejection".
`cmp` confirmed each source matched its saved copy after the restore. M2 is killed by the existing
working.test:555, so per the brief working.test.mjs isn't changed. Logs: `/mnt/storage/scratch/megavolt-r241/`
(m1.log to m3.log, with each diff).
## Gate
Per the brief: Duckworth approves the manifest; the bus, seat and cli suites and every
`scripts/test-*.sh` are green on Sage's gate; the three mutants are killed.
-108
View File
@@ -1,108 +0,0 @@
# Row 242 build: a retry after publish-timeout pushes once
Row 242, #1748, P19 440. Brief: docs/plans/2026-10-20_agent-credentials.md, "Brief: row 237
follow-up N1, a retry after publish-timeout pushes once" (9d165be8c). Base: origin/refactor
8fede5fc6 (row 210 landed f5753c0e8).
## The defect
The git socket polls publishResult until publishDeadlineMs, then refuses publish-timeout
(packages/cli/src/git-socket.mjs:433) while the bus process's push (runtime publishPush) can
still be running. The socket then calls publishRefused, which went straight to
broker.publishRefused: it filed the refusal and deleted the ticket. checkPublish no longer saw an
open ticket on the decision, so a retry got a new ticket and pushed a second ref while the first
push was still on its way to the fork.
## Fix chosen: hold the decision until the push ends, then record a push that reached the fork
packages/bus/src/runtime.mjs, `publishRefused` and `settle`:
- If the bus has no push for the ticket, publishRefused goes to the broker as before.
- If the push is still running, the refusal code is held on the entry and the ticket stays open.
checkPublish refuses another publish on the decision with publish-busy (row 237's check),
the same as when the socket is still polling.
- When the push ends, `settle` runs:
- A push that reached the fork is recorded with broker.recordPublish, which spends the
decision. The bus logs `pushed after the git socket refused <code>; recorded`.
- A push that failed is filed as publishRefused with the socket's code.
- If recordPublish refuses with a BusError, that code is filed instead.
- If the push has already ended when the refusal arrives, it's settled at once. The sage-1440 test
relies on this: its push fails fast, so publish-timeout is filed once, immediately, as before.
Why hold rather than kill. Killing the git client doesn't undo a push the fork has already
received: receive-pack can update the ref after the client dies. After a kill the bus would have
to ls-remote the fork to learn what landed, and the result could still race the server. Holding
waits for the push's own outcome. Each git run in publishRemote already has its own limit
(publishRemote defaults to checkout.mjs GIT_LIMITS), so a hold is bounded. Holding also reuses the existing publish-busy
state, and the refusal text is unchanged.
Why record and not only hold. If the hold just filed publish-timeout once the push ended, the
ticket would close and the decision would be free again with a ref on the fork. A retry would then
push a second ref. Recording the late push spends the decision, so the retry gets
decision-consumed.
The publish-timeout refusal text is unchanged (brief condition).
## Test
packages/cli/tests/publish-socket.test.mjs:302, "row 242: a retry after publish-timeout refuses
publish-busy while the bus process's push runs; the late push is recorded and the decision pushes
once".
- publishDeadlineMs 150 and a store token. The fork's pre-receive hook writes `held`, then loops
until the test writes `release`.
- The first publish gives publish-timeout. The test waits for `held`.
- A retry to branch `other` gives publish-busy (:322). Refused events: ["publish-busy"]. No
git.push.remote allowed event.
- The test writes `release` and waits until there is exactly one allowed event, with ref
refs/heads/coder/feat and the work commit.
- The next retry gives decision-consumed (:330).
- Exactly one git-receive-pack request reached the fake Gitea, and the fork has only
refs/heads/coder/feat. Refused events: ["publish-busy", "decision-consumed"]. No TOKEN appears in
events or logs.
Fail fast (sage-1681): the body is wrapped in try/finally, and the finally writes `release`. Under
the first mutant run, the assert at the retry failed and the hook was never released. setup's
teardown waits for the fake Gitea, which waited for the held push, so the file hung until I stopped
it. Now a failed assert ends the hook and the run. Each wait is bounded at 10 s with an assert.fail.
## Mutants
Script: /mnt/storage/scratch/megavolt-r242/mutants.sh. Each mutant: save, one-line sed, run
publish-socket.test.mjs whole, restore, cmp.
| Mutant | Change | Expected kill |
|---|---|---|
| M1 | export `publishRefused: (ticket, args) => broker.publishRefused(ticket, args)` again (removes the fix) | :302 at :322: the retry gets a new ticket and its own push, so publish-timeout, not publish-busy (seen in the stopped 03:06Z run) |
| M2 | `settle` files publishRefused without trying recordPublish (hold only) | :302 at the wait for one allowed event, then :330 |
Results (Sage's slot, sage-1686, 2026-10-21T03:35:00Z to 03:35:15Z): both killed, both restored
(cmp ok).
- M1: publish-socket 8/1, the row 242 test fails at :322 in 0.5 s.
- M2: publish-socket 8/1, the row 242 test fails at :327, the wait for one allowed event, through
the assert.fail inside `until` (:311) after its 10 s bound, in 10.3 s.
## Suites
On Sage's slot (sage-1686), on base 8fede5fc6 plus this row:
- bus `node --test tests/*.test.mjs`: 499/499, 03:35:20Z to 03:35:34Z. This is one fewer than
Sage's 241 gate (500) because the base predates row 241's land.test case.
- cli `node --test tests/*.test.mjs`: 566/566, 03:35:34Z to 03:43:47Z. That's 565 plus the
row 242 test.
Singles before the slot: publish-socket.test.mjs 9/9, bus publish.test.mjs 13/13. The seat suite
and scripts/test-*.sh are left for Sage's landing gate.
## Residuals
- The agent was told publish-timeout, but the push is recorded later and the run isn't
notified. The allowed event and the bus log line are the only trace. A later publish on the
decision gets decision-consumed, which tells the agent the decision was used. Whether the run
should get a RESULT for a late record is a follow-up question for Sage.
- A late push whose recordPublish refuses (Duckworth N2) is filed with that code: the ref stays on
the fork and the decision is not spent, the same residual as an in-band push whose record
refuses.
- The hold lives in the bus process's memory, like the broker's open tickets. A bus restart during
a hold drops both, the same as today for any publish in flight.
-8
View File
@@ -1,8 +0,0 @@
# PRD 1.8 r7: no in-Stack raise (mos-1154)
Sage, 2026-10-21T01:20:06Z. Jason approved PRD 1.8 in T3 (thread 38bc4c81, before 2026-10-21T01:16:55Z), verbatim:
"PRD 1.8: I am preemptively approving it. Do not surface in the Mosaic Stack board. Just proceed."
Mos records it as DECISIONS Q192 (mos-1154). The [mos test] raise request drafted here was dropped
unsent, and the template is deleted. Jason closed 3a6c8fb9 himself at 01:05:45Z (no, "superseded by r7").
Scope (Mos's reading): r7 at 47c2cb33c plus 82df17347, 342 lines, sha256
de079281ff2760911039276379ec4b190287b2ce2e4e8a79c9459939727440cd, Duckworth duckworth-0073 (pass, no findings).
@@ -1,35 +0,0 @@
{
"seq": 22,
"id": "3a6c8fb9-a5ab-42f8-9755-902a88375ca2",
"at": "2026-10-20T22:41:14.444Z",
"business": "mosaic-stack",
"project": null,
"raised_by_role": "pm",
"raised_by_run": "rc9e993da24cf",
"class": "gated",
"action": "prd.approve",
"route_to": "human",
"question": "Approve PRD 1.8 exactly as drafted?\n\nFile: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Duckworth reviewed it (duckworth-0061, confirmed duckworth-0062) and Mos checked it (mos-1140). The tip of branch honker/row194-b2 holds a later r6 (aba3cbdeb), which is not used here. This approval covers only the r5 commit and sha256 above. The PM copied these values from the request and did not compute or check the sha256.\n\nNotes the approval carries:\n1. Mos's reading: \"the main agent or chief of staff\" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.\n2. Mos's reading: revoke by \"you, Mos or the tester\" means the user, the assistant (\"Mos\", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.\n3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.\n4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.\n5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.\n\nSource: a tester request relayed by the chief (message 6990efb4, tester message d54a190e). It is a test request, not Jason's instruction. It takes the same route as PRD 1.7 (decision d52febb5): non-blocking, no task, uncited, so it goes to the human's Inbox. I recommend yes because two reviewers have passed r5 and the open points are written down above, not hidden.",
"options": [
{
"key": "yes",
"text": "Approve PRD 1.8 exactly as drafted: the r5 file at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 with the sha256 below, and only that."
},
{
"key": "no",
"text": "Don't approve. Say what changes, and a new revision comes back for review."
}
],
"recommendation": "yes",
"task_ref": null,
"requirement_ref": null,
"blocking": false,
"supersedes": null,
"spec_path": null,
"spec_commit": null,
"authorization": {
"action": "prd.approve",
"target": null,
"approvalChoice": "yes"
}
}
-37
View File
@@ -1,37 +0,0 @@
[
{
"seq": 22,
"id": "3a6c8fb9-a5ab-42f8-9755-902a88375ca2",
"at": "2026-10-20T22:41:14.444Z",
"business": "mosaic-stack",
"project": null,
"raised_by_role": "pm",
"raised_by_run": "rc9e993da24cf",
"class": "gated",
"action": "prd.approve",
"route_to": "human",
"question": "Approve PRD 1.8 exactly as drafted?\n\nFile: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Duckworth reviewed it (duckworth-0061, confirmed duckworth-0062) and Mos checked it (mos-1140). The tip of branch honker/row194-b2 holds a later r6 (aba3cbdeb), which is not used here. This approval covers only the r5 commit and sha256 above. The PM copied these values from the request and did not compute or check the sha256.\n\nNotes the approval carries:\n1. Mos's reading: \"the main agent or chief of staff\" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.\n2. Mos's reading: revoke by \"you, Mos or the tester\" means the user, the assistant (\"Mos\", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.\n3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.\n4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.\n5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.\n\nSource: a tester request relayed by the chief (message 6990efb4, tester message d54a190e). It is a test request, not Jason's instruction. It takes the same route as PRD 1.7 (decision d52febb5): non-blocking, no task, uncited, so it goes to the human's Inbox. I recommend yes because two reviewers have passed r5 and the open points are written down above, not hidden.",
"options": [
{
"key": "yes",
"text": "Approve PRD 1.8 exactly as drafted: the r5 file at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 with the sha256 below, and only that."
},
{
"key": "no",
"text": "Don't approve. Say what changes, and a new revision comes back for review."
}
],
"recommendation": "yes",
"task_ref": null,
"requirement_ref": null,
"blocking": false,
"supersedes": null,
"spec_path": null,
"spec_commit": null,
"authorization": {
"action": "prd.approve",
"target": null,
"approvalChoice": "yes"
}
}
]
-13
View File
@@ -1,13 +0,0 @@
[mos test] Please have the PM raise a prd.approve decision for PRD 1.8, the same route as PRD 1.7 (decision d52febb5): blocking false, no task_ref, recommendation yes, gated to the human. The PM must not compute or change the sha256; it copies the values below.
Question: "Approve PRD 1.8 exactly as drafted?"
File: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit 762a6711ff7ed049aa73dfde98b75ebf326bdd99 (revision 5), 319 lines, sha256 8eee52470b1d7df139164e3ba5cc128dc469457b2b52f298c12685c3483011a8. Reviewed by Duckworth (duckworth-0061, confirmed duckworth-0062) and checked by Mos (mos-1140). The branch tip of honker/row194-b2 holds a later r6 (aba3cbdeb) that is not used; the approval covers only the r5 commit and sha256 above.
The decision notes must carry these five items:
1. Mos's reading: "the main agent or chief of staff" in Q10 is one role under two names, the chief of staff. Only the chief raises a grant request; the assistant passes the user's request to the chief. To change it, say so at approval and REQ-GRANT-1 changes first.
2. Mos's reading: revoke by "you, Mos or the tester" means the user, the assistant ("Mos", Jason's assistant) for grants in its own organization, and the tester only on a dev instance.
3. N5: host.sudo keeps the arbiter route version 1.7 set for git.push.remote. A raise for it that cites the spec and isn't within-role goes to the arbiter for its domain, not to the user. In manual mode it comes to the user.
4. B7 residual: only an Approve creates a grant, but an agent running as the stack's OS user can still write the stack's own store (Q182 on dragon-lin, #423 on .53 and .54). This version doesn't close it.
5. N6: the broker enforces only that the chief of staff raised a grant request. The rest of the chain of command is the messaging lists and role contracts, not a broker check.
Reply with the decision id once raised. Do not resolve it.
-8
View File
@@ -1,8 +0,0 @@
sent d54a190e to chief
chief (RESULT, re d54a190e) cc46a7b4:
I passed your request to the pm without changing any of it, and launched the pm so it can act on it. No decision id yet, because the pm hasn't raised the decision.
- **Message to pm:** 6990efb4-8321-4d0e-9b8a-771f65625b83. It says the request comes from a tester and authorizes nothing, and it carries your PRD 1.8 values and the five notes as you wrote them.
- **pm launch:** run rc9e993da24cf.
When the pm answers with the decision id, I'll send it to you.
-5
View File
@@ -1,5 +0,0 @@
trail 3a6c8fb9-a5ab-42f8-9755-902a88375ca2: 4 row(s)
2026-10-20T22:40:47.949Z events#1550 session.launched pm
2026-10-20T22:40:47.969Z role_claims#102 claim pm by rc9e993da24cf
2026-10-20T22:41:14.444Z decisions#22 gated prd.approve → human: Approve PRD 1.8 exactly as drafted? File: agents/honker/work/queue-194/PRD-1.8-DRAFT.md on mosaicstack/stack at commit …
2026-10-20T22:41:14.445Z events#1553 action.allowed pm decision.raise decision 3a6c8fb9
-21
View File
@@ -1,21 +0,0 @@
# Row 66 round 3: rebase only
Sage, 2026-10-20. Round 2 (candidate 0f4a57956, manifest cee63e06fdea) was approved by Filbert (29442) and
Dewey (29443). Its base 5487ed39d is now far behind refactor, and rows 190 and 192 changed four of its paths.
Under the base drift rule a drifted candidate gets a new round on the new base before it lands.
- Candidate: branch `sage/row66-r3`, commit 2369c00e36874e1100b301e0d01cbd105c0c5d4f, two cherry-picks of 8ec03b45a and 0f4a57956 onto c40d846cb.
No conflicts and no hand edits.
- `git patch-id --stable` of the r3 diff (c40d846cb..2369c00e36874e1100b301e0d01cbd105c0c5d4f) equals the r2 diff's (5487ed39d..0f4a57956):
6d085621fd355d43ccaf0efaefe7e74179888ff2.
- Manifest `candidate-manifest-r3.sha256`, sha256 42e3876ca1689719f0e07e66fc9fbd6e4faf3c07678da024accb54c58caef0d1. Same 23 paths as r2. Four hashes differ, and only
because the base moved: `docs/TOOLS.md`, `scripts/stack-copy.sh`, `packages/cli/tests/stack-copy.test.mjs`,
`packages/webui/tests/pinned-copy.test.mjs`. The other 19 are byte-identical to r2.
- Singles on the new base (`singles-r3.log`): every touched test file plus the unslop selftest, all green.
`packages/bus/tests/channel-proof.test.mjs` pins plug-in hashes and names `index.mjs`; it reads only channel
plug-ins, not the two harness files this row changes, and it passes (2/0).
- Still to do: the full gate on my slot after the MVP gates, then the landing sessions on both harnesses that
the row's gate names.
What I ask the reviewers: confirm the rebase. In the four drifted files, check that row 66's hunks sit correctly
next to rows 190 and 192 (`stack-copy.sh` COPY_PATHS in particular). Nothing else changed.
@@ -1,23 +0,0 @@
b99e7f4bf58ac68c8f5fa58eb7b90c0f85b15ca9a80fd093f6aaeafeac931608 adapters/README.md
c32d2a3276db92433d4e3c4f4b45fab048d9a46119d9f69cead6a11cfacfeadb agents/sage/work/queue-66/BUILD.md
ac318150f5e6193adf3750d76b64c643c29f324310c4004b0bdde8caef863b53 docs/TOOLS.md
64a2d00ba57b8673c0905f322711023c10d4dfdc0ccc7e6daa2c440b4d57e34e packages/cli/tests/stack-copy.test.mjs
b027dc74583217b4151173ecbabfcfbfc7b108ac6b7a8e93dd21a2834051714f packages/harness/README.md
bfb2156c609eeaf87c9e209c562576a5ef47535eaeee61d841bb0bd75a76a69d packages/harness/src/runner.mjs
0a27f2c8de7b9191f9c0d4a209331c63ff7630923e669838b41715924b0a64b0 packages/harness/tests/bundle.test.mjs
81d0352f2dcd41eb2e2ef962365d9b97515d7e8d9668a35bb9ce9dce4f1451ea packages/harness/tests/claude-session.test.mjs
b16f33442cefd5b5ae2af85edab33250ee6b31025e437edf0e4eba9fe0afa793 packages/harness/tests/fixtures/fake-adapter.mjs
1e187a2e23a197640d139e6975dfaf83dd7e337c360d128833678dc60f3e2046 packages/harness/tests/pi-session.test.mjs
37ca6ad6467573658c1ac6807b6cd00e3f9da289107fb689190a1872bff9b89c packages/harness/tests/runner.test.mjs
04c24058f37260c9192d5036c8cf28b50faba8e3e3bf57f198d233485d0d57e6 packages/plugins/harness/claude-code/index.mjs
2c1252e8e8db3ba4551bf1e34156ddf4e933cc664fc157105bef9a2705d6ce3b packages/plugins/harness/pi/index.mjs
7e0bea27d4360a39874fa64b3506a85f83888613a285ec0b087d74bf22b76421 packages/plugins/README.md
ec4ec9502ee8e766e3abd04d9f724c0d8ff4ef114394a0f76d5320c2dc715c5c packages/plugins/tests/harness.test.mjs
f6141dac9a8c61573e4198480fed85e093992ee9c19fb55e9bc2752381b7b07a packages/seat/tests/sandbox.test.mjs
09c6cadaf2e27677c95ba5bcb6a359cb6b3ce596ba1db6171b26586d4f9834a8 packages/webui/tests/pinned-copy.test.mjs
e9ff91cd073a1b784f9ea440b18db913ce8107df6246e3b0b0e962eda43f046b scripts/stack-copy.sh
7ee6ed6e6ac55b1348d40a358372e66c86fdebacd90001d447040133e1187882 tools/unslop-hook/claude-hook.js
ce1dfb11d0a8e8efd3d6dae67cc259c2e7443a62c7416bdedd4c71306d8c6df1 tools/unslop-hook/extension.ts
f07d67806f40763b4be87d31c2976448b5cfc34d7328519fae2d8313b1ca3e20 tools/unslop-hook/README.md
dbe3d5c9ae654d2401e3c496a6b03757a438e4a551fd8e460df8c82902bb80a7 tools/unslop-hook/test-unslop-check.js
76be78178a0703b9d20a7df2278ecb13f429dea90cea45180f22302e267ea25e tools/unslop-hook/unslop-check.js
-11
View File
@@ -1,11 +0,0 @@
packages/cli/tests/stack-copy.test.mjs rc=0 ℹ pass 14 ℹ fail 0
packages/harness/tests/bundle.test.mjs rc=0 ℹ pass 22 ℹ fail 0
packages/harness/tests/claude-session.test.mjs rc=0 ℹ pass 10 ℹ fail 0
packages/harness/tests/pi-session.test.mjs rc=0 ℹ pass 10 ℹ fail 0
packages/harness/tests/runner.test.mjs rc=0 ℹ pass 39 ℹ fail 0
packages/plugins/tests/harness.test.mjs rc=0 ℹ pass 9 ℹ fail 0
packages/seat/tests/sandbox.test.mjs rc=0 ℹ pass 10 ℹ fail 0
packages/webui/tests/pinned-copy.test.mjs rc=0 ℹ pass 10 ℹ fail 0
unslop rc=0 28/28 passed
SINGLES_DONE
2026-10-20T23:15:14Z
@@ -196,37 +196,3 @@ a piece. It passed, so the "pm doesn't answer" branch does not apply and
no defect is filed.
Piece 1 is Jason's next real Mosaic Stack request to pm. It is not open yet.
## 2026-10-21T01:14Z dates (sage-1622, Mos mos-1148, Vikunja #442)
Mos accepted the plan with changes C1 and C2. The dates are latest-by markers (Q111).
1. Pieces 1-5 open by 2026-10-23 23:59Z. Mos gives Jason the plain command after tonight's open items with him
(Q13 on PRD 1.8, the 1012 and 1008 boot). I don't ask again.
2. C1: no piece is pinned to a copy. A piece runs on whatever copy is live when Jason sends it. For each piece
this log records the activation id at send. A piece still open across a window gets the new activation id
recorded too.
3. C2: neither the Q175 phase 2 window (10-23) nor the 10-24 re-pin window rolls back or restores while a piece
has history, unless that window has failed. If a restore happens anyway, this log names the pieces to redo.
4. All five closed and reviewed by 2026-10-26 18:00Z. Jason reads the five trails at his gate on 10-26 or
10-27. A pass starts merge plan step 1 under decision 146, and step 4 stays his.
5. Fallback: if fewer than five pieces are open by 10-23, the gate moves to 10-28. No agent opens a piece in
Jason's place.
When Jason sends a piece, I tell Mos its message id.
Correction (2026-10-21T01:06Z): the heading above says 01:14Z. The clock read 2026-10-21T01:05:56Z when I wrote that entry, so 01:06Z is the right time.
## 2026-10-21T01:20Z Jason's pre-S7 checks (mos-1154)
Between 01:11:06Z and 01:13:03Z Jason sent pm five requests with `mosaic talk pm`: ed9e1e14, 51c19692,
94f5d62c, 0645a3b7, f75449c0 (bus seq 1583 to 1591). pm run rc9e993da24cf answered each in 4 to 16 s. They were
two jokes, the sandwich, the purpose of Mosaic Stack and pm's mission. pm made no task and raised no decision
for any of them, and said so each time. Mos's reading, which I take: none is a piece, because a piece has to
become work that gets closed and reviewed. They are recorded as Jason's pre-S7 checks. The five pieces are
still open, latest-by 10-23 23:59Z. Activation at send: copy 4429a104f, as in the 01:06Z entry (C1). Source: Mos
(mos-1154) read the bus. I didn't read the message bodies.
pm also told Jason that its state snapshot lists only the first 50 of 198 open tasks and that it pages the rest
with list_tasks. That is the row 109 (#1603) design: broker.mjs `state` returns `cap: TASK_PAGE` (50) with
`total`, and list_tasks pages up to TASK_PAGE_MAX (200).
@@ -1,27 +0,0 @@
{
"at": "2026-10-20T23:19:35.714Z",
"comments": [
{
"task": 413,
"status": 201,
"id": 411,
"readBack": 200,
"found": true
},
{
"task": 424,
"status": 201,
"id": 412,
"readBack": 200,
"found": true
},
{
"task": 414,
"status": 201,
"id": 413,
"readBack": 200,
"found": true
}
],
"problems": []
}
@@ -1,51 +0,0 @@
// Sage, 2026-10-20: row 192 (Q175 phase 1, #1690) landed. One comment on P19 412, per mos-0882 (every
// landing or decision gets a Vikunja comment). Sage's own key (bot user 13); the sync key reads back. Comments only:
// no task edits, labels, assignees or bucket moves. Checks the task's title before posting. Prints no secret.
// Usage: node comment-412-r192-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
const POSTS = [
{ id: 412, title: /^Console SSO: zero-trust OIDC verification/, body: [
"Row 192 (Q175 phase 1, Gitea #1690) landed and pushed. Landing commit 4429a104f (Launchpad's launchpad/row192 53bb59462..37582e820, manifest round 2, 18 paths; Webby approved). Patch-id 4a486902614d matches the reviewed diff. Queue revs 2429-2430 (landing note and done) are in 305857c8f, range 32b54ac3b..305857c8f on refactor. #1690 is closed.",
"Sage gate on 088087ac9: package suites 2758 pass 0 fail, scripts/test-*.sh 429 pass 0 fail, mutants 22 of 23 killed (#23, the second origin check, ruled equivalent). The Console now verifies SSO itself under --sso as an OIDC relying party on oauth4webapi 3.8.8, pinned exactly. mosaic-config.mjs load is now async, so in-flight rows that call it rebase and await it (row 190 round 3 is doing this).",
"Not live. The live stack still runs copies/490e2771d3d0. The new dependency needs npm ci in a new stack copy, so the live re-pin is a window with Mos (mos-1099 item 3); Sage sends Mos the plan. Row 200 (phase 2) follows.",
] },
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
for (const P of POSTS) {
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
}
writeFileSync(OUT, "", { flag: "wx" });
for (const P of POSTS) {
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
rec.comments.push(e);
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(e, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
save();
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);
@@ -1,59 +0,0 @@
// Sage, 2026-10-20: catch-up landing and status comments on P19 413 (row 189), 424 (row 210) and 414 (rows 201
// and 204), per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
// Checks each task's title before posting. Prints no secret.
// Usage: node comment-catchup-2026-10-20.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
const POSTS = [
{ id: 413, title: /^stack-release\.sh activate --no-send/, body: [
"Row 189 landed on refactor as ace27dc5e (Gitea #1687): stack-release.sh activate --no-send drops the Console's --send and keeps the pointer and the unit in step. Dewey approved (comment 29485), and Sage's gate passed.",
"The stack activation log shows the same-copy activate on 490e2771d3d0 after the landing: activation 20261020T192956Z-490e2771d3d0, result ok at 19:30:00Z, with backup 20261020T192956Z-490e2771d3d0. Since then the live Console runs without --send. The re-pin to 4429a104f at 22:02Z (row 192) carried the pointer's flags, no --send and no --sso, as its W3 plan check required.",
] },
{ id: 424, title: /^Credential store: per-agent host access/, body: [
"Status, 2026-10-20 23:20Z. Row 210 (Gizmoduck) builds this task. Its design r3 is approved, and the machine-grant design final r6 f9039682e is approved for code once row 210 lands (mos-1141, mos-1143). Every grant name sits under grant.* behind one guard (mos-1144). No agent creates a grant: the chief requests, Jason approves, and protected code creates it (Q188).",
"Order (mos-1135, mos-1137): row 210 lands only after row 199, the per-agent credential store. Row 199's gate is green on c6868ba41. Megavolt fixed five surviving mutants and is rerunning all 28 now on 7c3cdd940, before the Duckworth review. Rows 190, 199 and 210 go live together in the one 10-24 window with the 3i-3j migration (mos-1145 line 9), backup first, under Q173.",
] },
{ id: 414, title: /^MVP platform: the Given list for Cases 1 and 2/, body: [
"RBAC rows landed (Given list, RBAC enforcement). Row 201, FED R1: access.json loader and validator with covers, afad4993f (Gitea #1699), Gizmoduck, Duckworth approved (comment 29581). Gate: business 173/0, scripts/test-*.sh 422/0, mutants 44 of 44 killed.",
"Row 204, 193a: the access level check widened to viewer, d0bbc5c1c (Gitea #1702), Morgana, Duckworth approved (comment 29595). LEVELS gains collaborator and viewer, covers() takes a required least level, and an unknown grant level covers nothing. Gate: packages and scripts 2789/0, mutants 7 of 7 killed.",
"Neither changes the live stack. Rows 205-208 (193b1 to 193d) follow, and 206 adds a schema step, so it goes live only in a --migrate window.",
] },
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
for (const P of POSTS) {
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
}
writeFileSync(OUT, "", { flag: "wx" });
for (const P of POSTS) {
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
rec.comments.push(e);
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(e, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
save();
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);
@@ -1,60 +0,0 @@
// Sage, 2026-10-21: re-plan comments on P19 405-408 and 411 (due 10-22). Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
// Checks each task's title before posting. Prints no secret.
// Usage: node comment-p19-405-411-replan.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
const POSTS = [
{ id: 405, title: /^stack-release --plan: no test plans a missing store/, body: [
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
] },
{ id: 406, title: /^stack-release --plan: backup line printed/, body: [
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
] },
{ id: 407, title: /^stack-release\.sh: the proof_for_target comment/, body: [
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
] },
{ id: 408, title: /^docs\/TOOLS\.md: the row 178 paragraph/, body: [
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
] },
{ id: 411, title: /^stack-release\.sh and webui-service\.sh: the proof file repository check/, body: [
"Re-plan (Sage, 2026-10-21, due 10-22). Tasks 405, 406, 407, 408 and 411 become one queue row, a stack-release follow-up row. All five touch scripts/stack-release.sh, scripts/webui-service.sh or docs/TOOLS.md, and rows 232 (Darkwing) and 238 (Launchpad) are changing stack-release.sh now. So I brief the row after 232 and 238 land, from their landing, which avoids base drift (the row 68 rule). It lands after the 10-24 window, because that window holds the four service scripts at NEW (P5). 411's hold was the Part B window, and Part B is superseded (Q175, mos-0970), so that hold is gone. Each item keeps the test and mutant its task names. I'll name the owner when I brief it, either the seat that frees up first of Darkwing and Launchpad, or me.",
] },
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
for (const P of POSTS) {
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
}
writeFileSync(OUT, "", { flag: "wx" });
for (const P of POSTS) {
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
rec.comments.push(e);
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(e, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
save();
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);
@@ -1,53 +0,0 @@
// Sage, 2026-10-21: landing comments on P19 412 (row 200) and 424 (row 199), per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
// Checks each task's title before posting. Prints no secret.
// Usage: node comment-r200-r199-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
const POSTS = [
{ id: 412, title: /^Console SSO: zero-trust OIDC verification/, body: [
"Row 200 landed on refactor as 9e1085816 (Gitea #1698, closed): stack-release.sh activate --sso, the phase 2 prerequisite. Launchpad built it and Webby approved r3. Sage's run killed 13 of 14 mutants (M12 is the equivalent ruled in round 2). Gate: 15 package suites and 10 scripts/test-*.sh suites, all green, at 00:07:57Z.",
"Follow-up N1 is row 238 (Gitea #1744, Launchpad, Webby). The sso-config hint will carry --instance NAME for a named instance. Nothing is live until a window activates a copy that includes 9e1085816.",
] },
{ id: 424, title: /^Credential store: per-agent host access/, body: [
"Row 199 landed on refactor as cb5fdb064 (Gitea #1697, closed). It adds the per-agent credential store, the agent's own git identity and pushes to a remote. Megavolt built it and Duckworth approved r1 (comment 29779). Gate on 9e1085816 plus the patch: packages 2735/0, scripts 429/0.",
"Row 210 (Gizmoduck) now rebases onto cb5fdb064 on a new branch (mos-1135, mos-1137). Rows 234 and 235 follow it. Rows 190, 199 and 210 go live together in the 10-24 window with the 3i-3j migration, backup first, under Q173 (mos-1145).",
] },
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
for (const P of POSTS) {
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
}
writeFileSync(OUT, "", { flag: "wx" });
for (const P of POSTS) {
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
rec.comments.push(e);
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(e, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
save();
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);
@@ -1,66 +0,0 @@
// Sage, 2026-10-20: W11 of the re-pin window 490e2771d to 4429a104f (STATE.md in commit 99a406ed3) on P19 386, 412
// and 393, and the row 190 landing (00d4cc1f6, #1688) on P19 414, per mos-0882. Sage's own key (bot user 13); the
// sync key reads back. Comments only: no task edits. Checks each task's title before posting. Prints no secret.
// Usage: node comment-w11-r190-2026-10-20.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
const STATE = "agents/sage/work/window-repin-4429a104f/STATE.md in commit 99a406ed3 (pushed), sha256 a4d0a7b12f431f15";
const POSTS = [
{ id: 386, title: /^Q173 test system: tester writes/, body: [
"Q173 re-pin window, 490e2771d to 4429a104f (row 192), done. STATE report: " + STATE + ".",
"Activation 20261020T220206Z-4429a104f6d6 with --pm, exit 0, every gate check including pm_launched. Store stays on 3i, migration none. Backup ~/.mosaic-dev/backups/stack/20261020T220206Z-4429a104f6d6 (SHA256SUMS b61d6484), rollback set beside it; rollback would go to 490e2771d. Business file unchanged (sha256 1e49bdf0). Console flags unchanged: no --send, no --sso, --tester-port 7333.",
"W0 to W10 passed, nothing rolled back. W10 stopped once on two browser checks; both were checks written before row 192 (the hidden #sign-out link, and a chat check that read only the first project list). Mos passed them (mos-1135) and the corrected checks are in the plan. Matrix line g is 6575 bytes, not 6454, because row 192 grows index.html by 121 bytes; Mos passed it (mos-1137).",
"S1 to S2 store deltas include task_snapshots +7 and task_comment_cursors +5. The writer is the bus's Vikunja poll (packages/tasks/src/adapter.mjs tick, sync.mjs recordTask), source 'poll', not the window. Nobody acted as Jason: no --by jason, no pi, no Save, no mosaic name, bus.tester untouched.",
] },
{ id: 412, title: /^Console SSO: zero-trust OIDC verification/, body: [
"Row 192 (Q175 phase 1) is live in copy 4429a104f since activation 20261020T220206Z (Q173 window with Mos). The sign-in code is in the copy but dormant: the Console runs without --sso, and the proof pair stays unset. The new oauth4webapi 3.8.8 dependency came in through npm ci in the new copy and imports cleanly from it.",
"STATE report: " + STATE + ". The webui journal since activation shows one 'Proxy: proxy proof off' line and no 'Sign-in:' line, as expected without --sso.",
] },
{ id: 393, title: /^dragon-lin: seven legacy user units in failed state/, body: [
"Current failure times read during the Q173 re-pin window W11 (systemctl --user show, CDT with UTC): mosaic-standup-pm.service failed 2026-10-19 19:00:28 CDT (10-20 00:00:28Z), exit 1. mosaic-install-drift.service failed 2026-10-20 00:12:54 CDT (05:12:54Z), exit 9. [email protected] failed 2026-10-20 13:16:12 CDT (18:16:12Z), exit 4.",
"The marker's journal at 18:16:12Z says its tmux send to jarvis failed ('tmux target not found', socket mosaic-fleet), so that page never arrived; the record is in ~/.local/state/mosaic-unit-failures/20261020.jsonl. mosaic-reconcile.service itself last ran OK at 17:16:27 CDT (22:16:27Z), so the marker is stale until reset.",
"All three predate the re-pin at 22:02Z and come from the legacy ~/.mosaic and jarvis-brain setup. The window changed none of them. Disabling or resetting any still needs Jason or ops-01's charter. Details: " + STATE + ", section 'Failed user units'.",
] },
{ id: 414, title: /^MVP platform: the Given list for Cases 1 and 2/, body: [
"Row 190 (several Mosaic Stack instances on one host, MVP T1.1, Gitea #1688) landed and pushed: 00d4cc1f6 on refactor (99a406ed3..00d4cc1f6). Squash of Darkwing's darkwing/row190-r3 176a6bf4b..df65a3d4c, 25 paths, manifest cc0ea9e8, Filbert approved round 3; patch-id matches the branch diff.",
"Sage gate on d0bbc5c1c with the patch: 15 package suites 2664 pass 0 fail, 10 scripts/test-*.sh suites 429 pass 0 fail. Mutants 20 of 20 killed. Not live: the stack runs copy 4429a104f until a re-pin. Row 230 (C2-13a) now carries row 190's @CONFIG_ENV@ and @SESSION_ENV@ tokens (gosalyn-0058 item 4).",
] },
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
for (const P of POSTS) {
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
}
writeFileSync(OUT, "", { flag: "wx" });
for (const P of POSTS) {
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
rec.comments.push(e);
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
const found = list(back.json).find((x) => x.id === c.json.id);
Object.assign(e, { readBack: back.status, found: Boolean(found) });
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
save();
}
save();
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
if (rec.problems.length) process.exit(2);
@@ -1,66 +0,0 @@
// Sage, 2026-10-21: files row 237's review follow-ups N1 and N3 (duckworth-0069, #1743 comment 29810) as two P19 tasks.
// Sage's own key (bot user 13); the sync key reads back. No labels, assignees, due dates or bucket moves. Refuses if
// the title exists. Prints no secret.
// Usage: node file-r237-followups.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
const PROJECT = 19;
const SIG = "Created by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const ps = (sig, xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(sig)}</p>`;
const TASKS = [
{
title: "Bus publish: a retry after publish-timeout can push a second fork ref while the first push still runs (row 237 N1)",
description: ps(SIG, [
"Duckworth's row 237 review N1 (#1743 comment 29810, duckworth-0069). Row 237 makes checkPublish refuse publish-busy while an open ticket holds the same business and decision. publishRefused drops the ticket on publish-timeout, but the bus process's push may still be running, so a retry to another branch can push a second ref to the fork.",
"The fix belongs in packages/bus/src/runtime.mjs, in `remotes`: either keep the ticket until the push process exits, or kill the push on timeout and wait for it. A test should start a slow push, time it out, retry to another branch, and expect one ref on the fork. Not in row 237's scope. Megavolt's BUILD.md residual 2 (agents/megavolt/work/queue-237/BUILD.md). Needs a queue row once 237 lands.",
]),
},
{
title: "Bus land: check whether checkLand has the same open-ticket race that row 237 fixed in checkPublish (row 237 N3)",
description: ps(SIG, [
"Row 237 residual 3 (Megavolt's BUILD.md) and Duckworth's N3 (#1743 comment 29810): checkLand has the same ticket shape as checkPublish had before row 237. Two lands on the same decision may both pass the check before either records. Duckworth didn't check it.",
"First step: read checkLand and its ticket code in packages/bus/src/broker.mjs against row 237's fix (broker.mjs:1038-1043). If the race exists, file a queue row with a race test like publish-socket.test.mjs's; if not, record why and close this task.",
]),
},
];
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
async function api(method, path, body, auth) {
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
return { status: r.status, json };
}
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
async function existing() {
const out = [];
for (let page = 1; page < 20; page++) {
const r = await api("GET", `/api/v2/projects/${PROJECT}/tasks?per_page=50&page=${page}`, undefined, sync);
if (r.status !== 200) throw new Error(`list tasks: ${r.status}`);
out.push(...list(r.json));
if (list(r.json).length < 50) return out;
}
throw new Error("more than 19 pages");
}
const rec = { at: new Date().toISOString(), project: PROJECT, created: null, problems: [] };
const save = () => {
const out = JSON.stringify(rec, null, 1) + "\n";
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
writeFileSync(OUT, out);
};
const have = await existing();
for (const T of TASKS) if (have.some((t) => t.title === T.title)) throw new Error("title exists; refusing");
writeFileSync(OUT, "", { flag: "wx" });
rec.created = [];
for (const T of TASKS) {
const r = await api("PUT", `/api/v1/projects/${PROJECT}/tasks`, { title: T.title, description: T.description }, key);
const c = { status: r.status, id: r.json?.id ?? null };
rec.created.push(c);
if (r.status >= 300 || !Number.isSafeInteger(r.json?.id)) { rec.problems.push(`create: ${r.status} ${r.json?.message ?? ""}`); continue; }
const back = await api("GET", `/api/v2/tasks/${r.json.id}`, undefined, sync);
Object.assign(c, { readBack: back.status, done: back.json?.done });
if (back.status !== 200 || back.json?.title !== T.title || back.json?.done) rec.problems.push(`read-back differs for ${r.json.id}`);
}
save();
console.log(JSON.stringify(rec));
@@ -1,41 +0,0 @@
{
"at": "2026-10-21T01:06:25.071Z",
"comments": [
{
"task": 405,
"status": 201,
"id": 416,
"readBack": 200,
"found": true
},
{
"task": 406,
"status": 201,
"id": 417,
"readBack": 200,
"found": true
},
{
"task": 407,
"status": 201,
"id": 418,
"readBack": 200,
"found": true
},
{
"task": 408,
"status": 201,
"id": 419,
"readBack": 200,
"found": true
},
{
"task": 411,
"status": 201,
"id": 420,
"readBack": 200,
"found": true
}
],
"problems": []
}
@@ -1,13 +0,0 @@
{
"at": "2026-10-20T21:35:49.240Z",
"comments": [
{
"task": 412,
"status": 201,
"id": 395,
"readBack": 200,
"found": true
}
],
"problems": []
}
@@ -1,20 +0,0 @@
{
"at": "2026-10-21T00:28:32.038Z",
"comments": [
{
"task": 412,
"status": 201,
"id": 414,
"readBack": 200,
"found": true
},
{
"task": 424,
"status": 201,
"id": 415,
"readBack": 200,
"found": true
}
],
"problems": []
}
@@ -1,19 +0,0 @@
{
"at": "2026-10-21T00:38:47.170Z",
"project": 19,
"created": [
{
"status": 201,
"id": 440,
"readBack": 200,
"done": false
},
{
"status": 201,
"id": 441,
"readBack": 200,
"done": false
}
],
"problems": []
}
@@ -1,16 +0,0 @@
// Sage, 2026-10-21: read-only. Prints id, done, title, the last update and the description as plain text for the
// given P19 tasks, with the sync key. Writes nothing. Prints no secret.
// Usage: node read-tasks.mjs https://tasks.woltje.com SYNC_TOKEN_FILE ID...
import { readFileSync } from "node:fs";
const [ORIGIN, SYNC, ...IDS] = process.argv.slice(2);
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
const sync = readFileSync(SYNC, "utf8").trim();
const text = (h) => (h ?? "").replace(/<\/p>|<br\s*\/?>|<\/li>/g, "\n").replace(/<[^>]+>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").trim();
for (const id of IDS) {
const r = await fetch(`${ORIGIN}/api/v2/tasks/${id}`, { headers: { Authorization: `Bearer ${sync}` }, redirect: "error" });
const j = await r.json().catch(() => null);
if (r.status !== 200 || j?.project_id !== 19) { console.log(`== ${id}: status ${r.status}, not P19`); continue; }
const out = `== ${id} done=${j.done} updated=${j.updated}\n${j.title}\n${text(j.description)}\n`;
if (out.includes(sync)) throw new Error("secret in output");
console.log(out);
}
@@ -1,34 +0,0 @@
{
"at": "2026-10-20T22:51:44.218Z",
"comments": [
{
"task": 386,
"status": 201,
"id": 406,
"readBack": 200,
"found": true
},
{
"task": 412,
"status": 201,
"id": 407,
"readBack": 200,
"found": true
},
{
"task": 393,
"status": 201,
"id": 408,
"readBack": 200,
"found": true
},
{
"task": 414,
"status": 201,
"id": 409,
"readBack": 200,
"found": true
}
],
"problems": []
}
@@ -1,74 +0,0 @@
# 10-24 window: re-pin 4429a104f to the row 234 landing, with the 3i-3j migration (draft 0)
Sage, 2026-10-21T01:05Z, draft 0, for mos-1145 under DECISIONS Q173. This is the skeleton. NEW isn't fixed yet,
so Mos doesn't check this draft. Draft 1 names NEW and goes to Mos for the P0 check. The step layout follows
`repin-4429a104f.md`. Steps that match are named, not copied.
## Scope (mos-1145, mos-1134)
- One window carries the code of rows 190, 199 and 210, plus row 234 (row a) with its schema step 3i to 3j
through `activate --migrate`, agent-run under Q173, backup first.
- Row a lands before the .53 and .54 installs, by 10-25 (mos-1134). If row 234 hasn't landed by 10-24 12:00Z,
I ask Mos whether to run the window without it (no migration) or to move it.
- Jason has no step. Nobody acts as Jason. No pi (P19 383). No Save, no `mosaic name`. Nothing is pruned.
- `--sso` stays off. Row 200's `activate --sso` is in NEW as a tool. Turning it on changes who reaches the Console
from outside, so that's Jason's (Q173 exclusion). It has its own window, the Q175 phase 2 window on 10-23
(mos-1148). That window runs first, so this window's OLD may be the phase 2 copy, and P1 reads it from
`status`.
## Names
- OLD, live: `4429a104f6d65ccf165f6c83b5d1dce3147dfde0`, schema 3i, pointer `pm: true` (record 99a406ed3).
P1 re-reads the activation id and the Console flags from `status`. I don't copy them from the last window.
- NEW: open. It's the row 234 landing, or a later code landing if Mos rules. Candidates in landing order:
194, 234 (210 and 237 have landed).
- REPO, `P`, `WIN=agents/sage/work/window-repin-<NEW12>/`, `SNAP`: as in `repin-4429a104f.md`, with the
sha checks at P0 and before S1, S2 and any D3.
## What NEW carries over OLD (so far, refactor 8fede5fc6)
`git log --first-parent 4429a104f..HEAD`, path-limited to packages, scripts, roles, contracts, adapters, skills
and the package files:
| Row | Commit | Live effect to check |
|---|---|---|
| 201 | afad4993f | business: access.json loader. Check: the live business has no access.json, and load stays LOAD_OK (P1). |
| 204 | d0bbc5c1c | business: 193a viewer level. Same check. |
| 190 | 00d4cc1f6 | Several instances per host. Touches `stack-release.sh`, `webui-service.sh`, `bus-service.sh`, `stack-copy.sh`. The default instance must render the same units: P2 diffs the rendered units OLD vs NEW for `mosaic-stack`, and any difference stops the window until it's explained. |
| 200 | 9e1085816 | Tool: `activate --sso`. Off here. `--plan` must show no `--sso` line. |
| 199 | cb5fdb064 | Credential store per agent. Check: bus start with no store entries for the live agents changes nothing, and the chief's and pm's launches still pass. The P3 rehearsal covers start. |
| N3 | 390ea5261 | README only. |
| 230 | c8bc5cf4c | Case 2 scripts, tools only. Not run live. |
| 210 | f5753c0e8 | Host access. The live business has no `hosts`, so nothing is granted. Bus start runs the host-run sweep (M34d). P3 must show it removes nothing. |
| 237 | d7d943199 | Publish busy check (199 N1). |
| 234 | open | Machine grant row a and the 3i to 3j schema step. `--plan` must say `migration: 3i -> 3j`. |
| PRD 1.7+1.8 | 0adf8ddfe | `docs/prd/mosaic-stack.md` and `packages/tasks/requirements.json` move to version 1.8 (mos-1154, mos-1156, DECISIONS Q192). The live pm cites 1.6 until this window. Check after: the pm's requirements read answers version 1.8. |
Service script drift OLD to HEAD is 415+/91- over the four scripts, mostly row 190. P5 holds them at NEW.
## Backups and the migration
- W5: the stopped store backup that every activate takes (row 185), with SHA256SUMS, SCHEMA 3i and STAMP,
plus the rollback set. Before `--migrate` writes, I check that SCHEMA reads 3i and the backup SHA256SUMS hash.
- W0b: the business file copy, as in the last window.
- Rollback after a migration isn't the plain rollback. OLD is 3i and the store is 3j, so going back needs
`--restore <W5 activation>`, and that loses the events written after W5. The row 181 restore guard counts
11 tables, and the untimed ones need the D3 snapshot. Mos decides any restore past W5, as before.
- P3 is the rehearsal with `--migrate` on a copy of the live backup. It must show the 3j step and the row
counts before and after.
## Row 42 (S7) interaction
Row 42's pieces may be in flight at window start (sage-1622, mos-1148 C1 and C2). W1 adds: list open pm tasks, and either pm
finishes or parks them before W5, or I record which piece crosses the window. No restore while a piece has
history unless the window fails.
## Open before draft 1
- Q1: NEW, once 234 lands. Do 194 and 237 go in if they land first? I recommend yes for 237 (small, bus
only) and asking Mos about 194, since 194 changes the lifecycle verbs and roles/chief-of-staff.*.
- Q2, settled by mos-1148: phase 2 is its own window on 10-23. Draft 1 takes OLD and the Console flags,
`--sso` included, from `status` after that window.
- Q3: who operates. If Darkwing is free, Darkwing operates and I check. Otherwise I operate as in the last
window.
- The P-step evidence list for rows 199 and 210 at bus start, written once both have landed.
@@ -1,125 +0,0 @@
# Re-pin 490e2771d to 4429a104f (row 192, Q175 phase 1), with npm ci in the new copy
Sage, 2026-10-20, draft 1, for mos-1099 item 3 under DECISIONS Q173. Mos checks this file at its commit before
P0. Any change to this file or to NEW after the check means Mos checks again (mos-0849/0850).
This is a smaller window than `repin-rows-177-185.md` (draft 2, which Mos checked in mos-0929). Where a step is
the same, this file names that file's step and says what differs. The 7332 matrix, the snapshot, the W9 list and
the rollback compare are reused unchanged.
## Who, and what nobody does
- Sage operates, as the tester. Darkwing holds the gate lane for row 190 round 3 tonight, so I take the
operator seat. Mos verifies each step's output before the next one and runs `rehearse` (P3), which reads the
live config and a live backup (`docs/TOOLS.md` `rehearse`: Jason or Mos runs it).
- Jason has no step. Q173 covers a re-pin of this test system with a backup first and a STATE report after.
Nothing is pruned.
- Nobody acts as Jason: no `--by jason`, no human shell or tmux pane, every bus read and write carries
`--tester`, and no request carries `jason.woltje`. `bus.tester` stays as it is; if it isn't `true`, stop at P1.
- No pi (mos-0865 D2, P19 #383). No Save in the human Console, no `mosaic name`.
- Outside reach doesn't change. The Console keeps its current flags. `--sso` stays off: row 192's sign-in code is
dormant without the flag, and `cli.mjs:57` refuses `--sso` with the preview flags the pointer carries anyway.
The proof pair stays unset. The tunnel still forwards 7332 only (P4 and W10 check it).
## Names
- OLD, live: `490e2771d3d057ab76991e2c7859cd251857e64c`, activation `20261020T192956Z-490e2771d3d0`, pointer
`pm: true`, schema 3i, Console `--port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username
--allow-user jason.woltje --tester-port 7333` (no `--send`, read from `stack-release.sh status` between 21:36Z and 21:39Z).
- NEW: `4429a104f6d65ccf165f6c83b5d1dce3147dfde0`, row 192's landing, 103 commits past OLD (`git rev-list --count 490e2771d..4429a104f`).
The 106 that `status` showed is OLD to HEAD 305857c8f, so it also counts the three record commits below.
The commits after it (5b8def2f1 plans, 176a6bf4b sessions, 305857c8f queue) are records and don't move NEW. If
another code row lands before W0 (row 190 is the likely one), NEW stays here unless Mos rules otherwise.
- REPO: the canonical checkout. `activate` runs REPO's `webui-service.sh`, so the four service scripts are held
at NEW for the window (P5).
- `P=/mnt/storage/scratch/sage-pin-de352dfaf` (`business-check.mjs`, `console-check.sh`, checked against
`agents/sage/work/window-plan/repin-de352dfaf/SCRIPTS.sha256`).
- `WIN=agents/sage/work/window-repin-4429a104f/`, where every transcript goes.
- `SNAP=$WIN/snapshot.mjs`, a copy of `agents/darkwing/work/window-rows-177-185/snapshot.mjs`, sha256
`639fd28a2674aaa03ce824e3394b4052f21cdef7d6fa49bae61a9927ed341e67` (mos-0899). I check the sha before S1, S2
and any D3.
## What NEW carries over OLD
The path-limited log over `packages`, `scripts`, `roles`, `contracts`, `adapters`, `skills` and the package files:
| Row | Commit | Live effect |
|---|---|---|
| 186 | 269134d8d | Tool only: the activate plan's backup line says what the run will do. |
| 187 | 9a71e4e80 | Tool: activate sets, carries and drops the proof pair. The pointer has no pair, so nothing changes. |
| 188 | 8fc0ee216 | Tool: proof file checks on a carried pair. No pair, no effect. |
| 163 | 8ecc7a239 | Tool: `rehearse`, which P3 uses, and `bus start --no-launch`. CLI `cli.mjs` and `launcher.mjs` gain the `--no-launch` path; the default start is unchanged. |
| 189 | ace27dc5e | Tool: `activate --no-send`. Live already runs without `--send`; the pointer carries that. |
| 192 | 4429a104f | Console `--sso` (off here), `scripts/mosaic-config.mjs` load becomes async, `webui-service.sh` renders `--sso` only when given, and a new dependency, oauth4webapi 3.8.8. `cli.mjs` imports `sso.mjs` at the top, so the Console won't start from a copy without that module. |
No schema step: `SCHEMA_VERSION` (`packages/bus/src/store.mjs:17`) is 3i at OLD and NEW, and no migration was
added. `--plan` must say `migration: none`. No roles, skills, contracts, adapters or plug-in kinds changed. So: no
harness set, no skill carry, no `pm-on`. One `activate --pm`.
## npm ci in the new copy
mos-1099 asks for npm ci in the new copy. `stack-copy.sh copy` already does it: it extracts the archive to a temp
dir, refuses without `package.json` and `package-lock.json` (:55), runs `npm ci --no-audit --no-fund` there and
writes no copy if it fails (:56). W0 checks the result: `node_modules/oauth4webapi/package.json` in the copy reads
3.8.8, and `node --input-type=module -e "await import('oauth4webapi')"` from the copy exits 0. The live copy is
163M; `/home` has 30G free (94% used), enough for one more.
## Backups
| Before | Backup | Taken by |
|---|---|---|
| W5 | the stopped store, `$DATA/backups/stack/<W5 activation id>/` with SHA256SUMS, DATA_ROOT, SCHEMA 3i, STAMP | `stack-release.sh` (row 185: every activate) |
| W5 | the rollback set (units, wrapper, pointer) | `stack-release.sh` |
| W9 | the business file, `cp -p` to `~/.config/mosaic-dev/businesses/mosaic-stack.json.pre-window-<stamp>` with its sha256, as evidence nothing wrote it | Sage, in W0b |
## Before the window (nothing stopped)
| # | Who | Step |
|---|-----|------|
| P0 | Mos | The `$P` sha check, as in the 177-185 plan P0. |
| P1 | Sage | The business file, read only, the same values as the 177-185 plan P1. I report its sha256 now rather than assume the 06:15Z value. `business-check.mjs` for OLD now and for NEW after W0: LOAD_OK both. |
| W0 | Sage | `cd $REPO && scripts/stack-copy.sh copy $NEW`, in a gap between gate-lane runs, since npm ci is heavy. Then the two npm checks above, and `diff <(git -C $REPO show $NEW:package-lock.json) $COPIES/$NEW/package-lock.json` empty. W0 writes only under `copies/` and changes nothing live, so it moves before P2 and P3, which need the copy. |
| P2 | Sage | `console-check.sh $NEW` against a stub Console of NEW on a scratch port, once without and once with `--tester-port` on a second scratch port. CONSOLE_PASS both. |
| P3 | Mos | `scripts/stack-release.sh rehearse $NEW mosaic-stack --backup 20261020T192956Z-490e2771d3d0 --port <scratch>`. It must pass every gate check but the PM's. It stops no unit and writes no pointer. Transcript in `$WIN/p3/`. Correction (mos-1107): `rehearse` starts NEW's Console with only `--port` and `--business`, not the pointer's flags. So Mos also starts NEW's Console by hand with the pointer's flags on two scratch ports and reads healthz and `/api/config` on both, then stops it (`$WIN/p3/flags-check.txt`, `flags-console.log`). |
| P4 | Sage | The tunnel read, as in the 177-185 plan P4. |
| P5 | Sage | `git -C $REPO diff --quiet $NEW HEAD -- scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh` and an empty `git status --porcelain` on the same four. Row 190 round 3 touches `webui-service.sh`; if it lands before W5, P5 fails and the window waits for Mos's ruling. |
| P6 | Sage | The 7332 matrix (a)-(g) on OLD, as in the 177-185 plan, saved as `$WIN/matrix-old.txt`. |
## The window
| # | Who | Step |
|---|-----|------|
| W0b | Sage | The business file copy. |
| W1 | Sage | Idle check (`mosaic agents --business mosaic-stack --tester`, `mosaic launches list`), the start stamp from `date -u`, then S1 `node $SNAP --store "$DATA/bus/bus.sqlite" --held`. |
| W2 | Sage | `stack-release.sh status mosaic-stack`: pointer on OLD, `pm: true`, 3i, last line ok, unit flags match. |
| W3 | Sage | `stack-release.sh activate $NEW mosaic-stack --pm --plan`. Must show from OLD to NEW, `migration: none`, the backup line, and the Console line with OLD's flags exactly: no `--send`, no `--sso`, no proof pair, `--tester-port 7333`. Any other difference stops here. |
| W4 | Sage | The unit reads, as in the 177-185 plan W4. |
| W5 | Sage, on Mos's go | `stack-release.sh activate $NEW mosaic-stack --pm`. Exit 0, `result ok`, every gate check including `pm_launched`. Record the activation id, the backup path and STAMP, and the backup SHA256SUMS sha256. Then `status`. |
| W9 | Sage, after Mos verifies W5 | The 177-185 plan's W9 list unchanged, the chief turn and the one `assistant` thread turn included (mos-0929 Q3), plus two lines: the webui journal since the W5 stamp (with `UTC`) has one `Proxy: proxy proof off` and no `Sign-in:` line, and `ls -l /proc/<Console pid>/cwd` names the NEW copy. |
| W10 | Sage | The browser check on 127.0.0.1:7333 and the matrix again as `$WIN/matrix-new.txt`, identical to `matrix-old.txt`. Then P4 again. Corrections after the window (mos-1135, mos-1137): the matrix line for a file the pin changes isn't "identical". Line (g) serves `index.html`, which row 192 grows by 121 bytes (6470 to 6591), so (g) is 200 non-json at 6575 bytes, not 6454. The two browser checks come from the 177-185 window, before row 192 and before a second thread project. "Only tester forms" now allows `#sign-out` while it is `hidden` and `/api/config` has no `sso: true`. "Chat lists the W9 thread" now reads every `.chat-projects section.panel` list (chat.js:93 renders one per project), not the first. |
| W11 | Sage | STATE report in `$WIN/STATE.md`, posted on P19 386 and P19 412 and on Gitea #1690. |
## Stop conditions and rollback
The same as the 177-185 plan. Two failures in one window: stop and report (mos-0823).
Rollback: `stack-release.sh rollback mosaic-stack --plan`, then the real run. The previous distinct copy in the
log is 490e2771d, both are 3i, so no `--restore` and no data loss. OLD takes the same Console flags. The R1 and
R1post compares are as in the 177-185 plan. A restore past W5 follows that plan's D3 steps, with Mos deciding.
Keep every copy. Prune nothing.
## Questions for the plan check
- Q1: NEW is row 192 alone, not bundled with row 190. Row 190 (several instances) changes `stack-release.sh`
and `webui-service.sh` and deserves its own window. I recommend running this one first.
- Q2: Sage operates instead of Darkwing, who is on the row 190 gate. OK?
- Q3: W0 before P2 and P3. It writes a copy only. OK?
## What Sage ran for this plan
Read only, 2026-10-20 between 21:36Z and 21:39Z (clock read 21:39:50Z at the commit): `stack-release.sh status mosaic-stack` (pointer, last five log
lines, unit flags match, 106 commits of drift), `git rev-list --count`, `SCHEMA_VERSION` at both ends, the
path-limited log, `git diff --stat` on the service scripts, CLI and config loader, `stack-copy.sh:23-56`,
`webui/src/cli.mjs:15-120`, `docs/TOOLS.md` on `rehearse`, `du -sh` of the live copy and `df -h /home`. Nothing
live was changed.
@@ -1,3 +0,0 @@
chief rbb708846b605 claude-code since 2026-10-20T19:29:59.053Z
pm rc36b17ef3788 claude-code since 2026-10-20T20:20:56.278Z
# exit 0
@@ -1,256 +0,0 @@
{
"settings loaded": {
"pass": true
},
"settings": {
"banner": "Tester Console. Changes are recorded as tester, never as the human.",
"h1": "Settings",
"forms": [
"sign-out",
"conv-form",
"routes-form",
"instance-add-form",
"instance-change-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form"
],
"convFormHidden": true,
"panels": [
"Routing",
"Role instances",
"Seat names"
],
"instancesTable": true,
"nameForms": 6,
"routesForm": true,
"routesSubmitEnabled": true,
"forbidden": [],
"tokenLimitsPanel": false
},
"tester banner": {
"pass": true,
"detail": "Tester Console. Changes are recorded as tester, never as the human."
},
"instance, routes and name editors present": {
"pass": true
},
"only tester forms (conv-form is index.html:21, global and hidden)": {
"pass": false,
"detail": [
"sign-out",
"conv-form",
"routes-form",
"instance-add-form",
"instance-change-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form",
"tester-name-form"
]
},
"no launch settings, assistant, limit reset or send": {
"pass": true,
"detail": []
},
"no Token limits panel (plan W10 item 2, amended)": {
"pass": true
},
"board": {
"busViewHidden": true,
"banner": true,
"headings": [
"Waiting on you –",
"All sessions –"
],
"unavailable": false,
"length": 179,
"forms": [
"sign-out",
"conv-form"
]
},
"board reads": {
"pass": true,
"detail": {
"busViewHidden": true,
"banner": true,
"headings": [
"Waiting on you –",
"All sessions –"
],
"unavailable": false,
"length": 179,
"forms": [
"sign-out",
"conv-form"
]
}
},
"chat": {
"hash": "#/chat",
"banner": true,
"threads": [
{
"href": "#/chat/threads/d0ba7a66-aa3f-4918-9518-94132bce6b28",
"name": "w9",
"instance": "assistant"
}
],
"conversationOpen": false,
"forms": [
"sign-out",
"conv-form",
"chat-project-form"
]
},
"chat lists the W9 thread": {
"pass": false,
"detail": [
{
"href": "#/chat/threads/d0ba7a66-aa3f-4918-9518-94132bce6b28",
"name": "w9",
"instance": "assistant"
}
]
},
"W9 thread page not opened": {
"pass": true,
"detail": "#/chat"
},
"tester banner on chat": {
"pass": true
},
"requests": [
{
"method": "GET",
"url": "/api/config",
"refused": false
},
{
"method": "GET",
"url": "/api/config",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/messages",
"refused": false
},
{
"method": "GET",
"url": "/api/bus-status",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/messages",
"refused": false
},
{
"method": "GET",
"url": "/api/settings",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/usage",
"refused": false
},
{
"method": "GET",
"url": "/api/settings",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/usage",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/names",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/names",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/instances",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/instances",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/routes",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/routes",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/limits",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/limits",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/inbox",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/agents",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/messages",
"refused": false
},
{
"method": "GET",
"url": "/api/chat/projects",
"refused": false
},
{
"method": "GET",
"url": "/api/chat/threads",
"refused": false
},
{
"method": "GET",
"url": "/api/business",
"refused": false
}
],
"no write through the listener": {
"pass": true,
"detail": []
},
"no request to the W9 thread messages or open": {
"pass": true,
"detail": []
}
}
W10 browser: FAIL
# exit 1
@@ -1,200 +0,0 @@
{
"config": {
"sso": null
},
"forms": [
{
"id": "sign-out",
"hidden": true,
"action": "/auth/logout",
"method": "post"
},
{
"id": "conv-form",
"hidden": true,
"action": null,
"method": null
},
{
"id": "routes-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "instance-add-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "instance-change-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
},
{
"id": "tester-name-form",
"hidden": false,
"action": null,
"method": null
}
],
"chat": {
"hash": "#/chat",
"open": false,
"projects": [
{
"project": "window-20261020-0801",
"threads": [
"#/chat/threads/d0ba7a66-aa3f-4918-9518-94132bce6b28"
]
},
{
"project": "window-20261020-2205",
"threads": [
"#/chat/threads/446201de-e8b5-4df3-9356-d0d5422cc899"
]
}
]
},
"requests": [
{
"method": "GET",
"url": "/api/config",
"refused": false
},
{
"method": "GET",
"url": "/api/config",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/messages",
"refused": false
},
{
"method": "GET",
"url": "/api/bus-status",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/messages",
"refused": false
},
{
"method": "GET",
"url": "/api/settings",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/usage",
"refused": false
},
{
"method": "GET",
"url": "/api/settings",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/usage",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/names",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/names",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/instances",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/instances",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/routes",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/routes",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/limits",
"refused": false
},
{
"method": "GET",
"url": "/api/bus/limits",
"refused": false
},
{
"method": "GET",
"url": "/api/config",
"refused": false
},
{
"method": "GET",
"url": "/api/chat/projects",
"refused": false
},
{
"method": "GET",
"url": "/api/chat/threads",
"refused": false
},
{
"method": "GET",
"url": "/api/business",
"refused": false
}
]
}
sign-out hidden true, sso null; W9 thread listed true; thread page open false; writes 0
# exit 0
@@ -1,8 +0,0 @@
a 200 ok=true
b 401 error="sign-in required"
c 401 error="sign-in required"
d 403 error="proxy Host refused"
e 403 error="tester listener required"
f 403 error="proxy header refused"
g 200 non-json 6575 bytes
# exit 0
@@ -1,7 +0,0 @@
# P4 2026-10-20T22:31:29Z
DropInPaths=
ExecStart={ path=/home/jwoltje/.local/bin/chisel ; argv[]=/home/jwoltje/.local/bin/chisel client --fingerprint ce+ISFvMAhuoCRDuc7riIf+hYz87TLHk/C9C/H8QsUg= --keepalive 25s --max-retry-interval 10s http://10.1.1.227:8080 R:0.0.0.0:7332:127.0.0.1:7332 ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
# exit 0
# R: specs (1): R:0.0.0.0:7332:127.0.0.1:7332
P4 PASS: one R: spec R:0.0.0.0:7332:127.0.0.1:7332, DropInPaths empty
# exit 0
@@ -1,6 +0,0 @@
ActiveState=active
DropInPaths=
MainPID=2950923
ExecStart={ path=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/scripts/mosaic ; argv[]=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/scripts/mosaic bus start mosaic-stack --pm ; ignore_errors=no ; start_time=[Tue 2026-10-20 17:02:08 CDT] ; stop_time=[n/a] ; pid=2950923 ; code=(null) ; status=0/0 }
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
# exit 0
@@ -1,12 +0,0 @@
● jarvis-curation-gate.service loaded failed failed Jarvis curation-gate liveness check (wake the agent if staged pulls sit uncurated)
● jarvis-retro-check.service loaded failed failed Jarvis weekly retrospective ARTIFACT check (did Sunday's tick produce a retro?)
● jarvis-retro.service loaded failed failed Jarvis weekly retrospective tick
● mosaic-install-drift.service loaded failed failed Assert the installed Mosaic binaries still match the committed source (drift, not failure)
● mosaic-standup-pm.service loaded failed failed Coordinator standup trigger (evening)
● [email protected] loaded failed failed Escalate failed Mosaic unit jarvis-curation-gate.service to the local agent seat
● [email protected] loaded failed failed Escalate failed Mosaic unit jarvis-retro-check.service to the local agent seat
● [email protected] loaded failed failed Escalate failed Mosaic unit jarvis-retro.service to the local agent seat
● [email protected] loaded failed failed Escalate failed Mosaic unit mosaic-install-drift.service to the local agent seat
● [email protected] loaded failed failed Escalate failed Mosaic unit mosaic-reconcile.service to the local agent seat
● setspark-tmp-guard.service loaded failed failed SetSpark: /tmp and scratch guard
# exit 0
@@ -1,5 +0,0 @@
ActiveState=active
SubState=running
ActiveEnterTimestamp=Tue 2026-10-13 08:40:19 CDT
MainPID=1317
# exit 0
@@ -1,6 +0,0 @@
ActiveState=active
DropInPaths=
MainPID=2950925
ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/packages/webui/src/cli.mjs serve --checkout /mnt/storage/src/mosaic-stack --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --tester-port 7333 ; ignore_errors=no ; start_time=[Tue 2026-10-20 17:02:08 CDT] ; stop_time=[n/a] ; pid=2950925 ; code=(null) ; status=0/0 }
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
# exit 0
@@ -1,2 +0,0 @@
chief r097436c91816 claude-code since 2026-10-20T22:02:08.951Z
# exit 0
@@ -1,7 +0,0 @@
PASS markdown-preview.js is served and equals the copy's
PASS /api/config on loopback: send=false edit=false
PASS project.files through the Console: head=ae0b17c9586e51da7bc1b7e548c6acf913a55665 files=2 spec=true error=none
PASS project.file spec/north-star.md through the Console: commit=ae0b17c9586e51da7bc1b7e548c6acf913a55665 length=99 error=none
PASS loopback save refused: HTTP 403 error=read-only sent=false
CONSOLE_PASS
# exit 0
@@ -1,19 +0,0 @@
[
{
"business": "mosaic-stack",
"instance": "chief",
"run": "r097436c91816",
"launchedBy": "jason",
"harness": "claude-code",
"model": "claude-opus-5-5",
"provider": "anthropic",
"modelKey": "opus",
"pid": 2951160,
"startTime": "63491912",
"runnerPid": 2951161,
"runnerStartTime": "63491912",
"runDir": "/home/jwoltje/.mosaic-dev/launches/mosaic-stack/r097436c91816",
"startedAt": "2026-10-20T22:02:08.941Z",
"live": true
}
]
@@ -1,2 +0,0 @@
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
# exit 0
@@ -1,2 +0,0 @@
[]
# exit 0
@@ -1,2 +0,0 @@
created project window-20261020-2205 id 77e7b45f-8c34-49a3-b2fa-9cf565177c30
# exit 0
@@ -1 +0,0 @@
1792533965
@@ -1,21 +0,0 @@
pointer: /home/jwoltje/.mosaic-dev/state/stack-active.json
{"copy":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","business":"mosaic-stack","pm":true,"claude":"/home/jwoltje/.local/bin/claude","console":["--port","7332","--public-host","mosaic.woltje.com","--auth-header","X-authentik-username","--allow-user","jason.woltje","--tester-port","7333"],"schema":"3i","activation":"20261020T220206Z-4429a104f6d6","at":"2026-10-20T22:02:09Z"}
log (last five lines):
{"at":"2026-10-20T07:58:02Z","op":"activate","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T075800Z-490e2771d3d0","activation":"20261020T075800Z-490e2771d3d0","result":"ok"}
{"at":"2026-10-20T19:29:56Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":null,"activation":"20261020T192956Z-490e2771d3d0","result":"started"}
{"at":"2026-10-20T19:30:00Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T192956Z-490e2771d3d0","activation":"20261020T192956Z-490e2771d3d0","result":"ok"}
{"at":"2026-10-20T22:02:06Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":null,"activation":"20261020T220206Z-4429a104f6d6","result":"started"}
{"at":"2026-10-20T22:02:09Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":"20261020T220206Z-4429a104f6d6","activation":"20261020T220206Z-4429a104f6d6","result":"ok"}
check:
bus: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
Console: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
CLI: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
result: pass
bus status:
host: mosaic-stack, pid 2950923, running, since 2026-10-20T22:02:08.133Z, notifier off
socket: present
writer.lock: pid 2950981
boot: organization file loaded at 2026-10-20T22:02:08.125Z
unit flags: match the pointer
drift: 6 commit(s) on refactor since the pointer's copy
# exit 0
@@ -1,4 +0,0 @@
sent c5b43f04 to chief
chief (RESULT, re c5b43f04) c3e236d7:
Got the re-pin of 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 from the tester; I took no action because a tester message authorizes nothing, and I launched and changed nothing.
# exit 0
@@ -1,4 +0,0 @@
sent b4b66ced to window-20261020-2205/w9
assistant (RESULT, re b4b66ced) cfa7ea99:
Assistant (run r497f1c817d4d) got the [mos test] message and is replying in one line as asked, without treating it as Jason's instruction or acting on it.
# exit 0
@@ -1,32 +0,0 @@
[
{
"seq": 570,
"id": "b4b66ced-d088-4b9c-a944-306f164028f9",
"at": "2026-10-20T22:05:58.283Z",
"business": "mosaic-stack",
"from_role": "tester",
"from_run": "tester-cli",
"to_role": "assistant",
"class": "REQUEST",
"in_reply_to": null,
"decision": null,
"corrects": null,
"body": "[mos test] reply with one line, then stop",
"to_name": null
},
{
"seq": 571,
"id": "cfa7ea99-7ccb-4225-80a6-9a903540e337",
"at": "2026-10-20T22:06:04.247Z",
"business": "mosaic-stack",
"from_role": "assistant",
"from_run": "r497f1c817d4d",
"to_role": "tester",
"class": "RESULT",
"in_reply_to": "b4b66ced-d088-4b9c-a944-306f164028f9",
"decision": null,
"corrects": null,
"body": "Assistant (run r497f1c817d4d) got the [mos test] message and is replying in one line as asked, without treating it as Jason's instruction or acting on it.",
"to_name": null
}
]
@@ -1,2 +0,0 @@
created window-20261020-2205/w9 id 446201de-e8b5-4df3-9356-d0d5422cc899 instance assistant
# exit 0
@@ -1,3 +0,0 @@
window-20261020-0801/w9 assistant d0ba7a66
window-20261020-2205/w9 assistant 446201de
# exit 0
@@ -1 +0,0 @@
446201de-e8b5-4df3-9356-d0d5422cc899
@@ -1,165 +0,0 @@
{
"total": {
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
},
"byRun": [
{
"run": "r8fba7e598156",
"input": 0,
"output": 0,
"cacheWrite": 0,
"cacheRead": 0,
"unknownTurns": 1
},
{
"run": "r4a96e2142e4d",
"input": 2,
"output": 52,
"cacheWrite": 11750,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "rc78c9700c8e9",
"input": 14,
"output": 1519,
"cacheWrite": 18187,
"cacheRead": 89559,
"unknownTurns": 0
},
{
"run": "r293f33bcbc63",
"input": 2,
"output": 44,
"cacheWrite": 18595,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "r34520a581318",
"input": 6,
"output": 323,
"cacheWrite": 26349,
"cacheRead": 14307,
"unknownTurns": 0
},
{
"run": "r7e99838f9f93",
"input": 2,
"output": 44,
"cacheWrite": 9858,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "rc964611b2092",
"input": 4,
"output": 120,
"cacheWrite": 26138,
"cacheRead": 23870,
"unknownTurns": 0
},
{
"run": "rbb708846b605",
"input": 12,
"output": 2089,
"cacheWrite": 17285,
"cacheRead": 73823,
"unknownTurns": 0
},
{
"run": "rc36b17ef3788",
"input": 16,
"output": 5221,
"cacheWrite": 36717,
"cacheRead": 202573,
"unknownTurns": 0
},
{
"run": "r097436c91816",
"input": 2,
"output": 131,
"cacheWrite": 11915,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "r497f1c817d4d",
"input": 2,
"output": 80,
"cacheWrite": 9856,
"cacheRead": 0,
"unknownTurns": 0
}
],
"byInstance": [
{
"instance": "chief",
"input": 36,
"output": 4114,
"cacheWrite": 85486,
"cacheRead": 177689,
"unknownTurns": 1
},
{
"instance": "coder",
"input": 2,
"output": 44,
"cacheWrite": 18595,
"cacheRead": 0,
"unknownTurns": 0
},
{
"instance": "assistant",
"input": 4,
"output": 124,
"cacheWrite": 19714,
"cacheRead": 0,
"unknownTurns": 0
},
{
"instance": "pm",
"input": 20,
"output": 5341,
"cacheWrite": 62855,
"cacheRead": 226443,
"unknownTurns": 0
}
],
"byProvider": [
{
"provider": "anthropic",
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
}
],
"byModel": [
{
"provider": "anthropic",
"model": "claude-opus-5-5",
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
}
],
"byAccount": [
{
"account": null,
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
}
]
}
# exit 0
@@ -1,2 +0,0 @@
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
# exit 0
@@ -1,19 +0,0 @@
[
{
"business": "mosaic-stack",
"instance": "chief",
"run": "r097436c91816",
"launchedBy": "jason",
"harness": "claude-code",
"model": "claude-opus-5-5",
"provider": "anthropic",
"modelKey": "opus",
"pid": 2951160,
"startTime": "63491912",
"runnerPid": 2951161,
"runnerStartTime": "63491912",
"runDir": "/home/jwoltje/.mosaic-dev/launches/mosaic-stack/r097436c91816",
"startedAt": "2026-10-20T22:02:08.941Z",
"live": true
}
]
@@ -1,153 +0,0 @@
{
"schema": "3i",
"tables": {
"decision_events": {
"rows": 21,
"maxSeq": 21,
"sha256": "28dd95056663dd5eb7442a3d815b73a4a3cd311a0e854fd82dc54f7dc3e6ef26"
},
"decisions": {
"rows": 21,
"maxSeq": 21,
"sha256": "fef1058660b0d128ef21089dda5e0fba7f71763d8325148fe521de0d3f57e4ad"
},
"deliveries": {
"rows": 1726,
"maxSeq": 1726,
"sha256": "8667571f4a05ce3621b04cf2dc5eb0311bac3d3aff1858584c78445c5c4c2fb4"
},
"events": {
"rows": 1525,
"maxSeq": 1525,
"sha256": "e51827e31f2fa5a9a50993e5a10f37f1dee383b7d673f793006bd2ba1e9a5573"
},
"instance_names": {
"rows": 0,
"maxSeq": null,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"message_threads": {
"rows": 2,
"maxSeq": null
},
"messages": {
"rows": 567,
"maxSeq": 567
},
"meta": {
"rows": 3,
"maxSeq": null,
"sha256": "89fec56e7293d19e0b2f6cd8c0ef745ebf99e4115abeaea7fbdbc98a261332d9"
},
"projects": {
"rows": 1,
"maxSeq": 1,
"sha256": "a3945f7462895e0e302029c542d384a6d33e559da900e6461d78bbcd7cc28b16"
},
"role_claims": {
"rows": 98,
"maxSeq": 98,
"sha256": "ac6694fd0b365469f7486ae1f31486cd52b31be7bc0399938b7361f22454362e"
},
"task_comment_cursors": {
"rows": 215,
"maxSeq": 215,
"sha256": "7f80df8efb90b502a9530a7ed5068d59be59b2eb318aa4e64ce6bf481762a5e9"
},
"task_snapshots": {
"rows": 401,
"maxSeq": 401,
"sha256": "cced8c7f90ecaa60eee3010d987013d8c0301563cb888860fd91b8c5a02f0a00"
},
"thread_claims": {
"rows": 2,
"maxSeq": 2,
"sha256": "6046c7088779aa2f6e9b3fd2c7520404b1678a4fcae71c2b9157a008bba22a5f"
},
"thread_opens": {
"rows": 0,
"maxSeq": null,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"threads": {
"rows": 1,
"maxSeq": 1,
"sha256": "3043721efd386f23dfdaad4acf41e0d553ff6182f0068c2133a615141bcf772a"
},
"usage_records": {
"rows": 30,
"maxSeq": 30,
"sha256": "7ecbc0606273043b017d778bb23880377bfe367f788d234289b7c132caa588f2",
"byRun": [
{
"business": "mosaic-stack",
"run": "r8fba7e598156",
"input": "0",
"output": "0",
"unknownTurns": 1
},
{
"business": "mosaic-stack",
"run": "r4a96e2142e4d",
"input": "2",
"output": "52",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc78c9700c8e9",
"input": "14",
"output": "1519",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r293f33bcbc63",
"input": "2",
"output": "44",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r34520a581318",
"input": "6",
"output": "323",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r7e99838f9f93",
"input": "2",
"output": "44",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc964611b2092",
"input": "4",
"output": "120",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rbb708846b605",
"input": "12",
"output": "2089",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc36b17ef3788",
"input": "16",
"output": "5221",
"unknownTurns": 0
}
]
}
},
"untimed": [
"message_threads",
"meta",
"task_comment_cursors"
]
}
@@ -1,167 +0,0 @@
{
"schema": "3i",
"tables": {
"decision_events": {
"rows": 21,
"maxSeq": 21,
"sha256": "28dd95056663dd5eb7442a3d815b73a4a3cd311a0e854fd82dc54f7dc3e6ef26"
},
"decisions": {
"rows": 21,
"maxSeq": 21,
"sha256": "fef1058660b0d128ef21089dda5e0fba7f71763d8325148fe521de0d3f57e4ad"
},
"deliveries": {
"rows": 1740,
"maxSeq": 1740,
"sha256": "554ce5b377ffacc72bfde11c99c920cd5d30337c8bfa217275d0073b587e2169"
},
"events": {
"rows": 1543,
"maxSeq": 1543,
"sha256": "0151673193d0286ce8f3b234d7d21fa2b0f43f2ceec55644cccaa5cbd77603dd"
},
"instance_names": {
"rows": 0,
"maxSeq": null,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"message_threads": {
"rows": 4,
"maxSeq": null
},
"messages": {
"rows": 571,
"maxSeq": 571
},
"meta": {
"rows": 3,
"maxSeq": null,
"sha256": "89fec56e7293d19e0b2f6cd8c0ef745ebf99e4115abeaea7fbdbc98a261332d9"
},
"projects": {
"rows": 2,
"maxSeq": 2,
"sha256": "3435f574ab94bc2fcc65fb8605251f30d4e9e63baf38e6fdf3c39c126531aa63"
},
"role_claims": {
"rows": 101,
"maxSeq": 101,
"sha256": "fb47114defbb8a57dd148a06fbaf61b30f791a81f14b1510b8c32690506b4308"
},
"task_comment_cursors": {
"rows": 220,
"maxSeq": 220,
"sha256": "df03c84094999299499de01107ddd08636df6156f5c9ccd2ff6e8c43e459f7e4"
},
"task_snapshots": {
"rows": 408,
"maxSeq": 408,
"sha256": "e2243a89388afba07e8d096b4961deeb2d17c656cdcace54b46eac7001393814"
},
"thread_claims": {
"rows": 4,
"maxSeq": 4,
"sha256": "088b8236570d8afe03364b85803fdc5034cce2cef6ed7b16f8b99bf6366524ad"
},
"thread_opens": {
"rows": 0,
"maxSeq": null,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"threads": {
"rows": 2,
"maxSeq": 2,
"sha256": "819fd24c64e279d0debad5ab5dbbe533d8e6f26de58277c69510b9cbbaadbc3a"
},
"usage_records": {
"rows": 34,
"maxSeq": 34,
"sha256": "de614f3d62c9b3819757c5659bee40085cb86cc873b7bfe381b13e254ddb7cad",
"byRun": [
{
"business": "mosaic-stack",
"run": "r8fba7e598156",
"input": "0",
"output": "0",
"unknownTurns": 1
},
{
"business": "mosaic-stack",
"run": "r4a96e2142e4d",
"input": "2",
"output": "52",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc78c9700c8e9",
"input": "14",
"output": "1519",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r293f33bcbc63",
"input": "2",
"output": "44",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r34520a581318",
"input": "6",
"output": "323",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r7e99838f9f93",
"input": "2",
"output": "44",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc964611b2092",
"input": "4",
"output": "120",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rbb708846b605",
"input": "12",
"output": "2089",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc36b17ef3788",
"input": "16",
"output": "5221",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r097436c91816",
"input": "2",
"output": "131",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r497f1c817d4d",
"input": "2",
"output": "80",
"unknownTurns": 0
}
]
}
},
"untimed": [
"message_threads",
"meta",
"task_comment_cursors"
]
}
@@ -1,193 +0,0 @@
# Re-pin window 490e2771d to 4429a104f (row 192, Q175 phase 1), STATE report (W11)
Operator and tester: Sage. Plan: `agents/sage/work/window-plan/repin-4429a104f.md` at f41729178, sha256
e1272d1a391e00fb1291d47da9973c0ba342d65fcd42f583ea007e883c1f32b9 (checked before W0b, W9). The plan file in
this commit carries three corrections made after the window. They are listed at the end, so its sha256 differs
now. Tool blob 3b068b1546d4 (`scripts/stack-release.sh`). OLD 490e2771d3d057ab76991e2c7859cd251857e64c, NEW
4429a104f6d65ccf165f6c83b5d1dce3147dfde0. Every stamp below comes from `date -u` or from the transcript `W.txt`
in this directory. Mos's gos: mos-1113 (W5), mos-1114 (W9 to W11), mos-1128 (W9b accepted), mos-1135 (W10
browser part), mos-1137 (line g, post W11).
## Outcome
NEW is live for mosaic-stack with its PM, and the store stays on 3i. Row 192's sign-in code is in the copy but
dormant, because the Console runs without `--sso`. W0 to W10 passed, and nothing was rolled back. W10 stopped
once on two failed browser checks. I diagnosed both read-only as checks written before row 192, not faults in
NEW, and Mos passed them (mos-1135). Matrix line (g) isn't byte-identical, for a known cause, and Mos passed it
(mos-1137). Nobody acted as Jason. There was no `--by jason`, no pi, no Save and no `mosaic name`, and
`bus.tester` was not touched.
## Stamps and activation
| Step | Start (UTC) | Result |
| --- | --- | --- |
| P1 values, business-check OLD | file written 21:43:13Z | values as planned, LOAD_OK (`p1.txt`) |
| W0 copy NEW with npm ci, business-check NEW | 21:45:57Z | copied, lockfile diff empty, LOAD_OK (`w0.txt`) |
| P2 console-check on a stub of NEW | 21:46:09Z | CONSOLE_PASS without and with `--tester-port` (`p2.txt`) |
| P3 rehearsal (Mos) | 21:52:31Z to 21:52:34Z | REHEARSAL_PASS, rc 0 (`p3/`) |
| P3 flags check (Mos) | 21:53:52Z to 21:53:56Z | NEW Console with the pointer flags on 7341/7342: healthz 200 both, then gone, 0 listeners |
| P4, P5, P6 | 22:00:17Z | P4 PASS, P5 PASS (HEAD 4385b9ec1), `matrix-old.txt` |
| W0b business file copy | 22:00:49Z | sha256 1e49bdf0 |
| W1 idle check, S1 | 22:00:49.758Z | chief idle; S1 a7649760 |
| W2 status | 22:00:49.819Z | pointer OLD, pm true, 3i, unit flags match |
| W3 plan | 22:00:50.068Z | exit 0 (`W3-plan.txt` 45de68d4) |
| W4 unit read | 22:00:50.238Z | OLD units, no drop-ins, bus `--pm`, no quote or backslash |
| W5 activate (mos-1113) | 22:02:06.776Z, end 22:02:09.556Z | exit 0, gate passed, activate ok |
| W5 status | 22:02:13.885Z | pointer NEW, pm true, 3i, unit flags match |
| W9 checks and turns (mos-1114) | 22:05:51Z to 22:06:06Z | pass |
| W9b run end and S2 | 22:21:05.171Z, S2 22:21:07Z | ended by the launcher (thread-idle); S2 1a444f5a |
| W10 browser (stop), diagnosis | 22:29:14Z, 22:30:02Z | 10 of 12, then both failures traced to the checks |
| W10 matrix, P4 (mos-1135) | 22:31:29Z | a-f identical, g +121 bytes; P4 PASS |
| W11 reads | 22:44:33Z to 22:44:34Z | units, tunnel, business file, failed units |
Activation id 20261020T220206Z-4429a104f6d6. Pointer `at` 2026-10-20T22:02:09Z.
## Backups and hashes
- Store backup: `~/.mosaic-dev/backups/stack/20261020T220206Z-4429a104f6d6`, SHA256SUMS sha256
b61d64843c9e2a153842606353450838da9cb778c8f3b9c27cc9366dd72ec0c9.
- Rollback set: `~/.mosaic-dev/state/stack-rollback/20261020T220206Z-4429a104f6d6`. Rollback goes to 490e2771d.
- Business file: 1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93 at P1, at W0b and again at
W11. The W0b copy is `~/.config/mosaic-dev/businesses/mosaic-stack.json.pre-window-20261020T220049Z` with the
same hash. Nothing in the window wrote it. `backups.txt` holds the W0b and W5 lines.
## Pointer before and after
Before (W2): copy 490e2771d3d0, pm true, schema 3i, activation 20261020T192956Z-490e2771d3d0. Console flags
`--port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje
--tester-port 7333`.
After (W5, W9 and W11): copy 4429a104f6d6, pm true, schema 3i, activation 20261020T220206Z-4429a104f6d6. The
Console flags are the same, with no `--send`, no `--sso` and no proof pair. Bus pid 2950923 and Console pid
2950925, both since 22:02:08Z. Status shows drift of 6 commits on refactor since the pointer's copy (records).
## Unit ExecStart lines (no Environment values)
W11, after. Both units active, DropInPaths empty, WorkingDirectory the NEW copy:
```
bus: ~/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/scripts/mosaic bus start mosaic-stack --pm (MainPID 2950923)
webui: /usr/bin/node ~/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0/packages/webui/src/cli.mjs serve --checkout /mnt/storage/src/mosaic-stack --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --tester-port 7333 (MainPID 2950925)
```
W4 read the same two lines on OLD's copy (W.txt 329-344). The bus Environment has 2 assignments with no quote or
backslash. The values are not recorded. The Console's cwd is the NEW copy (W9). The webui journal since W5 has one
"Proxy: proxy proof off" line and no "Sign-in:" line.
Tunnel: `mosaic-preview-tunnel.service` active, running, MainPID 1317, since 2026-10-13 08:40:19 CDT. The window
didn't touch it. P4 before and after shows one R: spec, `R:0.0.0.0:7332:127.0.0.1:7332`.
## S1, S2 and the store
S1 (`S1.json`, sha256 a7649760c3439705c8b5f3af26d0c21aea1081c1eb295fd02aef3a642064194b) is W1's held read at
22:00:49Z. S2 (`agents/sage/work/window-repin-4429a104f/S2.json`, sha256
1a444f5ac0049bc78e397b6928e1008cfe8cbc30989bbf2042b4ed27f56a4173) is the held read after the assistant run
ended, at 22:21:07Z. Both are 3i. SNAP (`snapshot.mjs`, 639fd28a) was checked before each.
| Table | S1 rows | S2 rows | S2 vs S1 |
| --- | --- | --- | --- |
| decisions, decision_events | 21, 21 | 21, 21 | same sha256 |
| meta, instance_names, thread_opens | 3, 0, 0 | 3, 0, 0 | same sha256 |
| deliveries | 1726 | 1740 | +14 |
| events | 1525 | 1543 | +18 |
| role_claims | 98 | 101 | +3 |
| messages | 567 | 571 | +4 (counts only) |
| message_threads | 2 | 4 | +2 (counts only) |
| usage_records | 30 | 34 | +4 |
| projects, threads, thread_claims | 1, 1, 2 | 2, 2, 4 | +1, +1, +2 |
| task_snapshots | 401 | 408 | +7 |
| task_comment_cursors | 215 | 220 | +5 |
For every grown table with a seq, maxSeq grew by exactly the row delta.
The task_snapshots and task_comment_cursors rows (mos-1128 item 2). The writer is the bus's own Vikunja poll, not
an agent. `packages/bus/src/process.mjs:104` loads the tasks adapter. Its timer runs `tick` every `pollSeconds`
(`packages/tasks/src/adapter.mjs:192-197`), and `sync.mjs:169` calls `broker.recordTask` with no cap
(`packages/bus/src/broker.mjs:1123`). That writes `source 'poll'` with role and run null (`broker.mjs:1159-1174`)
and the comment cursors (`:1131-1138`). The adapter logs only on an error (`adapter.mjs:115`), so the bus journal
has no line for a good tick. That's why there's no sync line between 22:05Z and 22:22Z. The rows, read
read-only after the window:
- task_snapshots 402-404 at 22:05:08Z: P19 436, 435 and 434, all updated 22:04:55-56Z. 405-406 at 22:12:38Z: 438
and 437. 407 at 22:17:08Z: 438 again. 408 at 22:19:08Z: 431. All have `source poll`, `via cursor`, role null
and run null. These are tracker edits made by seats during the window, which the poll picked up.
- task_comment_cursors 216-220: tasks 424, 432, 438, 431 and 436, last comments 397 to 401. These are the
comments seats posted on P19 in the same minutes (398 is Mos's comment on #432).
Rows after S2 (task_snapshots 409 at 22:33Z, cursors 221-223) are the same poll and are outside the window.
## W9 output
- Status, console-check (CONSOLE_PASS), the journal counts, the Console cwd and one running chief from this
start (r097436c91816) all passed.
- `talk chief --tester` (c5b43f04): the chief replied that a tester message authorizes nothing and it took no
action.
- Project `window-20261020-2205`, thread 446201de-e8b5-4df3-9356-d0d5422cc899. `talk --thread` (b4b66ced) got one
assistant reply from run r497f1c817d4d. `thread read` shows the tester REQUEST and the assistant RESULT.
- `threads`, `names` (empty) and `usage` read fine. The human-thread probe was skipped: S1's one thread is the
177-185 window's tester thread d0ba7a66, so no human thread exists.
- W9b: the launcher logged launch at 22:06:00.766Z, thread-idle at 22:21:05.149Z and end at 22:21:05.171Z
(reason stopped, exit 0). Nobody stopped it by hand.
## W10
The browser check (`w10-browser.mjs`, dda57ccf) ran against 127.0.0.1:7333 with fetch and XHR limited to GET and
HEAD. It made no writes and sent no W9 thread request. 10 of 12 checks passed. The two that failed:
1. "Only tester forms": `#sign-out` was in the list. It is row 192's form at `packages/webui/src/public/index.html:12`,
`hidden`. `app.js:479` shows it only when `/api/config` has `sso: true`. The live config has no `sso`
key, and the form stayed hidden.
2. "Chat lists the W9 thread": the check read only the first `ul.chat-thread-list`. `chat.js:93` renders one list
per project, and W9 made the second project. The diagnosis (`w10-diag.mjs`, 42d0844b) read every panel and
found 446201de listed.
Both checks come from the 177-185 window (Darkwing's `window-rows-177-185/w10-browser.mjs`), written before row
192 and before a second thread project existed. Corrected, they now read:
- Only tester forms, allowing `#sign-out` when it is `hidden` and `/api/config` has no `sso: true`.
- The W9 thread is in some `.chat-projects section.panel` thread list, not only the first.
Both corrections are in the plan's W10 line, for the next window that copies this one.
The matrix (`matrix-new.txt` against `matrix-old.txt`). Lines a to f are byte-identical. Line g is 200 non-json
both times, but 6454 bytes on OLD and 6575 on NEW. `index.html` is 6470 bytes at 490e2771d and 6591 at 4429a104f,
+121, and the only hunk is the line 12 form. The served page grew by the same 121. The plan's W10 line now says a
line for a file the pin changes isn't "identical", with this case as its example. P4 again at 22:31:29Z: PASS.
## Failed user units (mos-1128 item 3)
All three predate the re-pin at 22:02Z and come from the legacy `~/.mosaic` and jarvis-brain setup, not the
stack. P19 #393, "dragon-lin: seven legacy user units in failed state", covers all three (filed by Mos on
10-20 at about 01:42Z). It has no comments yet. The current failure times are later than the ones #393 lists,
so I added a comment with them. Times are from `systemctl --user show` (CDT) with UTC alongside.
| Unit | Runs | Failed | #393 |
| --- | --- | --- | --- |
| mosaic-standup-pm.service | `~/.mosaic/tools/coordinator/standup_trigger.py pm` | 2026-10-19 19:00:28 CDT (10-20 00:00:28Z), exit 1 | listed |
| mosaic-install-drift.service | `~/src/jarvis-brain/scripts/alerting/mosaic-install-drift` | 2026-10-20 00:12:54 CDT (05:12:54Z), exit 9 | listed, with an older time |
| mosaic-unit-failed@mosaic-reconcile.service | `~/.local/bin/mosaic-unit-failed mosaic-reconcile.service` | 2026-10-20 13:16:12 CDT (18:16:12Z), exit 4 | listed, with an older time |
The marker's journal at 18:16:12Z says the escalation's tmux send to jarvis failed ("tmux target not found",
socket mosaic-fleet), so the page wasn't delivered. The record is on disk in
`~/.local/state/mosaic-unit-failures/20261020.jsonl`. `mosaic-reconcile.service` failed at that time (exit 7)
but last ran OK at 17:16:27 CDT (22:16:27Z). The marker stays failed until something resets it. The journal no
longer has lines for the other two units. The window changed none of these units, and touching them is
outside it (`~/.mosaic` is off limits, and #393 needs Jason or ops-01's charter to disable any). W11's list also
shows `setspark-tmp-guard.service` failed. It belongs to another project, and I've left it.
## Corrections to the plan, made after the window
- NEW is 103 commits past OLD (`git rev-list --count 490e2771d..4429a104f`), not 106. The 106 in `status` was OLD
to HEAD 305857c8f, so it also counted the three record commits.
- P3: `rehearse` starts NEW's Console with only `--port` and `--business`, not the pointer's flags. Mos's
separate flags check covers that (`p3/flags-check.txt`, `p3/flags-console.log`). The line was wrong as
written (mos-1107).
- W10: the matrix line g size change and the two corrected browser checks, as above.
## Files
`W.txt` (transcript), `S1.json`, `S2.json`, `matrix-old.txt`, `matrix-new.txt`, `backups.txt`, `W3-plan.txt`,
`W5.txt`, `W5-status.txt`, `w0.txt`, `p1.txt`, `p2.txt`, `p3/` (Mos's rehearsal and flags check), `p456.sh`,
`w9.sh`, `w9b.sh`, `w9.out`, `w9b.out`, `w10-browser.mjs`, `w10-diag.mjs` with their outputs (the three
screenshots stay on disk in this directory, uncommitted, since the push check refuses binaries),
and the `.w9*`, `.w10*` and `.w11*` reads. The store snapshot tool is `snapshot.mjs` (639fd28a).
File diff suppressed because it is too large Load Diff
@@ -1,116 +0,0 @@
plan: activate for mosaic-stack
from: 490e2771d3d057ab76991e2c7859cd251857e64c
to: 4429a104f6d65ccf165f6c83b5d1dce3147dfde0
store schema: 3i (read from the store the running bus holds)
target schema: 3i
migration: none; the store is at the target's version
backup: the run backs up the stopped store to /home/jwoltje/.mosaic-dev/backups/stack/<activation>/ before any migration
commits: the run adds 103 (newest first, at most 200):
4429a104f webui: Q175 phase 1, the Console verifies SSO itself under --sso, OIDC RP with oauth4webapi 3.8.8 (row 192, #1690)
32b54ac3b plans: Case 2 row briefs from Gosalyn's 51006710a (webby-0034 N1/N2 and webby-0035: row A README :711 one line per variable, C2-13 owns README :713 and says the precheck prints only variable names)
6025ddd9c plans: Case 2 row briefs from Gosalyn's a2d94b2c2 (Webby r9 notes, webby-0032: row A README line on claude-session.env and the dist/providers/data/zai.json path, C2-13 XDG_DATA_HOME precheck and last-assignment-wins key file parse)
ecbfd6deb plans: Case 2 row briefs from Gosalyn's 29e85196b (design r9 for mos-1090: row A out of scope points at design 10 step 2d, C2-13 precheck checks the hub key file by name and mode, claude marker check only for claude-code harness, EnvironmentFile= fixed in case2-unit.sh)
0498c0613 agents: Gosalyn seat dir, README from gosalyn/case2-design, so Case 2 rows can name the seat as owner (#416)
49d1f3a23 plans: Case 2 slice row briefs plus rows A and B, from Gosalyn's f325657c7 on gosalyn/case2-design (#416, design r8, C2-13 copy id and profile asks for row 195, C2-20 count-only note, row A ZAI_API_KEY per mos-1086/mos-1089, row B #428 after 10-27)
628e2ca5c sessions: Filbert row 190 manifest round 1 CHANGES (base drift on row 189)
088087ac9 queue: revs 2248-2361+. Sage: rows 210 (#424 host access, Gizmoduck) and 211 (#426 force-push guard, Rocko) added and briefed; row 195 re-briefed to the Case 1 install on .53; review requests for 200 r1/r2 and 202; plus earlier Sage revs. Other seats' revs: Launchpad 2254, 2283-2284, 2286, 2311-2313, 2328-2329, 2344, 2347-2348 (rows 192/200); Honker 2255; Morgana 2257, 2320, 2326-2327 (row 204); Gander 2262; Herb 2265; Dewey 2268; Bushroot 2273; Megavolt 2276; Webby 2303, 2316, 2339, 2355 (row 192/200 verdicts); Gizmoduck 2304, 2318-2319 and the row 210 claim (rows 201/210); Quackerjack 2317, 2341-2342 (row 202); Duckworth 2323, 2335, 2353 (rows 201/204/202 verdicts); Darkwing 2354, 2357 (row 190 in-review)
a0fbefd48 plans: pre-push force-push hook and failing busy check brief (Vikunja #426, mos-1078, mos-1083; owner Rocko, reviewer Dewey)
f24f2cd40 plans: row 195 brief becomes the Case 1 install on 10.1.1.53 (Q181 hosts, Q182/Q183 recorded acceptance, Q185 existing account, chained on rows 190 and C2-13; ops-01 root steps under #423 step 1; sage-1439)
9ba31b5b3 agents: Honker's PRD 1.7 draft r2 on refactor for the prd.approve raise (11cb0423b, sha256 e896746df595d1b5155130ca163fbf63b0dc38a87a8dd154af77f1a6d1328647, Duckworth approved duckworth-0039, Vikunja #420)
9db578ff5 plans: agent host access with sudo through the credential store brief (#1707, Vikunja #424, Q183/Q184, chained on row 199)
8eb9ddd12 plans: FED R8 brief to r4.11 (b93f098f0, recorded acceptance, Duckworth N1/N2, #416 date), R5 context source (sage-1419), 193a makes covers level required (duckworth-0033 N1)
ae3c36f24 agents: Gizmoduck seat dir and roster row (FED R1 owner, #414)
a2af7ab49 plans: FED R1, R5, R2 and R8 briefs (#414, r4.9 a1daa3c07) and row 193a-d build briefs (#1691); Quackerjack seat dir and roster row
aa1bd209e gate-lane: row 189 gate timing table for row 197 (Herb)
ace27dc5e row 189 (#1687): stack-release.sh activate --no-send drops the Console's --send (Sage; Dewey approve 29485; Sage gate passed)
a4a42dbac plans: stack-release --sso brief (#1698, Launchpad, reviewer Webby, row 192 F3; chained on 192)
a31f1a0f8 plans: row 198 brief records Mos's call, in-process board reads behind the Console's auth (mos-1019)
992650232 plans: agent credentials and git identity brief (MVP T1 item 6, #1697, Megavolt, reviewer Duckworth); Megavolt seat dir
a7661ada0 plans: row 195 acceptance script takes Gizmoduck's gate item list; B8 issuer and TLS front end (mos-1011)
f871cb309 plans: retire the control board writer on 7331 brief (#1696, Bushroot, reviewer Webby, mos-1008 item 2); Bushroot seat dir
0bbd23c61 plans: row 195 brief adds the identity provider check and flags any new model account for Jason (mos-1008)
0217b71c8 plans: row 195 brief rewritten as the hub on its own host, host left to Jason (#1693, mos-0998, mos-1005)
c6f528a32 plans: second gate lane brief (#1695, Herb, reviewer Webby, mos-0977 item 6); Herb seat dir; gate template and busy check copies for the row
96f243faf plans: human boundary row brief (P19 333, #1694, Gander, reviewer Duckworth, mos-0989); Gander seat dir; seat READMEs name the credentials row, not row 195 (now 190b)
c86269935 sessions: Filbert row 190 design review r3, approved
48ebee0ff plans: row 190 brief scopes isolation to addressing on one uid, points to row 190b (Filbert N7, mos-0986)
66bdd84b8 sessions: Filbert row 190 design review r2, changes requested
6966164f0 plans: row 190b brief, Melanie's hub under its own Linux user (#1693, Darkwing, reviewer Duckworth), mos-0986
53bb59462 sage: common start for MVP T1 builder seats (mos-0974, mos-0977, mos-0981)
5e01c3683 agents: seat dirs and roster rows for Launchpad, Morgana, Honker (Sage's T1 builders) and Webby, Duckworth (Mos's reviewers); row 193 brief takes Mos's B1 starting rule (mos-0985)
9f56d2098 plans: rows 192 and 194 review by Webby, not Filbert or Dewey (mos-0981)
0599b119f plans: MVP T1 briefs, SSO phase 1 (#1690, Launchpad), human users (#1691, Morgana), agent lifecycle (#1692, Honker), mos-0974/0977
b416d2ede sage: Q175 SSO design, Mos C1 condition (mos-0977): straggler check before Jason step 4 in the phase 2 window
5fc73a729 revs 2237-2247: row 189 added, briefed, started, in-review round 1 (Dewey, comment 29457); rows 190 (Darkwing) and 191 (Rocko) added and briefed for MVP T1 (mos-0974); darkwing-190-start (rev 2245, Darkwing's) and rocko-191-start (rev 2244, made for Rocko)
99772421a sage: row 189 candidate manifest for Dewey's review (d6116771f)
da230ca48 plans: MVP T1 briefs, several instances per host (#1688, Darkwing) and the kill switch (#1689, Rocko), mos-0974
4f2d1be68 sessions: Filbert Q175 SSO design D1/D2 ack
d55d86bb0 sage: Q175 SSO design r3 fix, Filbert r3 D1 (loader checks slot shape only) and D2 (slot from config dir, scratch Console dataRoot and unreachable issuer)
c56ccf916 sessions: Filbert Q175 SSO design review r3, approve with D1
817e8f51b sage: Q175 SSO design r3 for #412, Filbert r2 C1 (shared loader, config.json inside the phase 2 window, rollback order, test 25) and C2 (401 signed-out page, test 24), notes and Q-E
5d019d876 sessions: Filbert Q175 SSO design review r2, changes
59475fc5a sage: row 163 landing record, P19 comments 372/373 on 337/412, and P19 #413 filed for the --no-send row
1a268249e sage: Q175 SSO design r2 for #412, Filbert r1 B1-B6, rulings Q-B/Q-C/Q-D, config in config.json (mos-0973)
def2e7932 plans: brief for stack-release activate --no-send (Q175 interim, mos-0972/0973, #1687, P19 413)
c30b36fff sessions: Filbert Q175 SSO design review r1, changes
9ae8c73ac queue: revs 2235-2236: row 163 Sage landing note (8ecc7a239, patch-id b8d5f8a37c24, gate passed on eaa0f2666) and done
8ecc7a239 row 163 (#1660): stack-release.sh rehearse, the D2 rehearsal on a scratch root from a backup, and bus start --no-launch (Sage; Filbert approve 29447; Sage gate passed)
7f57419b3 sage: Q175 SSO design for #412, OIDC relying party on oauth4webapi 3.8.8, for Filbert review (mos-0970, mos-0971)
4ca8d6e94 sage: row 42 run log, S7 reachability pass (51cebff7 answered by cff1e9ad after a chief launch, mos-0969)
22e5e825b sage: row 42 run log, plain mosaic on a pinned host supersedes the 10-12 full-path line (mos-0968)
e6dea74b2 sessions: Sage row 163 r2 approval intake; P19 337 comment 368 script and record
cd2b377bb queue: rev 2234: row 163 r2 approve from Filbert (op filbert-163-r2, comment 29447, candidate c6f4f4c12596)
ceccb0ef2 sessions: Filbert row 163 r2 review, approve
10ab8ea62 sessions: Sage row 163 r2 in-review; P19 337 comment 367 script and record
c539995cf queue: revs 2231-2233: row 163 r2 in-review from Sage (ops sage-163-r2-start, sage-163-inreview-2, .outcome; request comment 29446, reviewer Filbert, manifest c6f4f4c12596)
92cc6011c queue: rev 2230: row 163 r1 changes from Filbert (op filbert-163-r1, comment 29445, candidate 430212299beb)
7af166574 sage: row 163 r2 packet (candidate 2c090eefd on code 2a5f3a3a0, base 4f02486d5: diff, manifest c6f4f4c12596, gate passed, mutants K34-K41 killed)
2244e8baf sessions: Filbert row 163 r1 review, changes
bde0fdec9 sessions: Sage row 163 r1 in-review; P19 337 comment 366 script and record
def677bc7 queue: revs 2228-2229: row 163 r1 in-review from Sage (ops sage-163-inreview-1, .outcome; request comment 29444, reviewer Filbert, manifest 430212299beb)
51f67f1f8 sage: row 163 packet (candidate 4be3f6aec on 580500699: diff, manifest 430212299beb, gate passed, mutants K1-K33 killed after four survivors got tests)
5b298bd00 sessions: Dewey row 66 r1 (changes) and r2 (approve) reviews
565bc32c2 queue: revs 2223-2227: row 66 r2 round from Sage (2223-2225, ops sage-r66-r2-start, sage-r66-inreview-2); r2 verdicts from Filbert (2226, op filbert-66-r2, approve) and Dewey (2227, op dewey-66-r2, approve)
5140aa9e8 sessions: Filbert row 66 r2 review, approve
973cb5a87 queue: revs 2220-2222: row 163 Sage start (2220); row 66 r1 verdicts from Filbert (2221, op filbert-66-r1) and Dewey (2222, op dewey-66-r1)
b82ec9353 sage: row 66 r2 packet (candidate 0f4a57956: diff, manifest cee63e06fdea, gate, mutants K1-K4 killed, singles rerun)
3560c450a sessions: Filbert row 66 r1 review, changes
4f02486d5 sage: row 66 r1 packet (candidate 8ec03b45a: diff, manifest e68c6bce5ec4, gate r2), row 42 run log (trigger (a), mos-0960), SESSIONS line
263ea8a9a queue: revs 2215-2219: row 66 Sage start (2215), in-review round 1 on candidate manifest e68c6bce5ec4 (8ec03b45a) and request comment 29437 (2216-2217); row 163 brief re-pinned to b1500d96ad27 (6020bb43a) and briefed (2218-2219)
6020bb43a plans: D2 brief owner is sage (row 163, #1660), and D2 lands after the Part B window (mos-0958, mos-0959)
5487ed39d sage: row 188 landing record: P19 411 (N1) and comments 361-363 script and record, 409/410 description restore after a partial done update, SESSIONS line
31e328958 queue: revs 2209-2214: row 188 Darkwing start, in-review and request 29432 (2209-2211); Filbert approve 29433 (2212); Sage landing note (8fc0ee216, patch-id c9a56b167c07, stack-release.sh blob 2586a2a37cd2, gate passed) and done (2213-2214)
8fc0ee216 row 188 (#1686, P19 409, 410): stack-release T54, the proof file checks on a carried pair and a proof file inside the repository (Darkwing; Filbert approve 29433; Sage gate passed)
23a609c5c sessions: Filbert row 188 r1 review, approve
b9c1c9fe2 sage: Part B draft 2 A2 passed (mos-0958), row 188 launched, Vikunja comments 358-360 script and record, SESSIONS line
525990ecb queue: revs 2207-2208: row 188 add and briefed (Sage), #1686, P19 409/410, gates Part B (mos-0956)
148a9503d sage: Part B window file draft 2 for mos-0957 (F1: B6 and B-R1 name the stop's endLaunch rows, PB5 counts per step; F2: PB6, PB7 and B-R1 matrices run (a)-(h)); Q2 answered, Q1 to Jason's list
240f21650 brief: stack-release tests for a carried proof pair and a proof file inside the repository (P19 409, 410, #1686, mos-0956)
742e98976 sage: Part B window file, the read window's end stamp from the clock (10:29Z, not 10:40Z)
288ab1b5a sage: Part B window file for Mos's A2 check (live-copy B3 and B-R1, Q1 on J2 order), SESSIONS lines for Darkwing (row 187, 10:25:22Z) and Sage
a843ae284 sage: row 187 landing, P19 407-410 filed and P19 385 comment 357, #1685 closed (comment 29425), SESSIONS line
652d6f3a8 queue: revs 2201-2206: row 187 Darkwing start, in-review and request comment 29423 (2201-2203); Filbert approve comment 29424 (2204); landing note (9a71e4e80, patch-id 6b57c47bc661, gate passed) and done (Sage 2205-2206)
9a71e4e80 row 187 (#1685): activate sets, carries and drops the Console's proof pair
d17328a55 sessions: Filbert row 187 r1 review, approve
f81ebfbae sage: row 186 follow-ups filed as P19 405 and 406, P19 404 closed (mos-0955)
9760d4967 sage: row 186 landing, P19 404 comment 354 and SESSIONS line, with Darkwing's row 186 SESSIONS line (09:13:21Z)
16a936c79 queue: revs 2198-2200: row 186 Filbert approve comment 29419 (2198); landing note (269134d8d, patch-id c97b9f6e25cd, gate passed) and done (Sage 2199-2200)
269134d8d row 186 (#1684): the activate plan's backup line says what the run will do
3c5da3a91 sessions: Filbert row 186 r1 review, approve
b367f5730 sage: rows 186-187 Vikunja comments (P19 404 351, P19 385 352-353) and SESSIONS line (mos-0954)
5a768d82c queue: row 187 added and briefed (Part B A0, #1685), with Darkwing's row 186 start (rev 2193)
6de3c86be plans: brief for row 187, activate carries the row 178 proof pair (mos-0928 Part B A0, #1685)
1d7fd75e2 queue: row 186 added and briefed (P19 404, #1684)
f5c72a5c3 plans: brief for row 186, the activate plan's backup line (P19 404, #1684)
b724b7b0b sessions: Sage Part A window line, with Darkwing's 06:53:53Z and 08:28:01Z lines
e24a13ad8 sage: Vikunja scripts and read-back records 2026-10-13 to 10-20 (decision 110, mos-0882)
21aa1cab0 darkwing: re-pin window rows 177-185 packet, STATE.md (sha256 b586f11c9b6f1329), W.txt (edd4622c5c45ef3f), S1/S2/S2b/B snapshots, matrices, W10 browser check and P3 rehearsals (#1683; P19 386)
9ddd8a92c plans: repin-rows-177-185 draft 4, R1 snapshot after the plain rollback (mos-0943)
e7637b99c plans: repin-rows-177-185 draft 3, R1post expects one tester action.refused per refused read (mos-0935, mos-0942)
2ea05700b plans: mos-0928 plan draft 2, NEW is the row 185 landing 490e2771d
010666074 queue: revs 2185-2190: row 185 start and in-review on bf87dbba with request comment 29403 (Darkwing 2185-2187); Filbert approve comment 29404 (2188); landing note (490e2771d, patch-id 774cd23186d5, gate passed) and done (Sage 2189-2190)
bus: scripts/bus-service.sh install --copy 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 --copies /home/jwoltje/.local/share/mosaic-stack/copies --pm --claude /home/jwoltje/.local/bin/claude
proof: off
send: off
Console: scripts/webui-service.sh install --copy 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 --copies /home/jwoltje/.local/share/mosaic-stack/copies --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --tester-port 7333
CLI: scripts/stack-copy.sh wrapper 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 --copies /home/jwoltje/.local/share/mosaic-stack/copies --bin /home/jwoltje/.npm-global/bin
@@ -1,21 +0,0 @@
pointer: /home/jwoltje/.mosaic-dev/state/stack-active.json
{"copy":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","business":"mosaic-stack","pm":true,"claude":"/home/jwoltje/.local/bin/claude","console":["--port","7332","--public-host","mosaic.woltje.com","--auth-header","X-authentik-username","--allow-user","jason.woltje","--tester-port","7333"],"schema":"3i","activation":"20261020T220206Z-4429a104f6d6","at":"2026-10-20T22:02:09Z"}
log (last five lines):
{"at":"2026-10-20T07:58:02Z","op":"activate","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T075800Z-490e2771d3d0","activation":"20261020T075800Z-490e2771d3d0","result":"ok"}
{"at":"2026-10-20T19:29:56Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":null,"activation":"20261020T192956Z-490e2771d3d0","result":"started"}
{"at":"2026-10-20T19:30:00Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T192956Z-490e2771d3d0","activation":"20261020T192956Z-490e2771d3d0","result":"ok"}
{"at":"2026-10-20T22:02:06Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":null,"activation":"20261020T220206Z-4429a104f6d6","result":"started"}
{"at":"2026-10-20T22:02:09Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":"20261020T220206Z-4429a104f6d6","activation":"20261020T220206Z-4429a104f6d6","result":"ok"}
check:
bus: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
Console: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
CLI: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
result: pass
bus status:
host: mosaic-stack, pid 2950923, running, since 2026-10-20T22:02:08.133Z, notifier off
socket: present
writer.lock: pid 2950981
boot: organization file loaded at 2026-10-20T22:02:08.125Z
unit flags: match the pointer
drift: 6 commit(s) on refactor since the pointer's copy
# exit 0
@@ -1,8 +0,0 @@
stack-release: captured the rollback set: /home/jwoltje/.mosaic-dev/state/stack-rollback/20261020T220206Z-4429a104f6d6
stack-release: stopped both units
stack-release: stopped store schema: 3i
stack-release: backup: /home/jwoltje/.mosaic-dev/backups/stack/20261020T220206Z-4429a104f6d6
stack-release: installed 4429a104f6d65ccf165f6c83b5d1dce3147dfde0
stack-release: gate passed on 4429a104f6d65ccf165f6c83b5d1dce3147dfde0
stack-release: activate ok: 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 is live for mosaic-stack
# exit 0
@@ -1,2 +0,0 @@
W0b 2026-10-20T22:00:49Z business file copy /home/jwoltje/.config/mosaic-dev/businesses/mosaic-stack.json.pre-window-20261020T220049Z sha256 1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93 (source /home/jwoltje/.config/mosaic-dev/businesses/mosaic-stack.json sha256 1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93)
W5 2026-10-20T22:02:13Z activation 20261020T220206Z-4429a104f6d6 backup /home/jwoltje/.mosaic-dev/backups/stack/20261020T220206Z-4429a104f6d6 SHA256SUMS sha256 b61d64843c9e2a153842606353450838da9cb778c8f3b9c27cc9366dd72ec0c9
@@ -1,7 +0,0 @@
a 200 ok=true
b 401 error="sign-in required"
c 401 error="sign-in required"
d 403 error="proxy Host refused"
e 403 error="tester listener required"
f 403 error="proxy header refused"
g 200 non-json 6575 bytes
@@ -1,8 +0,0 @@
# P6 2026-10-20T22:00:17Z
a 200 ok=true
b 401 error="sign-in required"
c 401 error="sign-in required"
d 403 error="proxy Host refused"
e 403 error="tester listener required"
f 403 error="proxy header refused"
g 200 non-json 6454 bytes
@@ -1,4 +0,0 @@
copy 490e2771d: roles 6, projects 1, arbiters 2
HOLDS chief instance.name yes
HOLDS pm instance.name no
LOAD_OK
@@ -1,19 +0,0 @@
{
"bus.tester": true,
"plugins": {
"usage-source": [
"transcript"
],
"channel": [
"cli",
"webui"
],
"harness": [
"claude-code"
]
},
"launch.by": "chief",
"launch.instances has assistant": true,
"roles with pi harness": [],
"sha256": "1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93"
}
@@ -1,35 +0,0 @@
start 2026-10-20T21:46:09Z
business-check.mjs: OK
console-check.sh: OK
1ba3c7efbc84a1d7eb8ee0068d32b7917f4e75cdc4066703944cea4c20db5b93 /mnt/storage/scratch/sage-pin-de352dfaf/fixture/console-stub.mjs
a4500094e7c7a6dc21797eaf2d696f9814f272ece945c2735b79e4593f9c9503 /mnt/storage/scratch/sage-p2-4429/console-stub-tester.mjs
== run 1: pinned stub, no tester listener
stub pid 2495939 port 39561
PASS markdown-preview.js is served and equals the copy's
PASS /api/config on loopback: send=false edit=false
PASS project.files through the Console: head=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa files=2 spec=true error=none
PASS project.file spec/north-star.md through the Console: commit=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa length=4 error=none
PASS loopback save refused: HTTP 403 error=read-only sent=false
CONSOLE_PASS
PORT 39561
CALLS []
run 1 console-check rc=0
== run 2: main listener plus a tester listener on a second port
stub pid 2496085 port 35213 tester port 35945
PASS markdown-preview.js is served and equals the copy's
PASS /api/config on loopback: send=false edit=false
PASS project.files through the Console: head=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa files=2 spec=true error=none
PASS project.file spec/north-star.md through the Console: commit=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa length=4 error=none
PASS loopback save refused: HTTP 403 error=read-only sent=false
CONSOLE_PASS
PASS main refuses x-mosaic-tester: HTTP 403 "tester listener required"
PASS tester /api/config: HTTP 200 "tester":true
PASS tester /api/config send and edit off: HTTP 200 "send":false,"edit":false
PASS tester refuses the main auth header: HTTP 403 "proxy header refused"
PASS tester refuses project-file save: HTTP 403
PORT 35213
TPORT 35945
CALLS []
TESTER_PASS
run 2 console-check rc=0 tester rc=0
end 2026-10-20T21:46:10Z
@@ -1 +0,0 @@
2026-10-20T21:52:34Z
@@ -1,10 +0,0 @@
flags-check start 2026-10-20T21:53:52Z
pid 2729699 alive: yes
127.0.0.1:7341
127.0.0.1:7342
main /healthz: 200
tester /healthz: 200
main /api/config: {"board":"http://127.0.0.1:7331","send":false,"edit":false}
pid 2729699 after TERM: gone
listeners after: 0
flags-check end 2026-10-20T21:53:56Z
@@ -1,7 +0,0 @@
Mosaic Console: http://127.0.0.1:7341/
Board: http://127.0.0.1:7331
Bus: mosaic-stack
Preview: https://mosaic.woltje.com/ through the proxy, signed-in user from x-authentik-username, read-only
Proxy: proxy proof off
Tester Console: http://127.0.0.1:7342/ (binds the tester)
Ctrl-C stops this server.
@@ -1 +0,0 @@
rc=0
@@ -1,20 +0,0 @@
PASS copy: 4429a104f6d65ccf165f6c83b5d1dce3147dfde0, schema 3i
PASS port: 127.0.0.1:7341 is free
PASS backup: /home/jwoltje/.mosaic-dev/backups/stack/20261020T192956Z-490e2771d3d0 matches its SHA256SUMS
PASS scratch-root: /home/jwoltje/.mosaic-dev/rehearsals/20261020T215231Z-4429a104f6d6/data from the backup, config and business file under /home/jwoltje/.mosaic-dev/rehearsals/20261020T215231Z-4429a104f6d6/config, business.diff beside them
PASS data-root-live: /home/jwoltje/.mosaic-dev/rehearsals/20261020T215231Z-4429a104f6d6/data is not the live data root
PASS sessions-live: no running session named in the data root
PASS data-root-scratch
PASS notify-off: binding null, so no Discord bridge
PASS tracker-configured: bootConfig sets no trackers
PASS credential-ref-live: 9 reference(s), each a placeholder
PASS migration: not needed: the backup is already 3i
PASS gate: host-up, bus-status, console-up, healthz and copy-match on 4429a104f6d65ccf165f6c83b5d1dce3147dfde0
PASS no-session: no sessions.json and no launches under /home/jwoltje/.mosaic-dev/rehearsals/20261020T215231Z-4429a104f6d6/data
PASS console-stopped: pid 2675437
PASS host-stopped: pid 2675436
PASS host-released-store
PASS backup-unchanged
PASS live-config-unchanged
REHEARSAL_PASS
rehearsal directory: /home/jwoltje/.mosaic-dev/rehearsals/20261020T215231Z-4429a104f6d6
@@ -1 +0,0 @@
2026-10-20T21:52:31Z
@@ -1,7 +0,0 @@
# P4 2026-10-20T22:00:17Z
DropInPaths=
ExecStart={ path=/home/jwoltje/.local/bin/chisel ; argv[]=/home/jwoltje/.local/bin/chisel client --fingerprint ce+ISFvMAhuoCRDuc7riIf+hYz87TLHk/C9C/H8QsUg= --keepalive 25s --max-retry-interval 10s http://10.1.1.227:8080 R:0.0.0.0:7332:127.0.0.1:7332 ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
# exit 0
# R: specs (1): R:0.0.0.0:7332:127.0.0.1:7332
P4 PASS: one R: spec R:0.0.0.0:7332:127.0.0.1:7332, DropInPaths empty
rc 0
@@ -1,67 +0,0 @@
#!/usr/bin/env bash
# P4-P6 of agents/sage/work/window-plan/repin-4429a104f.md (f41729178), adapted from Darkwing's window-rows-177-185/p456.sh, read only.
# P4 the tunnel unit, P5 the four REPO scripts against NEW, P6 the 7332 matrix on OLD (matrix-old.txt).
# usage: p456.sh p4|p5|p6
set -uo pipefail
REPO=/mnt/storage/src/mosaic-stack
WIN=$REPO/agents/sage/work/window-repin-4429a104f
NEW=4429a104f6d65ccf165f6c83b5d1dce3147dfde0
FILES=(scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh)
# The plan's <32 letters>: a fixed dummy, not a secret. matrix-new.txt uses the same value.
PROOF=abcdefghijklmnopqrstuvwxyzabcdef
p4() {
echo "# P4 $(date -u +%FT%TZ)"
local out; out="$(systemctl --user show mosaic-preview-tunnel.service -p ExecStart -p DropInPaths)"; local rc=$?
printf '%s\n' "$out"; echo "# exit $rc"
local specs; specs="$(grep -oE 'R:[^ ;]+' <<<"$out")"
local n; n="$(grep -c . <<<"$specs")"
echo "# R: specs ($n): $(tr '\n' ' ' <<<"$specs")"
local drop; drop="$(sed -n 's/^DropInPaths=//p' <<<"$out")"
if [[ $rc -eq 0 && $n -eq 1 && "$specs" == "R:0.0.0.0:7332:127.0.0.1:7332" && -z "$drop" ]]; then
echo "P4 PASS: one R: spec R:0.0.0.0:7332:127.0.0.1:7332, DropInPaths empty"
else
echo "P4 FAIL"; return 3
fi
}
p5() {
echo "# P5 $(date -u +%FT%TZ); HEAD $(git -C $REPO rev-parse HEAD)"
git -C $REPO diff --quiet $NEW HEAD -- "${FILES[@]}"; local d=$?
echo "# git diff --quiet $NEW HEAD -- ${FILES[*]}: exit $d"
local st; st="$(git -C $REPO status --porcelain -- "${FILES[@]}")"; local s=$?
echo "# git status --porcelain (exit $s): [${st}]"
for f in "${FILES[@]}"; do echo "# $f HEAD blob $(git -C $REPO rev-parse HEAD:$f) NEW blob $(git -C $REPO rev-parse $NEW:$f)"; done
if [[ $d -eq 0 && $s -eq 0 && -z "$st" ]]; then echo "P5 PASS"; else echo "P5 FAIL"; return 3; fi
}
# one matrix line: label, then curl arguments; prints "<label> <code> <error or ok field>"
req() {
local label="$1"; shift
local body; body="$(mktemp -p "${TMPDIR:-/tmp}")"
local code; code="$(curl -s -m 10 -o "$body" -w '%{http_code}' "$@")"
local field; field="$(node -e '
let s = require("fs").readFileSync(process.argv[1], "utf8"), j;
try { j = JSON.parse(s); } catch { console.log("non-json " + s.length + " bytes"); process.exit(0); }
console.log(j.error !== undefined ? "error=" + JSON.stringify(j.error) : "ok=" + JSON.stringify(j.ok));
' "$body")"
rm -f "$body"
echo "$label $code $field"
}
p6() {
req a http://127.0.0.1:7332/healthz
req b -H 'Host: mosaic.woltje.com' http://127.0.0.1:7332/
req c -H 'Host: mosaic.woltje.com' -H "X-Mosaic-Proof: $PROOF" http://127.0.0.1:7332/
req d -H 'X-authentik-username: tester-probe' http://127.0.0.1:7332/
req e -H 'x-mosaic-tester: 1' http://127.0.0.1:7332/
req f -H 'X-authentik-username: tester-probe' http://127.0.0.1:7333/
req g -H "X-Mosaic-Proof: $PROOF" http://127.0.0.1:7333/
}
case "${1:-}" in
p4) p4 ;;
p5) p5 ;;
p6) p6 ;;
*) echo "usage: p456.sh p4|p5|p6" >&2; exit 2 ;;
esac
@@ -1,9 +0,0 @@
# P5 2026-10-20T22:00:17Z; HEAD 4385b9ec183a2da4136ccd639d2a6b041eb9c68f
# git diff --quiet 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 HEAD -- scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh: exit 0
# git status --porcelain (exit 0): []
# scripts/stack-release.sh HEAD blob 3b068b1546d4020cffde5d73929d9130afe91911 NEW blob 3b068b1546d4020cffde5d73929d9130afe91911
# scripts/webui-service.sh HEAD blob 0a32547c54ac233e75a21f4817ae230855dbc088 NEW blob 0a32547c54ac233e75a21f4817ae230855dbc088
# scripts/bus-service.sh HEAD blob f71250204dfeceb51beeea5941ebbf3a33ce4b92 NEW blob f71250204dfeceb51beeea5941ebbf3a33ce4b92
# scripts/stack-copy.sh HEAD blob 24bbb02a6aa8d7857da7be3b6815105a9fef629f NEW blob 24bbb02a6aa8d7857da7be3b6815105a9fef629f
P5 PASS
rc 0
@@ -1,235 +0,0 @@
// Merged window B+C, S1, S2 and D3 (repin-merged-bc.md "S1 and S2"): a read-only snapshot of every
// table in a bus store, so the 3h to 3i migration and a restore can be checked without the bus.
// OLD's broker doesn't take --tester on usage, names or threads.
//
// usage: node snapshot.mjs --store /abs/bus.sqlite (--held | --stopped) [--after STAMP] [--backup /abs/bus.sqlite]
// --held the running bus holds the store: a plain readOnly open, which adds no file to a WAL
// store that already has -wal and -shm (as stack-release.sh does for a held store).
// Refuses store-not-held when either is absent
// --stopped the bus is stopped: refuses store-unclean when -wal or -shm is present, else opens
// with immutable=1 and readOnly, which creates no file and leaves the store's mtime
// and sha256 alone
// --after also counts each timed table's rows with `at` after STAMP (mos-0838 D3, before a
// --restore). STAMP is a backup's STAMP file, in now_ms form (stack-release.sh:43,
// 2026-10-20T00:11:14.123Z). The count is a text compare, as the tool's own heldcount
// does, so every `at` in every timed table must have that form: a table with any
// other value refuses at-format and names the table (sage-1125)
// --backup also compares every table with a stack-release backup's bus.sqlite, which opens as
// --stopped opens a store: immutable=1 and readOnly, refusing backup-unclean when -wal
// or -shm is present, so nothing writes into the backup set
//
// It prints one JSON object: the schema version, then for every table in sqlite_master (type
// table, less sqlite_*) the row count, the max seq and a sha256 of the rows (in seq order, else in
// column order). `untimed` names the tables with no `at` column. For usage_records it adds per-run
// input and output sums, counted as `mosaic usage` counts them (usage.mjs turnRecords). messages
// and message_threads hold bodies and names, so they print counts only, no sha256 (sage-1125). It
// prints counts, hashes and table and meta key names only: no message body, no instance, project
// or thread name, no credential and no file content. It reads no config: the store path is always
// given.
//
// With --backup, each table gets `vsBackup` and `stopTables` names every table other than events
// whose rows differ from the backup's (sage-1123): a row with `at` after STAMP, a row past the
// backup's max seq, a changed row at or below it, a changed untimed table, or a table only one
// side has. meta counts only when a key other than schema_version and schema_digest changed (the
// 3h to 3i migration sets those two). Events are left to the tool's own --events N.
import { DatabaseSync } from 'node:sqlite';
import { createHash } from 'node:crypto';
import { existsSync, lstatSync } from 'node:fs';
import { isAbsolute } from 'node:path';
import { pathToFileURL } from 'node:url';
// usage_records is hashed on these columns in this order, never SELECT * (mos-0888 F1), as
// 3h-3i.sql:9 copies it. Every other table is hashed on its own columns in name order, so a
// rebuild that reorders columns keeps the hash. usage_records with any other column set refuses.
const USAGE_COLUMNS = ['seq', 'at', 'business', 'run', 'instance', 'provider', 'model', 'account', 'turn', 'source',
'phase', 'status', 'code', 'cursor', 'input', 'output', 'cache_write', 'cache_read'];
const COUNTS_ONLY = new Set(['messages', 'message_threads']);
const SCHEMA_KEYS = ['schema_digest', 'schema_version'];
const STAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
function refuse(code, detail) {
process.stderr.write(`snapshot: ${code}: ${detail}\n`);
process.exit(1);
}
function args(argv) {
const o = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--store' || a === '--after' || a === '--backup') {
if (i + 1 >= argv.length) refuse('usage', `${a} needs a value`);
o[a.slice(2)] = argv[++i];
} else if (a === '--held' || a === '--stopped') {
if (o.mode) refuse('usage', 'give one of --held and --stopped');
o.mode = a.slice(2);
} else refuse('usage', `unknown argument ${a}`);
}
if (!o.store || !o.mode) refuse('usage', 'node snapshot.mjs --store /abs/bus.sqlite (--held | --stopped) [--after STAMP] [--backup /abs/bus.sqlite]');
if (!isAbsolute(o.store)) refuse('usage', '--store must be an absolute path');
if (o.backup !== undefined && !isAbsolute(o.backup)) refuse('usage', '--backup must be an absolute path');
if (o.after !== undefined && !(STAMP.test(o.after) && Number.isFinite(Date.parse(o.after))))
refuse('usage', '--after must be a STAMP like 2026-10-20T00:11:14.123Z');
return o;
}
function file(path, code) {
if (!existsSync(path) || !lstatSync(path).isFile()) refuse(code, `${path} is not a file`);
}
function immutable(path, code) {
for (const f of [path + '-wal', path + '-shm'])
if (existsSync(f)) refuse(code, `${f} is present; an immutable read would skip the WAL`);
return new DatabaseSync(pathToFileURL(path).href + '?immutable=1', { readOnly: true });
}
function open({ store, mode }) {
file(store, 'store-missing');
if (mode === 'stopped') return immutable(store, 'store-unclean');
// A plain readOnly open of a WAL store with no -wal and -shm creates them, so a store that
// nothing holds refuses here instead of gaining two files.
for (const f of [store + '-wal', store + '-shm'])
if (!existsSync(f)) refuse('store-not-held', `${f} is absent; use --stopped for a store the bus doesn't hold`);
return new DatabaseSync(store, { readOnly: true });
}
const q = (name) => `"${name.replaceAll('"', '""')}"`;
const value = (v) => (typeof v === 'bigint' ? String(v) : v instanceof Uint8Array ? { hex: Buffer.from(v).toString('hex') } : v);
// The same records `mosaic usage` counts: per (run, turn, source) the latest row, an end row
// winning; a turn is known only when that row is an end row with status known.
function byRun(rows) {
const turns = new Map();
for (const row of rows) {
const key = JSON.stringify([row.business, row.run, row.turn, row.source]);
if (row.phase === 'end' || !turns.has(key)) turns.set(key, row);
}
const runs = new Map();
for (const row of turns.values()) {
const key = JSON.stringify([row.business, row.run]);
if (!runs.has(key)) runs.set(key, { business: row.business, run: row.run, input: 0n, output: 0n, unknown: new Set() });
const r = runs.get(key);
if (row.phase === 'end' && row.status === 'known') {
r.input += BigInt(row.input);
r.output += BigInt(row.output);
} else r.unknown.add(row.turn);
}
return [...runs.values()].map(({ unknown, input, output, ...r }) => ({
...r, input: String(input), output: String(output), unknownTurns: unknown.size,
}));
}
// meta is compared by key: the names of the keys whose value differs, never the values.
function metaKeys(db) {
return new Map(db.prepare('SELECT key, value FROM meta').all().map((r) => [String(r.key), String(r.value)]));
}
// One table: rows, max seq and sha256; rows after `after`; with `priorMax` (the backup's max
// seq) a second hash of the rows at or below it and the count of rows above it. The hashes stay
// inside: a COUNTS_ONLY table's `t` gets none.
function read(db, table, { after, priorMax } = {}) {
const cols = db.prepare('SELECT name FROM pragma_table_info(?)').all(table).map((c) => c.name);
if (!cols.length) refuse('table-missing', table);
let hashed = [...cols].sort();
if (table === 'usage_records') {
if (JSON.stringify(hashed) !== JSON.stringify([...USAGE_COLUMNS].sort()))
refuse('columns-changed', `usage_records has columns ${hashed.join(',')}`);
hashed = USAGE_COLUMNS;
}
const seq = cols.includes('seq');
const at = cols.includes('at');
const stmt = db.prepare(`SELECT ${hashed.map(q).join(',')} FROM ${q(table)} ORDER BY ${seq ? 'seq' : hashed.map(q).join(',')}`);
stmt.setReadBigInts(true);
const full = createHash('sha256');
const prior = createHash('sha256');
const limit = priorMax === undefined ? undefined : BigInt(priorMax ?? 0);
let rows = 0;
let rowsAfter = 0;
let badAt = 0;
let newRows = 0;
const kept = [];
for (const row of stmt.iterate()) {
rows++;
const line = JSON.stringify(hashed.map((c) => value(row[c]))) + '\n';
full.update(line);
if (seq && limit !== undefined) {
if (row.seq <= limit) prior.update(line);
else newRows++;
}
if (after !== undefined && at) {
if (typeof row.at !== 'string' || !STAMP.test(row.at)) badAt++;
else if (row.at > after) rowsAfter++;
}
if (table === 'usage_records') kept.push(row);
}
if (badAt) refuse('at-format', `${table} has ${badAt} row(s) whose at is not in the STAMP form, so a text compare with --after would miscount`);
const sha256 = full.digest('hex');
const t = { rows, maxSeq: seq ? (db.prepare(`SELECT max(seq) AS m FROM ${q(table)}`).get().m ?? null) : null };
if (!COUNTS_ONLY.has(table)) t.sha256 = sha256;
if (after !== undefined && at) t.rowsAfter = rowsAfter;
if (table === 'usage_records') t.byRun = byRun(kept);
return { t, sha256, cols: hashed, seq, at, newRows, priorSha: seq && limit !== undefined ? prior.digest('hex') : null };
}
// Tables only: no index, view or trigger, and none of SQLite's own sqlite_* tables.
const tablesOf = (db) => db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' ORDER BY name")
.all().map((r) => r.name);
const o = args(process.argv.slice(2));
if (o.backup !== undefined) file(o.backup, 'backup-missing');
const db = open(o);
const bdb = o.backup === undefined ? null : immutable(o.backup, 'backup-unclean');
const out = {};
try {
db.exec('BEGIN');
out.schema = db.prepare("SELECT value FROM meta WHERE key='schema_version'").get()?.value ?? null;
const names = tablesOf(db);
const base = new Map();
if (bdb) {
out.backupSchema = bdb.prepare("SELECT value FROM meta WHERE key='schema_version'").get()?.value ?? null;
for (const name of tablesOf(bdb)) base.set(name, read(bdb, name));
}
out.tables = {};
out.untimed = [];
const stop = [];
for (const name of names) {
const b = base.get(name);
const r = read(db, name, { after: o.after, priorMax: b ? b.t.maxSeq : undefined });
if (!r.at) out.untimed.push(name);
if (bdb) {
const v = { backupRows: b ? b.t.rows : null };
let changed;
if (!b) changed = true;
else if (name === 'meta') {
const now = metaKeys(db);
const then = metaKeys(bdb);
v.keysChanged = [...new Set([...now.keys(), ...then.keys()])].filter((k) => now.get(k) !== then.get(k)).sort();
changed = v.keysChanged.some((k) => !SCHEMA_KEYS.includes(k));
} else if (r.seq) {
v.backupMaxSeq = b.t.maxSeq;
v.newRows = r.newRows;
v.priorSame = JSON.stringify(r.cols) === JSON.stringify(b.cols) && r.priorSha === b.sha256;
changed = r.newRows > 0 || !v.priorSame;
} else {
v.same = JSON.stringify(r.cols) === JSON.stringify(b.cols) && r.sha256 === b.sha256;
changed = !v.same;
}
if ((r.t.rowsAfter ?? 0) > 0) changed = true;
r.t.vsBackup = v;
if (changed && name !== 'events') stop.push(name);
}
out.tables[name] = r.t;
}
if (bdb) {
out.onlyInBackup = [...base.keys()].filter((n) => !names.includes(n));
out.stopTables = [...stop, ...out.onlyInBackup].sort();
}
if (o.after !== undefined) {
out.after = o.after;
out.eventsAfter = out.tables.events?.rowsAfter ?? null;
}
} finally {
try { db.exec('ROLLBACK'); } catch {}
db.close();
bdb?.close();
}
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
@@ -1,16 +0,0 @@
#!/usr/bin/env bash
# Re-pin 4429a104f window step wrapper (copied from Darkwing's window-rows-177-185/w.sh): w.sh FILE TITLE CMD... runs CMD in REPO under a clean env (no ANTHROPIC_*, CLAUDE_*,
# MOSAIC_* or pi variables), appends the title, command, output, exit code and date -u stamps to window/FILE,
# prints the output, and exits with CMD's exit code.
set -uo pipefail
W=/mnt/storage/src/mosaic-stack/agents/sage/work/window-repin-4429a104f
f="$W/$1"; title="$2"; shift 2
E=(env -i HOME="$HOME" PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin" LANG=C.UTF-8
XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=$XDG_RUNTIME_DIR/bus}")
t0=$(date -u +%FT%T.%3NZ)
{ echo; echo "## $title"; echo "# start $t0"; echo "# command: (cd \$REPO && <clean env> $*)"; } >> "$f"
out=$(cd /mnt/storage/src/mosaic-stack && "${E[@]}" "$@" 2>&1); rc=$?
printf '%s\n' "$out" >> "$f"
echo "# exit $rc, end $(date -u +%FT%T.%3NZ)" >> "$f"
printf '%s\n# exit %s\n' "$out" "$rc"
exit $rc
@@ -1,9 +0,0 @@
npm warn deprecated [email protected]: Use your platform's native DOMException instead
added 128 packages in 2s
npm warn install-scripts 3 packages had install scripts blocked because they are not covered by allowScripts:
npm warn install-scripts @google/[email protected] (preinstall: echo 'preinstall: no-op')
npm warn install-scripts [email protected] (postinstall: node install.js)
npm warn install-scripts [email protected] (postinstall: node scripts/postinstall)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.
@@ -1 +0,0 @@
copied: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
@@ -1,16 +0,0 @@
start 2026-10-20T21:45:57Z
stack-copy rc=0
copied: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
npm warn install-scripts [email protected] (postinstall: node scripts/postinstall)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.
oauth4webapi 3.8.8
IMPORT_OK
LOCK_MATCH
163M /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
/dev/nvme1n1p5 458G 405G 30G 94% /home
copy 4429a104f: roles 6, projects 1, arbiters 2
HOLDS chief instance.name yes
HOLDS pm instance.name no
LOAD_OK
end 2026-10-20T21:45:59Z
@@ -1,109 +0,0 @@
// W10 of repin-4429a104f.md (e1272d1a), adapted from Darkwing's window-rows-177-185/w10-browser.mjs: the tester listener
// on 127.0.0.1:7333 through the webui browser harness (packages/webui/tests/browser.mjs, same blob at HEAD and NEW). Reads only: before any page
// script runs, fetch and XMLHttpRequest are wrapped so every call is logged and any method other than
// GET or HEAD is refused in the page, and the script submits no form and clicks no button. The chat
// check reads the #/chat list only and never opens the W9 thread page (the launcher reads lastOpen).
import { browser } from '/mnt/storage/src/mosaic-stack/packages/webui/tests/browser.mjs';
const OUT = '/mnt/storage/src/mosaic-stack/agents/sage/work/window-repin-4429a104f';
const BASE = 'http://127.0.0.1:7333/';
const W9_THREAD = '446201de-e8b5-4df3-9356-d0d5422cc899';
const GUARD = `(() => {
const log = window.__w10 = [];
const ok = m => m === 'GET' || m === 'HEAD';
const f = window.fetch;
window.fetch = (input, init = {}) => {
const method = String(init.method || (input instanceof Request ? input.method : 'GET')).toUpperCase();
const url = String(input instanceof Request ? input.url : input);
log.push({ method, url, refused: !ok(method) });
return ok(method) ? f(input, init) : Promise.reject(new TypeError('w10 refused ' + method));
};
const open = XMLHttpRequest.prototype.open, send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, ...rest) { this.__m = String(method).toUpperCase(); this.__u = String(url); return open.call(this, method, url, ...rest); };
XMLHttpRequest.prototype.send = function (...a) { log.push({ method: this.__m, url: this.__u, xhr: true, refused: !ok(this.__m) }); if (!ok(this.__m)) throw new TypeError('w10 refused ' + this.__m); return send.apply(this, a); };
})();`;
const b = await browser({ machine: false });
const result = {};
let failed = false;
const check = (name, pass, detail) => { result[name] = { pass, ...(detail === undefined ? {} : { detail }) }; if (!pass) failed = true; };
try {
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: GUARD });
await b.viewport(1280, 1600);
const wait = expr => b.evaluate(`(async()=>{for(let n=0;n<300;n++){if(${expr})return true;await new Promise(r=>setTimeout(r,50));}return false;})()`);
await b.navigate(BASE + '#/settings');
const ready = await wait('!!document.querySelector("#tester-banner") && !!document.querySelector("#routes-form") && !!document.querySelector(".tester-name-form")');
check('settings loaded', ready);
const s = await b.evaluate(`(() => {
const q = x => document.querySelector(x);
return {
banner: q('#tester-banner')?.textContent.trim() ?? null,
h1: q('#bus-view h1')?.textContent.trim() ?? null,
forms: [...document.querySelectorAll('form')].map(f => f.id || f.className),
convFormHidden: q('#conv-form')?.hidden ?? null,
panels: [...document.querySelectorAll('#bus-view h2')].map(h => h.textContent.trim()),
instancesTable: !!q('#instances-table'),
nameForms: document.querySelectorAll('.tester-name-form').length,
routesForm: !!q('#routes-form'),
routesSubmitEnabled: q('#routes-form button[type=submit]')?.disabled === false,
forbidden: [...document.querySelectorAll('#launch-settings-form, .assistant-form, #token-limits-table [data-limit-reset], #send-section')].map(e => e.id || e.className || e.tagName),
tokenLimitsPanel: !!q('#token-limits-title') || !!q('#token-limits-table'),
};
})()`);
result.settings = s;
check('tester banner', /tester/i.test(s.banner ?? ''), s.banner);
check('instance, routes and name editors present', s.instancesTable && s.routesForm && s.nameForms > 0);
check('only tester forms (conv-form is index.html:21, global and hidden)', s.convFormHidden === true && s.forms.every(f => f === 'conv-form' || f === 'routes-form' || /tester-name-form/.test(f) || /^instance-(add|change)-form$/.test(f)), s.forms);
check('no launch settings, assistant, limit reset or send', s.forbidden.length === 0, s.forbidden);
check('no Token limits panel (plan W10 item 2, amended)', s.tokenLimitsPanel === false);
await b.screenshot(`${OUT}/w10-settings.png`);
await b.navigate(BASE + '#/');
// The Board is index.html:22 #board-view; bus.js hides #bus-view on it.
const board = await wait('document.querySelector("#bus-view")?.hidden === true && !!document.querySelector("#board-view #waiting-title")');
await new Promise(r => setTimeout(r, 1500));
const bd = await b.evaluate(`(() => { const v = document.querySelector('#board-view'); return {
busViewHidden: document.querySelector('#bus-view')?.hidden ?? null,
banner: !!document.querySelector('#tester-banner'),
headings: [...v.querySelectorAll('h2')].map(h => h.textContent.replace(/\\s+/g, ' ').trim()),
unavailable: /unavailable|refused|not authorized/i.test(v.textContent),
length: v.textContent.length,
forms: [...document.querySelectorAll('form')].map(f => f.id || f.className),
}; })()`);
result.board = bd;
check('board reads', board && bd.busViewHidden === true && !bd.unavailable && bd.headings.length > 0, bd);
await b.screenshot(`${OUT}/w10-board.png`);
await b.navigate(BASE + '#/chat');
// chat.js threads(): ul.chat-thread-list with <a href="#/chat/threads/<id>">. Read only; no link is followed.
const chatReady = await wait('!!document.querySelector("ul.chat-thread-list") || /No threads yet/.test(document.body.textContent)');
const c = await b.evaluate(`(() => {
const list = document.querySelector('ul.chat-thread-list');
return {
hash: location.hash,
banner: !!document.querySelector('#tester-banner'),
threads: list ? [...list.querySelectorAll('li')].map(li => ({ href: li.querySelector('a')?.getAttribute('href') ?? null, name: li.querySelector('a')?.textContent.trim() ?? null, instance: li.querySelector('.muted')?.textContent.trim() ?? null })) : [],
conversationOpen: !!document.querySelector('.chat-conversation'),
forms: [...document.querySelectorAll('form')].map(f => f.id || f.className),
};
})()`);
result.chat = c;
check('chat lists the W9 thread', chatReady && c.threads.some(t => t.href === '#/chat/threads/' + W9_THREAD), c.threads);
check('W9 thread page not opened', c.hash === '#/chat' && c.conversationOpen === false, c.hash);
check('tester banner on chat', c.banner);
await b.screenshot(`${OUT}/w10-chat.png`);
const log = await b.evaluate('window.__w10');
result.requests = log;
check('no write through the listener', log.every(r => !r.refused && (r.method === 'GET' || r.method === 'HEAD')), log.filter(r => r.refused));
check('no request to the W9 thread messages or open', !log.some(r => r.url.includes(W9_THREAD)), log.filter(r => r.url.includes(W9_THREAD)));
} catch (e) {
failed = true;
result.error = String(e?.message ?? e);
} finally {
await b.close();
}
console.log(JSON.stringify(result, null, 2));
console.log(failed ? 'W10 browser: FAIL' : 'W10 browser: pass');
process.exit(failed ? 1 : 0);
@@ -1,30 +0,0 @@
// W10 diagnosis after the two failed checks of w10-browser.mjs (sign-out form listed; chat list missing the W9 thread).
// Read only, with the same GET/HEAD guard as w10-browser.mjs. Records each form's hidden flag on Settings, the /api/config
// sso value the page sees, and every ul.chat-thread-list on #/chat (chat.js:93 renders one per project). No screenshots.
import { browser } from '/mnt/storage/src/mosaic-stack/packages/webui/tests/browser.mjs';
import { readFileSync } from 'node:fs';
const src = readFileSync(new URL('./w10-browser.mjs', import.meta.url), 'utf8');
const GUARD = eval(src.slice(src.indexOf('const GUARD = ') + 14, src.indexOf('`;', src.indexOf('const GUARD = ')) + 1));
const BASE = 'http://127.0.0.1:7333/';
const W9_THREAD = '446201de-e8b5-4df3-9356-d0d5422cc899';
const b = await browser({ machine: false });
const out = {};
try {
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: GUARD });
const wait = expr => b.evaluate(`(async()=>{for(let n=0;n<300;n++){if(${expr})return true;await new Promise(r=>setTimeout(r,50));}return false;})()`);
await b.navigate(BASE + '#/settings');
await wait('!!document.querySelector("#routes-form")');
out.config = await b.evaluate(`fetch('/api/config').then(r=>r.json()).then(j=>({sso:j.sso??null}))`);
out.forms = await b.evaluate(`[...document.querySelectorAll('form')].map(f=>({id:f.id||f.className,hidden:f.hidden,action:f.getAttribute('action'),method:f.getAttribute('method')}))`);
await b.navigate(BASE + '#/chat');
await wait('document.querySelectorAll("ul.chat-thread-list").length >= 1');
await new Promise(r => setTimeout(r, 1000));
out.chat = await b.evaluate(`({hash:location.hash,open:!!document.querySelector('.chat-conversation'),projects:[...document.querySelectorAll('.chat-projects section.panel')].map(s=>({project:s.querySelector('h2')?.textContent.trim(),threads:[...s.querySelectorAll('ul.chat-thread-list li a')].map(a=>a.getAttribute('href'))}))})`);
out.requests = await b.evaluate('window.__w10');
} finally { await b.close(); }
const signOut = out.forms.find(f => f.id === 'sign-out');
const listed = out.chat.projects.some(p => p.threads.includes('#/chat/threads/' + W9_THREAD));
const writes = out.requests.filter(r => r.refused || !['GET', 'HEAD'].includes(r.method));
console.log(JSON.stringify(out, null, 2));
console.log(`sign-out hidden ${signOut?.hidden}, sso ${out.config.sso}; W9 thread listed ${listed}; thread page open ${out.chat.open}; writes ${writes.length}`);
process.exit(signOut?.hidden === true && listed && !out.chat.open && writes.length === 0 ? 0 : 1);
@@ -1,277 +0,0 @@
# W9 go mos-1114, start 2026-10-20T22:05:51Z
# plan e1272d1a, tool blob 3b068b1546d4, console-check.sh d068fe02, wrapper on NEW: checked
pointer: /home/jwoltje/.mosaic-dev/state/stack-active.json
{"copy":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","business":"mosaic-stack","pm":true,"claude":"/home/jwoltje/.local/bin/claude","console":["--port","7332","--public-host","mosaic.woltje.com","--auth-header","X-authentik-username","--allow-user","jason.woltje","--tester-port","7333"],"schema":"3i","activation":"20261020T220206Z-4429a104f6d6","at":"2026-10-20T22:02:09Z"}
log (last five lines):
{"at":"2026-10-20T07:58:02Z","op":"activate","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T075800Z-490e2771d3d0","activation":"20261020T075800Z-490e2771d3d0","result":"ok"}
{"at":"2026-10-20T19:29:56Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":null,"activation":"20261020T192956Z-490e2771d3d0","result":"started"}
{"at":"2026-10-20T19:30:00Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T192956Z-490e2771d3d0","activation":"20261020T192956Z-490e2771d3d0","result":"ok"}
{"at":"2026-10-20T22:02:06Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":null,"activation":"20261020T220206Z-4429a104f6d6","result":"started"}
{"at":"2026-10-20T22:02:09Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"4429a104f6d65ccf165f6c83b5d1dce3147dfde0","backup":"20261020T220206Z-4429a104f6d6","activation":"20261020T220206Z-4429a104f6d6","result":"ok"}
check:
bus: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
Console: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
CLI: /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
result: pass
bus status:
host: mosaic-stack, pid 2950923, running, since 2026-10-20T22:02:08.133Z, notifier off
socket: present
writer.lock: pid 2950981
boot: organization file loaded at 2026-10-20T22:02:08.125Z
unit flags: match the pointer
drift: 6 commit(s) on refactor since the pointer's copy
# exit 0
PASS markdown-preview.js is served and equals the copy's
PASS /api/config on loopback: send=false edit=false
PASS project.files through the Console: head=ae0b17c9586e51da7bc1b7e548c6acf913a55665 files=2 spec=true error=none
PASS project.file spec/north-star.md through the Console: commit=ae0b17c9586e51da7bc1b7e548c6acf913a55665 length=99 error=none
PASS loopback save refused: HTTP 403 error=read-only sent=false
CONSOLE_PASS
# exit 0
# journal since W5: proxy proof off 1, Sign-in 0
# Console pid 2950925 cwd /home/jwoltje/.local/share/mosaic-stack/copies/4429a104f6d65ccf165f6c83b5d1dce3147dfde0
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
# exit 0
[
{
"business": "mosaic-stack",
"instance": "chief",
"run": "r097436c91816",
"launchedBy": "jason",
"harness": "claude-code",
"model": "claude-opus-5-5",
"provider": "anthropic",
"modelKey": "opus",
"pid": 2951160,
"startTime": "63491912",
"runnerPid": 2951161,
"runnerStartTime": "63491912",
"runDir": "/home/jwoltje/.mosaic-dev/launches/mosaic-stack/r097436c91816",
"startedAt": "2026-10-20T22:02:08.941Z",
"live": true
}
]
# exit 0
# one running chief from this start: run r097436c91816
chief r097436c91816 claude-code since 2026-10-20T22:02:08.951Z
# exit 0
sent c5b43f04 to chief
chief (RESULT, re c5b43f04) c3e236d7:
Got the re-pin of 4429a104f6d65ccf165f6c83b5d1dce3147dfde0 from the tester; I took no action because a tester message authorizes nothing, and I launched and changed nothing.
# exit 0
created project window-20261020-2205 id 77e7b45f-8c34-49a3-b2fa-9cf565177c30
# exit 0
created window-20261020-2205/w9 id 446201de-e8b5-4df3-9356-d0d5422cc899 instance assistant
# exit 0
# project window-20261020-2205, thread 446201de-e8b5-4df3-9356-d0d5422cc899
sent b4b66ced to window-20261020-2205/w9
assistant (RESULT, re b4b66ced) cfa7ea99:
Assistant (run r497f1c817d4d) got the [mos test] message and is replying in one line as asked, without treating it as Jason's instruction or acting on it.
# exit 0
# reply received 2026-10-20T22:06:05Z
[
{
"seq": 570,
"id": "b4b66ced-d088-4b9c-a944-306f164028f9",
"at": "2026-10-20T22:05:58.283Z",
"business": "mosaic-stack",
"from_role": "tester",
"from_run": "tester-cli",
"to_role": "assistant",
"class": "REQUEST",
"in_reply_to": null,
"decision": null,
"corrects": null,
"body": "[mos test] reply with one line, then stop",
"to_name": null
},
{
"seq": 571,
"id": "cfa7ea99-7ccb-4225-80a6-9a903540e337",
"at": "2026-10-20T22:06:04.247Z",
"business": "mosaic-stack",
"from_role": "assistant",
"from_run": "r497f1c817d4d",
"to_role": "tester",
"class": "RESULT",
"in_reply_to": "b4b66ced-d088-4b9c-a944-306f164028f9",
"decision": null,
"corrects": null,
"body": "Assistant (run r497f1c817d4d) got the [mos test] message and is replying in one line as asked, without treating it as Jason's instruction or acting on it.",
"to_name": null
}
]
# exit 0
# thread read: 2 messages: tester->assistant REQUEST, assistant->tester RESULT
window-20261020-0801/w9 assistant d0ba7a66
window-20261020-2205/w9 assistant 446201de
# exit 0
[]
# exit 0
{
"total": {
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
},
"byRun": [
{
"run": "r8fba7e598156",
"input": 0,
"output": 0,
"cacheWrite": 0,
"cacheRead": 0,
"unknownTurns": 1
},
{
"run": "r4a96e2142e4d",
"input": 2,
"output": 52,
"cacheWrite": 11750,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "rc78c9700c8e9",
"input": 14,
"output": 1519,
"cacheWrite": 18187,
"cacheRead": 89559,
"unknownTurns": 0
},
{
"run": "r293f33bcbc63",
"input": 2,
"output": 44,
"cacheWrite": 18595,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "r34520a581318",
"input": 6,
"output": 323,
"cacheWrite": 26349,
"cacheRead": 14307,
"unknownTurns": 0
},
{
"run": "r7e99838f9f93",
"input": 2,
"output": 44,
"cacheWrite": 9858,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "rc964611b2092",
"input": 4,
"output": 120,
"cacheWrite": 26138,
"cacheRead": 23870,
"unknownTurns": 0
},
{
"run": "rbb708846b605",
"input": 12,
"output": 2089,
"cacheWrite": 17285,
"cacheRead": 73823,
"unknownTurns": 0
},
{
"run": "rc36b17ef3788",
"input": 16,
"output": 5221,
"cacheWrite": 36717,
"cacheRead": 202573,
"unknownTurns": 0
},
{
"run": "r097436c91816",
"input": 2,
"output": 131,
"cacheWrite": 11915,
"cacheRead": 0,
"unknownTurns": 0
},
{
"run": "r497f1c817d4d",
"input": 2,
"output": 80,
"cacheWrite": 9856,
"cacheRead": 0,
"unknownTurns": 0
}
],
"byInstance": [
{
"instance": "chief",
"input": 36,
"output": 4114,
"cacheWrite": 85486,
"cacheRead": 177689,
"unknownTurns": 1
},
{
"instance": "coder",
"input": 2,
"output": 44,
"cacheWrite": 18595,
"cacheRead": 0,
"unknownTurns": 0
},
{
"instance": "assistant",
"input": 4,
"output": 124,
"cacheWrite": 19714,
"cacheRead": 0,
"unknownTurns": 0
},
{
"instance": "pm",
"input": 20,
"output": 5341,
"cacheWrite": 62855,
"cacheRead": 226443,
"unknownTurns": 0
}
],
"byProvider": [
{
"provider": "anthropic",
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
}
],
"byModel": [
{
"provider": "anthropic",
"model": "claude-opus-5-5",
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
}
],
"byAccount": [
{
"account": null,
"input": 62,
"output": 9623,
"cacheWrite": 186650,
"cacheRead": 404132,
"unknownTurns": 1
}
]
}
# exit 0
# human-thread probe: skipped. S1 has 1 threads row, and threads --tester lists the 177-185 window's tester thread d0ba7a66, so no human thread exists to read
# W9 checks and turns end 2026-10-20T22:06:06Z; the assistant run wait and S2 are w9b.sh
@@ -1,93 +0,0 @@
#!/usr/bin/env bash
# Re-pin 4429a104f, W9 checks and the turns (repin-4429a104f.md at f41729178, sha256 e1272d1a; the W9 list of
# repin-rows-177-185.md plus the journal Sign-in line and the Console cwd), on Mos's go mos-1114.
# Adapted from Darkwing's window-rows-177-185/w9.sh. Every call goes through w.sh into W.txt; every bus call carries
# --tester. A failed check stops the script. The wait for the assistant run and S2 are in w9b.sh, so the interim
# report to Mos goes out between the two (report rule, waits over 5 minutes).
# usage: w9.sh
set -uo pipefail
REPO=/mnt/storage/src/mosaic-stack
WIN=$REPO/agents/sage/work/window-repin-4429a104f
NEW=4429a104f6d65ccf165f6c83b5d1dce3147dfde0
W5_STAMP="2026-10-20 22:02:06 UTC"
W5_MS=$(date -d "2026-10-20T22:02:06Z" +%s)000
PLAN=$REPO/agents/sage/work/window-plan/repin-4429a104f.md
PLAN_SHA=e1272d1a391e00fb1291d47da9973c0ba342d65fcd42f583ea007e883c1f32b9
TOOL_BLOB=3b068b1546d4020cffde5d73929d9130afe91911
CC=/mnt/storage/scratch/sage-pin-de352dfaf/console-check.sh
CC_SHA=d068fe02da897b700110a162d13d9671fe5994f053c45aff99113fade737094c
CONSOLE_PID=2950925
PRIOR_TID=d0ba7a66-aa3f-4918-9518-94132bce6b28
MOSAIC=$HOME/.npm-global/bin/mosaic
C_NEW=(node "$HOME/.local/share/mosaic-stack/copies/$NEW/packages/cli/src/cli.mjs")
BZ=(--business mosaic-stack)
W=$WIN/W.txt
note() { echo "# $*" >> "$W"; echo "# $*"; }
stop() { note "STOP: $*"; exit 1; }
sha() { sha256sum < "$1" | cut -c1-64; }
step() { local f="$1"; shift; $WIN/w.sh W.txt "$@" > "$WIN/$f"; local rc=$?; cat "$WIN/$f"; return $rc; }
note "W9 go mos-1114, start $(date -u +%FT%TZ)"
[ "$(sha $PLAN)" = "$PLAN_SHA" ] || stop "the plan file is not e1272d1a"
[ "$(git -C $REPO rev-parse HEAD:scripts/stack-release.sh)" = "$TOOL_BLOB" ] || stop "tool blob moved"
[ "$(sha $CC)" = "$CC_SHA" ] || stop "console-check.sh is not d068fe02 (SCRIPTS.sha256)"
grep -q "/copies/$NEW/scripts/mosaic" "$MOSAIC" || stop "the wrapper is not pinned to NEW"
note "plan e1272d1a, tool blob 3b068b1546d4, console-check.sh d068fe02, wrapper on NEW: checked"
# checks
step .w9-status.txt "W9 status" scripts/stack-release.sh status mosaic-stack || stop "W9 status exit"
grep -q "\"copy\":\"$NEW\"" $WIN/.w9-status.txt && grep -q '"pm":true' $WIN/.w9-status.txt && grep -q '"schema":"3i"' $WIN/.w9-status.txt || stop "W9 status: pointer not on NEW, pm true, 3i"
grep -q 'unit flags: match the pointer' $WIN/.w9-status.txt || stop "W9 status: unit flags"
step .w9-cc.txt "W9 console-check NEW ($CC, d068fe02)" bash $CC $NEW || stop "console-check exit"
grep -qx CONSOLE_PASS $WIN/.w9-cc.txt || stop "console-check not CONSOLE_PASS"
J=$(journalctl --user -u mosaic-webui.service --since "$W5_STAMP" --no-pager)
N=$(printf '%s\n' "$J" | grep -c 'Proxy: proxy proof off'); S=$(printf '%s\n' "$J" | grep -c 'Sign-in:')
{ echo; echo "## W9 webui journal since $W5_STAMP: 'Proxy: proxy proof off' and 'Sign-in:' counts"; echo "proxy proof off $N, Sign-in $S"; } >> "$W"
note "journal since W5: proxy proof off $N, Sign-in $S"
[ "$N" = 1 ] || stop "proxy proof off count is $N, not 1"
[ "$S" = 0 ] || stop "Sign-in count is $S, not 0"
systemctl --user show -p MainPID --value mosaic-webui.service | grep -qx "$CONSOLE_PID" || stop "Console MainPID is not $CONSOLE_PID"
CWD=$(readlink /proc/$CONSOLE_PID/cwd)
{ echo; echo "## W9 Console pid $CONSOLE_PID cwd"; ls -l /proc/$CONSOLE_PID/cwd | sed 's/^.* -> /-> /'; } >> "$W"
note "Console pid $CONSOLE_PID cwd $CWD"
[ "$CWD" = "$HOME/.local/share/mosaic-stack/copies/$NEW" ] || stop "Console cwd is not the NEW copy"
step .w9-launches.txt "W9 launches list" $MOSAIC launches list || stop "launches list"
step .w9-launches.json "W9 launches list --json" $MOSAIC launches list --json || stop "launches list --json"
sed -i '$d' $WIN/.w9-launches.json
CHIEF=$(node -e '
const l=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8")),since=Number(process.argv[2]);
const c=l.filter(x=>x.business==="mosaic-stack"&&x.instance==="chief"&&x.live===true);
if(c.length!==1||!(Date.parse(c[0].startedAt)>=since)){console.error(`running chiefs ${c.length}`);process.exit(1)}
process.stdout.write(c[0].run)' $WIN/.w9-launches.json "$W5_MS") || stop "launches: not one running chief from this start"
note "one running chief from this start: run $CHIEF"
step .w9-agents.txt "W9 agents --tester" $MOSAIC agents "${BZ[@]}" --tester || stop "agents"
grep -E '(^|[[:space:]])chief[[:space:]].*working' $WIN/.w9-agents.txt && stop "the chief is working"
# turns
step .w9-talk-chief.txt "W9 talk chief --tester" $MOSAIC talk chief "[mos test] re-pin $NEW: reply with one line, no staffing request" "${BZ[@]}" --tester || stop "talk chief"
PROJ=window-$(date -u +%Y%m%d-%H%M)
step .w9-project.txt "W9 project create $PROJ --tester" $MOSAIC project create $PROJ "${BZ[@]}" --tester || stop "project create"
step .w9-thread.txt "W9 thread create $PROJ w9 --tester" $MOSAIC thread create $PROJ w9 "${BZ[@]}" --tester || stop "thread create"
TID=$(sed -n "s/^created $PROJ\/w9 id \([0-9a-f-]*\) instance .*$/\1/p" $WIN/.w9-thread.txt)
[ -n "$TID" ] || stop "no thread id"
note "project $PROJ, thread $TID"
echo "$TID" > $WIN/.w9-tid.txt
step .w9-talk-thread.txt "W9 talk --thread --wait 180 --tester" $MOSAIC talk --thread "$TID" "[mos test] reply with one line, then stop" --wait 180 "${BZ[@]}" --tester || stop "talk --thread"
note "reply received $(date -u +%FT%TZ)"
date -u +%s > $WIN/.w9-reply-at.txt
step .w9-thread-read.json "W9 thread read --json --tester" $MOSAIC thread read "$TID" --json "${BZ[@]}" --tester || stop "thread read"
sed -i '$d' $WIN/.w9-thread-read.json
node -e '
const t=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));
console.log(`# thread read: ${t.length} messages: ${t.map(m=>`${m.from_role}->${m.to_role} ${m.class}`).join(", ")}`);
process.exit(t.length===2&&t[0].from_role==="tester"&&t[1].from_role==="assistant"?0:1)' $WIN/.w9-thread-read.json | tee -a "$W" || stop "thread read is not the tester message plus one assistant reply"
step .w9-threads.txt "W9 threads --tester" $MOSAIC threads "${BZ[@]}" --tester || stop "threads"
grep -q "$TID\|$PROJ" $WIN/.w9-threads.txt || stop "threads does not list the W9 thread"
step .w9-names.json "W9 C_NEW names --tester --json" "${C_NEW[@]}" names "${BZ[@]}" --tester --json || stop "names"
step .w9-usage.json "W9 usage --tester --json" $MOSAIC usage "${BZ[@]}" --tester --json || stop "usage"
if grep -q "${PRIOR_TID:0:8}" $WIN/.w9-threads.txt; then
note "human-thread probe: skipped. S1 has 1 threads row, and threads --tester lists the 177-185 window's tester thread ${PRIOR_TID:0:8}, so no human thread exists to read"
else
note "human-thread probe: S1's one thread is not in threads --tester; probe it by hand before S2"
fi
note "W9 checks and turns end $(date -u +%FT%TZ); the assistant run wait and S2 are w9b.sh"
@@ -1,185 +0,0 @@
# W9b start 2026-10-20T22:06:27Z: waiting for the launcher to end r497f1c817d4d (thread 446201de-e8b5-4df3-9356-d0d5422cc899), no stop by hand
# r497f1c817d4d not live in launches list --json by 2026-10-20T22:21:07Z (5 s poll)
r097436c91816 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T22:02:08.941Z running
# exit 0
## W9b launches log lines for r497f1c817d4d (at, event, instance, thread, run, reason, exitCode, signal only)
at=2026-10-20T22:06:00.766Z event=launch instance=assistant thread=446201de-e8b5-4df3-9356-d0d5422cc899 run=r497f1c817d4d
at=2026-10-20T22:21:05.149Z event=thread-idle instance=assistant thread=446201de-e8b5-4df3-9356-d0d5422cc899 run=r497f1c817d4d
at=2026-10-20T22:21:05.171Z event=end instance=assistant thread=446201de-e8b5-4df3-9356-d0d5422cc899 run=r497f1c817d4d reason=stopped exitCode=0 signal=null
## W9b bus and webui journal since 2026-10-20 22:05:00 UTC, lines naming r497f1c817d4d or thread-idle
2026-10-20T17:21:05.171444-05:00 dragon-lin mosaic[2950923]: mosaic-bus: run r497f1c817d4d (assistant) ended: stopped
(grep exit 0)
# SNAP sha256 639fd28a checked before S2 2026-10-20T22:21:07Z
{
"schema": "3i",
"tables": {
"decision_events": {
"rows": 21,
"maxSeq": 21,
"sha256": "28dd95056663dd5eb7442a3d815b73a4a3cd311a0e854fd82dc54f7dc3e6ef26"
},
"decisions": {
"rows": 21,
"maxSeq": 21,
"sha256": "fef1058660b0d128ef21089dda5e0fba7f71763d8325148fe521de0d3f57e4ad"
},
"deliveries": {
"rows": 1740,
"maxSeq": 1740,
"sha256": "554ce5b377ffacc72bfde11c99c920cd5d30337c8bfa217275d0073b587e2169"
},
"events": {
"rows": 1543,
"maxSeq": 1543,
"sha256": "0151673193d0286ce8f3b234d7d21fa2b0f43f2ceec55644cccaa5cbd77603dd"
},
"instance_names": {
"rows": 0,
"maxSeq": null,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"message_threads": {
"rows": 4,
"maxSeq": null
},
"messages": {
"rows": 571,
"maxSeq": 571
},
"meta": {
"rows": 3,
"maxSeq": null,
"sha256": "89fec56e7293d19e0b2f6cd8c0ef745ebf99e4115abeaea7fbdbc98a261332d9"
},
"projects": {
"rows": 2,
"maxSeq": 2,
"sha256": "3435f574ab94bc2fcc65fb8605251f30d4e9e63baf38e6fdf3c39c126531aa63"
},
"role_claims": {
"rows": 101,
"maxSeq": 101,
"sha256": "fb47114defbb8a57dd148a06fbaf61b30f791a81f14b1510b8c32690506b4308"
},
"task_comment_cursors": {
"rows": 220,
"maxSeq": 220,
"sha256": "df03c84094999299499de01107ddd08636df6156f5c9ccd2ff6e8c43e459f7e4"
},
"task_snapshots": {
"rows": 408,
"maxSeq": 408,
"sha256": "e2243a89388afba07e8d096b4961deeb2d17c656cdcace54b46eac7001393814"
},
"thread_claims": {
"rows": 4,
"maxSeq": 4,
"sha256": "088b8236570d8afe03364b85803fdc5034cce2cef6ed7b16f8b99bf6366524ad"
},
"thread_opens": {
"rows": 0,
"maxSeq": null,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"threads": {
"rows": 2,
"maxSeq": 2,
"sha256": "819fd24c64e279d0debad5ab5dbbe533d8e6f26de58277c69510b9cbbaadbc3a"
},
"usage_records": {
"rows": 34,
"maxSeq": 34,
"sha256": "de614f3d62c9b3819757c5659bee40085cb86cc873b7bfe381b13e254ddb7cad",
"byRun": [
{
"business": "mosaic-stack",
"run": "r8fba7e598156",
"input": "0",
"output": "0",
"unknownTurns": 1
},
{
"business": "mosaic-stack",
"run": "r4a96e2142e4d",
"input": "2",
"output": "52",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc78c9700c8e9",
"input": "14",
"output": "1519",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r293f33bcbc63",
"input": "2",
"output": "44",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r34520a581318",
"input": "6",
"output": "323",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r7e99838f9f93",
"input": "2",
"output": "44",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc964611b2092",
"input": "4",
"output": "120",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rbb708846b605",
"input": "12",
"output": "2089",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "rc36b17ef3788",
"input": "16",
"output": "5221",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r097436c91816",
"input": "2",
"output": "131",
"unknownTurns": 0
},
{
"business": "mosaic-stack",
"run": "r497f1c817d4d",
"input": "2",
"output": "80",
"unknownTurns": 0
}
]
}
},
"untimed": [
"message_threads",
"meta",
"task_comment_cursors"
]
}
# exit 0
# S2.json sha256 1a444f5ac0049bc78e397b6928e1008cfe8cbc30989bbf2042b4ed27f56a4173
# W9b end 2026-10-20T22:21:07Z
rc=0
@@ -1,57 +0,0 @@
#!/usr/bin/env bash
# Re-pin 4429a104f, W9 tail (adapted from Darkwing's window-rows-177-185/w9b.sh, under mos-0948's ruling for that window: the launcher
# ends the assistant run at threadIdleSeconds 900 after the 22:06:05Z reply, about 22:21:05Z). Nothing is stopped by hand. Report if live at 22:27Z.
# usage: w9b.sh
set -uo pipefail
REPO=/mnt/storage/src/mosaic-stack
WIN=/mnt/storage/src/mosaic-stack/agents/sage/work/window-repin-4429a104f
RUN=r497f1c817d4d
TID=446201de-e8b5-4df3-9356-d0d5422cc899
DATA=/home/jwoltje/.mosaic-dev
SNAP=$WIN/snapshot.mjs
SNAP_SHA=639fd28a2674aaa03ce824e3394b4052f21cdef7d6fa49bae61a9927ed341e67
PLAN=$REPO/agents/sage/work/window-plan/repin-4429a104f.md
PLAN_SHA=e1272d1a391e00fb1291d47da9973c0ba342d65fcd42f583ea007e883c1f32b9
TOOL_BLOB=3b068b1546d4020cffde5d73929d9130afe91911
MOSAIC=$HOME/.npm-global/bin/mosaic
DEADLINE=$(date -d "2026-10-20T22:27:00Z" +%s)
MARK18=$(date -d "2026-10-20T22:22:00Z" +%s)
W=$WIN/W.txt
note() { echo "# $*" >> "$W"; echo "# $*"; }
stop() { note "STOP: $*"; exit 1; }
sha() { sha256sum < "$1" | cut -c1-64; }
step() { local f="$1"; shift; $WIN/w.sh W.txt "$@" > "$WIN/$f"; local rc=$?; cat "$WIN/$f"; return $rc; }
# launches log lines for the run: at, event, instance, thread, run, reason, exitCode, signal only
runlog() { node -e '
const keep=["at","event","instance","thread","run","reason","exitCode","signal"];
for(const s of require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n")){const o=JSON.parse(s);if(o.run!==process.argv[2])continue;
console.log(keep.filter(k=>k in o).map(k=>`${k}=${o[k]}`).join(" "))}' "$DATA/launches/mosaic-stack.jsonl" "$RUN"; }
note "W9b start $(date -u +%FT%TZ): waiting for the launcher to end $RUN (thread $TID), no stop by hand"
[ "$(sha $PLAN)" = "$PLAN_SHA" ] || stop "the plan file is not e1272d1a"
[ "$(git -C $REPO rev-parse HEAD:scripts/stack-release.sh)" = "$TOOL_BLOB" ] || stop "tool blob moved"
END=; M18=
while :; do
$MOSAIC launches list --json > $WIN/.w9b-poll.json 2>&1 || stop "launches poll"
LIVE=$(node -e 'const l=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(l.filter(x=>x.run===process.argv[2]&&x.live===true).map(x=>x.run).join(" "))' $WIN/.w9b-poll.json $RUN)
[ -z "$LIVE" ] && { END=$(date -u +%FT%TZ); break; }
NOW=$(date +%s)
if [ -z "$M18" ] && [ $NOW -ge $MARK18 ]; then M18=1; note "22:22Z mark $(date -u +%FT%TZ): $RUN still live; launches log lines for it:"; runlog | tee -a "$W"; fi
[ $NOW -ge $DEADLINE ] && break
sleep 5
done
if [ -z "$END" ]; then
note "STOP: $RUN still live at $(date -u +%FT%TZ) (22:27Z mark); a stop is Mos's call. S2 waits."
runlog | tee -a "$W"
exit 4
fi
note "$RUN not live in launches list --json by $END (5 s poll)"
step .w9b-launches-after.txt "W9b launches list after the assistant run" $MOSAIC launches list
{ echo; echo "## W9b launches log lines for $RUN (at, event, instance, thread, run, reason, exitCode, signal only)"; runlog; } | tee -a "$W"
{ echo; echo "## W9b bus and webui journal since 2026-10-20 22:05:00 UTC, lines naming $RUN or thread-idle"; journalctl --user -u [email protected] -u mosaic-webui.service --since "2026-10-20 22:05:00 UTC" --no-pager -o short-iso-precise | grep -E "$RUN|thread-idle|threadIdle"; echo "(grep exit ${PIPESTATUS[1]})"; } | tee -a "$W"
[ "$(sha $SNAP)" = "$SNAP_SHA" ] || stop "SNAP sha before S2"
note "SNAP sha256 639fd28a checked before S2 $(date -u +%FT%TZ)"
step S2.json "S2 snapshot --held" node $SNAP --store "$DATA/bus/bus.sqlite" --held || stop "S2"
sed -i '$d' $WIN/S2.json
note "S2.json sha256 $(sha $WIN/S2.json)"
note "W9b end $(date -u +%FT%TZ)"
@@ -1,43 +0,0 @@
# W0b-W4 start 2026-10-20T22:00:49Z
# plan e1272d1a, tool blob 3b068b1546d4, P5 four scripts at NEW and clean, business-check.mjs eece4d46: checked 2026-10-20T22:00:49Z
# W0 copy present, w0.txt LOAD_OK
# W0b: /home/jwoltje/.config/mosaic-dev/businesses/mosaic-stack.json.pre-window-20261020T220049Z, sha256 1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93, recorded in backups.txt
rbb708846b605 mosaic-stack/chief claude-code claude-opus-5-5 by jason since 2026-10-20T19:29:59.045Z running
rc36b17ef3788 mosaic-stack/pm claude-code claude-opus-5-5 by chief since 2026-10-20T20:20:56.252Z running
# exit 0
# W1 start stamp 2026-10-20T22:00:49Z
# SNAP sha256 639fd28a checked before S1
# S1.json sha256 a7649760c3439705c8b5f3af26d0c21aea1081c1eb295fd02aef3a642064194b
pointer: /home/jwoltje/.mosaic-dev/state/stack-active.json
{"copy":"490e2771d3d057ab76991e2c7859cd251857e64c","business":"mosaic-stack","pm":true,"claude":"/home/jwoltje/.local/bin/claude","console":["--port","7332","--public-host","mosaic.woltje.com","--auth-header","X-authentik-username","--allow-user","jason.woltje","--tester-port","7333"],"schema":"3i","activation":"20261020T192956Z-490e2771d3d0","at":"2026-10-20T19:30:00Z"}
log (last five lines):
{"at":"2026-10-20T01:41:05Z","op":"pm-on","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","backup":null,"activation":"20261020T014103Z-89af38a83afa","result":"ok","pm":"on"}
{"at":"2026-10-20T07:58:00Z","op":"activate","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":null,"activation":"20261020T075800Z-490e2771d3d0","result":"started"}
{"at":"2026-10-20T07:58:02Z","op":"activate","from":"89af38a83afa22a8a4f0d368a0a4030ad15a0efa","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T075800Z-490e2771d3d0","activation":"20261020T075800Z-490e2771d3d0","result":"ok"}
{"at":"2026-10-20T19:29:56Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":null,"activation":"20261020T192956Z-490e2771d3d0","result":"started"}
{"at":"2026-10-20T19:30:00Z","op":"activate","from":"490e2771d3d057ab76991e2c7859cd251857e64c","to":"490e2771d3d057ab76991e2c7859cd251857e64c","backup":"20261020T192956Z-490e2771d3d0","activation":"20261020T192956Z-490e2771d3d0","result":"ok"}
check:
bus: /home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c
Console: /home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c
CLI: /home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c
result: pass
bus status:
host: mosaic-stack, pid 3229540, running, since 2026-10-20T19:29:57.662Z, notifier off
socket: present
writer.lock: pid 3229657
boot: organization file loaded at 2026-10-20T19:29:57.661Z
unit flags: match the pointer
drift: 109 commit(s) on refactor since the pointer's copy
# exit 0
# W3-plan.txt sha256 45de68d42b4cf88640fe0986d071dba4fa076b7db86ff4ed34de4bb6fa2915c6
DropInPaths=
ExecStart={ path=/home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c/scripts/mosaic ; argv[]=/home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c/scripts/mosaic bus start mosaic-stack --pm ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c
# exit 0
DropInPaths=
ExecStart={ path=/usr/bin/node ; argv[]=/usr/bin/node /home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c/packages/webui/src/cli.mjs serve --checkout /mnt/storage/src/mosaic-stack --port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --tester-port 7333 ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }
WorkingDirectory=/home/jwoltje/.local/share/mosaic-stack/copies/490e2771d3d057ab76991e2c7859cd251857e64c
# exit 0
# W4 checks: DropInPaths empty on both, both ExecStart name the OLD copy, bus has --pm, no quote or backslash in either ExecStart
# W4 bus Environment: 2 assignments; quote or backslash: none (values not recorded)
# W0b-W4 end 2026-10-20T22:00:50Z
@@ -1,80 +0,0 @@
#!/usr/bin/env bash
# Re-pin 4429a104f, W0b-W4 (repin-4429a104f.md at f41729178, sha256 e1272d1a...32b9; mos-1105, mos-1107 go). W0 ran before P2.
# Adapted from Darkwing's window-rows-177-185/wsteps.sh.
# Nothing is stopped. Each step goes through w.sh into W.txt; a failed check stops the script.
# usage: wsteps.sh
set -uo pipefail
REPO=/mnt/storage/src/mosaic-stack
WIN=$REPO/agents/sage/work/window-repin-4429a104f
OLD=490e2771d3d057ab76991e2c7859cd251857e64c
NEW=4429a104f6d65ccf165f6c83b5d1dce3147dfde0
PLAN=$REPO/agents/sage/work/window-plan/repin-4429a104f.md
PLAN_SHA=e1272d1a391e00fb1291d47da9973c0ba342d65fcd42f583ea007e883c1f32b9
TOOL_BLOB=3b068b1546d4020cffde5d73929d9130afe91911
BC=$REPO/agents/sage/work/window-plan/repin-de352dfaf/business-check.mjs
BC_SHA=eece4d4671e1ce15ba81e1654c641925ac4988b343687b062897392285a8a1a7
BIZ=$HOME/.config/mosaic-dev/businesses/mosaic-stack.json
BIZ_SHA=1e49bdf05e62607972958f5b7298cfd600c7825812370e26d7c2a842f2af9b93
DATA=/home/jwoltje/.mosaic-dev
SNAP=$WIN/snapshot.mjs
SNAP_SHA=639fd28a2674aaa03ce824e3394b4052f21cdef7d6fa49bae61a9927ed341e67
MOSAIC=$HOME/.npm-global/bin/mosaic
W=$WIN/W.txt
note() { echo "# $*" >> "$W"; echo "# $*"; }
stop() { note "STOP: $*"; exit 1; }
sha() { sha256sum < "$1" | cut -c1-64; }
note "W0b-W4 start $(date -u +%FT%TZ)"
[ "$(git -C $REPO show f41729178:agents/sage/work/window-plan/repin-4429a104f.md | sha256sum | cut -c1-64)" = "$PLAN_SHA" ] || stop "plan at f41729178 is not e1272d1a"
[ "$(sha $PLAN)" = "$PLAN_SHA" ] || stop "the plan file in REPO is not e1272d1a"
[ "$(git -C $REPO rev-parse HEAD:scripts/stack-release.sh)" = "$TOOL_BLOB" ] || stop "tool blob moved"
git -C $REPO diff --quiet $NEW HEAD -- scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh || stop "P5: the four scripts differ from NEW"
[ -z "$(git -C $REPO status --porcelain -- scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh)" ] || stop "P5: the four scripts are dirty"
[ "$(sha $BC)" = "$BC_SHA" ] || stop "business-check.mjs sha"
note "plan e1272d1a, tool blob 3b068b1546d4, P5 four scripts at NEW and clean, business-check.mjs eece4d46: checked $(date -u +%FT%TZ)"
# W0 ran at 21:45:57Z (w0.txt): the copy must still be there with LOAD_OK recorded.
[ -f "$HOME/.local/share/mosaic-stack/copies/$NEW/scripts/mosaic" ] || stop "W0 copy missing"
grep -qx LOAD_OK $WIN/w0.txt || stop "w0.txt has no LOAD_OK"
note "W0 copy present, w0.txt LOAD_OK"
# W0b
[ "$(sha $BIZ)" = "$BIZ_SHA" ] || stop "business file sha256 is not P1's 1e49bdf0"
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
COPY=$BIZ.pre-window-$STAMP
[ ! -e "$COPY" ] || stop "$COPY exists"
cp -p "$BIZ" "$COPY" || stop "W0b cp"
[ "$(sha "$COPY")" = "$BIZ_SHA" ] || stop "W0b copy sha256"
echo "W0b $(date -u +%FT%TZ) business file copy $COPY sha256 $(sha "$COPY") (source $BIZ sha256 $(sha "$BIZ"))" >> $WIN/backups.txt
note "W0b: $COPY, sha256 $(sha "$COPY"), recorded in backups.txt"
# W1
$WIN/w.sh W.txt "W1 agents --tester" $MOSAIC agents --business mosaic-stack --tester > $WIN/.w1-agents.txt || stop "W1 agents"
grep -E '(^|[[:space:]])chief[[:space:]].*working' $WIN/.w1-agents.txt && stop "W1: the chief is working"
$WIN/w.sh W.txt "W1 launches list" $MOSAIC launches list || stop "W1 launches list"
note "W1 start stamp $(date -u +%FT%TZ)"
[ "$(sha $SNAP)" = "$SNAP_SHA" ] || stop "SNAP sha before S1"
note "SNAP sha256 639fd28a checked before S1"
$WIN/w.sh W.txt "S1 snapshot --held" node $SNAP --store "$DATA/bus/bus.sqlite" --held > $WIN/S1.json || stop "S1"
sed -i '$d' $WIN/S1.json # w.sh prints "# exit N" last; S1.json keeps the snapshot only
note "S1.json sha256 $(sha $WIN/S1.json)"
# W2
$WIN/w.sh W.txt "W2 status" scripts/stack-release.sh status mosaic-stack || stop "W2 status"
# W3
$WIN/w.sh W.txt "W3 activate NEW --pm --plan" scripts/stack-release.sh activate $NEW mosaic-stack --pm --plan > $WIN/W3-plan.txt || stop "W3 plan exit"
sed -i '$d' $WIN/W3-plan.txt
note "W3-plan.txt sha256 $(sha $WIN/W3-plan.txt)"
# W4
$WIN/w.sh W.txt "W4 bus unit (Environment omitted from the transcript)" systemctl --user show [email protected] -p ExecStart -p WorkingDirectory -p DropInPaths || stop "W4 bus"
$WIN/w.sh W.txt "W4 webui unit" systemctl --user show mosaic-webui.service -p ExecStart -p WorkingDirectory -p DropInPaths || stop "W4 webui"
BUSL=$(systemctl --user show [email protected] -p ExecStart -p DropInPaths -p WorkingDirectory)
WEBL=$(systemctl --user show mosaic-webui.service -p ExecStart -p DropInPaths -p WorkingDirectory)
for u in BUSL WEBL; do v=${!u}; grep -qx 'DropInPaths=' <<< "$v" || stop "W4 $u DropInPaths not empty"; grep '^ExecStart=' <<< "$v" | grep -q "copies/$OLD/" || stop "W4 $u ExecStart does not name the OLD copy"; grep '^ExecStart=' <<< "$v" | grep -q "copies/$NEW" && stop "W4 $u ExecStart names NEW"; grep '^ExecStart=' <<< "$v" | grep -q "[\"'\\\\]" && stop "W4 $u ExecStart has a quote or backslash"; done
grep '^ExecStart=' <<< "$BUSL" | grep -q -- ' --pm' || stop "W4 bus ExecStart has no --pm"
note "W4 checks: DropInPaths empty on both, both ExecStart name the OLD copy, bus has --pm, no quote or backslash in either ExecStart"
ENVL=$(systemctl --user show [email protected] -p Environment)
note "W4 bus Environment: $(tr ' ' '\n' <<< "${ENVL#Environment=}" | grep -c =) assignments; quote or backslash: $(grep -q "[\"'\\\\]" <<< "$ENVL" && echo FOUND || echo none) (values not recorded)"
note "W0b-W4 end $(date -u +%FT%TZ)"
-32
View File
@@ -1363,35 +1363,3 @@ are never rewritten or removed; corrections are new entries.
2026-10-20T18:41:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design D1/D2 check (DESIGN.md blob eba861f5d2bb at d55d86bb0), static git diff 817e8f51b..d55d86bb0 | ack: D1 and D2 match; design approved; Mos C1 sequence confirm still open; no Gitea/queue record
2026-10-20T18:57:10Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r2 (#1688, DESIGN.md sha256 5493c9df3ec0) | CHANGES to Darkwing, cc Sage: B1 claim check on MOSAIC_CONFIG-only selection, B2 shared prefix drop-in dirs; N1-N7 notes; static read, no slot, no Gitea or queue record
2026-10-20T19:00:04Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r3 (#1688, DESIGN.md sha256 98236a1aff98) | APPROVE to Darkwing, cc Sage: B1 and B2 closed with M13 and M14, N7 settled by Sage's 48ebee0ff; static read, no slot; manifest round gets a queue review record (sage-1337)
2026-10-20T20:40:15Z | Filbert (Opus 5.5, 048f3c63) | row 190 (#1688) manifest round 1 review, candidate 8862feccc (static) | CHANGES: B1 base drift, row 189 ace27dc5e conflicts in stack-release.sh, rebase + full gate rerun; N1-N7 notes; Gitea 29634, queue rev 2362 (uncommitted, left for Sage)
2026-10-20T21:29:20Z | Filbert (Opus 5.5, 048f3c63) | row 190 (#1688) manifest round 2 review, candidate 537a02392 (static) | CHANGES: B1 base drift, row 192 4429a104f conflicts in mosaic-config.mjs and webui-service.sh (async load); round 2 content fine; Gitea 29685, queue rev 2447 (uncommitted, left for Sage)
2026-10-20T22:16:20Z | Filbert (Opus 5.5, 048f3c63) | row 190 (#1688) manifest round 3 review, candidate df65a3d4c on darkwing/row190-r3 | APPROVE, Gitea 29713, queue rev 2504 (left for Sage's queue-commit); static review, no slot used
2026-10-20T22:53:10Z | Darkwing (Opus 5.5, 7525c8e4) | row 190 (#1688) rounds 1-3; row 195 (#1693) design r5-r6 | 190 landed by Sage as 00d4cc1f6 (patch-id matches 176a6bf4b..df65a3d4c, gate 2664/0 + 429/0, rev 2508 done, #1688 closed, not live). My r3 gate 2042/0 and mutants 20/20 in slot sage-1512; Filbert APPROVE 29713; sha sent to Rocko for row 191. 195 r6 b7239bb66 approved (duckworth-0055); build held for the Q187 installer (Vikunja #432)
2026-10-20T23:20:31Z | Filbert (Opus 5.5, 048f3c63) | row 66 (#1559) round 3 rebase review, candidate 2369c00e3 (manifest 42e3876ca168) | APPROVE, Gitea 29770, queue rev 2526 (left for Sage's queue-commit); static review, no slot used
2026-10-20T18:45:06Z | Darkwing (Opus 5.5, 7525c8e4) | row 188 (#1686, P19 409/410): stack-release tests for a carried proof pair and a proof file inside the repository (late line; the row went done 11:14Z) | landed: 8fc0ee216 (Sage, pushed with 31e328958); candidate 86aa549bad75 matches (patch-id c9a56b167c07); script blob 2586a2a37cd2 unchanged; stack-release 151/151, control 19/19, M8/M9/M10 killed by T54 (T53 passes under each); readings 1 (refused log line) and 2 (hard link keeps the watched inode) accepted in sage-1282; Filbert approve 29433 (his M11 killed by test 4)
2026-10-20 18:51:10 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 design submitted to Dewey, Sage copied | MVP context read-only; worktree /mnt/storage/scratch/rocko-r191 at da230ca48, immediate node_modules link; durable per-root control/termination design ad6736e0; no product edits or tests, waits design approval.
2026-10-20 18:57:51 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 design r2 submitted | Dewey comment 29465 all eight items mapped; DESIGN-r2.md e480c76a, r1 unchanged; scope reduction to named/labelled Docker run retaining spawnSync recommended to Sage; no source edits/tests/live actions.
2026-10-20 19:04:45 UTC | Launchpad (Claude Opus 5.5, 28f6cc53) | row 192 #1690 Q175 SSO phase 1 build | started by Sage (sage-1332) under mos-0974/0977, worktree launchpad/row192, reviewer Webby
2026-10-20 19:04:45 UTC | Morgana (Claude Opus 5.5, 99d5ef45) | row 193 #1691 human users and roles, design | started by Sage (sage-1333) under mos-0974/0977, worktree morgana/row193, reviewer Duckworth
2026-10-20 19:04:45 UTC | Honker (Claude Opus 5.5, ecd17786) | row 194 #1692 agent lifecycle, design | started by Sage (sage-1334) under mos-0974/0977, worktree honker/row194, reviewer Webby
2026-10-20 19:04:45 UTC | Gander (Codex gpt-6.1-sol, 0bf412c3) | row 196 #1694 human boundary across uids (P19 333), design | started by Sage (sage-1339) under mos-0989, worktree gander/row196, reviewer Duckworth
2026-10-20 19:04:45 UTC | Herb (Codex gpt-6.1-sol, f6cf4287) | row 197 #1695 second gate lane, design | started by Sage (sage-1344) under mos-0977 item 6, worktree herb/row197, reviewer Webby
2026-10-20 19:04:45 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | MVP T1 staffing | rows 192-197 briefed (190b as 195, P19 333 as 196, gate lane as 197), issues 1690-1695, seats Launchpad/Morgana/Honker/Gander/Herb started; refactor pushed to 96f243faf
2026-10-20T19:08:56Z | Dewey (Opus 5.5, 84fb346c) | row 189 r1 review (#1687), row 191 design review r1+r2 (#1689) | 189 approve (29485, rev 2268; single 174/174, cli 480/480, D1-D5 killed); 191 design r1 changes (29465), r2 approve (29473)
2026-10-20T19:50:56Z | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 kill switch | approved design implementation in scratch; state/broker/launcher, Discord, tracked task fixtures and HTTP/UI node checks pass singly; browser, suites, mutants and final packet pending; no live action or commit.
2026-10-20 20:31:30 UTC | Rocko (d99326cc) | row 191 #1689 focused browser stage | scripted busy gate passed; processing browser 2/2 and script-list browser regression 1/1; awk total 3/3; cwd node/chromium sweep clear; slot returned to Sage, no live action.
2026-10-20T22:30:20Z | Morgana (Claude Opus 5.5, 99d5ef45) | row 204 (193a, #1702) and row 213 (C2-08, #1714) | 204 landed by Sage as d0bbc5c1c (patch-id 92c18aea matches, gate 2789/0), done rev 2506, #1702 closed 29724; 213 r2 aee32d185 on morgana/row213, Webby APPROVE (webby-0042, manifest b5ff0ac0), suites and 31 mutants wait on Sage's go; line left uncommitted for Sage's queue-commit
2026-10-20T22:43:28Z | Dewey (Opus 5.5, 84fb346c) | #436b design r1 review (Megavolt, 714c07304, sage-1545) | CHANGES: 4 blocking (foreign check by id + crash gap, append-only events and unreserved grant.* records, seat-token rotation rows, approval note on admin reach), 8 notes; agents/dewey/work/436b/design-r1-verdict.md; dewey-0754/0755 |
2026-10-20T22:54:15Z | Dewey (Opus 5.5, 84fb346c) | #436b design r2 review (Megavolt 6db7cc45d, sha256 9f1978a0) | CHANGES: r1 items 1-12 closed; blocks B1 step 0 grant.failed reads as spent under #spent, B2 settle seat-token introspection unproven and non-401 outcomes undefined, B3 step 5 reach vs forks from earlier grants; notes N1-N6. Verdict agents/dewey/work/436b/design-r2-verdict.md (bfdc0b9f), dewey-0756 to Megavolt, dewey-0757 to Sage
2026-10-20 22:58:45 UTC | Rocko (d99326cc) | row 191 #1689 rebase stage | new branch rocko/row191-r2-00d4 on 00d4cc1f6; 26 touched files 249/249 and 15 overlapping Client readers 102/102 singly; candidate a4f780c2, 78 paths; packet 185 files verified; no own cwd node/chromium; mutants and owner gate pending; no commit/push/live action.
2026-10-20T23:21:32Z | Dewey (Opus 5.5, 84fb346c) | row 66 (#1559) round 3 review, rebase of r2 onto c40d846cb (sage/row66-r3 2369c00e3, manifest 42e3876ca168) | APPROVE 29771, rev 2529: patch-id 6d085621 equals r2, range-diff =, 23 manifest hashes match, no drift past the base, four drifted files sit beside rows 190/192 cleanly, no hash pins; singles are Sage's (not rerun). Also #436b Q-B4 answered by mos-1147, verdict addendum
2026-10-20T23:31:31Z | Dewey (Opus 5.5, 84fb346c) | #436b design r3 review (megavolt-0050, DESIGN.md fa5658941) | CHANGES: restart reconcile spends step 0 refusals (#436a r6 X3 conflict), settle unknown id keeps unverified token in use; N7 secretCheck; B1-B3, N1-N6, D12 closed; verdict design-r3-verdict.md 4c87d859; dewey-0759 Megavolt, dewey-0760 Sage |
2026-10-20T23:32:42Z | Sage (Claude Opus 5.5, 1ef1e4f8) | machine grant rows, slot relay, queue sweep | rows 234 (#1740, 3i-3j) and 235 (#1741) added, row 206 moved to 3j-3k after 234 (f519fb1d2); row 199 r1 request as jarvis 29778 (manifest 35f900fd rebuilt from git objects); #436b r3 item 1 ruled (b), sage-1578; queue-commit 1c46d0abf revs 2456-2539; slot Megavolt to Gosalyn row 230
2026-10-21T01:28:05Z | Darkwing (Opus 5.5, 7525c8e4) | row 232 I1 build (sage-1558, sage-1561 ruling a) | rebased onto cb5fdb064 (row 200), gate green and 17/17 mutants killed on Sage's slot; in review round 1, manifest 7a3b4ea1 at 84f1a91e6 on darkwing/row232-r1, comment 29825, revs 2595-2596; Filbert woken
2026-10-21T01:34:13Z | Filbert (Opus 5.5, 048f3c63) | row 232 installer I1 review r1 (#1734, manifest 7a3b4ea133cc) | APPROVE, Gitea 29829, queue rev 2600 (left for Sage's queue-commit); static review, gate and mutants are Darkwing's receipts; 3 non-blocking notes
2026-10-21T01:49:55Z | Sage (Opus 5.5, 1ef1e4f8) | row 237 landed d7d943199 (#1743, gate green, M1 killed); PRD 1.7+1.8 landed 0adf8ddfe (Honker 2884ee496, Duckworth approve bf52628f9, tasks 97/0 bus 478/0 on the tip, DECISIONS Q192); rows 239 (#1745) and 240 (#1746) added; 233 briefed; queue revs 2540-2603 committed 584e2bd82 | landed, push next
2026-10-21T02:51:46Z | Sage (Opus 5.5, 1ef1e4f8) | row 210 landing (r5 0485384e9, Duckworth r3 29851) | landed f5753c0e8, patch-id 723b1c83 = r5 diff, manifest 688d38b9 OK; gate 2796/0 packages, 429/0 scripts; mutants 52/53 (M22b equivalent); #1707 closed (29853), Vikunja 424 comment 429; queue revs 2604-2617 c084d8d42; row 234 briefed; Megavolt has the slot for 241 (sage-1668)
2026-10-21T03:01:43Z | Filbert (Opus 5.5, 048f3c63) | row 232 installer I1 review r2, rebase scope (#1734, manifest 7af3c94308e0, da2da2f6c on 8fede5fc6) | APPROVE, Gitea 29865, queue rev 2622 (left for Sage's queue-commit); README = predicted 3-way blob 89a95b32, 7 blobs same as r1, BUILD.md +31/-0; corrected my r1 reader claim (host.test.mjs:855 reads cli README)
+12 -195
View File
@@ -15,7 +15,7 @@ and changes nothing.
| Command | Purpose | Notes |
|---|---|---|
| `scripts/bootstrap.sh [--instance NAME]` | Create `~/.config/mosaic-dev/config.json` if absent; with `--instance NAME`, `~/.config/mosaic-NAME/config.json` and the claimed root `~/.mosaic-NAME` | Idempotent; existing config validated, never rewritten. See [Stack instances](#stack-instances---instance-name-row-190) |
| `scripts/bootstrap.sh` | Create `~/.config/mosaic-dev/config.json` if absent | Idempotent; existing config validated, never rewritten |
| `scripts/build.sh` | Build the release image | Tag derived from `RELEASE` + pinned pi version |
| `scripts/hello.sh` | One-shot startup request | Prints model response on stdout |
| `scripts/verify.sh` | Full gated test | Exit 0 only on exact `MOSAIC_HELLO_OK`; `EXPECTED_MARKER` overrides for negative drills |
@@ -238,12 +238,6 @@ Exit codes: `0` ok · `2` invalid file or credential reference · `3` system
config problem · `4` usage or a required file missing. Details:
`packages/business/README.md`.
`mosaic credentials status|issue|rotate|revoke|delete` (row 199) goes to
`packages/cli` and the bus's human-only credential verbs: an agent's own
token in `<configDir>/secrets/agents/<business>/<agent>/<kind>`, read on
stdin, never printed. The tester may run `status` and `revoke`. Details:
`packages/cli/README.md`.
`instances` and `instance add|set` (row 165) go to `packages/cli` and the
bus's human-only `instances` and `instances.set` verbs, so they need a live
bus host and refuse inside an agent session. `instances` prints the revision,
@@ -276,9 +270,9 @@ scripts/mosaic follow --thread <thread> [--tester]
scripts/mosaic stop <run>
scripts/mosaic launches off | on [--business <id>]
scripts/mosaic launches list [--json]
scripts/bus-service.sh [--instance NAME] render [--copy ID [--copies DIR]] [--pm] [--claude PATH]
scripts/bus-service.sh [--instance NAME] install [--copy ID [--copies DIR]] [--pm] [--dir DIR] [--no-reload] [--claude PATH]
scripts/bus-service.sh [--instance NAME] uninstall | status <business>
scripts/bus-service.sh render [--copy ID [--copies DIR]] [--pm] [--claude PATH]
scripts/bus-service.sh install [--copy ID [--copies DIR]] [--pm] [--dir DIR] [--no-reload] [--claude PATH]
scripts/bus-service.sh uninstall | status <business>
```
The human commands read and resolve through the broker's human transport
@@ -327,90 +321,12 @@ stdin and stdout it exits 4 with the usage.
Exit codes: `0` ok · `1` failed or outcome unknown · `2` invalid input ·
`3` refused or config problem · `4` usage. Details: `packages/cli/README.md`.
## Stack instances (`--instance NAME`, row 190)
Several stacks can run on one host, each with its own config, data root,
units, Console port and CLI wrapper. The default instance is today's stack
and keeps every name it had. A named instance is created once:
```
scripts/bootstrap.sh --instance NAME
```
NAME matches `^[a-z][a-z0-9]{1,15}$`; anything else exits 4
(`instance-name-invalid`), and `--instance dev` is the default. Bootstrap
writes `~/.config/mosaic-NAME/config.json` (0644) and claims `~/.mosaic-NAME`
with the marker `instance.json`, `{"instanceVersion":1,"name":"NAME"}`. It
creates either directory 0700; one that already exists keeps its mode. It refuses with exit 3 and writes
nothing when the root exists, is not empty and holds no marker naming NAME.
A second run changes nothing. `~` is the home of the OS user that runs the
instance.
`--instance NAME` is taken, once and in any position, by `bootstrap.sh`,
`bus-service.sh`, `webui-service.sh` (all but `copy`), `discord-service.sh`,
`stack-copy.sh` (`wrapper` and `check`) and `stack-release.sh`. Copies serve
every instance, so both `copy` verbs refuse it with exit 4. Inside a process the config
travels as `MOSAIC_CONFIG`. Selection, checked before any read or write:
| Given | Config | Names |
|---|---|---|
| neither | `~/.config/mosaic-dev/config.json` | the default's |
| `MOSAIC_CONFIG` only | that file | the default's |
| `--instance NAME` only | `~/.config/mosaic-NAME/config.json` | NAME's |
| both | must be one file (a symlink to it agrees), else exit 4 `instance-config-mismatch` | NAME's |
Every selection checks the data root's marker. A named instance whose root
lacks its own marker refuses `instance-root-unclaimed`; a root whose marker
names another instance refuses `instance-root-mismatch`, also when a config
reaches it with no flag. Both exit 3 before anything is read or changed. A
missing config exits 3 with the bootstrap command that creates it.
| | Default | Instance NAME |
|---|---|---|
| bus unit | `[email protected]` | `[email protected]` |
| Console unit | `mosaic-webui.service` | `mosaic-webui-NAME.service` |
| Discord unit | `[email protected]` | `[email protected]` |
| CLI wrapper | `$BIN/mosaic` (Q81 marker) | `$BIN/mosaic-NAME`, marker `# mosaic-cli wrapper, instance NAME (row 190)`, exports `MOSAIC_CONFIG` |
| bus session env | `%h/.config/mosaic-dev/secrets/claude-session.env` | `~/.config/mosaic-NAME/secrets/claude-session.env` |
| Console port | `--port`, 7332 when absent | `--port` required; 7330, 7332 and 7333 exit 4 `port-reserved` |
A named instance's units add `Environment=MOSAIC_CONFIG=<its config>` and
` [NAME]` after the Description, and the Console unit's comment on the
variables it sets names `MOSAIC_CONFIG`. The default's renders are
byte-identical to the templates' renders before row 190. `stack-copy.sh
wrapper --instance NAME` creates `$BIN/mosaic-NAME` or replaces one that
carries NAME's marker, and refuses any other file. `check --instance NAME`
needs `--business ID`, and fails with `CLI: <copy> (MOSAIC_CONFIG mismatch)`
when the wrapper does not export NAME's config.
For every instance, a Console `--port` or `--tester-port` that another
installed Console unit in the unit directory already takes refuses
`port-claimed`, naming that unit: `webui-service.sh install` exits 1, and
`stack-release.sh activate` and `rollback` exit 3 before anything stops
(`--plan` lists it). A unit with no `--port` holds 7332.
`stack-release.sh --instance NAME` reads only NAME's pointer, log, store and
backups. Its busy check sees only NAME's bus units. Its drop-in scan adds
`mosaic-bus-NAME@*.service.d` and the shared `mosaic-bus-.service.d`. Its
rollback set holds NAME's two unit files, their own `.d` directories and
`$BIN/mosaic-NAME`; it never captures or restores the shared prefix
directories (`mosaic-bus-.service.d`, `mosaic-webui-.service.d`,
`mosaic-discord-.service.d`). The live-session guard refuses a path inside
any named instance's config directory or `~/.mosaic-NAME`, even when that
instance's config is unreadable.
All instances run under one OS user for now. That user's processes can
read and control every instance; a separate user per instance is row 195.
A real second instance on this host is a Q173 window, and tunnel or Traefik
exposure of a named Console waits for Jason after Q175 SSO.
## Stack copies (`scripts/stack-copy.sh`)
```
scripts/stack-copy.sh copy COMMIT [--copies DIR] [--allow-worktree]
scripts/stack-copy.sh copy --bundle FILE SHA [--copies DIR]
scripts/stack-copy.sh wrapper ID [--instance NAME] [--copies DIR] [--bin DIR] [--create]
scripts/stack-copy.sh check [--instance NAME] [--copies DIR] [--bin DIR] [--business ID] [--units user|external]
scripts/stack-copy.sh wrapper ID [--copies DIR] [--bin DIR]
scripts/stack-copy.sh check [--copies DIR] [--bin DIR] [--business ID]
```
One read-only tree supplies the bus, Console and human CLI (row 85,
@@ -424,14 +340,6 @@ renames beside the target. A published existing copy stays unchanged.
Dependencies use the exact lock and carry Pi's installed lock record.
Failure publishes nothing and removes the temporary tree.
`copy --bundle FILE SHA` (row 232) is for a host with no refactor checkout.
SHA is a full 40-character id. The bundle must pass `git bundle verify`,
carry `refs/heads/refactor`, and reach SHA from it; each miss refuses and
writes nothing. The copy is then made from SHA as `copy` makes it, and the
script prints `release:` with the RELEASE value at SHA and `bundle sha256:`
with the bundle's digest. That pair is the release identity on such a host.
Without `--bundle`, `copy`'s reachability rule is unchanged.
`wrapper` is a live switching step, reserved for Jason or Mos. It accepts
only an existing copy and a regular non-symlink mosaic wrapper with the
Q81 marker on line 2 and its exact lease line to mosaic-old. It preserves
@@ -439,16 +347,11 @@ both lines, adds comments naming the copy root and Q81's mosaic-old lease
probe, and atomically replaces the wrapper's final exec with the
copy's scripts/mosaic. --bin is a fixture override; the default is
~/.npm-global/bin. Neither the Q81 worktree nor mosaic-old is removed.
`wrapper ID --create` (row 232) writes the first wrapper on a new host, the
same file `wrapper` writes. It refuses when anything is at the wrapper path,
a symlink included, and never replaces it. Without `--create`, a missing
wrapper still refuses.
`check` changes nothing: it reads effective systemd WorkingDirectory and
ExecStart (including drop-ins) for the bus and Console, plus the wrapper
exec. It prints all three paths and succeeds only on the same existing
copy; absent/failed reads and a mismatched ExecStart fail. Business defaults
to mosaic-stack. `check --units external` (row 232) reads root's system units
with `systemctl show`, without `--user`. Queue work continues on the canonical scripts/mosaic.
to mosaic-stack. Queue work continues on the canonical scripts/mosaic.
Bus render/install accept --copy ID, --copies DIR, --pm and the unchanged
--claude pin. Without --copy the rendered unit stays the checkout unit.
@@ -465,14 +368,11 @@ reports checkout or copy identity and refactor drift, and writes nothing.
```
scripts/stack-release.sh adopt BUSINESS
scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
[--proof-header NAME --proof-file PATH | --no-proof | --sso] [--no-send] [--timeout S] [--plan]
[--proof-header NAME --proof-file PATH | --no-proof] [--no-send] [--timeout S] [--plan]
scripts/stack-release.sh pm-on BUSINESS [--timeout S]
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
scripts/stack-release.sh status BUSINESS
scripts/stack-release.sh confirm ID BUSINESS --units external [--rollback OLD --cause A3|A4|A5] [--timeout S]
scripts/stack-release.sh revert ID BUSINESS --units external
scripts/stack-release.sh gate ID BUSINESS --units external --since EPOCH [--timeout S]
common options: --copies DIR, --bin DIR, --instance NAME, --units user|external
common options: --copies DIR, --bin DIR
--events N is the total of the per-table counts the post-backup-events refusal prints: events and
every table in COUNTED.
```
@@ -588,36 +488,6 @@ rollbacks carry the pointer, so send stays off. No flag puts `--send` back.
`--plan` prints one `send:` line: `dropped`, `off` with or without the
`--no-send` note, or `carried`.
`activate ... --sso` (row 200, #1698) starts the Console with Q175 sign-in
(row 192). It drops `--public-host`, `--auth-header`, `--allow-user` and any
proof pair from the pointer's Console flags and the unit, and puts `--sso` after
the other flags, before `--send` and `--tester-port`. It may name the live copy
once, so a copy already at row 192 or later turns sign-in on without a copy
change. Later activates, `pm-on` and rollbacks to a copy with `--sso` carry it.
`--sso` with a proof pair or `--no-proof`, or on any other verb, is a usage
error. Before anything stops, a run whose Console flags hold `--sso` refuses
`sso-target` for a target Console without it, `sso-config` when the target's own
`checkConsole` refuses the config's `console` key (or there is none, in which
case, from another copy, it names the first step: activate the target without
`--sso`, then write `console.sso`), and `sso-slot` for each provider's slot file
that is missing, a symlink, not a regular file, not owned by this uid, has group
or other bits, resolves inside the repository or the copies, or is empty. It
uses `lstat`, `stat` and `realpath` on the slot and never reads it. `--plan`
prints one `sso:` line: `on` with what it drops and carries, `carried`, or
`dropped` on a rollback.
A rollback from an `--sso` pointer to a copy whose Console lacks `--sso` takes
the Console flags from the rollback set of the activation that moved the stack
onto the pointer's copy from the target, past any same-copy re-pin, checked
against the set's `SHA256SUMS`. `--send` and `--tester-port` follow the pointer.
When that set is missing, fails its sums, holds no readable argv or already ran
`--sso`, the run refuses `sso-rollback-flags`. Any activate or rollback also
refuses `config-console-key` before anything stops when the config holds a
`console` key the target's `scripts/mosaic-config.mjs validate` refuses (a copy
before row 192 refuses the key). The message says to remove the key first. So a
window re-pins to row 192 or later before it writes the key, and a rollback past
row 192 removes the key first.
#### A target past c216f47db: the PM after the harness kind
From c216f47db on, the bus launches the PM only with a `harness` kind in the
@@ -728,62 +598,10 @@ Usage exits 4. Nothing prunes rehearsal directories.
starts no thread ticker, and leaves an earlier host's `sessions.json` and
its runs alone. No unit passes it, and `--pm` with it is a usage error.
#### Root-owned units (`--units external`, row 232)
On a host whose bus and Console are root's system units, the stack user runs
`stack-release.sh` with `--units external` from its own clone of the release
bundle (sage-1561). The script then calls `systemctl show`, `is-active` and
`list-units` without `--user` and reads no journal. It starts, stops and
installs nothing. Root does those steps, and the I3 driver calls the verbs
below in order. Without `--units`, every command line and every byte of
output is what it was before row 232. `confirm`, `revert`, `gate`, `--to`
and `--expect-from` are usage errors without `--units external`.
- `adopt`, `status`, `activate ID` and `rollback --to ID` take the flag.
`rollback` then needs `--to`, and `--to` must be the copy `rollback` alone
would pick from the log, or it refuses `to-not-previous`.
- `--plan --expect-from OLD` refuses `pointer-differs` when the pointer
names a copy other than OLD.
- `pm-on` and `activate --pm-after-harness` refuse `external-pm`; the PM
flag is in root's bus unit. `--pm`, `--tester-port`, the proof flags,
`--no-proof`, `--no-send` and `--sso` are usage errors; root's Console
unit carries the Console flags.
- `activate` and `rollback` refuse `units-running` unless both units read
inactive or failed and the store is closed. They make the rollback set
with an exclusive `mkdir`, write `AWAITING-UNITS` (the target copy) first,
capture the pointer, the wrapper and both units' show output and unit file
sums, back up and migrate or restore the store, and write the wrapper.
They write no pointer and start nothing. The last line says the run is
staged and names `confirm`. A failure after the capture writes `FAILED`
with its code, logs nothing more and exits 1; the activation stays open.
- `confirm ID BUSINESS` needs exactly one `AWAITING-UNITS` for copy ID whose
activation the log holds open, or it refuses `open-activation`. It gates
the start root made and writes the pointer and an `ok` line with the
started line's op, from, to and backup.
`confirm --rollback OLD --cause A3|A4|A5` gates OLD after root put the old
units back and logs `failed` or `gate-failed` with `rollback ok` (exit 1)
or `rollback-failed` (exit 2). Either removes `AWAITING-UNITS` and `FAILED`.
- `revert ID BUSINESS` puts the saved wrapper back and, when the run had
started a migration, restores the backup taken before it. It writes no log
line and can run again.
- `gate ID BUSINESS --since EPOCH` gates a copy with no open activation,
takes no lock and writes nothing: 0 on a pass, 1 otherwise. EPOCH is root's
clock before root started the units.
The gate under `--units external` replaces the journal read. The bus unit's
`ActiveEnterTimestamp`, in whole seconds, must be at or after the started
line's second (`confirm`) or `--since` (`gate`); an older start fails
`stale-start`, so a unit that never stopped can't pass. With the PM on, the
gate reads `launches list --json` for exactly one live launch of the PM's
instance (the business's `launch.by`), for this business, whose run started at
or after the floor. `status` adds an
`awaiting units:` line for an open activation.
## Console service (`scripts/webui-service.sh`)
```bash
scripts/webui-service.sh copy COMMIT [--copies DIR] # make a pinned copy
# render, install, uninstall and status also take --instance NAME (row 190); copy refuses it (exit 4)
scripts/webui-service.sh render [--copy ID [--copies DIR]] [FLAGS] # print the unit
scripts/webui-service.sh install --copy ID [--copies DIR] [--dir DIR] [--no-reload] [FLAGS]
scripts/webui-service.sh uninstall [--dir DIR] [--no-reload]
@@ -861,9 +679,8 @@ Each value is held to a plain character set the unit can carry (no space,
`%`, `$`, quote, backslash, `&` or `|`), and the preview values must start
with a letter or digit; anything else is refused. With `--auth-header
X-authentik-uid`, in any letter case, `--allow-user` must be the Authentik
uid, `^[0-9a-f]{64}$` (lead decision 101). The default's unit sets no `MOSAIC_*`,
`CLAUDE*`, `CODEX*` or `PI_*` variable; a named instance's sets only
`MOSAIC_CONFIG` (row 190). Serve's exit 2 (usage, config, or a
uid, `^[0-9a-f]{64}$` (lead decision 101). The unit sets no `MOSAIC_*`,
`CLAUDE*`, `CODEX*` or `PI_*` variable. Serve's exit 2 (usage, config, or a
port already in use) is not restarted.
The update/rollback sequence is [Pinned stack update](#pinned-stack-update)
@@ -888,7 +705,7 @@ scripts/discord.sh stop <binding>
scripts/discord.sh unlock <binding>
scripts/discord.sh recover <binding>
scripts/discord.sh reload <binding>
scripts/discord-service.sh [--instance NAME] render | install [--dir DIR] [--no-reload] | uninstall [--dir DIR] [--no-reload] | status <binding>
scripts/discord-service.sh render | install [--dir DIR] [--no-reload] | uninstall [--dir DIR] [--no-reload] | status <binding>
```
One seat's conversation reachable from listed channels of one Discord

Some files were not shown because too many files have changed in this diff Show More