Compare commits

...
Author SHA1 Message Date
jason.woltje 32b54ac3b0 plans: Case 2 row briefs from Gosalyn's 51006710a (webby-0034 N1/N2 and webby-0035: row A README :711 one line per variable, C2-13 owns README :713 and says the precheck prints only variable names) 2026-10-20 15:57:07 -05:00
jason.woltje 6025ddd9cf plans: Case 2 row briefs from Gosalyn's a2d94b2c2 (Webby r9 notes, webby-0032: row A README line on claude-session.env and the dist/providers/data/zai.json path, C2-13 XDG_DATA_HOME precheck and last-assignment-wins key file parse) 2026-10-20 15:55:52 -05:00
jason.woltje ecbfd6deb6 plans: Case 2 row briefs from Gosalyn's 29e85196b (design r9 for mos-1090: row A out of scope points at design 10 step 2d, C2-13 precheck checks the hub key file by name and mode, claude marker check only for claude-code harness, EnvironmentFile= fixed in case2-unit.sh) 2026-10-20 15:44:09 -05:00
jason.woltje 0498c0613f agents: Gosalyn seat dir, README from gosalyn/case2-design, so Case 2 rows can name the seat as owner (#416) 2026-10-20 15:43:18 -05:00
jason.woltje 49d1f3a231 plans: Case 2 slice row briefs plus rows A and B, from Gosalyn's f325657c7 on gosalyn/case2-design (#416, design r8, C2-13 copy id and profile asks for row 195, C2-20 count-only note, row A ZAI_API_KEY per mos-1086/mos-1089, row B #428 after 10-27) 2026-10-20 15:41:39 -05:00
jason.woltje 628e2ca5cd sessions: Filbert row 190 manifest round 1 CHANGES (base drift on row 189) 2026-10-20 15:40:15 -05:00
jason.woltje 088087ac96 queue: revs 2248-2361+. Sage: rows 210 (#424 host access, Gizmoduck) and 211 (#426 force-push guard, Rocko) added and briefed; row 195 re-briefed to the Case 1 install on .53; review requests for 200 r1/r2 and 202; plus earlier Sage revs. Other seats' revs: Launchpad 2254, 2283-2284, 2286, 2311-2313, 2328-2329, 2344, 2347-2348 (rows 192/200); Honker 2255; Morgana 2257, 2320, 2326-2327 (row 204); Gander 2262; Herb 2265; Dewey 2268; Bushroot 2273; Megavolt 2276; Webby 2303, 2316, 2339, 2355 (row 192/200 verdicts); Gizmoduck 2304, 2318-2319 and the row 210 claim (rows 201/210); Quackerjack 2317, 2341-2342 (row 202); Duckworth 2323, 2335, 2353 (rows 201/204/202 verdicts); Darkwing 2354, 2357 (row 190 in-review) 2026-10-20 15:34:55 -05:00
jason.woltje a0fbefd48d plans: pre-push force-push hook and failing busy check brief (Vikunja #426, mos-1078, mos-1083; owner Rocko, reviewer Dewey) 2026-10-20 15:31:17 -05:00
jason.woltje f24f2cd40d plans: row 195 brief becomes the Case 1 install on 10.1.1.53 (Q181 hosts, Q182/Q183 recorded acceptance, Q185 existing account, chained on rows 190 and C2-13; ops-01 root steps under #423 step 1; sage-1439) 2026-10-20 15:24:33 -05:00
jason.woltje 9ba31b5b3f agents: Honker's PRD 1.7 draft r2 on refactor for the prd.approve raise (11cb0423b, sha256 e896746df595d1b5155130ca163fbf63b0dc38a87a8dd154af77f1a6d1328647, Duckworth approved duckworth-0039, Vikunja #420) 2026-10-20 15:20:37 -05:00
jason.woltje 9db578ff54 plans: agent host access with sudo through the credential store brief (#1707, Vikunja #424, Q183/Q184, chained on row 199) 2026-10-20 15:09:59 -05:00
jason.woltje 8eb9ddd12c plans: FED R8 brief to r4.11 (b93f098f0, recorded acceptance, Duckworth N1/N2, #416 date), R5 context source (sage-1419), 193a makes covers level required (duckworth-0033 N1) 2026-10-20 15:06:22 -05:00
jason.woltje ae3c36f248 agents: Gizmoduck seat dir and roster row (FED R1 owner, #414) 2026-10-20 14:42:15 -05:00
jason.woltje a2af7ab495 plans: FED R1, R5, R2 and R8 briefs (#414, r4.9 a1daa3c07) and row 193a-d build briefs (#1691); Quackerjack seat dir and roster row 2026-10-20 14:41:03 -05:00
jason.woltje aa1bd209e3 gate-lane: row 189 gate timing table for row 197 (Herb) 2026-10-20 14:25:17 -05:00
jason.woltje ace27dc5eb row 189 (#1687): stack-release.sh activate --no-send drops the Console's --send (Sage; Dewey approve 29485; Sage gate passed) 2026-10-20 14:21:56 -05:00
jason.woltje a4a42dbac2 plans: stack-release --sso brief (#1698, Launchpad, reviewer Webby, row 192 F3; chained on 192) 2026-10-20 14:18:08 -05:00
jason.woltje a31f1a0f8b plans: row 198 brief records Mos's call, in-process board reads behind the Console's auth (mos-1019) 2026-10-20 14:17:03 -05:00
jason.woltje 992650232f plans: agent credentials and git identity brief (MVP T1 item 6, #1697, Megavolt, reviewer Duckworth); Megavolt seat dir 2026-10-20 14:14:33 -05:00
jason.woltje a7661ada06 plans: row 195 acceptance script takes Gizmoduck's gate item list; B8 issuer and TLS front end (mos-1011) 2026-10-20 14:12:19 -05:00
jason.woltje f871cb3096 plans: retire the control board writer on 7331 brief (#1696, Bushroot, reviewer Webby, mos-1008 item 2); Bushroot seat dir 2026-10-20 14:11:21 -05:00
jason.woltje 0bbd23c619 plans: row 195 brief adds the identity provider check and flags any new model account for Jason (mos-1008) 2026-10-20 14:10:28 -05:00
jason.woltje 0217b71c8f plans: row 195 brief rewritten as the hub on its own host, host left to Jason (#1693, mos-0998, mos-1005) 2026-10-20 14:07:57 -05:00
jason.woltje c6f528a32f 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 2026-10-20 14:03:36 -05:00
jason.woltje 96f243fafc 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) 2026-10-20 14:00:58 -05:00
jason.woltje c862699352 sessions: Filbert row 190 design review r3, approved 2026-10-20 14:00:05 -05:00
jason.woltje 48ebee0ffe plans: row 190 brief scopes isolation to addressing on one uid, points to row 190b (Filbert N7, mos-0986) 2026-10-20 13:58:07 -05:00
jason.woltje 66bdd84b8f sessions: Filbert row 190 design review r2, changes requested 2026-10-20 13:57:10 -05:00
jason.woltje 6966164f0c plans: row 190b brief, Melanie's hub under its own Linux user (#1693, Darkwing, reviewer Duckworth), mos-0986 2026-10-20 13:56:34 -05:00
39 changed files with 6501 additions and 41 deletions
+6
View File
@@ -19,6 +19,12 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
| Launchpad | Builder, MVP T1 row 192 (Q175 SSO phase 1) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Morgana | Builder, MVP T1 row 193 (human users and RBAC) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Honker | Builder, MVP T1 row 194 (agent lifecycle) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Gander | Builder, the human boundary across uids (P19 333, #1694) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
| Herb | Builder, a second gate lane (#1695) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
| Bushroot | Builder, retire the control board writer on 7331 (#1696) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1008 | none, T3 only |
| Megavolt | Builder, MVP T1 item 6 (agent credentials and git identity, #1697) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Quackerjack | Builder, federation R5 and R2 (#414) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1023 | none, T3 only |
| Gizmoduck | Federation design (#414) and builder of FED R1 | T3 session (Claude, Opus 5.5, thread 9312a896) | 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` |
+8
View File
@@ -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.
+1 -1
View File
@@ -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.
+8
View File
@@ -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.
+9
View File
@@ -0,0 +1,9 @@
# Gizmoduck
Design and builder seat, Claude Opus 5.5, a T3 thread (9312a896). Author of the federation design
(`agents/gizmoduck/work/federation/DESIGN.md` on `gizmoduck/federation-design`, #414). Builds FED R1, the
access.json loader and validator, under Mos's order mos-1023 item 1.
No launcher in this directory. The seat works in its own worktree and has no credentials of its own; until
the credentials row settles per-agent identity, it pushes only its own `gizmoduck/*` branches through the
repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
+11
View File
@@ -0,0 +1,11 @@
# Gosalyn
Worker seat for Mosaic Stack, MVP track T2 (Case 2, the creative writing hub, Vikunja #416).
- T3 thread: 7604438a-0ded-41c8-bd3f-26f57fdb5cd2
- Model: Opus 5.5 (Claude Code, T3)
- Launched by Mos (Jason's main agent, T3 38bc4c81-6270-4021-b00c-6a15c43b9b0e), who briefs the seat
- Repository lead and queue: Sage (T3 1ef1e4f8-3ead-4208-beca-38f9f1add079)
- Branch: gosalyn/case2-design, pushed only by this seat, landed on refactor by Sage
Work lives in `work/`. The Case 2 design is `work/case2/DESIGN.md`.
+8
View File
@@ -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.
+1 -1
View File
@@ -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.
@@ -0,0 +1,158 @@
# PRD 1.7 draft: the chief of staff's hire and fire authority, the coder's push to its fork, and agent
# host access through the credential store
Draft by Honker for row 194 (#1692), under Mos's ruling mos-1017 (through Sage, sage-1365). Not approved.
It changes nothing until Jason approves it as a `prd.approve` decision in the Console. Sage opens that
decision and sends Mos its id. Base: `docs/prd/mosaic-stack.md` version 1.6 at 53bb59462.
## What it changes
Row 194 adds `role.hire` and `role.fire` to the vocabulary, and row 199 adds `git.push.remote`. Under
REQ-WAIT-3 all three are hard waits, so today Jason resolves every hire, every fire and every push to a
remote. This version routes hire and fire for the chief of staff within fixed bounds, so the chief hires
and fires as needed and the user's per-agent forbid still holds. It routes `git.push.remote` for the coder
only (Mos, mos-1028 item 3). It adds agent host access with sudo through the credential store for
agents with a grant (Q183, Q184, Vikunja #424, due 2026-10-27). One `prd.approve` decision covers it all
(Vikunja #420).
When this version lands, a reviewed commit:
1. adds `role.hire`, `role.fire` and `git.push.remote` to `ROUTED` in
`packages/business/src/vocabulary.mjs` and in `packages/bus/src/broker.mjs`.
2. adds `role.hire` and `role.fire` to `roles/chief-of-staff.json` withinRole, and `git.push.remote` to
`roles/coder.json` withinRole. No other role file gets `git.push.remote`.
3. switches the broker's check on `instance.hire` and `instance.fire` from a cited decision to the
`reason` form, as `role.end` works for REQ-ROLE-2, and makes `publish`'s `decision` optional for a
within-role class (row 199 D9 step 1).
4. states, for a `git.push.remote` with no decision, where the source commit comes from: the latest work
commit the bus recorded for the coder's task. The check still matches it against the work ref tip and
the push sends it by oid (row 199, duckworth-0022 N9).
5. adds the host credential grant check and the host access it allows (REQ-CRED-3, Vikunja #424).
Every other check stays as rows 194 and 199 build it. Filling `hireable` stays a separate reviewed role
file commit in `roles/`.
## Text for the PRD
Status line, appended to the current paragraph:
> Version 1.7 routes the chief of staff's hire and fire actions and the coder's push to its own fork,
> and adds agent host access through the credential store, approved by Jason in decision
> <prd.approve decision id>.
Source, a new bullet above "Version 1.6 source":
> - Version 1.7 source, TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as
> needed; unless user forbids per agent". And Jason through Mos (mos-1067), DECISIONS Q183, in part: "I will
> still want to allow infra agent(s) on my .53 instance to access other servers, etc. However, not all
> agents will need to have that access. I envision a credential store within Mosaic Stack that works
> as some sort of variable for the agent to call. The system will enforce access to the credentials to
> keep the agents sandboxed with proper security. I need to be able to trust my agents with assisting
> me. This will require sudo access to several systems. If we can utilize the credential store in
> Mosaic Stack and allow agents to perform their jobs with NOPASSWD sudo on various machines, I am a
> happy man." DECISIONS Q184, on whether that is in place by 10-27: "Q5: by 10.27"
Under "Added in version 1.2", after REQ-COS-2:
> Added in version 1.7:
>
> - **REQ-COS-3.** The chief of staff hires and fires role instances without a decision each time. Hiring
> adds a role instance to the organization file and lists it for launch. Firing ends the instance's
> runs and removes it from the launch list. It stays in the organization file. Only the user deletes
> an instance. These actions are `role.hire` and `role.fire`. They leave the hard waits and route
> under this requirement, within these bounds:
> - The chief acts only in its own organization.
> - The chief hires only role definitions that need no credentials, until a later version adds
> credentialed roles.
> - The chief hires only the role kinds its role file lists. Changing that list is a reviewed commit.
> - The chief hires only a role whose every action is on a fixed list of plain work actions: sending
> messages, writing records, asking for review, updating its assigned tasks, and the scope and
> working-push actions of an assigned task. A role holding any other action, such as launch, end,
> organization, naming, hire, review verdict or merge authority, or any action added later, is never
> hired. So the chief never hires a chief, never picks its own reviewers or merge gate, and the author
> is never the reviewer. This holds whatever the role file's list says. Adding to that list is a PRD
> change.
> - The chief may set a hired instance's model and narrow its limits, and nothing else about it.
> - The user can forbid per agent. The user marks an instance so the chief can't fire it, end its runs
> or change its model, and marks role kinds the chief can't hire. Only the user changes these marks, through the
> WebUI or the CLI.
> - The kill switch stops every hire and fire while it is on.
> - Every hire and fire is logged with the chief's instance, its run and the reason.
Under "Landing", after REQ-LAND-1:
> Added in version 1.7:
>
> - **REQ-LAND-2.** A coder pushes its own work to its own fork. The action is `git.push.remote`.
> - What it pushes: one commit, by its oid, never a ref name. That commit is the latest work commit the
> bus recorded for the coder's task, and the coder made that push. A decision that authorizes a push
> names that commit, and it doesn't authorize any other.
> - Where it lands: only the branch `refs/heads/<its Gitea user>/<branch>`. The host builds that
> name, the agent never does. A push never forces.
> - Which remote: the agent's own fork of the project's upstream. The business file names the upstream,
> and the host never contacts it. For each push the host builds the fork's address over https from
> the upstream's host, port and repository name and the agent's own Gitea user. The history check,
> the push and the credential match all use that address. So the agent's own user owns the receiving
> repository on every push, and the agent never names it.
> - The host refuses an upstream on a host or port other than the agent's Gitea, and an upstream the
> agent's own user owns. It refuses a fork that is missing, empty or doesn't share the project's
> history. Gitea's push-to-create stays off, so a push never creates a repository.
> - Who may: the action routes under REQ-DEC-2 and is granted within-role to the coder role only. No
> other role file grants it.
> - Before version 1.7 the action was a hard wait under REQ-WAIT-3, so each push needed a decision
> the human resolved, and that decision named the commit.
>
> Every push is logged with the remote, the ref, the commit and, when one was given, the decision.
Under "Credentials", after REQ-CRED-2:
> Added in version 1.7:
>
> - **REQ-CRED-3.** An agent with a grant can run commands with sudo on another host. The agent asks
> for the host credential by name. The stack checks that a grant exists for that agent and that host
> before the credential is used. The agent never sees the key or any other secret value (REQ-CRED-1,
> REQ-VAR-2). An agent without a grant for that host is refused. Not every agent gets a grant. Who
> grants a host credential, and how, is set by the #424 design, which Jason approves.
REQ-WAIT-3, appended:
> `role.hire` and `role.fire` were added as hard waits and route under REQ-COS-3 since version 1.7.
> `git.push.remote` was added as a hard wait and routes under REQ-LAND-2 since version 1.7.
## Notes for the reviewer and for Mos
- REQ-COS-1 (1.2) already says the chief "hires and terminates role instances". REQ-COS-3 gives that
sentence its actions and bounds. It doesn't widen REQ-COS-1.
- The forbid now names what r3 called "replace": ending a kept instance's run with a reason and changing
its model (webby-0010 N2). A user-resolved decision still ends a kept run (row 194 D2.4).
- The fixed list is `HIRE_ALLOWED` in row 194 D2.3, an allowlist since r4 (webby-0010 B1). r3's text
named the refused authorities instead, which let merge-gate and any later action through.
- The kill switch is row 191's. If row 191 hasn't landed when this version is approved, the bound still
holds through row 194's chain on its candidate.
- REQ-LAND-2 follows row 199's design r6 (4d3007ec9 on megavolt/row199, approved by Duckworth in
duckworth-0027: D9 to D11, D9a, D9c, Q-M1 and Q-M2, and the oid binding from mos-1034 item 5) and
Megavolt's wording in megavolt-0005, megavolt-0008 and megavolt-0014, through Sage (sage-1377,
sage-1380, sage-1387). The fork bullet answers duckworth B5 and B6 and webby-0011 N1. Commit-email
checks, the refusal codes and the URL parse rules (D9c) are host checks with tests in row 199, not
bounds, so they stay out of the text (megavolt-0008).
- Once the routing lands and the decision is optional for the within-role class, the commit is the task's
latest recorded push at check time. It is still checked against the work ref tip and pushed by oid, so
the binding holds without a decision (megavolt-0008). Step 4 of "When this version lands" puts that in
the routing commit (duckworth-0022 N9, sage-1387).
- REQ-LAND-2 and REQ-COS-3 meet at the coder. Coder declares Gitea credentials, so REQ-COS-3's
credentials bound keeps the chief from hiring one until a later version lifts it. `git.push.remote` isn't
on REQ-COS-3's fixed list either, so a coder holding it stays unhireable until a PRD version adds it
(row 194 D2.3).
- Adding to the fixed action list is a PRD change (mos-1035 item 3, sage-1395). The role file's
`hireable` list stays a reviewed commit in `roles/`.
- The credentials bound names no row number, so the text doesn't go stale when row 199 lands. A later
version lifts it.
- REQ-CRED-3 states only what Jason said in Q183 and Q184 (sage-1422). It names the outcome and the
check, not how the stack reaches the host. Proxying the ssh and a signing socket are both designs, and
the #424 design picks one (duckworth-0038 B1).
- Open point: Q183 doesn't say who grants host access or how, so the text leaves it open and defines no
mechanism. The #424 design settles it, Jason approves that design, and Duckworth reviews it because it
gives agents sudo on hosts (Q184, Mos's reading, duckworth-0038 B2). The build is row 210 (#1707,
owner Gizmoduck), a new row on top of row 199 (megavolt-0019, sage-1430).
- This draft follows the PRD's own style: plain words, no semicolons in new sentences, and the status
paragraph never edited in place except to append.
+1 -1
View File
@@ -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.
+8
View File
@@ -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.
+1 -1
View File
@@ -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.
+10
View File
@@ -0,0 +1,10 @@
# Quackerjack
Builder seat, Claude Haiku 5.5 at high effort, started by Sage under mos-1023 item 1 and Q177 for federation
rows R5 (`mosaic access list, grant, revoke`) and R2 (the `federation` config key), per Gizmoduck's design
section 7 (#414).
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
`quackerjack/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or
another seat's branch.
+8
View File
@@ -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
+24
View File
@@ -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
+36
View File
@@ -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%.
+86
View File
@@ -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.
@@ -0,0 +1,5 @@
M1 KILLED fail=5
M2 KILLED fail=2
M3 KILLED fail=7
M4 KILLED fail=2
M5 KILLED fail=2
+34
View File
@@ -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
+1 -1
View File
@@ -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.
+3
View File
@@ -1361,3 +1361,6 @@ are never rewritten or removed; corrections are new entries.
2026-10-20T18:34:53Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r2 (DESIGN.md blob 3eb718b5e131 at 1a268249e), static | changes: C1 config.json console key fails old loaders (mosaic-config.mjs:113 rejects unknown keys; key add inside phase 2 window, rollback removes it, loader test), C2 app.js signs out only on opaqueredirect so 401 never shows sign-in; notes on tests 15/7/9a, Q-B 200-HTML classification, scratch Console flags; Q-E keep iss/sub escaped and capped; verdict sent to Sage, no Gitea/queue record
2026-10-20T18:40:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r3 (DESIGN.md blob a152fd58e18e at 817e8f51b), static | approve on condition D1 (slot file checks stay out of shared loader, shape-only there) as a line edit; D2 notes: slot root from configDir, scratch dataRoot, name discovery GETs for Mos; C1/C2 and notes verified; Mos C1 sequence confirm still open; no Gitea/queue record
2026-10-20T18:41:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design D1/D2 check (DESIGN.md blob eba861f5d2bb at d55d86bb0), static git diff 817e8f51b..d55d86bb0 | ack: D1 and D2 match; design approved; Mos C1 sequence confirm still open; no Gitea/queue record
2026-10-20T18:57:10Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r2 (#1688, DESIGN.md sha256 5493c9df3ec0) | CHANGES to Darkwing, cc Sage: B1 claim check on MOSAIC_CONFIG-only selection, B2 shared prefix drop-in dirs; N1-N7 notes; static read, no slot, no Gitea or queue record
2026-10-20T19:00:04Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r3 (#1688, DESIGN.md sha256 98236a1aff98) | APPROVE to Darkwing, cc Sage: B1 and B2 closed with M13 and M14, N7 settled by Sage's 48ebee0ff; static read, no slot; manifest round gets a queue review record (sage-1337)
2026-10-20T20:40:15Z | Filbert (Opus 5.5, 048f3c63) | row 190 (#1688) manifest round 1 review, candidate 8862feccc (static) | CHANGES: B1 base drift, row 189 ace27dc5e conflicts in stack-release.sh, rebase + full gate rerun; N1-N7 notes; Gitea 29634, queue rev 2362 (uncommitted, left for Sage)
+9 -1
View File
@@ -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
+6 -5
View File
@@ -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.
@@ -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 `<seat>/*`
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.
+955
View File
@@ -0,0 +1,955 @@
# Case 2 slice rows: the creative writing hub (#416, T2)
Gosalyn, 2026-10-20 (from `date -u`), for sage-1329. Every section follows `docs/plans/BRIEF-TEMPLATE.md`.
The design they build is `agents/gosalyn/work/case2/DESIGN.md` on `gosalyn/case2-design`. Webby
approved r5 at e1635308b in webby-0021, with notes at dba33aaf6. r6 and r7 change only sections 9 and 10
and C2-13's row (mos-1057, Q183, webby-0023), and that diff is with Webby. Below, "the design" means that
file, with its section number. A builder follows the design and doesn't reopen a decision in it. A
departure goes to Sage before the build continues.
These are the slice rows from design 9.2, plus C2-11a, which the design starts in the first wave. The
other rows (C2-02, C2-03b, C2-09b, C2-10c, C2-11b, C2-15 to C2-19) get briefs when a seat is free for
them. Owners and models come from design section 8 (Q177, Q178). Where the design names only a model,
Sage picks the seat. The reviewers are my proposals, and Sage can change any of them with `queue set`.
Sage owns every gate.
Base: `refactor` at ae3c36f24. The design cites 9ae8c73ac. `git diff --stat 9ae8c73ac ae3c36f24 --
packages adapters roles scripts` touches only `scripts/stack-release.sh` and its test, and no cited line
is in either.
Order (design 8, "Parallel waves"):
1. Start now: C2-01, C2-08, C2-11a.
2. On C2-01's candidate: C2-04, C2-05a, C2-06, C2-12. On C2-08's: C2-09a, C2-10a, C2-20.
3. Then C2-03a and C2-07. Then C2-05b, C2-10b and C2-13.
4. Last: C2-14.
A row whose parent hasn't landed builds on the parent's candidate and rebases onto its landing (Q177).
Rules every row keeps:
- No test touches `~/.mosaic-dev`, `~/.config/mosaic-dev`, a live unit, a slot file, Melanie's books
repo, git.mosaicstack.dev or any Discord server. Tests use scratch roots, scratch config dirs and a
local bare repo the test builds in her layout's shape: spaces and capitals in folder names, a `.canvas`
file and an unmapped top-level note.
- No row reads her text. Fixtures are made up. A marker string in a fixture body stands in for her
text wherever a test checks that output holds none.
- No credential value in any output, test, log or message (CONSTITUTION gate 13). Tests use a fixture
token file with a made-up value.
- No live stack change. Nothing here re-pins, restarts or activates anything. C2-13's run on the host
needs Jason's go (design 10).
- Before in-review, grep `packages/*/tests` and `scripts/test-*.sh` for each changed path and for any
64-hex pin of it, and run every changed or touched test file on its own. Suites need Sage's slot.
- A refusal code named in the design is the code the row ships. A new code goes to Sage first.
---
## C2-01 packages/library: mapping, scope, reader, resolver, R-rules, renderItems, survey
### Problem
Nothing in the stack reads a writing repo by a mapping of folders to books, types, canon and playground.
`packages/project/src/read.mjs:19-26` reads blobs from a record repo but accepts executables (`:21`) and
knows no scope. Design 1.1 to 1.3 and 7.4 set out what Case 2 needs, and every later row builds on it.
### Owner and reviewer
- Owner: Gosalyn, Opus 5.5.
- Reviewer: Webby, who reviewed the design.
- After: none.
### Files owned
- `packages/library/` (new): `package.json`, `src/mapping.mjs`, `src/scope.mjs`, `src/reader.mjs`,
`src/links.mjs`, `src/problems.mjs`, `src/render.mjs`, `src/survey.mjs`, `src/index.mjs`, `tests/*.test.mjs`,
`tests/fixtures/`, `README.md`.
- `scripts/stack-copy.sh`: add `packages/library/src` to `COPY_PATHS`. Without it the live copy has no
library code, and no test catches the gap today.
- `packages/cli/tests/stack-copy.test.mjs`: add `packages/library/src/index.mjs` to the list at `:37`.
- `scripts/mosaic` or `packages/cli/src`: only the dispatch line for `library survey`.
### What ships
- The mapping schema of design 1.1, including `library-playground-outside-book` and the rule that no
playground prefix holds another. Business file loading wires it in C2-04, so here it is a pure
validator.
- The scope rule of design 1.2. Only `.md` files are playground or canon.
- The commit reader of design 1.3: `git ls-tree -r -z` and `cat-file` on a commit, never a working tree,
through the same `git()` and `gitEnv()` as `packages/project`. R1 to R7 as problems with path and line.
- The link resolver, in the order 1.3 gives: repo root, `vaultPrefix/` stripped, relative to the linking
file, then shortest path. `appears_in` and `relationships` wikilinks are read as links.
- `renderItems(body)` (7.4), one pure function for the item kinds and the three notices.
- `library survey <repo> [--mapping <file>]`, which prints folder names, file counts, front matter key
sets and link-form counts, and never text.
Tests: one case per R-rule and per resolver step. A link into another repo resolves to nothing. An
`appears_in` wikilink is indexed. `renderItems` has a snapshot for each kind and each notice. The survey
prints names, counts, key sets and link-form counts, and a marker string in a fixture body never
appears in its output. The stack-copy test lists the new path. The library tests and
`packages/cli/tests/stack-copy.test.mjs` pass.
### Out of scope
Business file loading and consumer skips (C2-04), sync (C2-03a), the index and queries (C2-05a), the
guard (C2-06). The survey run on the host is design 10 step 7, after the token slot exists.
### Gate
Webby approves the manifest. The library tests and the cli suite are green on Sage's gate, along with
every `scripts/test-*.sh`.
---
## C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file
### Problem
The stack has no role that may only read and answer in cited items, no way to carry Bean's and Chet's
personas, and several paths by which a non-human actor could change a creative instance (design 4, B3).
Both adapters pass the whole prompt on the command line (`adapters/claude/adapter.sh:79`, `:82`,
`adapters/pi/adapter.sh:106`, `:121`), so any local user can read it in `/proc/<pid>/cmdline`.
### Owner and reviewer
- Owner: an Opus 5.5 seat that Sage picks.
- Reviewer: Webby.
- After: none.
### Files owned
- `roles/companion.json`, `.md`, `roles/ideas.*`, `roles/research.*`, `roles/proofreader.*`,
`roles/fact-checker.*`, `roles/continuity.*` (new).
- `packages/business/src/vocabulary.mjs`: `THREAD_DEFINITIONS`, `END_PROTECTED` as `THREAD_DEFINITIONS`,
`CREATIVE_DEFINITIONS`, `CREATIVE_KINDS`, `CREATIVE_TOOLS`.
- `packages/business/src/role.mjs` (M1), `vars.mjs` (`persona.name`, `persona.sha256` in `REGISTRY`),
`launch-settings.mjs` (the `instances` and `models` checks and the `setRoutes` branch),
`instance-settings.mjs` (the creative branch).
- `packages/bus/src/broker.mjs`: M6 `decision-not-allowed` only.
- `packages/harness/src/bundle.mjs`: the persona file read and checks, the fenced block in `prompt()`, and
the `webPlan` refusal.
- `packages/harness/src/tools.mjs`: `raise_decision` left out for creative definitions.
- `adapters/claude/adapter.sh`, `adapters/pi/adapter.sh`, `adapters/README.md` (`:89`, `:107`).
- Tests in `packages/business/tests`, `packages/bus/tests`, `packages/harness/tests`.
### What ships
Design 4, 5.1 (M1, M2, M6, M10) and 5.3:
- The six role files, version 2, with the tools, authority, messaging and network of the design 4 table.
- The constants. `THREAD_DEFINITIONS` replaces `THREAD_DEFINITION` only where it means "may hold a
thread", and that use is C2-09a's. The `assistants` lines at `launch-settings.mjs:140`, `:186` and
`:191`, and `tools.mjs:186`, stay `THREAD_DEFINITION`.
- The persona file at `<configDir>/personas/<business>/<instance>.md`, read with `O_NOFOLLOW`. The
bundle refuses a non-regular file, another uid's, any group or other bit, more than 8,000 characters,
a digest off the var (`persona-mismatch`), and `</persona-file>` or a line starting `# ` or `## `
(`persona-invalid`). The block goes between the contract and "## This session", fenced as in design 4.
- One actor rule on all five write paths (design 4): a write that touches a `CREATIVE_DEFINITIONS`
instance is accepted only when its actor is the business file's `human`, otherwise
`organization-creative-human-only`. The `instances` key of `limits.set` and `organization.set` refuses
a creative instance for every actor with `organization-creative-instance`.
- Both adapters pass the prompt file, not its text. Claude uses `--system-prompt-file`. Pi gets the path
as `--system-prompt`, and its adapter checks `[ -r ]` first and refuses otherwise, because pi 0.85.1
uses the path string itself as the prompt when the file is missing or unreadable
(`resource-loader.js:21`, `:24-27`, `:30`).
Tests, all named in the design 8 C2-08 row:
- Role, gate and messaging tests. A creative raise is refused and no decision row appears.
`harness-builtin` is refused for `research` and `companion`. `scripts/test-config.sh` passes.
- Persona vars are refused at other layers, when malformed and on a non-creative definition. Each
persona file refusal above has a test. A prompt snapshot pins contract, persona, session, in that
order.
- M10:
- Each path refuses with the file unchanged, and the human's `instances.set` and `routes.set` on
`bean` work.
- The human's `limits.set` change to Bean's `launch.models` key works, next to the chief of staff's
`organization.set` refusal of the same change.
- Tester cases run through the broker with a bound tester session, and chief of staff cases with a
role session.
- A mutant without the `models` check fails.
- No `instances` read, `changes.jsonl` line or adapter argv holds a marker string from a fixture
persona.
- Adapter tests in `claude-session.test.mjs` and `pi-session.test.mjs`: argv holds the prompt path and no
marker from the prompt. Pi's adapter refuses a missing prompt file and an unreadable one.
- Business file, `.history` and `changes.jsonl` modes after an `instances.set` write.
### Out of scope
Thread creation per instance and the launcher (C2-09a), the lookup tools (C2-05b), the cited-reply guard
(C2-06). Checking the host's claude for `--system-prompt-file` is C2-13's precheck. If the C2-20 dry run
finds heading lines in a real persona, the fence rule changes in a follow-up to this row, reviewed,
before the import (design 9.1).
### Gate
Webby approves the manifest. The business, bus and harness suites, `scripts/test-config.sh` and every
`scripts/test-*.sh` are green on Sage's gate, and the `models` mutant is killed.
---
## C2-11a editor wikilink node, after measuring whether [[ round-trips
### Problem
The Console's editor opens a file in visual mode only if `roundTrip(text) === text`
(`packages/webui/src/public/bus.js:919-920`). Its commonmark rules (`packages/webui/editor/model.mjs:119-120`)
have no wikilink rule, and nobody has measured whether the serializer escapes `[[`. If it does, every
note with a wikilink opens in Source.
### Owner and reviewer
- Owner: Rocko, gpt-6.1-sol.
- Reviewer: Filbert.
- After: none.
### Files owned
`packages/webui/editor/model.mjs`, the editor's bundle output if the build commits one, and
`packages/webui/tests/` editor test files.
### What ships
First a test that measures today's behaviour on `[[a]]`, `[[a|b]]`, `[[a#h]]`, in a list item, and next to
front matter, with the result recorded in the manifest. Then a wikilink node that keeps each form byte
for byte. `roundTrip` tests cover plain, labelled, heading, in a list, and next to front matter.
### Out of scope
Library pages and saving (C2-11b), link clicking to open a target (C2-11b), any `documentPath` change.
### Gate
Filbert approves the manifest. The webui suite is green on Sage's gate, along with every
`scripts/test-*.sh`.
---
## C2-04 the library entry in the business file, consumer skips, M9, mirror, library.file and library.status
### Problem
The business file accepts only `root` and `repo` per project (`packages/business/src/business.mjs:186-187`),
and several consumers assume every project has a root (design 2.1). Nothing stops an organization write
from changing `projects`, and nothing creates a mirror.
### Owner and reviewer
- Owner: Rocko, gpt-6.1-sol.
- Reviewer: Filbert.
- After: C2-01.
### Files owned
- `packages/business/src/business.mjs`, `organization.mjs` (M9 in `organizationWriter.write`, `:177`),
`project.mjs`, `cli.mjs`, `resolve.mjs`, `instance-settings.mjs` (only the skip at `:142`).
- `packages/seat/src/checkout.mjs` (`:310`, `:319`), `packages/cli/src/launcher.mjs` (`:353`).
- `packages/project/src/paths.mjs` (the overlap loop at `:41-44`, and the mirror path).
- `packages/bus/src/broker.mjs`: `library.file` and `library.status` only.
- `packages/webui/src/cli.mjs`: dispatch lines for the two verbs.
- Tests in the matching `tests/` directories.
### What ships
The `kind: "library"` entry of design 1.1, validated by C2-01's schema. Each consumer in design 2.1
skips it, with its own test. M9: `organization-projects-protected` for any organization write whose
`projects` differ from the file's in canonical JSON. The bare mirror is created at the `projectRepo` path
with no remote in its config. `library.file` (read at the served commit) and `library.status` (reads the
sync state, empty until C2-03a) are human only, refused from an agent session with `human-required`
(M4).
Tests: business file tests. One test per consumer skip. A launcher test that the workspace has no clone.
The M9 test calls `write` directly, once with a changed playground prefix and once with a changed hybrid
entry, and the file and `changes.jsonl` are unchanged. Each verb from an agent session refuses.
### Out of scope
Fetching (C2-03a). Bus writes (C2-02). The Console's library pages (C2-11b).
### Gate
Filbert approves the manifest. The business, seat, cli, project, bus and webui suites are green on Sage's
gate, along with every `scripts/test-*.sh`.
---
## C2-05a lookup index and queries
### Problem
Agents need typed lookups over her repo that keep universes apart and default to canon (design 3). The
reader from C2-01 has no index or query layer.
### Owner and reviewer
- Owner: a Haiku 5.5 subagent at high effort, dispatched by Sage (lead decision 129).
- Reviewer: Filbert.
- After: C2-01.
### Files owned
`packages/library/src/index-build.mjs`, `packages/library/src/query.mjs`, their exports in
`packages/library/src/index.mjs`, and `packages/library/tests/` query test files.
### What ships
An index per commit (types, names, links, backlinks), cached in memory by commit id. Queries for find, get,
search, manuscript and universe, with the arguments and result shapes in the design 3 table. `universe`
is required. Results never leave it, except a hybrid's declared sources minus exclusions. Every result
carries `scope`. Unmapped files never appear. `links_out` and `links_in` drop other-universe and unmapped
targets. Results page under `REPLY_CAP` (`packages/harness/src/tools.mjs:271-280`) and are never cut.
Tests: isolation, scope labels on every query, unmapped files never returned, paging under `REPLY_CAP`,
and a rebuild time on a 3,000-file fixture recorded in the manifest.
### Out of scope
Broker verbs and typed tools (C2-05b). Hybrid entries in the business file (C2-15). The query code takes
hybrids as a shape, and its tests use a hand-built hybrid mapping.
### Gate
Filbert approves the manifest. The library suite is green on Sage's gate, along with every
`scripts/test-*.sh`.
---
## C2-06 the cited-reply guard, the page store and the three notices
### Problem
Today a creative agent's free text would reach her as a stored message. Jason's ruling says agents never
write prose for her and always refer to the source (Q176). Design 5.2 puts one guard in the broker before
every agent-to-human message in a library business.
### Owner and reviewer
- Owner: an Opus 5.5 seat that Sage picks.
- Reviewer: Webby.
- After: C2-05a.
### Files owned
- `packages/library/src/guard.mjs` and its export.
- `packages/bus/src/broker.mjs`: the pre-check on `message.send` to `human`, and the page store beside the
web verbs (`:1516`, `:1521`).
- `packages/bus/src/web.mjs`, only if the page store needs the fetched text passed out.
- Tests in `packages/library/tests` and `packages/bus/tests`.
### What ships
The guard as design 5.2 sets it out: the JSON body, the M5 limits, the checks and refusal codes of the
5.2 table, and the three notices. It runs before the insert, outside the SQL transaction, as
`#requestProject` does (`broker.mjs:1378-1430`). It doesn't look at the sender's definition to decide
whether to run. The page store keeps the last 20 broker fetches per session, 200 KB each, in memory,
dropped when the session ends.
Tests: one pass and one refusal for each check in M5 and 5.2, each limit refused one past its boundary
and accepted at it. Each goes through the runner's RESULT path and through `send_message`, and once
with a non-creative sender in a library business. The `messages` count is unchanged on each refusal.
### Out of scope
The runner retry and the runner's notices (C2-07). The Console's rendering (C2-10b). Any classifier of
prose. The idea-text residual is Jason's or Melanie's to accept (design 5.1, 10 step 8).
### Gate
Webby approves the manifest. The library, bus and harness suites are green on Sage's gate, along with
every `scripts/test-*.sh`.
---
## C2-12 the setup sheet for Melanie and the survey run sheet
### Problem
Melanie keeps writing in Obsidian, and she needs to know what the agents read, how a playground folder
works, what changes for Bean and Chet, and two surprises (design 7.1, N2, N4). Whoever runs the survey
on the host needs exact steps.
### Owner and reviewer
- Owner: gpt-6-luna.
- Reviewer: Webby.
- After: C2-01.
### Files owned
`docs/case2/SETUP-SHEET.md` and `docs/case2/SURVEY-RUN.md` (new).
### What ships
The setup sheet in plain words:
- Which folders the agents read, and that a folder named in `playground` is her playground.
- How to add a graph colour group for that path in her own vault.
- That moving a note into a book folder makes it canon, so agents may cite it as fact.
- That a short link to a name that also exists in another of her repos can point somewhere else for the
agents, and that a full-path link avoids this.
- What changes for Bean and Chet: short cited items, no passages (design 4).
The survey run sheet gives the exact `library survey` command with C2-01's real flags, and states what
the output holds: names and counts only.
### Out of scope
Sending it to her. Design 10 step 7 makes that Jason's call.
### Gate
Doc only. Webby approves it against C2-01's real flags. Before landing, grep `packages/*/tests` and
`scripts/test-*.sh` to confirm no test reads either file.
---
## C2-09a one thread per creative instance
### Problem
`thread.create` picks the single `assistant` instance and refuses more than one
(`packages/bus/src/broker.mjs:2331-2334`). The same rule sits in `#threaded` (`:290-291`) and the launcher
(`packages/cli/src/launcher.mjs:135`, `:236-238`). So she can't open a thread with Bean beside one with
Chet.
### Owner and reviewer
- Owner: Rocko, gpt-6.1-sol.
- Reviewer: Dewey.
- After: C2-08.
### Files owned
`packages/bus/src/broker.mjs` (`#threaded`, `thread.create`), `packages/cli/src/launcher.mjs`,
`packages/webui/src/public/bus.js` and `serve.mjs` (the new-thread control only), and tests in
`packages/bus/tests`, `packages/cli/tests` and `packages/webui/tests`.
### What ships
Design 6, C2-09a: `#threaded` and the launcher read `THREAD_DEFINITIONS`. `thread.create` takes
`instance`, which must be a `THREAD_DEFINITIONS` instance. With no `instance` and exactly one such
instance, today's behaviour holds. With more than one, it refuses `thread-instance-ambiguous`. The
Console's new-thread control lists thread instances by `persona.name` or instance name and passes the
one she picks. No schema change.
Tests: a creative instance's thread starts a run, `thread.create` with `instance` binds it, and two thread
instances with no `instance` still refuse.
### Out of scope
Rooms and @tags (C2-09b, C2-10c).
### Gate
Dewey approves the manifest. The bus, cli and webui suites are green on Sage's gate, along with every
`scripts/test-*.sh`. Watch for hash pins on `bus.js` and `broker.mjs`
(`packages/bus/tests/channel-proof.test.mjs` pins a webui file).
---
## C2-10a no raw session view (M7) and no raw Discord engine (M8)
### Problem
The Console's conversation view shows a model's raw text from session files
(`packages/conversation/src/reader.mjs:225-240`, `events.mjs:58`, `:82`), so a refused answer would still
show. The Discord connector posts its engine's output and the broker never sees it
(`packages/discord/src/connector.mjs:381-387`).
### Owner and reviewer
- Owner: a Haiku 5.5 subagent at high effort, dispatched by Sage.
- Reviewer: Filbert.
- After: C2-08.
### Files owned
`packages/conversation/src/reader.mjs`, `packages/discord/src/cli.mjs` (`check` and `run` only), and
tests in `packages/conversation/tests` and `packages/discord/tests`.
### What ships
M7 and M8 as design 5.1 states them. On a business with a library project, `catalogue()` leaves creative
sessions out and `open()` refuses them with `conversation-not-listed`. `discord check` and `discord run`
refuse a binding whose engine isn't `mosaic-bus` with `discord-engine-not-allowed`, and also refuse a
`mosaic-bus` binding with `tools`.
Tests: a creative session file under the root isn't listed and won't open by id, and an assistant session
still lists. Both Discord verbs refuse on a library business, and a pi binding on a business without a
library still checks clean.
### Out of scope
The relay engine itself (C2-18).
### Gate
Filbert approves the manifest. The conversation and discord suites and `scripts/test-discord.sh` are
green on Sage's gate, along with every `scripts/test-*.sh`. No test reaches a Discord server.
---
## C2-20 Bean and Chet persona import, with a count-only dry run
### Problem
Bean's and Chet's personas live in her two Hermes profiles on 10.1.1.54. The hub needs each one as a
0600 persona file with its digest in the business file (design 4), and nothing should read her memory,
sessions or messages to get them (design 9.1).
### Owner and reviewer
- Owner: Gosalyn, Opus 5.5. The script runs only under Jason's go, by whoever he names.
- Reviewer: Webby.
- After: C2-08.
### Files owned
`scripts/case2-persona-import.mjs` (new), `packages/cli/tests/case2-persona-import.test.mjs` (new) and its
fixture tree.
### What ships
A script that reads only the persona definition of each of the two Hermes profiles. It confirms profile
to name from each profile's own configured name, prints the pairing, and stops if a profile names
neither Bean nor Chet. Per-channel prompts are listed by name and size only. It writes each persona to
a 0600 staging file. A second step, run as `<user>`, installs it at
`<configDir>/personas/<business>/<instance>.md` and prints `persona.name` and `persona.sha256`.
Otherwise it prints names, byte counts and sha256 only. `--dry-run` writes nothing and prints only counts
of files, lines, and lines starting `# ` or `## `.
Build note (gosalyn-0030, mos-1087). The dry run also counts lines that name a Hermes tool or channel,
so Jason knows before the import whether a persona refers to things Bean and Chet won't have in
Mosaic. The words come from a fixed list in the script: the tool names in the hermes-agent checkout at
df5700ebe, read from its code and not her profiles, plus `Hermes` and `Discord`. It prints one count
per persona file and never the line, its number or the matched word. A nonzero count changes nothing
in the import. It goes to Jason with the other counts.
Tests, on fixture Hermes profile trees:
- The right fields are taken.
- Memory and session files are never opened, shown by a read trace on the fixture.
- A profile with no persona field refuses, and a persona that fails C2-08's fence check refuses.
- Output holds no persona text. Each written file is 0600 and its digest matches.
- `--dry-run` writes nothing, and prints the right counts and no marker string for a fixture with two
heading lines.
- `--dry-run` counts three lines that name a listed Hermes tool or channel in a fixture. Its output
holds none of the matched words.
### Out of scope
Running it. Design 10 step 6 and 9.1 make the dry run, the import and the pairing check Jason's go. If
the dry run counts heading lines, the C2-08 follow-up changes the fence rule before the import, and her
text is rewritten only if she says so through Jason.
### Gate
Webby approves the manifest. The test file runs green on its own and the cli suite is green on Sage's
gate, along with every `scripts/test-*.sh`.
---
## C2-03a fetch-only sync
### Problem
The mirror from C2-04 holds nothing until something fetches her repo, safely and read-only, without the
token ever reaching argv, config, logs or replies (design 7.2, CONSTITUTION gate 13).
### Owner and reviewer
- Owner: an Opus 5.5 seat that Sage picks.
- Reviewer: Dewey.
- After: C2-04.
### Files owned
`packages/library/src/sync.mjs`, `packages/library/bin/askpass` (new), `packages/bus/src/broker.mjs`
(`library.sync`, the 60-second timer and the library lock), `packages/webui/src/cli.mjs` (dispatch line),
and tests in `packages/library/tests` and `packages/bus/tests`. If the helper sits outside
`packages/library/src`, add its path to `COPY_PATHS` in `scripts/stack-copy.sh`.
### What ships
Design 7.2's slice:
- `syncCommand(kind, url)`: its argv is `SAFE`, exactly one `-c protocol.https.allow=always`, `-C
<mirror>`, then the command. Its env is `gitEnv()` plus `GIT_ASKPASS` naming a helper outside the
mirror, which reads the `library-gitea` slot.
- An `https://` URL only.
- The served ref `refs/heads/served`, and `library-diverged`, held until a human `library.sync --accept
<commit>`.
- The sync state file, written atomically inside the mirror, and the 60-second timer.
- `library.sync`, human only. A test reaches a local bare repo only through the module parameter that
replaces `syncCommand`.
Tests: the C2-03a tests in design 7.2. A mutant that sets `protocol.allow=always` fails the argv test.
### Out of scope
Push and every bus write (C2-03b, C2-02). Creating the token slot, which is design 10 step 4.
### Gate
Dewey approves the manifest. The library and bus suites are green on Sage's gate, along with every
`scripts/test-*.sh`, and the mutant is killed. A search of the test run's output for the fixture token
finds nothing.
---
## C2-07 runner retry on a guard refusal, and the runner's notices
### Problem
When `message.send` refuses, the runner logs it and marks the message read, so the answer is lost
(`packages/harness/src/runner.mjs:451-456`). Its empty and failed turn texts (`:449`) wouldn't pass the
guard.
### Owner and reviewer
- Owner: a Haiku 5.5 subagent at high effort, dispatched by Sage.
- Reviewer: Filbert.
- After: C2-06.
### Files owned
`packages/harness/src/runner.mjs` and `packages/harness/tests/runner.test.mjs`.
### What ships
On a refusal with a guard code, a new turn in the same session with the code and reason, at most twice,
then `{"items": [], "notice": "no-cited-answer"}`. In a library business, `no-answer` and `turn-failed`
replace the two texts at `:449`. The failure reason stays in the run log.
Tests: a fake harness answers free text, then a valid reply, and exactly one message is stored. An empty
turn and a failed turn store the fixed notices.
### Out of scope
The guard itself (C2-06).
### Gate
Filbert approves the manifest. The harness suite is green on Sage's gate, along with every
`scripts/test-*.sh`.
---
## C2-05b broker lookup verbs and typed tools
### Problem
The queries from C2-05a aren't reachable from an agent session. Design 3 and M3 call for typed tools
that the broker re-checks on every call.
### Owner and reviewer
- Owner: Rocko, gpt-6.1-sol.
- Reviewer: Dewey.
- After: C2-05a, C2-03a.
### Files owned
`packages/bus/src/broker.mjs` (the lookup verbs), `packages/harness/src/tools.mjs` (the five typed tools),
and tests in `packages/bus/tests` and `packages/harness/tests`.
### What ships
The five lookup tools of design 3, for every agent session in a business with a library project. Each
read is at the served commit, and a read whose last fetch is older than 60 seconds syncs first. The
verbs refuse `library-not-configured` in a business without a library and refuse a missing `universe`.
The creative tool set is exact: the lookups and `send_message`, with no write tool and no
`raise_decision`.
Tests: broker verb tests and a snapshot of typed tool names per creative role (M3).
### Out of scope
The guard (C2-06). Write verbs (C2-02, C2-19).
### Gate
Dewey approves the manifest. The bus and harness suites are green on Sage's gate, along with every
`scripts/test-*.sh`.
---
## C2-10b Console chat: cited items and the status line
### Problem
The Console would show a cited reply as raw JSON, and she can't see when the hub falls behind her repo
(design 7.4).
### Owner and reviewer
- Owner: a Haiku 5.5 subagent at high effort, dispatched by Sage.
- Reviewer: Dewey.
- After: C2-06, C2-09a, C2-03a.
### Files owned
`packages/webui/src/public/bus.js`, its stylesheet, `packages/webui/src/serve.mjs` (a `library.status`
route only), and tests in `packages/webui/tests`.
### What ships
Design 7.4. The chat view renders a cited reply through `renderItems`: each item's kind, text and cites,
with a scope badge (canon, playground or web) and "changed since" when the file's last commit is newer
than the cited one. Each notice shows its fixed line. A status line reads `library.status`: the served
commit and its age, `library-diverged`, a fetch error, and the count of R-problems with a link to the
list.
Tests: webui tests for items, scope badges, notices, "changed since" and the status line.
### Out of scope
The room view (C2-10c), library pages (C2-11b). Her sign-in from her machine waits on SSO (#412) or
Jason's interim ruling (design 10 step 5).
### Gate
Dewey approves the manifest. The webui suite is green on Sage's gate, along with every
`scripts/test-*.sh`. Grep for hash pins on `bus.js` and `serve.mjs` before in-review.
---
## C2-13 install script and precheck for the host
### Problem
The hub runs on 10.1.1.54 as `mosaic` under `/srv/mosaic`, as `mosaic-stack.service` with
`ProtectHome` (design 9, Q181). Nothing installs a library business there, or checks the host before it
starts.
### Owner and reviewer
- Owner: Gosalyn, Opus 5.5. The script runs only under Jason's go, by him or his delegate (design 10).
- Reviewer: Webby.
- After: C2-04, C2-03a, C2-20.
### Files owned
`scripts/case2-install.sh`, `scripts/case2-precheck.sh` and `scripts/case2-unit.sh` (new), the template
`packages/cli/systemd/mosaic-stack.service.in` (new), `packages/cli/tests/case2-install.test.mjs`
(new), and one line of `packages/cli/README.md`: at `:713`, "nothing in the repository creates or reads
it" changes to say that nothing creates it, that only the Case 2 precheck reads it, and that the
precheck prints only variable names, never values (webby-0034 N2, webby-0035).
### What ships
The install does: bootstrap, release package and activate, the business file with the mapping, `bean`
and `chet` with their persona vars and files, any specialists she picks, her human id, the token slot
path. It runs as `<user>` through `sudo -u mosaic -H` from `localadmin`, and `mosaic` gets no key. It
doesn't create `<user>` and writes no unit file. Root does those in design 10 steps 2a and 2c. Every
host value is an input, so a later host move changes only inputs.
Row 195 runs these scripts by input for Case 1 on 10.1.1.53 (darkwing to gosalyn, design at
`agents/darkwing/work/queue-195/DESIGN.md` section 2). They don't copy them. So the install and the
precheck take `--profile factory|library`, default `library`:
- `library` is everything this brief lists.
- `factory` skips the library mapping, the persona files and digests, the token slot, and
`library.sync` and `library.status`. It requires `bus.tester` on, because Case 1's acceptance uses the
tester.
- `factory` refuses a business file that has a `kind: "library"` project, with
`profile-library-business`. So Case 2 can't reach a tester-on check through a wrong flag.
- `library` refuses with `bus.tester` on, as before.
`case2-unit.sh` is the 2c step. Root runs it from a root-owned checkout of the reviewed commit outside
`/srv/mosaic`, never from the release, because `mosaic` can write the release. It does the following:
- It renders the unit from the checkout's template, with the user, the home and the copy id as inputs.
- It refuses unless `User=`, `Group=`, `ExecStart=`, `EnvironmentFile=` and the hardening lines match
fixed values. `EnvironmentFile=` is the line in `packages/cli/systemd/mosaic-bus.service.in:24`. The
copy id is the one part of `ExecStart=` that varies, so a re-pin to a new copy can re-render the
unit (row 195). It must be exactly 40 lowercase hex characters naming a copy that holds `scripts/mosaic`,
directly under `<home>/.local/share/mosaic-stack/copies`. That is the rule `copy_root` uses
(`scripts/stack-copy.sh:6`, `:20`). Anything else refuses before the render.
- It refuses any `PermissionsStartOnly=`, and any `Exec*=` value whose leading run of prefix
characters (`@`, `-`, `:`, `+`, `!`) holds a `+` or `!`. systemd takes those prefixes in any order,
so `-+/srv/mosaic/x` runs as root even with `User=mosaic`. The named-line check doesn't read these
lines (webby-0026 N1, webby-0027, defence in depth).
- It writes `/etc/systemd/system/mosaic-stack.service` as `root:root` 0644. It never uses
`systemctl link`, and it refuses a symlink at the unit path.
- On a re-install, it refuses an installed unit that differs from the render.
- It reads nothing from under `<user>`'s home.
The precheck refuses unless all of these hold:
- The config validates.
- `sudo -n true` fails as `<user>`, and `id -G` prints exactly one entry, its primary group. A system
user always has one, so the check is for supplementary groups. `bus.tester` is not on.
- The business file, `.history`, `changes.jsonl` and persona files have the modes in design 4, and
each persona digest matches.
- The run root is 0700.
- `XDG_DATA_HOME` is unset or is `<home>/.local/share`. `copy_root` reads the copies dir from it
(`scripts/stack-copy.sh:6`), and the unit fixes `<home>/.local/share`, so any other value would
put the copy where the unit refuses it (webby-0032 N2). A test sets it elsewhere and the precheck
refuses.
- `node --version` is 24 or later, because every package's `engines` field says `>=24` (row 195: .53
has v22.22.2).
- `bwrap --unshare-all --ro-bind / / true` exits 0 as `<user>`. Without it no session starts. Row 195
found no bwrap on .53, and `kernel.apparmor_restrict_unprivileged_userns=1`, and .54 may be the same.
On a refusal, the precheck prints the sysctl's value and whether `bwrap` is on PATH.
- When some creative instance's harness is claude-code, one marker prompt run through the host's claude
with `--system-prompt-file` shows that it replaces the default prompt. The claude version is printed.
With design r9's model path, Bean and Chet run pi, so this check is skipped unless she adds a
claude-code specialist.
- When an instance's provider is `zai`, `<configDir>/secrets/claude-session.env` exists, is a regular
file owned by `<user>` at 0600, and sets a nonempty `ZAI_API_KEY` (design 10 step 2d). It parses the
file the way systemd does, so the last assignment of a name wins (webby-0032 N3). A pi launch
has no credential check, so without this the first turn is where a missing key shows. The precheck
prints the name and never the value.
- `library.sync` fetches her repo read-only, and `library.status` lists R-problems.
- No Discord binding exists.
It also prints free memory and verifies HOME is `/srv/mosaic`.
Tests run against a scratch data root and a local bare repo. Each precheck refusal has a case. The
claude check runs against a fake claude that appends rather than replaces, and that case refuses. With
no claude-code instance the claude check doesn't run, and a test shows a missing claude passes. The key
file refuses when missing, at 0644, a symlink, with `ZAI_API_KEY=` empty, and with `ZAI_API_KEY=x`
followed by `ZAI_API_KEY=`. A search of the test
output for the fixture key value finds nothing.
`case2-unit.sh` runs against a scratch unit directory. These cases refuse:
- an installed unit edited to `User=root`
- an edited `ExecStart=`
- a template with a hardening line removed
- a template with the `EnvironmentFile=` line removed or pointing elsewhere
- a symlink at the unit path
- a template with `ExecStartPre=+/srv/mosaic/x`
- a template with `ExecStartPre=-+/srv/mosaic/x`
- a copy id that is short, uppercase, holds `/` or `..`, or names no directory
Precheck cases for the new lines: a fake `node` printing v22 refuses, and a fake `bwrap` that exits 1
refuses. `--profile factory` passes with the tester on and no library items, and refuses a business
file with a library project. `--profile library` refuses with the tester on.
A clean render writes a 0644 file. A test checks the script for `systemctl link`.
### Out of scope
Running it on the host. Design 10 steps 2 to 7 are Jason's. Step 1 no longer waits on #421 (Q183), and
Mos lifted its hold (mos-1074). The root steps 2a and 2c are ops-01's, over ssh as `localadmin`,
so the install waits on #423 step 1 and on nothing else in #423. The Gitea bot account and the token slot are steps 3 and 4. Stopping Hermes
is step 9 and her choice.
### Gate
Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every
`scripts/test-*.sh`. The run on the host is a separate window with Jason's go.
---
## C2-14 end-to-end acceptance on a scratch root
### Problem
Each slice row tests its own part. Nothing yet shows the whole path from her push to a checked reply in
the Console.
### Owner and reviewer
- Owner: an Opus 5.5 seat that Sage picks.
- Reviewer: Dewey, who also runs the script.
- After: every other slice row above.
### Files owned
`scripts/test-case2.sh` (new) and its fixtures.
### What ships
One script on a scratch data root and a bare repo in her layout. It syncs, asks a creative agent in its
thread through a fake harness, and stores a reply whose cites verify. It refuses a playground cite on a
`fact`, holds a force-push as `library-diverged`, and checks that the Console renders the reply as
items.
### Out of scope
Any live host or real model call.
### Gate
Dewey runs the script and approves. It is green on Sage's gate with every `scripts/test-*.sh`.
---
## Brief: row A, ZAI_API_KEY in the session env allowlist
### Problem
For 10-27, Bean and Chet both run pi with provider `zai` on `glm-5.3` (Q186, mos-1090, sage-1469). Pi
reads the zai key from `ZAI_API_KEY` (pi-ai `env-api-keys.js:89`, with `glm-5.3` in `dist/providers/data/zai.json`).
`ENV_ALLOW` (`packages/seat/src/session.mjs:83-88`) doesn't hold that name, so `sessionEnv` (`:97-99`)
drops it. A pi launch has no credential check (`packages/plugins/harness/pi/index.mjs:42`
`countsAccount: false`, `packages/cli/src/launcher.mjs:299`), so today a zai role launches and fails at
its first turn, as `packages/cli/README.md:439-444` says.
### Owner and reviewer
- Owner: a Haiku 5.5 subagent at high effort, dispatched by Sage (lead decision 129). Size XS.
- Reviewer: Webby.
- After: nothing. It doesn't wait on any Case 2 row.
### Files owned
`packages/seat/src/session.mjs`, `packages/seat/tests/session.test.mjs` and `packages/cli/README.md`.
### What ships
- `"ZAI_API_KEY"` joins the provider line of `ENV_ALLOW` (`session.mjs:86`), after `OPENROUTER_API_KEY`.
Nothing else in the list changes. `SESSION_CREDENTIALS` and `hasSessionCredential` (`:94-95`) don't
change. They are Claude Code's login sets, and a zai key isn't one.
- `packages/cli/README.md`, after "A Pi session gets the same variables through the allowlist." (`:435`),
one sentence: a Pi session on provider `zai` logs in with `ZAI_API_KEY`, which the allowlist passes. Like
every allowlist variable, it reaches every session the host starts, Claude Code ones included, and a
shell inside any of them can read it. Where the README names `claude-session.env` ("Unit", `:711`),
"The file holds one line" becomes "The file holds one line per variable", followed by a sentence
saying it holds every session provider key, not only Claude's (webby-0032, webby-0034 N1). Leave
`:713`, "nothing in the repository creates or reads it", alone. C2-13 owns that line.
- One test in `session.test.mjs`: `ENV_ALLOW` includes `ZAI_API_KEY`, and
`sessionEnv({ ZAI_API_KEY: "fixture-zai", ZAI_TOKEN: "t" }, run)` returns `ZAI_API_KEY` with the fixture
value and no `ZAI_TOKEN`. The existing allowlist test (`:51-59`) stays as it is.
- One mutant: the same file with `"ZAI_API_KEY"` removed from `ENV_ALLOW`. The new test fails on it.
### Out of scope
Placing a key anywhere. The hub's own key on the Z.ai account is design 10 step 2d, and it is Jason's.
Any pi launch or live check: running a pi role waits on #383. Chet's Codex login (row B, #428). A
credential check for pi launches.
### Gate
Webby approves the manifest. The mutant is killed. The seat suite and the cli suite are green on
Sage's gate with every `scripts/test-*.sh`.
---
## Brief: row B, a per-instance pi agent dir for Chet's Codex login (#428)
### Problem
After 10-27, Chet moves from Bean's Z.ai account to his own ChatGPT/Codex login (Q186). Pi's
`openai-codex` provider logs in with OAuth and a refresh token, and keeps both in `auth.json` under
`PI_CODING_AGENT_DIR` (pi `config.js:405-438`). A role session's `HOME` is the run's own, so pi never
sees an agent dir from the host (`packages/cli/README.md:439-441`), and `ENV_ALLOW` doesn't pass
`PI_CODING_AGENT_DIR`. So no env variable can carry this login. Each instance needs its own agent dir
that survives runs, because a refresh rewrites the file.
Chet's Hermes profile uses the same login today. Until Hermes stops, two clients would hold one
refresh token. If a refresh by one revokes the other's, one of them drops out mid-session.
### Owner and reviewer
- Design: Gosalyn owns it (sage-1469). The build owner and reviewer are named when the design is
approved.
- Starts after 10-27. Nothing in this row is built or tested before then.
### Files owned
None yet. The design names them.
### What ships
The design first. It settles where the dir lives and who can read it, how the launcher mounts it into
the sandbox writable for one instance only, how a refresh write survives the run, what a shell in the
session can read, and how a launch with no login refuses. Build rows follow from it.
### Out of scope
Bean's model path. Any change to row A. Any change to Hermes or her server.
### Gate
A live check, run with Jason's or Mos's go, showing that two clients on one ChatGPT login don't revoke
each other's refresh tokens. Until that check passes, no build row for this design starts.
@@ -0,0 +1,120 @@
# Federation build rows R1, R2 and R5 (#414)
Sage, 2026-10-20, for Mos's orders mos-1023 item 1, mos-1028 item 2 and mos-1034 item 1. The design is
Gizmoduck's `agents/gizmoduck/work/federation/DESIGN.md` at a1daa3c07 on `gizmoduck/federation-design` (r4.9,
sha256 a31cbebd070f9e3a5ca4ccb158b1747c6f5a83c3469855705b0026a581fb5c6a). Duckworth approved r4.5
(duckworth-0012), r4.6, r4.7 (duckworth-0020) and r4.8 (duckworth-0021, 8a22d0897 on `duckworth/reviews`),
and acked r4.9 (duckworth-0024). r4.7 applies Jason's Q181 hosts, r4.8 adds N22's R8 checks and r4.9 names who
runs them. None of them changes R1, R2 or R5. Section 7's table
sets each row's scope and test gate. Vikunja P19 #414. Seats push only their own `<seat>/*` branches
(mos-0981).
The other R rows are filed when their parents have candidates: R3 after R1 and row 196, R4 after R3, R5 and
row 193's viewer level (193a, mos-1019), R6 after #412 phase 1, R1, R2 and R3, R7 after R1-R6 and row 190.
R8 is a live window, not a code row. Its brief is the last section here.
## Brief: FED R1, the access.json loader and validator
Owner Gizmoduck, reviewer Duckworth, gate owner Sage. Base: refactor at this brief's commit. No parent.
`packages/business/src/access.mjs`: the access.json loader and validator from DESIGN.md section 3.5, and the
pure `covers`. Providers, peers and businesses come in as arguments. One read plus the file checks. The
exported interface is what R5 and row 193a build against, so the manifest names it, and any change to it in
review goes to Sage before it lands. Section 7 names `covers` but not the loader and validator exports or
the context shape. Morgana's 193a (508ee4d61 on `morgana/row193a`) assumes `checkAccess(doc, {providers,
peers, businesses})`, with `businesses` mapping an id to `{human, instances}`, throwing a `BusinessError` that
names the key path. Use those names, or tell Morgana and Quackerjack yours before the manifest goes to
Duckworth. Row 193a later widens the level check to `viewer` on R1's candidate (mos-1019).
Tests, in `packages/business/tests/`: every rule in 3.5 refuses with the key path named. That covers a
symlink, a non-regular file, another owner, group or other write, expiry, `*`, a grant disjoint from the given
subjects, `human` and `tester` ids, any level other than `collaborator`, and a project entry that isn't a
UUID. For `covers`: `*` and listed ids match, while an unlisted project, instance or business doesn't, an
expired grant doesn't, and a level filter rejects another level. One mutant per refusal, each killed.
Boundaries: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or any live access.json. Tests
use scratch roots. Single test files run freely, and suites need Sage's slot.
Gate: Duckworth approves the manifest. The business suite and every `scripts/test-*.sh` are green on Sage's
gate, and the mutants are killed.
## Brief: FED R5, mosaic access list, grant and revoke
Owner Quackerjack, reviewer Duckworth, gate owner Sage. Chained on R1 (Q177). The build starts now against
R1's exported interface as the R1 manifest names it, and R5 lands after R1 and rebases onto R1's landing.
`mosaic access list`, `grant` and `revoke`: human proof, atomic write, revision bump, a receipt line. The help
text states the `instances` limit and the provider-removal order (DESIGN.md section 7, row R5). R4 later makes
these verbs call `access.reload`, so R5 leaves no other reload path.
Context (sage-1419). `ssoSubjects` is row 192's `console.sso.providers[].subjects` flattened to `{provider,
sub}` pairs; the design's "`console.sso.subjects`" in 3.5 is shorthand for it. `providers` is the list of
`providers[].id`, read through `loadSystem` and row 192's `checkConsole`. `peers` is `[]` until R2 lands, so a grant
naming a peer home refuses. R5 rebases onto row 192's landing as well as R1's.
Tests, in `packages/cli/tests/`: tester and agent callers are refused, a concurrent write refuses on a
revision conflict, the receipt line is appended, and a bad grant is refused before anything is written. One
mutant per refusal, each killed.
Boundaries: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or any live access.json. The
human proof in tests uses the scratch stand-in pattern (a scratch bus verifies its own human-cli path).
Single test files run freely, and suites need Sage's slot.
Gate: Duckworth approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate,
and the mutants are killed.
## Brief: FED R2, the federation key in the system config
Owner Quackerjack, reviewer Webby, gate owner Sage. Chained on row 192 (#412 phase 1). The build starts when
row 192 has a candidate, on that candidate.
A `federation` key in `scripts/mosaic-config.mjs` with the shape in DESIGN.md section 3. A config with no
`federation` key still loads as it does today. This changes the config schema, so the manifest lists every
reader of the key.
Tests, in `scripts/test-config.sh`: a valid key loads, an unknown sub-key and a bad origin refuse, and a
config without the key loads. One mutant per refusal, each killed.
Boundaries: `~/.config/mosaic-dev/config.json` is user-authored and is not touched. Tests use scratch config
dirs only.
Gate: Webby approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate, and
the mutants are killed.
## Brief: FED R8, the hub window on 10.1.1.54
A live window, not a code row (DESIGN.md section 7, row R8). The brief follows r4.11, b93f098f0 on
`gizmoduck/federation-design` (sha256 1bcee8df0a3a8628a6646535b756c936ca6d9e7bcfa7e1dd2bf1d3f01715c8e6),
which Duckworth acked with r4.10 (duckworth-0034). Mos closed design rounds there (mos-1072). Run by Jason and
Mos. Nothing here authorizes a step on any host; each step waits for the window's go.
What changed since r4.9 (DECISIONS Q182, Q183, mos-1055, mos-1063). R8 is a recorded acceptance. It starts
with Jason's acceptance of the factory paths to .54 on record, quoted in section 8. The privilege and host
checks still run, and each result goes into the R8 record with the name of whoever ran it. They report and
don't refuse. The four functional checks named at the end of section 7's R8 row still stop R8 when they fail
(mos-1066 item 1).
Order (mos-1042 item 2). R8 runs after the Case 2 install on .54 (#416, due by 2026-10-27, Jason's latest-by
date in Q176, mos-1069), after Bean and Chet are live in Case 2, and after both Hermes gateways on 10.1.1.54
have stopped. Case 2 go-live doesn't wait on R8. The risk, stated for 10-27: federation between the two
instances depends on Melanie's switch-over date.
Entry checks, all before anything starts:
- R7, #412 phase 2 and Jason's #419 answer, as section 7 lists.
- The four hub user checks on 10.1.1.54 (N22, mos-1035 item 1): `sudo -l -U mosaic` grants nothing, `id
mosaic` shows only its own group, `~mosaic/.ssh/authorized_keys` is absent or empty, and no file under
`/etc/sudoers.d` names `mosaic`. An agent or Jason runs them as localadmin (mos-1055), and the output goes
into the R8 record with who ran them.
- The factory privilege checks, the key fingerprint comparison and the section 8 privilege path table, each
as section 7's R8 row states them.
Build notes:
- N1 (Duckworth, duckworth-0034, mos-1072). A check that fails on a path Jason didn't name in Q182 or Q183 is
marked "not named in Q182 or Q183" in the record, not filed under the acceptance. Examples: a factory mount
of hub data, another agent on .54, a provider admin token. Mos takes each one to Jason.
- N2 (Duckworth, doc-only). The Q183 and Q4 quotes in section 8 are excerpts. When the R8 record quotes them,
it marks each omission with "…" and checks the full text at DECISIONS.md:1535-1541 in jason-master-restart
(read only).
Done when every entry check has its output recorded, Jason signs in to the hub and sends one message, Melanie
revokes, and his next request is refused.
+81
View File
@@ -0,0 +1,81 @@
# Brief: a pre-push hook that refuses force-pushes, and a busy check that fails on BUSY (#426)
Sage, 2026-10-20, for Mos's order mos-1078 (Vikunja P19 #426) and its go mos-1083 item 1. Owner Rocko,
reviewer Dewey, gate owner Sage. Base: refactor at this brief's commit. Starts after row 191.
## Why
Morgana force-pushed a seat branch on 2026-10-20 (morgana-0016). Mos took it to Jason. Force-push is a
Q173 exclusion, and canon says never force-push, but nothing mechanical stops one. Mos: "Done means a
force-push from a seat worktree is refused, with a test that shows it."
Earlier the same day, a busy check chained as `| tail -1 &&` passed while the slot was BUSY. The script it
called prints BUSY or CLEAR, but it exits 0 either way.
The guard has two layers:
- (a) Gitea protection with force-push off on seat branch patterns and on refactor, next and main. ops-01
sets it up, through Mos (mos-1082). That part isn't this row.
- (b) The client hook below. It can be bypassed with `--no-verify`, so (a) is the real guard and (b) is the
early refusal.
## What ships
1. `scripts/git-hooks/pre-push` (new). For each ref line git passes on stdin:
- It refuses when the remote sha isn't all zeros and isn't an ancestor of the local sha (`git
merge-base --is-ancestor`). The refusal names the ref and both shas, and the hook exits 1.
- A new branch passes. A delete (local sha all zeros) is refused.
- Any failure of the check refuses too, for example an unknown remote object. The hook fetches nothing.
If the remote tip isn't present locally, it refuses and says to fetch first.
2. Install, following `scripts/queue-commit.sh --install-hook`. Either add a `--install-push-hook` there or
ship a small `scripts/install-git-hooks.sh`, whichever fits the existing code better. Say which in BUILD.md.
- The install copies the hook into the common `.git/hooks`, which every seat worktree shares.
- It refuses when `core.hooksPath` is set (the husky trap, 2026-10-09). It refuses when an installed
pre-push differs from the repository copy, unless `--force-install` is given.
- It never edits the installed copy in place.
3. `scripts/slot-busy.sh` (new), the shared busy check.
- It prints one BUSY line per matching process, or CLEAR.
- It exits 0 on CLEAR, 1 on BUSY and 2 on its own error.
- It matches `node --test`, `gate.sh`, `mutant`, `scripts/test-*.sh` and `docker compose` command lines.
It skips its own process and shell snapshots.
- It reads `/proc/<pid>/cmdline` and the cwd link only, never file contents under another seat's
directory.
- Usage in `docs/TOOLS.md`: `bash scripts/slot-busy.sh > LOG && gate`, with `set -o pipefail` in any
script that pipes it. Never `| tail -1 &&`.
## Tests
Add them to `scripts/test-land-work.sh` or a new `scripts/test-git-guard.sh` (say which). Run against a scratch
bare remote and a scratch clone with a worktree, never against origin:
- A rewritten branch pushed with `--force` from a worktree is refused, and the remote ref is unchanged.
- A `+refspec` push is refused.
- A fast-forward push passes.
- A new branch passes.
- A delete is refused.
- A remote tip that's missing locally is refused.
- The install refuses with `core.hooksPath` set, and refuses a differing installed hook.
- `slot-busy.sh` exits 1 with a fake `node --test` process running (a sleeping stand-in with that argv),
and 0 without one.
- `slot-busy.sh | tail -1 && false` doesn't apply here. The test runs `bash scripts/slot-busy.sh >/dev/null
&& echo ran`, and asserts it doesn't print `ran` while BUSY.
Mutants, each killed:
- drop the ancestor check
- treat an unknown remote object as passing
- let a delete pass
- install over a set `core.hooksPath`
- `slot-busy.sh` exits 0 on BUSY
## Boundaries
- Install the hook in the canonical checkout only after landing, as its own step with Sage. Tests use
scratch repositories.
- No push to origin from a test. No change to Gitea config. That's ops-01's.
- Don't touch `~/.mosaic`, `~/.mosaic-dev` or `~/.config/mosaic-dev`.
## Gate
Dewey approves the manifest. The new or changed `scripts/test-*.sh` file and every other `scripts/test-*.sh`
are green on Sage's gate, and the mutants are killed. Done when the hook is installed in the canonical
checkout and a scripted force-push from a seat worktree to a scratch remote is refused there. ops-01's
server proof (a refused force-push, plus a fast-forward push and a new branch that go through) is recorded
in Vikunja #426 by Mos.
+70
View File
@@ -0,0 +1,70 @@
# Brief: agent host access with sudo through the credential store (#424)
Sage, 2026-10-20, for Jason's rulings Q183 and Q184 relayed by Mos (mos-1056 item 3, mos-1067). Gitea #1707, Vikunja P19
#424, due 2026-10-27 (Q184: "Q5: by 10.27"). Owner Gizmoduck (design and build), reviewer Duckworth, gate owner
Sage. Chained on row 199's candidate (Q177): row 199 design r6 (4d3007ec9 on `megavolt/row199`, approved in
duckworth-0027) is not reopened. This row lands after 199 and rebases onto its landing.
## What Jason asked for
Q183, through Mos: "I envision a credential store within Mosaic Stack that works as some sort of variable for
the agent to call. The system will enforce access to the credentials to keep the agents sandboxed with proper
security... If we can utilize the credential store in Mosaic Stack and allow agents to perform their jobs with
NOPASSWD sudo on various machines, I am a happy man." Q183 also says not every agent needs that access.
Mos's requirements (mos-1067 item 1):
- An agent asks for a host credential by name.
- The stack checks a grant for that agent and that host, and does the ssh and sudo for it.
- The agent never sees the key or any secret value.
- Only agents with a grant can do it.
The PRD text is REQ-CRED-3, drafted by Honker for PRD 1.7 (sage-1422). If it misses 1.7 it goes into 1.8, and
this row lands only after the version that holds it is approved.
## What row 199 gives and what it doesn't (megavolt-0019)
It builds on 199 as 199 stands: the store layout under `<configDir>/secrets/agents/<business>/<agent>/`, the
lifecycle verbs and their `changes.jsonl` receipts, `useFor(session, service, fn)` with the D5 allowlist test,
`assertClean` on every result, and the grant shape of `publish` (a list on the role entry, checked by the
broker, plus a hard-wait action that isn't routed).
This row's own changes:
- A host kind in the closed kind set (vocabulary.mjs `SERVICES`, credentials.mjs, the broker's service check,
the CLI date flag, the remove regex), with a naming rule for one credential per host or per agent.
- A per-kind value check. An OpenSSH private key fails today's `TOKEN` pattern.
- Handing the key to ssh without the agent seeing it: an ssh-agent on a private socket, or a 0600 temp file
removed on every path, the way publish's one-shot helper works.
- Pinned host keys per host, and no trust on first use.
- The command boundary: what an agent may run under sudo, and how output returns without echoing the secret.
This is the main design work.
- The executor: a ticket and a host op running in the bus child, the same split row 199 uses for publish
(Megavolt's deviation in 199's BUILD.md).
## Steps
1. DESIGN.md first, in `agents/gizmoduck/work/queue-ROW/`. It answers each item above, names the grant shape
(`roles.<agent>.hosts` and a `host.sudo` action, or a reasoned alternative), says who grants (Jason, through
a reviewed role change, unless REQ-CRED-3 says otherwise), and lists tests and mutants. Duckworth approves it
before code. Sage reads it too, because it gives agents sudo on hosts.
2. Build on 199's candidate. Single test files run freely. Suites and mutant batches need Sage's slot.
3. The live part is not this row. Putting a key in the store and granting a host is a separate window with
Jason or Mos, after landing.
## Hosts
The first targets are `localadmin` with NOPASSWD sudo on 10.1.1.53 and 10.1.1.54 (#423 step 1, applied by
ops-01 at 20:05:47Z), and the servers agents already reach as localadmin (Q172). ops-01 owns the host side.
Tests use scratch sshd fixtures on 127.0.0.1 or fakes, never a real host.
## Boundaries
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files, the live units or `~/.mosaic`. No key is
read, copied or printed. No ssh to any real host from a test or a seat.
- No widening of row 199's kinds or regex inside row 199. Both rows touch them, so this row rebases after 199
lands.
## Gate
Duckworth approves DESIGN.md, then the manifest. The bus, business and cli suites and every
`scripts/test-*.sh` are green on Sage's gate, and the mutants are killed (one per refusal, plus one that leaks
the key into the agent's output and one that skips the host key pin).
+66
View File
@@ -0,0 +1,66 @@
# Brief: the Case 1 install on 10.1.1.53 (row 195)
Sage, 2026-10-20, third version, for Darkwing's rewrite request and sage-1439. Gitea #1693.
Why it changed:
- The first two versions put Melanie's hub on its own Linux user, then on its own host, Jason's choice under
#419.
- Q181 settled the hosts: Case 1 (Jason's software factory) runs on 10.1.1.53 and Case 2 (Melanie's hub) on
10.1.1.54.
- Q182 and Q183 made the hub's privilege checks a recorded acceptance. They report and don't refuse
(federation R8, mos-1055, mos-1063, mos-1066).
- The Case 2 install on .54 is Gosalyn's C2-13 plus its window (#416, due by 2026-10-27). It is not this row.
This row now installs Case 1 on .53. On the 10-27 MVP path (mos-0974, T1 item 1).
Owner Darkwing (Opus 5.5), reviewer Duckworth, gate owner Sage. Chained on row 190's candidate (the config
canon) and on C2-13's candidate (the install script), Q177. It reuses C2-13's `case2-install.sh`,
`case2-precheck.sh` and `case2-unit.sh` by input, not by copy. A gap in them goes to Gosalyn as a C2-13
change, or into this row's manifest as a named, reviewed change to those files. Design first:
`agents/darkwing/work/queue-195/DESIGN.md`.
## Goal
Jason's software factory runs on 10.1.1.53 as `mosaic` under `/srv/mosaic`, in the same unit form as Case 2
(design 9, Q181). It uses row 190's config canon and the model account the factory uses today (Q185,
mos-1081 item 1). No new provider account.
## Provision
- ops-01 runs the root steps over ssh as `localadmin`: `useradd mosaic`, `/srv/mosaic` and the unit through
`case2-unit.sh` from a root-owned checkout. #423 step 1 (localadmin with NOPASSWD sudo on .53 and .54) was
applied at 20:05:47Z (mos-1066, mos-1077), and nothing else in #423 gates this row.
- #423 step 4 (jarvis sudo) waits until Case 1 is live on .53.
- The host list comes from Q181. Agents may run read-only checks on .53 as `localadmin`.
## The design must state
1. The install as input values to C2-13's scripts: user, home, business file, roles, the human id, the
token slot path. Which commands run as `mosaic` through `sudo -u mosaic -H`, and which as root through
ops-01.
2. Where Case 1 differs from Case 2: the factory roles in place of the library business, no library sync,
and any precheck item that doesn't apply, each with a reason.
3. Model access through the existing factory account (Q185). Placing the credential on .53 is a step under
the Q173 secrets rule, done in the window. It is never copied by the script, and its value is never
printed.
4. Re-pin, rollback and backup on .53, and who runs them.
5. The acceptance checks, as code Duckworth can read. Each check names a path and gives it one of three
marks: closed, accepted under Q182 or Q183, or "not named in Q182 or Q183". An accepted path doesn't
fail the row. An unnamed one is reported to Mos, who takes it to Jason (the R8 N1 rule). The functional
checks fail the row when they fail: the unit runs as `mosaic`, the Console answers on its port on the
internal network only, and one tester message gets a reply.
## Boundaries
- Infra and k3s changes, exposure outside the internal network, and spend stay Jason's (Q173 exclusions).
- No change to dragon-lin's `~/.mosaic-dev`, `~/.config/mosaic-dev`, its live units or `~/.mosaic`.
- No credential value is printed, logged or committed.
- Tests use scratch roots and stand-ins. Nothing in a test reaches .53.
- The run on .53 is a window with Jason's or Mos's go, a backup first where state exists, and a STATE report
after.
## Gate
Duckworth approves DESIGN.md, then the manifest. Any repository change is green on Sage's gate (cli suite and
every `scripts/test-*.sh`), with the design's mutants killed. The row is done when the window's acceptance
run passes on .53, Duckworth reads its output, and Mos confirms.
+62
View File
@@ -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.
@@ -0,0 +1,70 @@
# Row 193 build rows: 193a, 193b1, 193b2, 193c and 193d (#1691)
Sage, 2026-10-20, for Mos's go mos-1034 item 4. Row 193's design is Morgana's
`agents/morgana/work/queue-193/DESIGN.md` at e2d161e2e on `morgana/row193` (r5, sha256
dd7fcb419ed8510e9d8d74b569183386d0942af8b68eecc0bbb5e182e677b523), approved by Duckworth (duckworth-0018).
Section 8 lists the tests and mutants, and section 9 the split. The brief behind them is
`docs/plans/2026-10-20_human-users.md`. Vikunja P19 #414/#415, Gitea #1691.
Owner Morgana for every row, reviewer Duckworth, gate owner Sage. Each row chains on its parent's candidate
(Q177) and rebases onto the parent's landing. Seats push only their own `<seat>/*` branches (mos-0981).
Single test files run freely, and suites and mutant batches need Sage's slot.
Boundaries for every row: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files, any
live access.json or `~/.mosaic`. No request to the live Console, mosaic.woltje.com or auth.diversecanvas.com.
No user thread goes live before 193b1's message filter has landed (mos-1021 B2, FED R8).
The owner-visible records are fixed by Mos (mos-1023 item 2) and named in one user sentence, which FED R6
reuses word for word: "The business owner sees your thread's agent's decisions, inbox entries, claims, tasks
and project files, but not your messages."
## Brief: 193a, the access level check widened to viewer
Chained on FED R1. Started early on `morgana/row193a` (508ee4d61) against R1's interface as the FED brief
names it, with Sage's go (sage-1373). R1's level check accepts `viewer` and still refuses any other new
level. Tests in `packages/business/tests/` (section 8, Business), with the four mutants in Morgana's
BUILD.md. FED R4 chains on this row.
Duckworth's N1 on R1 (duckworth-0033, sage-1417): with viewer added, `covers` called with no level would let a
viewer grant cover a collaborator ask. This row makes `level` required in `covers`, with a test and a mutant that
makes it optional again. R3 and R4 call `covers` with a level.
Gate: Duckworth approves the manifest. The business suite and every `scripts/test-*.sh` are green on Sage's
gate, and the mutants are killed.
## Brief: 193b1, viewer verbs, board refusals and the message filter
Chained on FED R3, FED R4 and row 196's verifier. The bus gets the viewer verb list, per-grant level
checks, board reads refused to users, and the section 5.7 `trail` and `replies` filter with `#readable`
owner-only. No schema change. The governance pin test names each item in the user sentence above. Bus tests
and mutants 1, 3, 6, 10, 11 and 12 from section 8.
Gate: Duckworth approves the manifest. The bus suite and every `scripts/test-*.sh` are green on Sage's gate,
and the mutants are killed.
## Brief: 193b2, thread shares (schema 3j)
Chained on 193b1. `thread_shares`, the share verbs, the share rules of `#readable` and `#threadRead`, and the
section 5.5 share filters. Schema 3i to 3j. The table is append-only, and row 181's restore guard and
`COUNTED` include it. Bus tests for shares and the migration, and mutants 2, 4, 7, 8 and 9 from section 8.
Going live needs a `--migrate` window with Jason. Sage takes it to Mos when this row is near.
Gate: Duckworth approves the manifest. The bus suite, the cli suite and every `scripts/test-*.sh` are green on
Sage's gate, and the mutants are killed.
## Brief: 193c, mosaic access user verbs and --level viewer
Chained on FED R5. `mosaic access user add/remove` and `grant --level viewer`, only where R5 lacks them. They
refuse tester and agent callers, write a receipt line and call `access.reload` (section 8, CLI). If R5
already has all of them, this row records that and closes with no code.
Gate: Duckworth approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate,
and the mutants are killed.
## Brief: 193d, the Console's shared list, share controls and viewer read-only
Chained on FED R6, row 192 and 193b2 (for share controls). A viewer has no composer or share control, and
with each UI check removed the bus still refuses (section 8, Webui).
Gate: Duckworth approves the manifest. The webui suite and every `scripts/test-*.sh` are green on Sage's
gate, and the mutants are killed.
+4 -2
View File
@@ -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)
@@ -0,0 +1,57 @@
# Brief: retire the control board writer on 7331
Sage, 2026-10-20, for Mos's ruling mos-1008 item 2. Gitea #1696. MVP 10-27 work. Owner Bushroot (Haiku 5.5, a
low-level model under Q177), reviewer Webby, gate owner Sage. Base: refactor at the brief commit.
## Why
`packages/control-board/src/serve.mjs` is a loopback HTTP service on 7331 with no authentication. It reads
transcripts and writes through `/api/reply`. Any host process that reaches the port can do both. That fails
Q175 ("zero trust with verification"). Gander found it in the row 196 inventory (gander-0004). Nothing listens
on 7331 now (`ss -ltnp`, 2026-10-20T19:05:09Z), so retiring it costs the least.
## Steps, in order
1. Inventory first. List every reference to the control board and to 7331: scripts, units, `scripts/mosaic`
and other CLI dispatch, docs (AGENTS.md, READMEs, TOOLS.md), package.json scripts, tests, and the Console
(`packages/webui`). For each, say whether it starts the writer, reads its code, or only mentions it.
Commit the list as `agents/bushroot/work/queue-ROW/INVENTORY.md` and send it to Sage and Webby.
2. Stop if something depends on it. If a reference shows a live dependency (the Console or another package
imports code that only the control board has, or a unit or script still starts it), stop. Sage takes it to
Mos before anything is removed.
3. Otherwise retire the writer. Either remove the `serve` entry point, or make it refuse to start with a
message that points to the Console. Keep any read-only code another package imports, and say which.
Update the docs that tell people to start it. The AGENTS.md text about the control-board reply convention
is a separate matter. Change it only if it tells someone to start 7331.
## Mos's call on the Console dependency (mos-1019)
The inventory (004f133bd) found that the Console proxies `/api/board`, `/api/seen`, `/api/reply` and the
conversation routes to `DEFAULT_BOARD` on 7331 (`packages/webui/src/serve.mjs:12`), so the board view returns
502 today. Mos took Sage's recommendation: retire the 7331 listener and its README start commands, and have
the Console call the board's read code in-process, behind the Console's own auth (Q175 SSO on 7332).
`/api/reply` stays only under `--send`. The DESIGN.md for that change comes before code, and Webby approves
it.
## Acceptance
Nothing in the stack can start a writer on 7331. A test proves it: the entry point refuses (or is gone), no
script or unit starts it, and a grep check finds no starter. Each change gets a mutant that brings the writer
back and the test kills it. Mos added three items (mos-1019):
- The board view answers from the Console with nothing listening on 7331.
- Every board route sits behind the Console's auth.
- A test proves `/api/reply` refuses without `--send`.
Test fixtures that bind port 0 may stay if they only test the code in-process.
## Boundaries
- No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files or `~/.mosaic`.
- No request to any live port.
- Single test files run freely. Suites need Sage's slot.
## Gate
Webby approves INVENTORY.md, then DESIGN.md, then the manifest. The control-board and webui package suites, the cli suite and
every `scripts/test-*.sh` are green on Sage's gate, and the mutants are killed.
+42
View File
@@ -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.
@@ -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).
+23 -3
View File
@@ -33,7 +33,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
<!-- mosaic-queue:begin -->
Generated from `docs/plans/queue.json` revision 2247 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
Generated from `docs/plans/queue.json` revision 2361 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|---|---|---|---|---|---|---|---|---|
@@ -225,9 +225,29 @@ Generated from `docs/plans/queue.json` revision 2247 by `queue render`. Do not e
| 186 | stack-release --plan: the backup line says what the run will do (P19 404, row 185 N1/N2) | darkwing; reviewers filbert | #1684 | done | — | Filbert approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the three brief mutants killed (sage) | `docs/plans/2026-10-20_plan-backup-line.md` § Brief: the activate plan's backup line says what the run will do | Landed 269134d8d (author Darkwing) on 3c5da3a91. Patch-id c97b9f6e25cd matches the r1 candidate (manifest 449e9121, Filbert approve 29419). Sage gate on b367f5730+patch 09:00:20Z-09:08:59Z: node 2681/0, scripts 422/0, queue 156, network removed; 3c5da3a91 adds only a SESSIONS line. Not live: the stack runs copy 490e2771d3d0. Follow-ups on P19 404: Filbert M6 gap, store-unclean reading. |
| 187 | stack-release activate carries the row 178 proof pair (mos-0928 Part B A0) | darkwing; reviewers filbert | #1685 | done | 186 done | Filbert approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the brief mutants killed (sage) | `docs/plans/2026-10-20_activate-proof-pair.md` § Brief: activate carries the row 178 proof pair | Landed 9a71e4e80 (author Darkwing) on d17328a55; patch-id 6b57c47bc661 matches candidate 52a5ecc1d143 and Filbert's r1 reading. Sage gate passed 10:21:55Z: cli 446/446 and every package and scripts/test-*.sh green. Filbert approve 29424, rev 2204; follow-ups N1-N4 filed on P19. |
| 188 | stack-release tests: a carried proof pair and a proof file inside the repository (P19 409, 410; gates Part B) | darkwing; reviewers filbert | #1686 | done | 187 settled | Filbert approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the brief mutants killed (sage) | `docs/plans/2026-10-20_proof-test-gaps.md` § Brief: tests for the two proof-file paths row 187 left open | Landed 8fc0ee216 (author Darkwing) on 23a609c5c; patch-id c9a56b167c07 matches candidate 86aa549bad75 and Filbert's r1 reading; stack-release.sh blob 2586a2a37cd2 unchanged. Sage gate passed 10:58Z: cli 453/453 and every package and scripts/test-*.sh green. Filbert approve 29433, rev 2212; N1 (REPO logical path) to P19 after Part B (mos-0959). |
| 189 | stack-release activate --no-send drops the Console's --send and keeps the pointer in step (Q175 interim, mos-0972, P19 413) | sage; reviewers dewey | #1687 | in-review, round 1, request posted | 163 done | Dewey approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the brief mutants killed (sage) | `docs/plans/2026-10-20_activate-no-send.md` § Brief: activate --no-send drops the Console's --send | — |
| 190 | several Mosaic Stack instances on one host: per-instance config, data root, units and ports (MVP T1.1, mos-0974, Vikunja #414) | darkwing; reviewers filbert | #1688 | in-progress | — | Filbert approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the design mutants killed (sage) | `docs/plans/2026-10-20_multi-instance.md` § Brief: several Mosaic Stack instances on one host | — |
| 189 | stack-release activate --no-send drops the Console's --send and keeps the pointer in step (Q175 interim, mos-0972, P19 413) | sage; reviewers dewey | #1687 | done | 163 done | Dewey approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the brief mutants killed (sage) | `docs/plans/2026-10-20_activate-no-send.md` § Brief: activate --no-send drops the Console's --send | Landed ace27dc5e on refactor (cherry-pick of candidate d6116771f onto a4a42dbac). Gate GATE_PASSED on 0c177e9ef (base 0217b71c8), 2026-10-20T19:09:37Z-19:20:36Z, 25 suites rc=0. patch-id ce89378718f6 for both 0c177e9ef and ace27dc5e; range-diff differs only in the message. Landed manifest hashes to 67af69bd (Dewey approve 29485). Base drift 0217b71c8..a4a42dbac: 7 doc/README files, none on manifest paths, no test reads them. Brief re-pinned to blob 29a9e6d2d. |
| 190 | several Mosaic Stack instances on one host: per-instance config, data root, units and ports (MVP T1.1, mos-0974, Vikunja #414) | darkwing; reviewers filbert | #1688 | in-review, round 1, request posted | — | Filbert approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the design mutants killed (sage) | `docs/plans/2026-10-20_multi-instance.md` § Brief: several Mosaic Stack instances on one host | — |
| 191 | one kill switch for the bus, every run, Discord and new launches, from the CLI and the Console (MVP T1.4, mos-0974, Vikunja #414) | rocko; reviewers dewey | #1689 | in-progress | — | Dewey approves DESIGN.md then the manifest; cli, bus, discord and webui suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_kill-switch.md` § Brief: one kill switch for the bus, every run, Discord and new launches | — |
| 192 | Q175 phase 1: the Console verifies SSO itself (--sso), OIDC RP with oauth4webapi 3.8.8, no live change (MVP T1.2, #412) | launchpad; reviewers webby | #1690 | in-review, round 2, request posted | — | Webby approves the manifest; webui and cli suites, scripts/test-config.sh and every scripts/test-*.sh green on Sage's gate; design section 9 mutants killed (sage) | `docs/plans/2026-10-20_sso-phase1.md` § Brief: Q175 phase 1, the Console verifies SSO itself (--sso) | — |
| 193 | many humans per business, human roles, private and shared threads, one access.json store (MVP T1.3, Vikunja #414/#415) | morgana; reviewers duckworth | #1691 | in-progress | — | Duckworth approves DESIGN.md then the manifest; bus, business, cli and webui suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users.md` § Brief: many humans per business, human roles, private and shared threads | — |
| 194 | agent lifecycle from the Console and the CLI (create, launch, terminate, regenerate, delete) with a per-agent hire/fire veto (MVP T1.5, Vikunja #414/#415) | honker; reviewers webby | #1692 | in-progress | — | Webby approves DESIGN.md then the manifest; bus, cli, business and webui suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_agent-lifecycle.md` § Brief: agent lifecycle from the Console and the CLI, with a per-agent hire/fire veto | — |
| 195 | row 195: the Case 1 install on 10.1.1.53 as mosaic under /srv/mosaic, reusing C2-13's scripts by input under row 190's canon (Q181, Q182/Q183, Q185; ops-01 root steps under #423 step 1) | darkwing; reviewers duckworth | #1693 | briefed | 190 done | Duckworth approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_hub-own-uid.md` § Brief: the Case 1 install on 10.1.1.53 (row 195) | sage-1439: rewritten to Case 1 on .53 (f24f2cd40). Chains on C2-13's candidate too once C2-13 is queued. Darkwing starts DESIGN.md after row 190's mutants. |
| 196 | the human boundary across uids and humans: Console, tester listener, broker.sock and the path-bound human proof (P19 333, before federation R3, mos-0989) | gander; reviewers duckworth | #1694 | in-progress | — | Duckworth approves DESIGN.md then the manifest; bus, webui and cli suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-boundary.md` § Brief: the human boundary across uids and humans (P19 333) | — |
| 197 | a second gate lane: two full gates at once on dragon-lin, design first (mos-0977 item 6) | herb; reviewers webby | #1695 | in-progress | — | Webby approves DESIGN.md then the manifest; cli suite and every scripts/test-*.sh green on Sage's gate, singly and in the two-lane proof (sage) | `docs/plans/2026-10-20_second-gate-lane.md` § Brief: a second gate lane, two full gates at once on dragon-lin | Order with row 198 (Sage, mos-1040 N1): 197 lands first. After B1 the shared tests are packages/webui/tests/serve.test.mjs:100 and preview.test.mjs:208. Row 198 rebases onto 197's landing before its manifest review. |
| 198 | retire the control board writer on 7331 (unauthenticated loopback writer, Q175; mos-1008) | bushroot; reviewers webby | #1696 | in-progress | 192 done, 197 done | Webby approves INVENTORY.md then the manifest; control-board, webui and cli suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_retire-control-board.md` § Brief: retire the control board writer on 7331 | Order with row 197 (Sage, mos-1040 N1): 197 lands first. Row 198 rebases onto 197's landing before its manifest review; shared tests are packages/webui/tests/serve.test.mjs:100 and preview.test.mjs:208. |
| 199 | MVP T1 item 6: a credential store per agent, own git identity, pushes to a remote (G11, G13; mos-0974) | megavolt; reviewers duckworth | #1697 | in-progress | — | Duckworth approves DESIGN.md; Mos rules on the authority model; Duckworth approves the manifest; bus, business, cli and tasks suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: a credential store per agent, own git identity, pushes to a remote | — |
| 200 | stack-release.sh activate --sso for the Q175 phase 2 window (row 192 F3; chained on 192) | launchpad; reviewers webby | #1698 | in-review, round 2, request posted | — | Webby approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_stack-release-sso.md` § Brief: stack-release.sh activates a Console with --sso (Q175 phase 2 prerequisite) | — |
| 201 | FED R1: access.json loader and validator, and covers (#414) | gizmoduck; reviewers duckworth | #1699 | in-review, round 1, request posted | — | Duckworth approves the manifest; business suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R1, the access.json loader and validator | — |
| 202 | FED R5: mosaic access list, grant and revoke (#414) | quackerjack; reviewers duckworth | #1700 | in-review, round 1, request posted | — | Duckworth approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R5, mosaic access list, grant and revoke | Chained on row 201's candidate (Q177), not its done: claimable now, lands after 201 and rebases onto its landing (Sage) |
| 203 | FED R2: the federation key in the system config (#414) | quackerjack; reviewers webby | #1701 | briefed | — | Webby approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R2, the federation key in the system config | Chained on row 192's candidate (Q177): starts on 192's candidate, lands after 192 (Sage) |
| 204 | 193a: access level check widened to viewer (#1691) | morgana; reviewers duckworth | #1702 | in-review, round 1, request posted | — | Duckworth approves the manifest; business suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193a, the access level check widened to viewer | Chained on row 201's candidate (Q177), not its done: claimable now, lands after 201 and rebases onto its landing (Sage) |
| 205 | 193b1: viewer verbs, board refusals and the message filter (#1691; also after FED R3 and R4 when filed) | morgana; reviewers duckworth | #1703 | queued | 196 done | Duckworth approves the manifest; bus suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193b1, viewer verbs, board refusals and the message filter | — |
| 206 | 193b2: thread shares, schema 3j (#1691; live needs a --migrate window with Jason) | morgana; reviewers duckworth | #1704 | queued | 205 done | Duckworth approves the manifest; bus suite, cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193b2, thread shares (schema 3j) | — |
| 207 | 193c: mosaic access user verbs and --level viewer (#1691) | morgana; reviewers duckworth | #1705 | queued | 202 done | Duckworth approves the manifest; cli suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193c, mosaic access user verbs and --level viewer | — |
| 208 | 193d: Console shared list, share controls and viewer read-only (#1691; also after FED R6 when filed) | morgana; reviewers duckworth | #1706 | queued | 192 done, 206 done | Duckworth approves the manifest; webui suite and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193d, the Console's shared list, share controls and viewer read-only | — |
| 209 | FED R8: the hub window on 10.1.1.54, a recorded acceptance (#414, r4.11 b93f098f0; live window run by Jason and Mos, after the Case 2 install #416, Bean and Chet live and both Hermes gateways stopped) | sage; reviewers duckworth | — | queued | — | Every entry check's output is in the R8 record with who ran it; Jason signs in to the hub and sends one message, Melanie revokes, his next request is refused; Duckworth checks the record (sage) | `docs/plans/2026-10-20_federation-r1-r2-r5.md` § Brief: FED R8, the hub window on 10.1.1.54 | Stays queued until its entry gates near: Case 2 install on .54 (#416, by 2026-10-27), Bean and Chet live, both Hermes gateways on .54 stopped, R7, #412 phase 2, #419 answer. Filed per mos-1073 after duckworth-0034 (Sage) |
| 210 | Agent host access with sudo through the credential store: grant per agent and host, the stack does ssh and sudo, the agent never sees the key (#1707, Vikunja #424, Q183/Q184, due 10-27; chained on row 199's candidate) | gizmoduck; reviewers duckworth | #1707 | in-progress | — | Duckworth approves DESIGN.md, then the manifest; bus, business, cli suites and every scripts/test-*.sh green on Sage's gate; the mutants killed (sage) | `docs/plans/2026-10-20_host-credentials.md` § Brief: agent host access with sudo through the credential store (#424) | Design runs before 199 lands (Q177 chain on 199's candidate). after cleared so Gizmoduck can claim; lands only after 199 and rebases onto it. |
| 211 | Force-push guard: scripts/git-hooks/pre-push refuses non-fast-forward and delete pushes from every worktree, and scripts/slot-busy.sh exits 1 on BUSY (#1709, Vikunja #426, mos-1078/1083) | rocko; reviewers dewey | #1709 | briefed | — | Dewey approves the manifest; the new or changed scripts/test-*.sh and every other scripts/test-*.sh green on Sage's gate; the mutants killed; hook installed in the canonical checkout with a scripted refused force-push (sage) | `docs/plans/2026-10-20_force-push-guard.md` § Brief: a pre-push hook that refuses force-pushes, and a busy check that fails on BUSY (#426) | — |
<!-- mosaic-queue:end -->
+4263 -14
View File
File diff suppressed because it is too large Load Diff
+112 -5
View File
@@ -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/<activation>/ 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
+38 -6
View File
@@ -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 ;;