From 6966164f0c69158459fbae45edeff57b8fae9147 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 13:56:34 -0500 Subject: [PATCH 01/15] plans: row 190b brief, Melanie's hub under its own Linux user (#1693, Darkwing, reviewer Duckworth), mos-0986 --- docs/plans/2026-10-20_hub-own-uid.md | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/plans/2026-10-20_hub-own-uid.md 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..ff96c06da --- /dev/null +++ b/docs/plans/2026-10-20_hub-own-uid.md @@ -0,0 +1,46 @@ +# Brief: Melanie's hub instance under its own Linux user (row 190b) + +Sage, 2026-10-20, from Darkwing's draft (agents/darkwing/work/queue-190b/BRIEF-DRAFT.md) for Mos's ruling +mos-0986 (2026-10-20T18:53:48Z) on row 190 R1. Mos ruled a separate uid per instance a hard precondition of +federation R8 (mos-0984, mos-0985) and of Case 2 row C2-13. The finding was federation review B5 +(duckworth-0001): on one uid any factory agent can read, write and control Melanie's hub directly, and SSO and +RBAC don't stop a local process with filesystem access. On the 10-27 MVP path. Gitea #1693. + +Owner Darkwing (Opus 5.5), reviewer Duckworth (author of B5; mos-0981 keeps new work off Filbert and Dewey), +gate owner Sage. Merge-chained on row 190 (Q177): it builds on row 190's candidate and keeps its own review and +gate. Design first. The first deliverable is `agents/darkwing/work/queue-190b/DESIGN.md`. + +## Goal + +Melanie's hub instance runs under its own Linux user. No process of the factory instance can read or write the +hub's config, data root or secrets, or control the hub's units. That covers the bus, role seats and their +sessions, task runs, the Discord connector and the Console. + +## The design must state + +1. How operators (Mos, Sage, Jason) deploy, re-pin and roll back the hub. Which commands they run, as which + user, and what each one needs. +2. Each root step Jason runs: useradd, the home and its modes, `loginctl enable-linger`, and any sudoers rule, + with the exact command lines. +3. How the hub reaches the Claude proxy and gets its session env without sharing jwoltje's + `claude-session.env`. +4. Whether real isolation needs the factory instance's seats to run apart from the operators' uid. If yes, say + so plainly with its cost. Don't design around it. + +It must also cover the copies (shared read-only trees under jwoltje's home, or the hub's own), the human CLI +wrapper and human proof for the hub's Console, and how the stack-release pointer, log and backups work under +the second user. + +## Boundaries + +- Row 190 lands first, as designed: one uid, scratch two-instance tests without root. +- No agent runs a root step. Every one goes to Jason as an exact command list. +- No real hub user, unit or data root is created in this row's tests. Tests use scratch roots and stand-ins as + row 190 does. Anything that needs a second real uid is a Q173 window with a backup first and a STATE report + after. +- No tunnel or Traefik exposure. That waits for Jason, after Q175 SSO. + +## Gate + +Duckworth approves DESIGN.md, then the manifest. The cli suite and every `scripts/test-*.sh` are green on +Sage's gate. The design's mutants are killed. From 66bdd84b8fce7dd94df81656dbc00e666cbd625e Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 13:57:10 -0500 Subject: [PATCH 02/15] sessions: Filbert row 190 design review r2, changes requested --- docs/SESSIONS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 9f20adc24..108cfd87d 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -1361,3 +1361,4 @@ 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 From 48ebee0ffe45642c10259cacc4fcfa4e08fb042f Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 13:58:07 -0500 Subject: [PATCH 03/15] plans: row 190 brief scopes isolation to addressing on one uid, points to row 190b (Filbert N7, mos-0986) --- docs/plans/2026-10-20_multi-instance.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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) From c862699352d1eeb1c8b1124e5b70c2ecc7478216 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:00:05 -0500 Subject: [PATCH 04/15] sessions: Filbert row 190 design review r3, approved --- docs/SESSIONS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 108cfd87d..bcdee3639 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -1362,3 +1362,4 @@ are never rewritten or removed; corrections are new entries. 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) From 96f243fafce02aa4af2cc077dff6f29a1ec734ec Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:00:58 -0500 Subject: [PATCH 05/15] 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) --- agents/README.md | 1 + agents/duckworth/README.md | 2 +- agents/gander/README.md | 8 ++++ agents/honker/README.md | 2 +- agents/launchpad/README.md | 2 +- agents/morgana/README.md | 2 +- agents/webby/README.md | 2 +- docs/plans/2026-10-20_human-boundary.md | 62 +++++++++++++++++++++++++ 8 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 agents/gander/README.md create mode 100644 docs/plans/2026-10-20_human-boundary.md diff --git a/agents/README.md b/agents/README.md index e698bfb0c..08a1b69de 100644 --- a/agents/README.md +++ b/agents/README.md @@ -19,6 +19,7 @@ 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 | | 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/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/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/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/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/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. From c6f528a32f01152ef102e1c5ca189b700242ae3b Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:03:36 -0500 Subject: [PATCH 06/15] 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 --- agents/README.md | 1 + agents/herb/README.md | 8 ++++ agents/sage/work/gate-lane/busy-now.sh | 8 ++++ agents/sage/work/gate-lane/gate-template.sh | 24 ++++++++++++ docs/plans/2026-10-20_second-gate-lane.md | 42 +++++++++++++++++++++ 5 files changed, 83 insertions(+) create mode 100644 agents/herb/README.md create mode 100644 agents/sage/work/gate-lane/busy-now.sh create mode 100755 agents/sage/work/gate-lane/gate-template.sh create mode 100644 docs/plans/2026-10-20_second-gate-lane.md diff --git a/agents/README.md b/agents/README.md index 08a1b69de..ae734f607 100644 --- a/agents/README.md +++ b/agents/README.md @@ -20,6 +20,7 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify | 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 | | 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/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/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/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. From 0217b71c8f837d9dab9f403e38903e5315f0fbb7 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:07:57 -0500 Subject: [PATCH 07/15] plans: row 195 brief rewritten as the hub on its own host, host left to Jason (#1693, mos-0998, mos-1005) --- docs/plans/2026-10-20_hub-own-uid.md | 81 +++++++++++++++++----------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/docs/plans/2026-10-20_hub-own-uid.md b/docs/plans/2026-10-20_hub-own-uid.md index ff96c06da..9341c5ed8 100644 --- a/docs/plans/2026-10-20_hub-own-uid.md +++ b/docs/plans/2026-10-20_hub-own-uid.md @@ -1,46 +1,63 @@ -# Brief: Melanie's hub instance under its own Linux user (row 190b) +# Brief: Melanie's hub instance on its own host (row 195, was 190b) -Sage, 2026-10-20, from Darkwing's draft (agents/darkwing/work/queue-190b/BRIEF-DRAFT.md) for Mos's ruling -mos-0986 (2026-10-20T18:53:48Z) on row 190 R1. Mos ruled a separate uid per instance a hard precondition of -federation R8 (mos-0984, mos-0985) and of Case 2 row C2-13. The finding was federation review B5 -(duckworth-0001): on one uid any factory agent can read, write and control Melanie's hub directly, and SSO and -RBAC don't stop a local process with filesystem access. On the 10-27 MVP path. Gitea #1693. +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; mos-0981 keeps new work off Filbert and Dewey), -gate owner Sage. Merge-chained on row 190 (Q177): it builds on row 190's candidate and keeps its own review and -gate. Design first. The first deliverable is `agents/darkwing/work/queue-190b/DESIGN.md`. +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 runs under its own Linux user. No process of the factory instance can read or write the -hub's config, data root or secrets, or control the hub's units. That covers the bus, role seats and their -sessions, task runs, the Discord connector and the Console. +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. How operators (Mos, Sage, Jason) deploy, re-pin and roll back the hub. Which commands they run, as which - user, and what each one needs. -2. Each root step Jason runs: useradd, the home and its modes, `loginctl enable-linger`, and any sudoers rule, - with the exact command lines. -3. How the hub reaches the Claude proxy and gets its session env without sharing jwoltje's - `claude-session.env`. -4. Whether real isolation needs the factory instance's seats to run apart from the operators' uid. If yes, say - so plainly with its cost. Don't design around it. - -It must also cover the copies (shared read-only trees under jwoltje's home, or the hub's own), the human CLI -wrapper and human proof for the hub's Console, and how the stack-release pointer, log and backups work under -the second user. +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. +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 checks, as a script Duckworth can read: 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, no factory agent holds a login or key for the hub host), plus B7. From the factory uid, `sudo -n`, + `doas -n`, `su`, polkit, and `ssh -o BatchMode=yes` to the hub host must all fail. ## Boundaries -- Row 190 lands first, as designed: one uid, scratch two-instance tests without root. -- No agent runs a root step. Every one goes to Jason as an exact command list. -- No real hub user, unit or data root is created in this row's tests. Tests use scratch roots and stand-ins as - row 190 does. Anything that needs a second real uid is a Q173 window with a backup first and a STATE report - after. -- No tunnel or Traefik exposure. That waits for Jason, after Q175 SSO. +- 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. +- 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. The cli suite and every `scripts/test-*.sh` are green on -Sage's gate. The design's mutants are killed. +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. From 0bbd23c6198b61c76ee08b11fa451339c64d7bd0 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:10:28 -0500 Subject: [PATCH 08/15] plans: row 195 brief adds the identity provider check and flags any new model account for Jason (mos-1008) --- docs/plans/2026-10-20_hub-own-uid.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/plans/2026-10-20_hub-own-uid.md b/docs/plans/2026-10-20_hub-own-uid.md index 9341c5ed8..beee2e283 100644 --- a/docs/plans/2026-10-20_hub-own-uid.md +++ b/docs/plans/2026-10-20_hub-own-uid.md @@ -33,7 +33,8 @@ as given: a fresh Linux host with a named admin path, reachable by the deploying 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 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 @@ -43,6 +44,9 @@ as given: a fresh Linux host with a named admin path, reachable by the deploying socket, data root and config dir don't exist on the factory host, the factory host mounts nothing of the hub's, no factory agent holds a login or key for the hub host), plus B7. From the factory uid, `sudo -n`, `doas -n`, `su`, polkit, and `ssh -o BatchMode=yes` to the hub host must all fail. +7. The identity provider path (mos-1008, mos-1006). 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 + or tenant the factory can't administer. The acceptance script or a named manual check shows which holds. ## Boundaries From f871cb30960f5945f7143effea2d756d9494cc35 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:11:21 -0500 Subject: [PATCH 09/15] plans: retire the control board writer on 7331 brief (#1696, Bushroot, reviewer Webby, mos-1008 item 2); Bushroot seat dir --- agents/README.md | 1 + agents/bushroot/README.md | 8 ++++ docs/plans/2026-10-20_retire-control-board.md | 42 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 agents/bushroot/README.md create mode 100644 docs/plans/2026-10-20_retire-control-board.md diff --git a/agents/README.md b/agents/README.md index ae734f607..8c9fb9dcc 100644 --- a/agents/README.md +++ b/agents/README.md @@ -21,6 +21,7 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify | 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 | | 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/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..f66c58a62 --- /dev/null +++ b/docs/plans/2026-10-20_retire-control-board.md @@ -0,0 +1,42 @@ +# 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. + +## 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. + +## 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 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. From a7661ada06677269f03d7b6bdbca8cd44fbb0bd7 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:12:19 -0500 Subject: [PATCH 10/15] plans: row 195 acceptance script takes Gizmoduck's gate item list; B8 issuer and TLS front end (mos-1011) --- docs/plans/2026-10-20_hub-own-uid.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/plans/2026-10-20_hub-own-uid.md b/docs/plans/2026-10-20_hub-own-uid.md index beee2e283..a9fbe3445 100644 --- a/docs/plans/2026-10-20_hub-own-uid.md +++ b/docs/plans/2026-10-20_hub-own-uid.md @@ -40,13 +40,21 @@ as given: a fresh Linux host with a named admin path, reachable by the deploying 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 checks, as a script Duckworth can read: 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, no factory agent holds a login or key for the hub host), plus B7. From the factory uid, `sudo -n`, - `doas -n`, `su`, polkit, and `ssh -o BatchMode=yes` to the hub host must all fail. -7. The identity provider path (mos-1008, mos-1006). 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 - or tenant the factory can't administer. The acceptance script or a named manual check shows which holds. +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 @@ -57,6 +65,8 @@ as given: a fresh Linux host with a named admin path, reachable by the deploying - 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. From 992650232f00c1e597d04f643149d51c55d6bdf7 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:14:33 -0500 Subject: [PATCH 11/15] plans: agent credentials and git identity brief (MVP T1 item 6, #1697, Megavolt, reviewer Duckworth); Megavolt seat dir --- agents/README.md | 1 + agents/megavolt/README.md | 8 +++ docs/plans/2026-10-20_agent-credentials.md | 57 ++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 agents/megavolt/README.md create mode 100644 docs/plans/2026-10-20_agent-credentials.md diff --git a/agents/README.md b/agents/README.md index 8c9fb9dcc..63e225e15 100644 --- a/agents/README.md +++ b/agents/README.md @@ -22,6 +22,7 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify | 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/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/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. From a31f1a0f8b3183b28695be08b841c3ed8426cf1a Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:17:03 -0500 Subject: [PATCH 12/15] plans: row 198 brief records Mos's call, in-process board reads behind the Console's auth (mos-1019) --- docs/plans/2026-10-20_retire-control-board.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/plans/2026-10-20_retire-control-board.md b/docs/plans/2026-10-20_retire-control-board.md index f66c58a62..11977a5cb 100644 --- a/docs/plans/2026-10-20_retire-control-board.md +++ b/docs/plans/2026-10-20_retire-control-board.md @@ -24,11 +24,26 @@ on 7331 now (`ss -ltnp`, 2026-10-20T19:05:09Z), so retiring it costs the least. 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. +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 @@ -38,5 +53,5 @@ back and the test kills it. ## Gate -Webby approves INVENTORY.md, then the manifest. The control-board and webui package suites, the cli suite and +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. From a4a42dbac2097db4ff9c71bab27e11076eb1344a Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:18:08 -0500 Subject: [PATCH 13/15] plans: stack-release --sso brief (#1698, Launchpad, reviewer Webby, row 192 F3; chained on 192) --- docs/plans/2026-10-20_stack-release-sso.md | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/plans/2026-10-20_stack-release-sso.md 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). From ace27dc5eb810970449056918b35793cd8080502 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:21:56 -0500 Subject: [PATCH 14/15] row 189 (#1687): stack-release.sh activate --no-send drops the Console's --send (Sage; Dewey approve 29485; Sage gate passed) --- agents/sage/work/queue-189/BUILD.md | 86 +++++++++++++++ agents/sage/work/queue-189/mutants-run.log | 5 + agents/sage/work/queue-189/mutants.sh | 34 ++++++ docs/TOOLS.md | 10 +- docs/plans/2026-10-20_activate-no-send.md | 11 +- packages/cli/tests/stack-release.test.mjs | 117 ++++++++++++++++++++- scripts/stack-release.sh | 44 ++++++-- 7 files changed, 290 insertions(+), 17 deletions(-) create mode 100644 agents/sage/work/queue-189/BUILD.md create mode 100644 agents/sage/work/queue-189/mutants-run.log create mode 100644 agents/sage/work/queue-189/mutants.sh 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/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/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 ;; From aa1bd209e30dc93ad90e4c5165c5b0d81e4408b5 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 14:25:17 -0500 Subject: [PATCH 15/15] gate-lane: row 189 gate timing table for row 197 (Herb) --- agents/sage/work/gate-lane/timing-g189.md | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 agents/sage/work/gate-lane/timing-g189.md 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%.