diff --git a/agents/README.md b/agents/README.md index e698bfb0c..63e225e15 100644 --- a/agents/README.md +++ b/agents/README.md @@ -19,6 +19,10 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify | Launchpad | Builder, MVP T1 row 192 (Q175 SSO phase 1) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only | | Morgana | Builder, MVP T1 row 193 (human users and RBAC) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only | | Honker | Builder, MVP T1 row 194 (agent lifecycle) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only | +| Gander | Builder, the human boundary across uids (P19 333, #1694) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only | +| Herb | Builder, a second gate lane (#1695) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only | +| Bushroot | Builder, retire the control board writer on 7331 (#1696) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1008 | none, T3 only | +| Megavolt | Builder, MVP T1 item 6 (agent credentials and git identity, #1697) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only | | Webby | Reviewer: Case 2 designs, rows 192 and 194 | T3 session (Claude, Opus 5.5, thread 03086006), started by Mos (mos-0981) | none, T3 only | | Duckworth | Reviewer: federation designs, row 193 | T3 session (Claude, Opus 5.5, thread ffceafec), started by Mos (mos-0977) | none, T3 only | | Sage | Project lead: coordination, review, integration; earlier DYOR strategy records retained | T3 session (Claude Code); Pi launcher `zai/glm-5.3:high` retained | `agents/sage/launch.sh` | diff --git a/agents/bushroot/README.md b/agents/bushroot/README.md new file mode 100644 index 000000000..50f216e8a --- /dev/null +++ b/agents/bushroot/README.md @@ -0,0 +1,8 @@ +# Bushroot + +Builder seat, Claude Haiku 5.5, started by Sage under mos-1008 item 2 (a low-level model under Q177) for the row +that retires the control board writer on 7331 (#1696). + +No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no +credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `bushroot/*` +branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch. diff --git a/agents/duckworth/README.md b/agents/duckworth/README.md index 04b947127..b9c2899e3 100644 --- a/agents/duckworth/README.md +++ b/agents/duckworth/README.md @@ -3,5 +3,5 @@ Reviewer seat, Claude Opus 5.5, started by Mos (mos-0977, thread ffceafec). Reviews federation designs and MVP T1 row 193. No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no -credentials of its own; until row 195 settles per-agent identity, it pushes only its own `duckworth/*` branches +credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `duckworth/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch. diff --git a/agents/gander/README.md b/agents/gander/README.md new file mode 100644 index 000000000..a8c3fe7a4 --- /dev/null +++ b/agents/gander/README.md @@ -0,0 +1,8 @@ +# Gander + +Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for the human +boundary row (P19 333, #1694, mos-0989). + +No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no +credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `gander/*` +branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch. diff --git a/agents/herb/README.md b/agents/herb/README.md new file mode 100644 index 000000000..07be48a59 --- /dev/null +++ b/agents/herb/README.md @@ -0,0 +1,8 @@ +# Herb + +Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for a second gate lane +design row (#1695, mos-0977 item 6). + +No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no +credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `herb/*` +branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch. diff --git a/agents/honker/README.md b/agents/honker/README.md index 936e95b47..b42abaad1 100644 --- a/agents/honker/README.md +++ b/agents/honker/README.md @@ -3,5 +3,5 @@ Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 194 (agent lifecycle, #1692) under mos-0974 and mos-0977. No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no -credentials of its own; until row 195 settles per-agent identity, it pushes only its own `honker/*` branches +credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `honker/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch. diff --git a/agents/launchpad/README.md b/agents/launchpad/README.md index 008806089..e6f101647 100644 --- a/agents/launchpad/README.md +++ b/agents/launchpad/README.md @@ -3,5 +3,5 @@ Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 192 (Q175 SSO phase 1, #1690) under mos-0974 and mos-0977. No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no -credentials of its own; until row 195 settles per-agent identity, it pushes only its own `launchpad/*` branches +credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `launchpad/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch. diff --git a/agents/megavolt/README.md b/agents/megavolt/README.md new file mode 100644 index 000000000..1d75bca85 --- /dev/null +++ b/agents/megavolt/README.md @@ -0,0 +1,8 @@ +# Megavolt + +Builder seat, Claude Opus 5.5, started by Sage under mos-0974 and mos-0977 item 5 for MVP T1 item 6, the +credential store per agent with its own git identity (#1697). + +No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no +credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `megavolt/*` +branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch. diff --git a/agents/morgana/README.md b/agents/morgana/README.md index 2bad8da25..eb896fa15 100644 --- a/agents/morgana/README.md +++ b/agents/morgana/README.md @@ -3,5 +3,5 @@ Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 193 (human users and RBAC, #1691) under mos-0974 and mos-0977. No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no -credentials of its own; until row 195 settles per-agent identity, it pushes only its own `morgana/*` branches +credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `morgana/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch. diff --git a/agents/sage/work/gate-lane/busy-now.sh b/agents/sage/work/gate-lane/busy-now.sh new file mode 100644 index 000000000..74b023b80 --- /dev/null +++ b/agents/sage/work/gate-lane/busy-now.sh @@ -0,0 +1,8 @@ +#!/bin/bash +n=0 +for p in /proc/[0-9]*; do + c=$(tr '\0' ' ' < $p/cmdline 2>/dev/null) || continue + case "$c" in *shell-snapshots*|*chrome-devtools-mcp*|*sage-busy-now*) continue;; esac + if echo "$c" | grep -qE 'node --test|gate\.sh|mutant|scripts/test-[a-z-]+\.sh|docker compose'; then echo "BUSY ${p#/proc/}: ${c:0:160}"; n=$((n+1)); fi +done +[ $n -eq 0 ] && echo CLEAR diff --git a/agents/sage/work/gate-lane/gate-template.sh b/agents/sage/work/gate-lane/gate-template.sh new file mode 100755 index 000000000..2d7fc22e1 --- /dev/null +++ b/agents/sage/work/gate-lane/gate-template.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -o pipefail +# Landing gate template (2026-10-14): sed the G= and OUT= lines and this comment. +# Sequential suites, each teed to its own log; stops at the first failure. +G=/mnt/storage/scratch/SET-ME-wt +OUT=/mnt/storage/scratch/SET-ME-out +cd "$G" || exit 9 +mkdir -p "$OUT" || exit 9 # a missing OUT failed the row 142 gate at step 1 (2026-10-15) +# test-task.sh live cases run `docker compose` from $G, so without a name the project is "wt" +# and leaves wt_default behind (row 142 gate, 2026-10-15). Name it per gate dir and remove it on exit. +export COMPOSE_PROJECT_NAME="$(basename "$(dirname "$G")")" +trap 'docker network rm "${COMPOSE_PROJECT_NAME}_default" >/dev/null 2>&1 && echo "removed network ${COMPOSE_PROJECT_NAME}_default"' EXIT +# A gate tree without the node_modules link fails cli tests with ENOENT (2026-10-13). +[ -d "$G/node_modules" ] || { echo "STOP no node_modules in $G"; exit 8; } +echo "base $(git rev-parse --short HEAD) dirty $(git status --short | grep -vc node_modules) DOCKER_HOST=${DOCKER_HOST:-unset}" +run() { local name="$1" dir="$2" cmd="$3" + (cd "$G/$dir" && bash -c "$cmd") > "$OUT/$name.log" 2>&1; local rc=$? + echo "$name rc=$rc $(grep -E '^ℹ (pass|fail) [0-9]+' "$OUT/$name.log" | tr '\n' ' ') | $(tail -n1 "$OUT/$name.log")" + [ $rc -eq 0 ] || { echo "STOP $name"; exit $rc; }; } +# No pkg-queue step: scripts/test-queue.sh runs packages/queue/tests/ itself (same 156 tests; CI debate item 2a, Vikunja #274). +for d in packages/*/; do p=$(basename "$d"); [ -d "$d/tests" ] || continue; [ "$p" = queue ] && continue + run "pkg-$p" "packages/$p" 'node --test tests/*.test.mjs'; done +for s in scripts/test-*.sh; do n=$(basename "$s" .sh); run "script-${n#test-}" . "bash $s"; done +echo GATE_PASSED diff --git a/agents/sage/work/gate-lane/timing-g189.md b/agents/sage/work/gate-lane/timing-g189.md new file mode 100644 index 000000000..da746c707 --- /dev/null +++ b/agents/sage/work/gate-lane/timing-g189.md @@ -0,0 +1,36 @@ +# Row 189 gate timing (for row 197, Herb) + +Sage, 2026-10-20T19:25:04Z. Gate on 0c177e9ef (base 0217b71c8), one lane. The slot was mine. Seats may have run single test files at the same time, which the slot rules allow, and I did not record that. +Start 2026-10-20T19:09:37Z, end 2026-10-20T19:20:36Z. Total wall time: 659 s. + +Wall time per suite is the gap between one suite's log mtime and the one before it (the first suite starts at start.txt), so it includes node start-up. node's duration_ms is listed where the suite prints it. Scripts print no duration. + +| Suite | Wall s | duration_ms s | Ended (UTC) | +|---|---|---|---| +| pkg-bus | 3 | 3.8 | 19:09:40 | +| pkg-business | 3 | 2.2 | 19:09:43 | +| pkg-cli | 388 | 388.5 | 19:16:11 | +| pkg-control-board | 1 | 0.8 | 19:16:12 | +| pkg-conversation | 35 | 35.4 | 19:16:47 | +| pkg-discord | 3 | 2.6 | 19:16:50 | +| pkg-harness | 12 | 11.6 | 19:17:02 | +| pkg-ledger | 13 | 13.5 | 19:17:15 | +| pkg-mosaic | 3 | 2.3 | 19:17:18 | +| pkg-plugins | 0 | 0.3 | 19:17:18 | +| pkg-project | 1 | 1.2 | 19:17:19 | +| pkg-runs | 1 | 0.8 | 19:17:20 | +| pkg-seat | 6 | 6.2 | 19:17:26 | +| pkg-tasks | 1 | 1.2 | 19:17:27 | +| pkg-webui | 58 | 57.9 | 19:18:25 | +| script-auth | 1 | - | 19:18:26 | +| script-conductor | 5 | - | 19:18:31 | +| script-config | 2 | - | 19:18:33 | +| script-discord | 8 | - | 19:18:41 | +| script-extension-package | 1 | - | 19:18:42 | +| script-foundation | 40 | - | 19:19:22 | +| script-land-work | 11 | - | 19:19:33 | +| script-queue | 21 | - | 19:19:54 | +| script-release | 11 | - | 19:20:05 | +| script-task | 31 | - | 19:20:36 | + +Source: /mnt/storage/scratch/sage-g189/out/*.log mtimes and gate.log. The cli suite is about 59% of the run, and webui plus conversation add about 14%. diff --git a/agents/sage/work/queue-189/BUILD.md b/agents/sage/work/queue-189/BUILD.md new file mode 100644 index 000000000..483f360e5 --- /dev/null +++ b/agents/sage/work/queue-189/BUILD.md @@ -0,0 +1,86 @@ +# Row 189: `stack-release.sh activate --no-send` + +Gitea #1687. Vikunja P19 #413 (Q175 interim, #412). Brief: docs/plans/2026-10-20_activate-no-send.md. +Rulings: mos-0972 (the Console runs without `--send` until Q175 phase 2), mos-0973 (go; Sage builds, +Dewey reviews; gate is the cli suite plus every scripts/test-*.sh). Base: def2e7932 (refactor, the +brief commit on 9ae8c73ac). + +## What it does + +`activate ID BUSINESS --no-send` drops `--send` from `RUN_CONSOLE` in a new `send_for_target`, which +`console_for_target` calls after `proof_for_target`. `install_stack` renders the unit from +`RUN_CONSOLE` and `write_pointer` writes the pointer from it, so both lose the flag and `flags_drift` +stays quiet afterwards. Only `activate` takes the flag; the parser sends every other op to usage +(exit 4). + +`check_target` lets a `--no-send` activate name the pointer's copy only when the pointer's Console +flags hold `--send`. That run is a live-copy re-pin, the same path row 187 uses for the proof pair: +the plan prints the live-copy note, the run stops both units, backs up the stopped store and starts +them again. A plain same-copy activate, or a same-copy `--no-send` once `--send` is gone, still +refuses `already-active`. + +`--plan` prints one `send:` line after the `proof:` line, for activate and rollback: + +- `send: dropped; --no-send was given` +- `send: off; --no-send was given and the pointer holds no --send` +- `send: carried` (the pointer's `--send` goes on unchanged) +- `send: off` + +The steps log gets `drop --send; --no-send was given`, or `--no-send was given and the pointer holds no +--send, so there is nothing to drop`. + +Rollback and pm-on carry the pointer's flags as they are, so once `--no-send` has run, nothing adds +`--send` back. There is no re-enable flag (mos-0973). Q175 phase 2 changes the Console flags in its +own row. + +docs/TOOLS.md: `[--no-send]` in the usage block and one paragraph after the proof paragraph. + +## Tests (packages/cli/tests/stack-release.test.mjs, T55) + +1. Seeded preview pointer with `--send --tester-port 7333`: a same-copy `activate B acme --no-send` + passes. The pointer and the unit ExecStart lack `--send` and keep the other flags in order, status + says the unit flags match, the steps log has the drop line, the log line has from = to = B and the + backup sum equals the stopped store's, and the units stop and start in order. +2. Plain same-copy activate, with `--tester-port` and with `--no-proof`, refuses `already-active`. So does + a same-copy `--no-send` once `--send` is gone, and on a pointer that never held it. +3. The four `send:` lines, the live-copy note on the same-copy plan only, and the Console line right + after `send:` without `--send`. The run on a pointer with no `--send` logs the nothing-to-drop line + and no drop line. +4. After `--no-send` on C1, a rollback to B leaves `--send` out of the pointer and the unit, the steps + log never names `--send`, and status matches. pm-on after `activate --pm-after-harness --no-send` + keeps it off. +5. `--no-send` on rollback (with and without `--plan`), pm-on, status, adopt and rehearse is a usage + error with the usage text, nothing written, started or logged. The usage text shows `[--no-send]`. +6. A pointer holding both the proof pair and `--send`: the plans for `--no-proof`, `--no-send` and both + name what each drops and carries. `--no-send` keeps the pair, `--no-send` with a new pair replaces it, + `--no-proof` keeps `--send`, and both together leave neither. Status matches after each. + +`planTail` gains `'send: off'` after `'proof: off'`, because every plan now prints the line. T36 takes +`out.slice(-n)` around `planTail`, so its four slice counts grow by one. + +## Mutants + +mutants.sh in this packet, run on the candidate. Each mutates scripts/stack-release.sh and runs T55 +plus T1 and T2. + +- M1: the same-copy exception ignores `--no-send`. +- M2: the same-copy exception allows `--no-send` without `--send` in the pointer. +- M3: with `--no-send`, the pointer is written from `P_CONSOLE`, so it keeps `--send` while the unit + drops it. +- M4: rollback re-adds `--send`. +- M5: the parser accepts `--no-send` for rollback. + +Results: mutants-run.log, all five killed (M1 5 failures, M2 2, M3 7, M4 2, M5 2). + +## Deviations from the brief + +- The brief's line numbers were taken before row 163 landed. The candidate corrects them to the + 9ae8c73ac lines (check_target :249-256, flags_drift :646-657, console_for_target :664-693, adopt + :828). +- Test 5 also covers rehearse and `rollback --plan`, which the brief did not list. + +## Outbound calls + +None. The change edits an array before the existing unit render and pointer write. Every test uses +the stand-in systemctl, units, stores and data root the file already builds; no test touches +`~/.mosaic-dev`, the live units or a slot file. diff --git a/agents/sage/work/queue-189/mutants-run.log b/agents/sage/work/queue-189/mutants-run.log new file mode 100644 index 000000000..aa18acf26 --- /dev/null +++ b/agents/sage/work/queue-189/mutants-run.log @@ -0,0 +1,5 @@ +M1 KILLED fail=5 +M2 KILLED fail=2 +M3 KILLED fail=7 +M4 KILLED fail=2 +M5 KILLED fail=2 diff --git a/agents/sage/work/queue-189/mutants.sh b/agents/sage/work/queue-189/mutants.sh new file mode 100644 index 000000000..c3daba508 --- /dev/null +++ b/agents/sage/work/queue-189/mutants.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Row 189 mutants M1-M5: each mutates scripts/stack-release.sh in the sage-b189 worktree, runs T55 and +# the plan-tail tests, and restores the file from a checked backup. +set -uo pipefail +WT=/mnt/storage/scratch/sage-b189 +OUT=/mnt/storage/scratch/sage-b189-mut/out +S=$WT/scripts/stack-release.sh +mkdir -p "$OUT" || exit 1 +cp -p "$S" "$OUT/stack-release.sh.orig" || { echo "backup failed"; exit 1; } +ORIG=$(sha256sum "$S" | cut -d' ' -f1) +[ "$(sha256sum "$OUT/stack-release.sh.orig" | cut -d' ' -f1)" = "$ORIG" ] || { echo "backup differs"; exit 1; } +restore() { cp -p "$OUT/stack-release.sh.orig" "$S"; [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ] || { echo "RESTORE FAILED"; exit 1; }; } +trap restore EXIT + +mutate() { + case "$1" in + M1) sed -i '/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/d' "$S" ;; + M2) sed -i 's/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/{ [ "$NO_SEND" = 1 ]; } ||/' "$S" ;; + M3) sed -i 's/^ write_pointer "\$TO" "\$RUN_PM" "\$RUN_PENDING" "\$P_CLAUDE" "\$T_SCHEMA" "\${RUN_CONSOLE\[@\]}"$/ if [ "$NO_SEND" = 1 ]; then write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${P_CONSOLE[@]}"; else write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${RUN_CONSOLE[@]}"; fi/' "$S" ;; + M4) sed -i 's/^ SEND_PLAN="send: off"$/ SEND_PLAN="send: off"; [ "$OP" != rollback ] || RUN_CONSOLE+=(--send)/' "$S" ;; + M5) sed -i 's/--no-send) \[ "\$OP" = activate \] || usage;/--no-send) { [ "$OP" = activate ] || [ "$OP" = rollback ]; } || usage;/' "$S" ;; + esac +} + +for m in M1 M2 M3 M4 M5; do + restore + mutate "$m" + if [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ]; then echo "$m NOT-APPLIED"; continue; fi + bash -n "$S" || { echo "$m SYNTAX"; continue; } + diff "$OUT/stack-release.sh.orig" "$S" > "$OUT/$m.diff" + (cd "$WT" && TMPDIR=/mnt/storage/scratch/t189 timeout 900 node --test --test-reporter=tap --test-name-pattern='T55|T1 |T2 ' packages/cli/tests/stack-release.test.mjs) > "$OUT/$m.log" 2>&1 + fails=$(awk '/^# fail /{print $3}' "$OUT/$m.log") + if [ "${fails:-0}" -gt 0 ]; then echo "$m KILLED fail=$fails"; else echo "$m SURVIVED"; fi +done diff --git a/agents/webby/README.md b/agents/webby/README.md index 6ba6c1765..c2bca8168 100644 --- a/agents/webby/README.md +++ b/agents/webby/README.md @@ -3,5 +3,5 @@ Reviewer seat, Claude Opus 5.5, started by Mos (mos-0981, thread 03086006). Reviews Case 2 designs and MVP T1 rows 192 and 194. No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no -credentials of its own; until row 195 settles per-agent identity, it pushes only its own `webby/*` branches +credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `webby/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch. diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 9f20adc24..bcdee3639 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -1361,3 +1361,5 @@ are never rewritten or removed; corrections are new entries. 2026-10-20T18:34:53Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r2 (DESIGN.md blob 3eb718b5e131 at 1a268249e), static | changes: C1 config.json console key fails old loaders (mosaic-config.mjs:113 rejects unknown keys; key add inside phase 2 window, rollback removes it, loader test), C2 app.js signs out only on opaqueredirect so 401 never shows sign-in; notes on tests 15/7/9a, Q-B 200-HTML classification, scratch Console flags; Q-E keep iss/sub escaped and capped; verdict sent to Sage, no Gitea/queue record 2026-10-20T18:40:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r3 (DESIGN.md blob a152fd58e18e at 817e8f51b), static | approve on condition D1 (slot file checks stay out of shared loader, shape-only there) as a line edit; D2 notes: slot root from configDir, scratch dataRoot, name discovery GETs for Mos; C1/C2 and notes verified; Mos C1 sequence confirm still open; no Gitea/queue record 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) diff --git a/docs/TOOLS.md b/docs/TOOLS.md index f2892ca04..c45b35984 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -368,7 +368,7 @@ 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] [--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 @@ -480,6 +480,14 @@ copies. It checks the file with `stat` and `realpath` and never reads the value. or `off`. On the live copy it also notes that the run re-pins the pointer's copy and only the Console flags change. +`activate ... --no-send` (row 189, mos-0972) drops `--send` from the pointer's +Console flags and the unit, so the Console serves reads and no human sends. It +may name the live copy while the pointer holds `--send`; once `--send` is gone a +same-copy `--no-send` refuses `already-active`. Later activates, `pm-on` and +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`. + #### 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 diff --git a/docs/plans/2026-10-20_activate-no-send.md b/docs/plans/2026-10-20_activate-no-send.md index 0444b774f..29a9e6d2d 100644 --- a/docs/plans/2026-10-20_activate-no-send.md +++ b/docs/plans/2026-10-20_activate-no-send.md @@ -1,7 +1,8 @@ # Brief: activate --no-send drops the Console's --send Sage, 2026-10-20, for Mos's ruling mos-0972 (Q175 interim, Vikunja #412). Owner Sage, reviewer Dewey, gate owner -Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit. +Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit. The candidate corrects them; +the first draft took them before row 163. This row builds the tool only. The live restart without `--send` is a separate step afterwards, a same-copy activate under Q173 with a backup first and a STATE report after. No test here touches `~/.mosaic-dev`, the @@ -14,11 +15,11 @@ Mos ruled that the Console runs without `--send` until Q175 phase 2, because wit process can forge `X-authentik-username` on 127.0.0.1:7332 and send as Jason (design section 8). On the pinned stack, the Console flags live in the pointer (`~/.mosaic-dev/state/stack-active.json`): -- `console_for_target` (scripts/stack-release.sh:658-687) carries the pointer's flags unchanged. Only +- `console_for_target` (scripts/stack-release.sh:664-693) carries the pointer's flags unchanged. Only `--tester-port` and the row 187 proof pair can change. -- `check_target` (:247-249) lets an activate name the pointer's copy only for a proof-pair change. -- `adopt` refuses once a pointer exists (:822). -- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:640-651) refuse +- `check_target` (:249-256) lets an activate name the pointer's copy only for a proof-pair change. +- `adopt` refuses once a pointer exists (:828). +- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:646-657) refuse `unit-flags-drift` on every later activate, pm-on and rollback. So the tool can't drop `--send` today. This row adds that one path. diff --git a/docs/plans/2026-10-20_agent-credentials.md b/docs/plans/2026-10-20_agent-credentials.md new file mode 100644 index 000000000..18d6a5180 --- /dev/null +++ b/docs/plans/2026-10-20_agent-credentials.md @@ -0,0 +1,57 @@ +# Brief: a credential store per agent, own git identity, pushes to a remote + +Sage, 2026-10-20, for mos-0974 (MVP T1 item 6) and the MVP plan +(`jason-master-restart/projects/mosaic-mvp/PLAN.md` item 6, GAP-MAP G11 and G13; TARGET.md: "credentials store +available for all agents; secrets protected with RLS equivalent" and "Agent uses own credentials for git and +other actions"). Gitea #1697. Owner Megavolt (Opus 5.5, a new builder seat), reviewer Duckworth, gate owner +Sage. Base: refactor at the brief commit. Design first. The first deliverable is +`agents/megavolt/work/queue-ROW/DESIGN.md`. + +This row draws a security boundary. Sage reviews the design's authority model, and Mos rules on it before any +code is written. + +## What exists today (GAP-MAP, measured at 9ae8c73ac) + +- G11. Credential references exist per instance for Gitea and Vikunja only + (`packages/business/src/credentials.mjs`, `packages/bus/src/credentials.mjs`, `vocabulary.mjs:103`). One + Claude env file is shared by every seat. +- G13. Agents use their own Vikunja token (`packages/tasks/src/adapter.mjs`). Git goes through the host + socket (`packages/cli/src/git-socket.mjs`). Gitea tokens are loaded but never used. +- The development repository's helper (`scripts/git-credential-mosaic.mjs`, wired in `.git/config` for + git.mosaicstack.dev) pushes as one identity. Until this row lands, seats push only their own `/*` + branches through it (mos-0981). + +## What the design must settle + +1. The store. Where each agent's credentials live, per instance, under the row 190 config canon. File modes, + ownership and the read path. Which process may read which entry, and how the bus enforces that (the "RLS + equivalent"). A process asking for another agent's entry is refused and logged. +2. Kinds. Gitea (git over HTTPS, and the API), Vikunja, and the model provider. Say which kinds 10-27 needs + and which wait. The model provider entry must not copy jwoltje's `claude-session.env`. If it needs a new + provider account or subscription, flag it for Jason and don't pick one. +3. Git identity and push. Each agent commits with its own name and email and pushes with its own token to a + remote it is granted. Which refs an agent may push (its own branches, never the default branch or another + agent's), and where that's enforced: the Gitea side (branch protection, token scopes) and our side. How + this fits the host git socket, or replaces it. +4. Lifecycle. Issue, rotate (`rotateBy`, `expires`), revoke and delete, and what row 194 (agent lifecycle) calls + when an agent is hired or fired. A fired agent's credentials stop working at once. +5. Who does what. Creating a Gitea user or token, or any account outside the fleet, is Jason's (Q173 + exclusions). The design lists each such step as a command or UI action for Jason. Agents never mint + accounts. +6. Migration. Today's seats (jarvis helper, seat tokens) keep working until each moves. The mos-0981 push + rule ends only when Mos says so. +7. What this row closes in G11 and G13, and what stays open. + +## Boundaries + +- No credential value is read, printed, logged or committed. Tests use fake tokens in scratch roots and a + fake Gitea. +- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files, `~/.mosaic` or the live + `.git/config` of the canonical checkout. +- No request to git.mosaicstack.dev, tasks.woltje.com or any provider from a test. + +## Gate + +Duckworth approves DESIGN.md, Mos rules on the authority model, then Duckworth approves the manifest. The bus, +business, cli and tasks suites and every `scripts/test-*.sh` are green on Sage's gate, and the design's +mutants are killed. diff --git a/docs/plans/2026-10-20_hub-own-uid.md b/docs/plans/2026-10-20_hub-own-uid.md new file mode 100644 index 000000000..a9fbe3445 --- /dev/null +++ b/docs/plans/2026-10-20_hub-own-uid.md @@ -0,0 +1,77 @@ +# Brief: Melanie's hub instance on its own host (row 195, was 190b) + +Sage, 2026-10-20. Rewritten for Mos's rulings mos-0998 and mos-1005 (Vikunja #418, #419). The first version +put the hub under its own Linux user on dragon-lin. That form can't pass Duckworth's B7 there, because the +factory uid's `sudo -n` succeeds on dragon-lin (Mos's recon, brain-redux +`projects/mosaic-mvp/MELANIE-HOST-RECON.md`). Melanie's own server is ruled out too: it has 1.2 GiB RAM free +with her two Hermes bots running, and co-tenancy would share her user and her passwordless sudo. The hub runs +on a separate host. Which host is Jason's decision (#419): hardware outside the cluster, a cluster VM with +factory PVE root removed, or a cluster VM with that residual accepted. This row is written so the host choice +doesn't change it. Gitea #1693. On the 10-27 MVP path. Federation R8 (Gizmoduck's design r4, 04a6112be) needs +this row or nothing. + +Owner Darkwing (Opus 5.5), reviewer Duckworth (author of B5 and B7), gate owner Sage. Chained on row 190 +(Q177): the hub runs under row 190's config canon. Design first. The first deliverable is +`agents/darkwing/work/queue-190b/DESIGN.md`. + +## Goal + +Melanie's hub instance (Case 2, C2-13) runs on its own host. No factory agent can read or write the hub's +config, data root or secrets, reach its bus socket, or log in to the hub host. Her writing reaches the hub only +through the existing git.mosaicstack.dev remotes, books repo only. + +## Provision (blank until Jason answers #419) + +Who provisions the host and how is left open. Sizing from Mos (mos-0998): 4 vCPU, 8 GB RAM, 64 GB disk, +internal network only. Each agent process measures 300 to 390 MB RSS on dragon-lin. The design takes the host +as given: a fresh Linux host with a named admin path, reachable by the deploying seat while the row runs. + +## The design must state + +1. The deploy. Exactly what Darkwing installs on the hub host and how: the stack copy, node, the units, the + config dir and data root under row 190's canon (O1: `~/.config/mosaic-NAME`, and "`~` is the home of the OS + user that runs the instance"). Which commands run as which user on the hub host, and which need its root. +2. Re-pin, rollback and backup on the hub host, and who runs them after the deploy key is gone. +3. How the hub gets model access without a copy of jwoltje's `claude-session.env` or any factory credential. + If that needs the credentials row (T1 item 6), say so and name the interim. If it needs a new subscription + or a new provider account, flag it for Jason and don't pick one (mos-1008). +4. The books repo path. Which git.mosaicstack.dev remote, which credential the hub holds for it, and that it + can't read or push any other repository. +5. The admin path and the end state. The host ends with no key the factory uid can read. The design names the + admin path that remains (for example the hypervisor console, or a key kept off the factory uid) and who + holds it. +6. The acceptance script, as code Duckworth can read. It takes Gizmoduck's list of infrastructure gate items + as its input (mos-1011), not a fixed set of checks. Each item names a path, its closed form and its + accepted form, and the script reports which form holds for each, or fails. The list covers at least: + - federation R8's separate-host form: the hub's bus socket, data root and config dir don't exist on the + factory host, the factory host mounts nothing of the hub's, and no factory agent holds a login or key + for the hub host + - B7: from the factory uid, `sudo -n`, `doas -n`, `su`, polkit, and `ssh -o BatchMode=yes` to the hub + host all fail + - the hypervisor and backup paths ops-01 measured (mos-1011: one PVE node the factory key has root on, + PBS on that node with unencrypted backups, a flat /16 LAN), each closed or accepted by Jason +7. The identity provider and TLS front end (B8, mos-1008, mos-1011). Either the factory holds no credential + that can create users, change groups, issue tokens or impersonate in the issuer the hub trusts, or the hub + uses an issuer the factory can't administer. A brand, tenant or app inside the same Authentik does not + count as separate. The same holds for the TLS front end that serves the hub. The proof is a failed admin + call from every token the factory's root can read. No credential value is printed in that proof. + +## Boundaries + +- Provisioning, hypervisor access and any infra or k3s change are Jason's (Q173 exclusions). Mos takes the + hypervisor residual (factory root on the PVE nodes, Q172) to Jason. It doesn't block this row. +- Melanie's server (`melanie@10.1.1.54`) is reference only, read only, under Jason's authorization in + mos-0995. Nothing on it changes, and her Hermes bots keep running. +- No tunnel, Traefik or other exposure outside the internal network. That waits for Jason, after Q175 SSO. +- No credential value is printed, logged or committed. Keys made for the deploy are removed at the end, and + the acceptance script proves it. +- The admin-call proof in point 7 and any call to the issuer or the TLS front end run only in a window Mos + approves, never as a test or an anonymous probe (Q175). +- Tests in the repository use scratch roots and stand-ins. The live deploy and the acceptance run on the hub + host are a window with a backup first (where there is state) and a STATE report after. + +## Gate + +Duckworth approves DESIGN.md, then the manifest. Any repository change is green on Sage's gate (cli suite and +every `scripts/test-*.sh`), with the design's mutants killed. The row is done when the acceptance script passes +on the hub host from the factory uid, Duckworth reads its output, and Mos confirms. diff --git a/docs/plans/2026-10-20_human-boundary.md b/docs/plans/2026-10-20_human-boundary.md new file mode 100644 index 000000000..3bf03db56 --- /dev/null +++ b/docs/plans/2026-10-20_human-boundary.md @@ -0,0 +1,62 @@ +# Brief: the human boundary across uids and humans (P19 333) + +Sage, 2026-10-20, for Mos's order mos-0989 (P19 333, MOSAICSTA-173, is now on the 10-27 path: federation R3 and +row 193 need it). Gitea #1694. Owner Gander (Codex gpt-6.1-sol, a new builder seat under mos-0977 item 5), +reviewer Duckworth, gate owner Sage. Latest-by: DESIGN.md approved by 2026-10-22, landed by 2026-10-24, and in +any case before federation R3 lands. R3 and row 193 may chain on this row's candidate (Q177). Base: refactor at +the brief commit. Design first. The first deliverable is `agents/gander/work/queue-ROW/DESIGN.md`. + +## The problem, as filed and as it now matters + +P19 333 as filed (row 87, mos-0657): the Console listens on a loopback TCP port, and any host process that +reaches it, under any uid, acts as Jason. Jason accepted that as a documented limit for a one-user host +(mos-0787, row 161). Two things change that: + +- Row 190b (#1693, mos-0986) puts Melanie's hub under its own Linux user. Factory processes then sit on another + uid, on the same host, next to the hub's ports and socket. The other-uid reach is now the Case 2 threat. +- Federation r2 (gizmoduck-0004, 60b9b112a) binds a bus user only from a user-cli whose proof passes the human + walk, whose direct parent is the bus's own Console, and which holds a live grant. That rule rests on the + path-bound human proof (#225): today a same-uid process that runs the tree's `human-cli.mjs` acts as the + human, without touching the Console. The Q175 design (section 8) names that limit and leaves it here. + +## What already settles part of it + +- Q175 phase 2 (#412) settles the Console-ingress part on the main port. Under `--sso`, 7332 answers any + non-public Host with 403 except `GET /healthz`, and every other request needs a verified OIDC session. A + process on another uid that reaches the port gets only `/healthz`. Phase 1 (row 192) builds it; it is live + only after the phase 2 window. +- Row 189 (`--no-send`) is interim only. Until phase 2 the Console takes no human writes, so the reach is + read-only. It settles nothing. +- Row 190b separates files and units by uid. Whether the hub's `broker.sock` refuses the factory uid depends on + its directory and socket modes, which this row checks and tests rather than assumes. + +## What the design must settle + +1. Inventory every listener and entry point per instance: the Console port, the tester listener (7333 today), + `broker.sock`, the human CLI wrapper, the Discord connector and anything row 190 adds. For each: who can + reach it today from the same uid and from another uid, with and without `--sso`. +2. The tester listener. It serves reads and the row 175 tester writes to anything that reaches it, on any uid. + Close the other-uid reach (for example a unix socket with 0600 modes, or a peer-uid check on loopback), or + bring Mos a concrete reason it must stay open. bus.tester stays Jason's switch. +3. The bus human boundary. How the bus tells the human's own verified call from a same-uid process that runs + `human-cli.mjs`. State what federation R3's direct-parent rule needs from the bus (peer credentials on + `broker.sock`, the parent check, how it can't be forged by a child of an agent), and what stays possible for + a same-uid process after this row. Say it plainly where a residual limit remains. +4. Fit with row 193 (Morgana, human users and roles; Mos's B1 rule: a principal comes only from something the + bus verifies) and with federation R3. One mechanism, not three. +5. Migration: the live single-user Console and bus keep working until each instance re-pins. +6. Which parts of P19 333 this row closes, which parts Q175 phase 2 closes, and what P19 333 says when it is + closed. + +## Boundaries + +- Who can reach the Console from outside stays Jason's. This row changes what a host process can do once it + reaches a local port or socket. +- No test touches `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or a slot file. No test creates a real + second uid; use stand-ins as rows 190 and 190b do, and name any part only a Q173 window can prove. +- No request to the live 127.0.0.1:7332 or 7333, mosaic.woltje.com or auth.diversecanvas.com. + +## Gate + +Duckworth approves DESIGN.md, then the manifest. The bus, webui and cli suites and every `scripts/test-*.sh` +are green on Sage's gate, and the design's mutants are killed. diff --git a/docs/plans/2026-10-20_multi-instance.md b/docs/plans/2026-10-20_multi-instance.md index 79fa60fbf..4e8f1e052 100644 --- a/docs/plans/2026-10-20_multi-instance.md +++ b/docs/plans/2026-10-20_multi-instance.md @@ -12,8 +12,10 @@ before any code, as Q175 did. Then the build follows the design, with tests and Two or more instances run side by side on dragon-lin. Each one has its own config file, data root (store, broker.sock, host.lock, state, runs, sessions, workspaces), businesses, bus unit, Console unit and port, -tester port, Discord unit, Claude session env file, stack-active pointer and activation log. Nothing one -instance does can read or change another's state. The current instance keeps working unchanged: its +tester port, Discord unit, Claude session env file, stack-active pointer and activation log. No instance's +commands, units or tools address another instance's state by default or by mistake. On one uid this is +addressing only, not a security boundary: a same-uid process can still read another instance's files. +Isolation against that is row 190b (#1693, mos-0986), one Linux user per instance. The current instance keeps working unchanged: its config path, data root, unit names and ports stay as they are, and no re-pin is needed for it. ## Known single-instance points (GAP-MAP.md, measured at 9ae8c73ac) diff --git a/docs/plans/2026-10-20_retire-control-board.md b/docs/plans/2026-10-20_retire-control-board.md new file mode 100644 index 000000000..11977a5cb --- /dev/null +++ b/docs/plans/2026-10-20_retire-control-board.md @@ -0,0 +1,57 @@ +# Brief: retire the control board writer on 7331 + +Sage, 2026-10-20, for Mos's ruling mos-1008 item 2. Gitea #1696. MVP 10-27 work. Owner Bushroot (Haiku 5.5, a +low-level model under Q177), reviewer Webby, gate owner Sage. Base: refactor at the brief commit. + +## Why + +`packages/control-board/src/serve.mjs` is a loopback HTTP service on 7331 with no authentication. It reads +transcripts and writes through `/api/reply`. Any host process that reaches the port can do both. That fails +Q175 ("zero trust with verification"). Gander found it in the row 196 inventory (gander-0004). Nothing listens +on 7331 now (`ss -ltnp`, 2026-10-20T19:05:09Z), so retiring it costs the least. + +## Steps, in order + +1. Inventory first. List every reference to the control board and to 7331: scripts, units, `scripts/mosaic` + and other CLI dispatch, docs (AGENTS.md, READMEs, TOOLS.md), package.json scripts, tests, and the Console + (`packages/webui`). For each, say whether it starts the writer, reads its code, or only mentions it. + Commit the list as `agents/bushroot/work/queue-ROW/INVENTORY.md` and send it to Sage and Webby. +2. Stop if something depends on it. If a reference shows a live dependency (the Console or another package + imports code that only the control board has, or a unit or script still starts it), stop. Sage takes it to + Mos before anything is removed. +3. Otherwise retire the writer. Either remove the `serve` entry point, or make it refuse to start with a + message that points to the Console. Keep any read-only code another package imports, and say which. + Update the docs that tell people to start it. The AGENTS.md text about the control-board reply convention + is a separate matter. Change it only if it tells someone to start 7331. + +## Mos's call on the Console dependency (mos-1019) + +The inventory (004f133bd) found that the Console proxies `/api/board`, `/api/seen`, `/api/reply` and the +conversation routes to `DEFAULT_BOARD` on 7331 (`packages/webui/src/serve.mjs:12`), so the board view returns +502 today. Mos took Sage's recommendation: retire the 7331 listener and its README start commands, and have +the Console call the board's read code in-process, behind the Console's own auth (Q175 SSO on 7332). +`/api/reply` stays only under `--send`. The DESIGN.md for that change comes before code, and Webby approves +it. + +## Acceptance + +Nothing in the stack can start a writer on 7331. A test proves it: the entry point refuses (or is gone), no +script or unit starts it, and a grep check finds no starter. Each change gets a mutant that brings the writer +back and the test kills it. Mos added three items (mos-1019): + +- The board view answers from the Console with nothing listening on 7331. +- Every board route sits behind the Console's auth. +- A test proves `/api/reply` refuses without `--send`. + +Test fixtures that bind port 0 may stay if they only test the code in-process. + +## Boundaries + +- No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files or `~/.mosaic`. +- No request to any live port. +- Single test files run freely. Suites need Sage's slot. + +## Gate + +Webby approves INVENTORY.md, then DESIGN.md, then the manifest. The control-board and webui package suites, the cli suite and +every `scripts/test-*.sh` are green on Sage's gate, and the mutants are killed. diff --git a/docs/plans/2026-10-20_second-gate-lane.md b/docs/plans/2026-10-20_second-gate-lane.md new file mode 100644 index 000000000..8af714d06 --- /dev/null +++ b/docs/plans/2026-10-20_second-gate-lane.md @@ -0,0 +1,42 @@ +# Brief: a second gate lane, two full gates at once on dragon-lin + +Sage, 2026-10-20, for Mos's order mos-0977 item 6 ("put the design for a second gate lane on the list now") +and the MVP push (mos-0974). Gitea #1695. Owner Herb (Codex gpt-6.1-sol, a new builder seat), reviewer +Webby, gate owner Sage. Base: refactor at the brief commit. Design first. The first deliverable is +`agents/herb/work/queue-ROW/DESIGN.md`. + +## Why + +Every full run shares one slot today. A full gate (cli suite plus every `scripts/test-*.sh`) takes about 13 +minutes or more, reviewers' runs and mutants take the same slot, and with eight or more seats building for +10-27 the slot is the bottleneck. The slot exists because concurrent runs broke each other. The busy check is +`agents/sage/work/gate-lane/busy-now.sh` (a copy of Sage's), and the gate is +`agents/sage/work/gate-lane/gate-template.sh` (a copy of Sage's template). The host has 16 cores and 62 GB. + +## What the design must settle + +1. Every shared resource two concurrent runs can collide on, found from the code and tests, not guessed: + Docker compose project names and networks (`scripts/test-task.sh` and others), fixed TCP ports in tests, + unix socket paths and their 107-byte limit under TMPDIR, the sandbox's private `/tmp`, shared caches, + scratch and worktree paths, the queue lock, and any test that reads host-wide state (process lists, + `pgrep`, `/proc`). For each: how a lane gets its own copy, or why it is safe to share. +2. Timing. Which tests have time limits that a doubled CPU load could break, measured, and what changes (a + bound, or a lane rule that keeps those suites apart). +3. The lane mechanism: lane ids, a lock per lane that a run takes and releases even on a crash, the busy + check that reports per lane, and how a seat asks for a lane. Sage still grants it. +4. The gate template takes a lane id and isolates everything from point 1 by it. +5. Proof: two full gates on different candidates at once, three times, with zero cross-failures, plus the + wall time of each against a single-lane run. Any flake found is named with its cause. + +## Boundaries + +- No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev` or slot files. The proof runs use + scratch roots only, and it needs Sage's grant of the whole slot while it runs. +- Test code changes that only isolate a resource are in scope. A change to what a test checks is not, and + goes to its owner. +- Docker networks and containers a run makes are removed by the run (trap), and nothing else is touched. + +## Gate + +Webby approves DESIGN.md, then the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's +gate, both singly and in the two-lane proof. diff --git a/docs/plans/2026-10-20_stack-release-sso.md b/docs/plans/2026-10-20_stack-release-sso.md new file mode 100644 index 000000000..426336b17 --- /dev/null +++ b/docs/plans/2026-10-20_stack-release-sso.md @@ -0,0 +1,39 @@ +# Brief: stack-release.sh activates a Console with --sso (Q175 phase 2 prerequisite) + +Sage, 2026-10-20, for Launchpad's finding F3 on row 192 (launchpad-0001) and Q175 (#412). Gitea #1698. Owner +Launchpad, reviewer Webby, gate owner Sage. Chained on row 192's candidate (Q177): it lands after row 192 and +before the phase 2 window. Base: row 192's candidate. + +## Why + +The phase 2 window re-pins the Console to the phase 1 commit with `--sso --send --tester-port 7333` +(`agents/sage/work/q175-sso/DESIGN.md`, section 5). `scripts/stack-release.sh` doesn't know `--sso` today. +Its `proof_checks` (`:751`) requires `--public-host`, `--auth-header` and `--allow-user` whenever a proof pair is +carried, and the pointer's Console flags still hold the forward-auth pair (`--auth-header X-authentik-username +--allow-user jason.woltje`) that `--sso` refuses to start with. So the window can't run through the script, and +a hand-edited pointer is not an option. + +## What the row does + +1. `activate ID BUSINESS --sso` writes Console flags with `--sso` and without `--auth-header`, `--allow-user` + and any proof pair, and `--plan` names each flag it drops and carries, in the style of row 189's `send:` + line. The design names the full flag set the phase 2 Console starts with and checks it against row 192's + refusals. +2. Before anything stops, the script refuses a target copy whose Console doesn't support `--sso`, and refuses + when the config's `console` key or its slot file fails the checks row 192's `--sso` start runs. It reads + neither value and only uses the same stat and shape checks. +3. Rollback from an `--sso` pointer to a pre-phase-1 copy follows the design's phase 2 rollback order (remove + the `console` key first). The script refuses a rollback that would start a Console on a config it can't + read, and says which step comes first. +4. `docs/TOOLS.md` gets the flag. + +## Boundaries + +- No change to the live pointer, units, `~/.mosaic-dev` or `~/.config/mosaic-dev`. Tests use scratch roots + and seeded pointers, as rows 187 and 189 do. +- No request to the live Console, mosaic.woltje.com or auth.diversecanvas.com. + +## Gate + +Webby approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate, and the +mutants are killed (one per refusal, and one that keeps `--auth-header` in the written flags). diff --git a/packages/cli/tests/stack-release.test.mjs b/packages/cli/tests/stack-release.test.mjs index ba200c76a..a104beb9a 100644 --- a/packages/cli/tests/stack-release.test.mjs +++ b/packages/cli/tests/stack-release.test.mjs @@ -381,7 +381,7 @@ async function seed(e,{copy=B,pm=false,store='3g',pointer:how='adopt'}={}){ const rolledBack=(code,to=B)=>`stack-release: ${code}; rolled back to ${to}, which passed the gate\n`; const planBackup=(e)=>`backup: the run backs up the stopped store to ${e.data}/backups/stack// before any migration`; const planNoBackup='backup: none; the run rolls back before the backup'; -const planTail=(e,to,pm=false)=>[`bus: scripts/bus-service.sh install --copy ${to} --copies ${COPIES}${pm?' --pm':''} --claude ${e.claude}`,'proof: off',`Console: scripts/webui-service.sh install --copy ${to} --copies ${COPIES} --port ${e.port} --business acme`,`CLI: scripts/stack-copy.sh wrapper ${to} --copies ${COPIES} --bin ${e.bin}`]; +const planTail=(e,to,pm=false)=>[`bus: scripts/bus-service.sh install --copy ${to} --copies ${COPIES}${pm?' --pm':''} --claude ${e.claude}`,'proof: off','send: off',`Console: scripts/webui-service.sh install --copy ${to} --copies ${COPIES} --port ${e.port} --business acme`,`CLI: scripts/stack-copy.sh wrapper ${to} --copies ${COPIES} --bin ${e.bin}`]; // --- The T table --- @@ -1030,17 +1030,17 @@ test('T36 --plan shows the pending state and lists the row 169 refusals it can s return r.out.split('\n'); }; let out=await quiet(0,'activate',C1,'acme','--pm-after-harness','--plan'); - assert.deepEqual(out.slice(-6),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,C1),'']); + assert.deepEqual(out.slice(-7),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,C1),'']); biz(e,HARNESS); out=await quiet(3,'activate',A,'acme','--pm-after-harness','--plan'); - assert.deepEqual(out.slice(-7),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,A),`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`,'']); + assert.deepEqual(out.slice(-8),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,A),`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`,'']); out=await quiet(3,'activate',C1,'acme','--pm-after-harness','--plan'); assert.equal(out.at(-2),'refuses pm-after-harness-set: the business file already sets harness ["claude-code"]; use --pm'); biz(e);assert.equal((await run(e,'activate',C1,'acme','--pm-after-harness')).code,0); out=await quiet(3,'activate',C,'acme','--plan'); - assert.deepEqual(out.slice(-7),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,C,true),`refuses pm-pending: ${PENDING_MSG}`,'']); + assert.deepEqual(out.slice(-8),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,C,true),`refuses pm-pending: ${PENDING_MSG}`,'']); out=await quiet(0,'rollback','acme','--plan'); - assert.deepEqual(out.slice(-6),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,B,true),'']); + assert.deepEqual(out.slice(-7),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,B,true),'']); }); // Row 176 test 11: supported and older Console copies deliberately differ from the checkout. @@ -1706,6 +1706,113 @@ test('T54 row 188: a carried pair gets the proof file checks, and a proof file i }); }); +// Row 189 (#1687, mos-0972/0973): activate --no-send drops the pointer's --send; nothing turns it back on. +const sendLines=(out)=>out.filter((l)=>l.startsWith('send:')); +const noSend=(c)=>c.filter((x)=>x!=='--send'); +const LIVE_NOTE=`note: ${B} is the pointer's copy; the run re-pins it and only the Console flags change`; +test('T55 row 189: activate --no-send drops --send, and rollback and pm-on carry it off',async(t)=>{ + await t.test('test 1: a live-copy --no-send drops --send from the pointer and the unit, keeps the rest in order, logs the step and backs up the stopped store',async(t)=>{ + const e=await mk(t),base=await seedPreview(e,undefined,['--send','--tester-port','7333']),want=noSend(base); + assert.deepEqual(want.slice(-2),['--tester-port','7333']); + const sum=createHash('sha256').update(readFileSync(e.store)).digest('hex'),n=calls(e).length; + let r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err); + assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,want); + assert.ok(conExec(e).endsWith(` --checkout ${repo} ${want.join(' ')}`),conExec(e));assert.doesNotMatch(conExec(e),/ --send( |$)/); + r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/); + assert.match(stepText(e),/Z drop --send; --no-send was given\n/); + const act=pointer(e).activation,l=logLines(e).at(-1); + assert.deepEqual([l.op,l.from,l.to,l.result,l.backup],['activate',B,B,'ok',act]);assert.equal(backupSum(e,act),sum); + assert.deepEqual(startsStops(e,n),[`stop ${CON}`,`stop ${BUS}`,`start ${BUS}`,`start ${CON}`]); + await noLeak(e); + }); + await t.test('test 2: a plain live-copy activate still refuses already-active, and so does --no-send once --send is gone',async(t)=>{ + const e=await mk(t);await seedPreview(e,undefined,['--send']); + await refused(e,['activate',B,'acme'],'already-active',`${B} is the pointer's copy`); + await refused(e,['activate',B,'acme','--tester-port','7333'],'already-active',`${B} is the pointer's copy`); + await refused(e,['activate',B,'acme','--no-proof'],'already-active',`${B} is the pointer's copy`); + const r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err); + await refused(e,['activate',B,'acme','--no-send'],'already-active',`${B} is the pointer's copy`); + const g=await mk(t);await seedPreview(g); + await refused(g,['activate',B,'acme','--no-send'],'already-active',`${B} is the pointer's copy`); + for(const x of [e,g])await noLeak(x); + }); + await t.test('test 3: --plan prints one send: line in each case, with the live-copy note on a same-copy run',async(t)=>{ + const e=await mk(t),base=await seedPreview(e,undefined,['--send']); + let out=await planOnly(e,0,'activate',B,'acme','--no-send'); + assert.deepEqual(sendLines(out),['send: dropped; --no-send was given']); + assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[LIVE_NOTE]); + assert.equal(out[out.indexOf('send: dropped; --no-send was given')+1],`Console: scripts/webui-service.sh install --copy ${B} --copies ${COPIES} ${noSend(base).join(' ')}`); + out=await planOnly(e,0,'activate',C1,'acme'); + assert.deepEqual(sendLines(out),['send: carried']);assert.deepEqual(out.filter((l)=>l.startsWith('note: ')),[]); + assert.ok(out.find((l)=>l.startsWith('Console: ')).endsWith(' --send')); + assert.deepEqual(sendLines(await planOnly(e,0,'activate',C1,'acme','--no-send')),['send: dropped; --no-send was given']); + let r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err); + assert.deepEqual(pointer(e).console,noSend(base));assert.match(stepText(e),/Z drop --send; --no-send was given\n/); + assert.deepEqual(sendLines(await planOnly(e,0,'activate',B,'acme','--no-send')),['send: off; --no-send was given and the pointer holds no --send']); + assert.deepEqual(sendLines(await planOnly(e,0,'activate',B,'acme')),['send: off']); + assert.deepEqual(sendLines(await planOnly(e,0,'rollback','acme')),['send: off']); + r=await prun(e,'activate',B,'acme','--no-send');assert.equal(r.code,0,r.out+r.err); + assert.match(stepText(e),/Z --no-send was given and the pointer holds no --send, so there is nothing to drop\n/); + assert.doesNotMatch(stepText(e),/drop --send/); + await noLeak(e); + }); + await t.test('test 4: after --no-send, a rollback to the earlier copy leaves --send out of the pointer and the unit',async(t)=>{ + const e=await mk(t),base=await seedPreview(e,undefined,['--send']); + let r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err); + r=await prun(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err); + assert.equal(pointer(e).copy,B);assert.deepEqual(pointer(e).console,noSend(base));assert.doesNotMatch(conExec(e),/ --send( |$)/); + assert.doesNotMatch(stepText(e),/--send/); + r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/); + await noLeak(e); + }); + await t.test('test 4: pm-on after --no-send keeps --send off',async(t)=>{ + const e=await mk(t),base=await seedPreview(e,{pm:true},['--send']); + let r=await prun(e,'activate',C1,'acme','--pm-after-harness','--no-send');assert.equal(r.code,0,r.out+r.err); + biz(e,HARNESS); + r=await prun(e,'pm-on','acme');assert.equal(r.code,0,r.out+r.err); + assert.equal(pointer(e).pm,true);assert.deepEqual(pointer(e).console,noSend(base));assert.doesNotMatch(conExec(e),/ --send( |$)/); + r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/); + await noLeak(e); + }); + await t.test('test 5: --no-send on rollback, pm-on, status, adopt or rehearse is a usage error',async(t)=>{ + const e=await mk(t);await seedPreview(e,undefined,['--send']); + const usageText=(await prun(e,'--help')).err,n=calls(e).length,logs=logLines(e).length,before=fp(e); + for(const args of [['rollback','acme','--no-send'],['rollback','acme','--plan','--no-send'],['pm-on','acme','--no-send'],['status','acme','--no-send'],['adopt','acme','--no-send'], + ['rehearse','acme','--backup','/tmp/x','--port','7400','--no-send']]){ + const r=await prun(e,...args);assert.equal(r.code,4,args.join(' '));assert.equal(r.out,'');assert.equal(r.err,usageText); + } + assert.match(usageText,/\| --no-proof\] \[--no-send\]/); + assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs); + await noLeak(e); + }); + await t.test('test 6: with both a pair and --send in the pointer, --no-send and --no-proof each drop what they name and keep the rest',async(t)=>{ + const e=await mk(t),base=await seedPreview(e,undefined,['--send']),pre=noSend(base),f=await proofFile(e),g=await proofFile(e,'proof2.189',0o400); + let r=await prun(e,'activate',B,'acme',...pair(f));assert.equal(r.code,0,r.out+r.err); + assert.deepEqual(pointer(e).console,[...pre,...pair(f),'--send']); + let out=await planOnly(e,0,'activate',C1,'acme','--no-proof'); + assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: dropped; --no-proof drops header x-console-proof','send: carried']); + out=await planOnly(e,0,'activate',C1,'acme','--no-send'); + assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: carried, header x-console-proof','send: dropped; --no-send was given']); + out=await planOnly(e,0,'activate',B,'acme','--no-send','--no-proof'); + assert.deepEqual([...proofLines(out),...sendLines(out)],['proof: dropped; --no-proof drops header x-console-proof','send: dropped; --no-send was given']); + r=await prun(e,'activate',C1,'acme','--no-send');assert.equal(r.code,0,r.out+r.err); + assert.deepEqual(pointer(e).console,[...pre,...pair(f)]); + r=await prun(e,'activate',C1,'acme','--no-send',...pair(g,'X-Proof-Two'));assert.equal(r.code,0,r.out+r.err); + assert.deepEqual(pointer(e).console,[...pre,...pair(g,'X-Proof-Two')]); + r=await prun(e,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/); + const h=await mk(t),hbase=await seedPreview(h,undefined,['--send']),hpre=noSend(hbase),k=await proofFile(h); + r=await prun(h,'activate',B,'acme',...pair(k));assert.equal(r.code,0,r.out+r.err); + r=await prun(h,'activate',C1,'acme','--no-proof');assert.equal(r.code,0,r.out+r.err); + assert.deepEqual(pointer(h).console,[...hpre,'--send']); + r=await prun(h,'activate',C1,'acme','--no-send','--no-proof');assert.equal(r.code,0,r.out+r.err); + assert.deepEqual(pointer(h).console,hpre);assert.doesNotMatch(conExec(h),/--proof-| --send( |$)/); + const steps=stepText(h); + assert.match(steps,/Z --no-proof was given and the pointer holds no proof pair, so there is nothing to drop\n/);assert.match(steps,/Z drop --send; --no-send was given\n/); + r=await prun(h,'status','acme');assert.equal(r.code,0,r.err);assert.match(r.out,/unit flags: match the pointer/); + for(const x of [e,h])await noLeak(x); + }); +}); + // --- rehearse (D2, row 163; the brief's "Stack rehearsal on a scratch root") --- // Copies of their own in RCOPIES: the real scripts/mosaic, config loader, business loader, roles and // mosaic-config.mjs, with stand-ins for the bus host (cli.mjs), the Console, the Store and diff --git a/scripts/stack-release.sh b/scripts/stack-release.sh index 0053f35d1..3b068b154 100755 --- a/scripts/stack-release.sh +++ b/scripts/stack-release.sh @@ -3,7 +3,7 @@ # Moves the bus, the Console and the human CLI from the pointer's copy to another copy together, # behind a health gate, and puts the old stack back inside the same run when the gate fails. # Console tester listener flags follow target support (row 176, #1674); so does the row 178 proof -# pair, which activate sets or drops (row 187, #1685). +# pair, which activate sets or drops (row 187, #1685). activate --no-send drops --send (row 189, #1687). # Design and tests: agents/darkwing/work/queue-162/BUILD.md. # # Who runs it. Decision 96 (Mos, mos-0716) lets this script stop and start the live units under @@ -30,13 +30,13 @@ BIN="$HOME/.npm-global/bin" UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" CONSOLE_UNIT=mosaic-webui.service TIMEOUT=60 WANT_PM=0 AFTER_HARNESS=0 MIGRATE=0 PLAN=0 RESTORE="" EVENTS="" TESTER_PORT="" -PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 R_BACKUP="" R_PORT="" +PROOF_HEADER="" PROOF_FILE="" NO_PROOF=0 NO_SEND=0 R_BACKUP="" R_PORT="" usage() { cat >&2 <<'USAGE' usage: stack-release.sh adopt BUSINESS stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N] - [--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan] + [--proof-header NAME --proof-file PATH | --no-proof] [--no-send] [--timeout S] [--plan] stack-release.sh pm-on BUSINESS [--timeout S] stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan] stack-release.sh status BUSINESS @@ -85,6 +85,7 @@ parse_opts() { --proof-header) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_HEADER="$2"; shift 2 ;; --proof-file) [ "$OP" = activate ] && [ $# -ge 2 ] && [ -n "$2" ] || usage; PROOF_FILE="$2"; shift 2 ;; --no-proof) [ "$OP" = activate ] || usage; NO_PROOF=1; shift ;; + --no-send) [ "$OP" = activate ] || usage; NO_SEND=1; shift ;; --migrate) [ "$OP" = activate ] || usage; MIGRATE=1; shift ;; --restore) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || usage; RESTORE="$2"; shift 2 ;; --backup) [ "$OP" = rehearse ] && [ $# -ge 2 ] && { [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}(-[1-9])?$ ]] || [[ "$2" =~ ^/[A-Za-z0-9/._+-]*$ ]]; } || usage; R_BACKUP="$2"; shift 2 ;; @@ -244,14 +245,16 @@ target_schema() { in_chain() { local v; for v in "${T_CHAIN[@]}"; do [ "$v" != "$1" ] || return 0; done; return 1; } # The item 3 checks on the target copy, in order: copy, schema, already-active. An activate that -# sets a proof pair, or drops the pointer's pair with --no-proof, may name the pointer's copy: it -# re-pins the live copy with new Console flags (row 187, Part B's B3 and B-R1). +# sets a proof pair, drops the pointer's pair with --no-proof, or drops the pointer's --send with +# --no-send, may name the pointer's copy: it re-pins the live copy with new Console flags (row 187, +# Part B's B3 and B-R1; row 189, mos-0972). check_target() { copy_exists "$TO" 2>/dev/null || refuse copy-missing "no copy $TO in $COPIES" copy_ok "$TO" 2>/dev/null || refuse copy-unreachable "$TO is not reachable from refactor and origin/refactor" target_schema "$TO" || refuse copy-schema-unknown "cannot read SCHEMA_VERSION and its chain from $COPIES/$TO/packages/bus/src/store.mjs" [ "$TO" != "$P_COPY" ] || [ -n "$PROOF_HEADER" ] || { [ "$NO_PROOF" = 1 ] && [[ " ${P_CONSOLE[*]} " == *" --proof-header "* ]]; } || + { [ "$NO_SEND" = 1 ] && [[ " ${P_CONSOLE[*]} " == *" --send "* ]]; } || refuse already-active "$TO is the pointer's copy" } @@ -690,6 +693,33 @@ console_for_target() { DROP_TESTER="--tester-port $inherited" fi proof_for_target + send_for_target +} + +# Row 189 (mos-0972): --no-send drops --send from RUN_CONSOLE, so the unit and the pointer both lose +# it; without the flag the pointer's --send goes on unchanged. Rollback and pm-on never take it, so +# they carry the pointer as it is. Sets SEND_PLAN (the plan's send: line) and SEND_STEP. +send_for_target() { + local had=0 i + local -a without_send=() + for ((i = 0; i < ${#RUN_CONSOLE[@]}; i++)); do + if [ "${RUN_CONSOLE[i]}" = --send ]; then had=1; else without_send+=("${RUN_CONSOLE[i]}"); fi + done + SEND_STEP="" + if [ "$NO_SEND" = 1 ]; then + RUN_CONSOLE=("${without_send[@]}") + if [ "$had" = 1 ]; then + SEND_PLAN="send: dropped; --no-send was given" + SEND_STEP="drop --send; --no-send was given" + else + SEND_PLAN="send: off; --no-send was given and the pointer holds no --send" + SEND_STEP="--no-send was given and the pointer holds no --send, so there is nothing to drop" + fi + elif [ "$had" = 1 ]; then + SEND_PLAN="send: carried" + else + SEND_PLAN="send: off" + fi } # Row 187: the row 178 proof pair in RUN_CONSOLE. A new pair replaces the pointer's and goes last; @@ -943,6 +973,7 @@ plan() { echo "bus: scripts/bus-service.sh install --copy $TO --copies $COPIES$([ "$RUN_PM" = 0 ] || echo ' --pm') --claude $P_CLAUDE" [ -z "$DROP_TESTER" ] || echo "drop $DROP_TESTER; the target Console does not support it" echo "$PROOF_PLAN" + echo "$SEND_PLAN" echo "Console: scripts/webui-service.sh install --copy $TO --copies $COPIES${RUN_CONSOLE[*]:+ ${RUN_CONSOLE[*]}}" echo "CLI: scripts/stack-copy.sh wrapper $TO --copies $COPIES --bin $BIN" local r @@ -956,6 +987,7 @@ run_steps() { capture || refuse capture-failed "could not write the rollback set $RS; nothing was stopped" [ -z "$DROP_TESTER" ] || say "drop $DROP_TESTER; the target Console does not support it" [ -z "$PROOF_STEP" ] || say "$PROOF_STEP" + [ -z "$SEND_STEP" ] || say "$SEND_STEP" trap 'after_fail signal' INT TERM HUP stop_both || rollback_in_run stop-timeout if [ -e "$STORE" ]; then @@ -1478,7 +1510,7 @@ STORE="$DATA/bus/bus.sqlite" BUS_UNIT="mosaic-bus@$BUSINESS.service" FROM="" TO="" ACT="" RS="" POST="" BACKUP="" TAKEN="" RESTORED="" STEPS="" COUNT="" UNCLEAN="" FAIL="" STORE_SCHEMA="" START_EPOCH="" START_MS=0 GATE="" GATE_WHY="" S_CHANGED="" -P_COPY="" P_PM=0 P_PENDING=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() RUN_PM=0 RUN_PENDING=0 RUN_CONSOLE=() DROP_TESTER="" PROOF_PLAN="" PROOF_STEP="" PLAN_REFUSE=() +P_COPY="" P_PM=0 P_PENDING=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() RUN_PM=0 RUN_PENDING=0 RUN_CONSOLE=() DROP_TESTER="" PROOF_PLAN="" PROOF_STEP="" SEND_PLAN="" SEND_STEP="" PLAN_REFUSE=() R="" R_RC=0 R_EARLY=() R_BK="" R_HOST_PID="" R_CONSOLE_PID="" R_STARTED="" LIVE_CFG="" LIVE_BIZ="" case "$OP" in adopt) do_adopt ;;