gosalyn: row 226 C2-13b design d2, release package and activate dropped, precheck split into pre-start checks and --library-status after the bus is up, Webby's N1-N6 taken in, design 10 text change listed (webby-0085, webby-0086, sage-2189, #1727)

This commit is contained in:
jason.woltje committed 2026-10-23 13:50:45 -05:00
1 parent bfed0aa6c7
commit 893b4fd250
1 file changed
+134 -73
+134 -73
View File
@@ -4,8 +4,9 @@ Brief: docs/plans/2026-10-20_case2-rows.md "C2-13b install script and library pr
d37210288). Design: agents/gosalyn/work/case2/DESIGN.md r11 sections 4 and 10, on gosalyn/case2-design
75fb3e10e. Row 195's needs: agents/darkwing/work/queue-195/DESIGN.md "What row 195 needs from row 226".
This is design round d1. Nothing is built yet. Webby reviews the design before I write code, as for row
252. D1 to D9 below are my choices, and Q1 to Q3 need a ruling.
This is design round d2. Nothing is built yet. d1 was f46e66883, and Webby's review of it is webby-0085
(CHANGES, B1 and N1-N6) and webby-0086 (the ordering problem). Sage ruled on both in sage-2189. The
changes from d1 are listed at the end.
## Base
@@ -20,28 +21,36 @@ Branch gosalyn/c2-13b from refactor d37210288. Rows 215, 221, 222 and 230 are do
- The library is a project, `projects.<id>.kind === "library"`, checked by `checkMapping`
(packages/library/src/mapping.mjs). There is no top-level `library` key.
- `library.sync` and `library.status` are bus verbs, human only (broker.mjs:92, :2452-2476). The only
client is `packages/bus/src/human-cli.mjs` over the bus socket, so they need a running bus. The
library package exports `sync(context, { slot })` (sync.mjs:92) and `libraryStatus(context)`
(mirror.mjs:236), which take the lock the bus takes (`withLibraryLock`, mirror.mjs:145).
- The token slot is `<configDir>/secrets/<business>/library-gitea`, a regular file owned by the uid with
no group or other bit (sync.mjs:17, :51-52).
client is `packages/bus/src/human-cli.mjs` over the bus socket.
- The bus makes each library mirror at start, `ensureMirror({ business, project, dataRoot })`
(packages/bus/src/runtime.mjs:143-145). Before the bus first starts there is no mirror.
- `sync()` fetches outside `withLibraryLock`. Only a per-process `running` set keeps two syncs apart
(packages/library/src/sync.mjs:97-98), so a second process calling `sync()` would race the bus's
timer.
- `libraryStatus(context)` reads the recorded state under the flock and writes nothing
(mirror.mjs:232-239). A missing or unsafe mirror fails `library-git` (`ownedRoot`, mirror.mjs:43-49).
A mirror that has never synced returns the empty state, `lastFetch: null`, `lastError: null`,
`problems: []` (mirror.mjs:27, :205-210). `lastError` is `{ code, at }` with `code` matching
`^[a-z-]{1,64}$` (mirror.mjs:225).
- The bus builds the library context as `{ business, project, dataRoot }` and the slot as
`<slotDir>/secrets/<business.id>/library-gitea` (runtime.mjs:146-151, :162-164).
- `case2-persona-import.mjs install --staging DIR --config-dir DIR --business ID` writes
`<configDir>/personas/<business>/<instance>.md` at 0600 in 0700 dirs, never overwriting, and prints
`instance bean: persona.name Bean, persona.sha256 <hex>` per persona.
- A Discord binding is `<dataRoot>/discord/<name>.json` (packages/discord/src/binding.mjs:70-83).
- `case2-unit.sh --copy ID` needs `<home>/.local/share/mosaic-stack/copies/<ID>`, which
`stack-copy.sh copy COMMIT --copies DIR` makes.
`stack-copy.sh copy COMMIT --copies DIR` makes. The copy runs `npm ci` from package-lock.json
(stack-copy.sh:70), which installs the pinned pi.
## Design
### D1. The business file is an input
`case2-install.sh` takes `--business-file FILE`, a JSON business document written for the host from
Jason's go: human id, arbiters, tracker, roles, the library project with its mapping, and any
specialists she picks. The script doesn't build one from flags. Bots, botIds and credential refs are host
values, and the brief says every host value is an input. A flag per field would copy business.mjs's
schema into bash. The script checks the input, adds what only it knows, writes it, and then runs
`mosaic business validate`, so business.mjs stays the only schema.
Unchanged from d1. `case2-install.sh` takes `--business-file FILE`, a JSON business document written for
the host from Jason's go. The script doesn't build one from flags, because bots, botIds and credential
refs are host values and the brief says every host value is an input. It checks the input, adds what
only it knows, writes it, and then runs `mosaic business validate`, so business.mjs stays the only
schema.
What the script checks before it writes anything, beyond `validate`:
- `id` equals `--business`.
@@ -54,36 +63,44 @@ What the script checks before it writes anything, beyond `validate`:
1. Refuse to run as root.
2. `scripts/bootstrap.sh`. It's idempotent and never rewrites an existing config.
3. `scripts/release.sh package`, then `scripts/release.sh activate` (design 10 step 2b).
4. `scripts/stack-copy.sh copy <--commit> --copies <home>/.local/share/mosaic-stack/copies`. The copy
3. `scripts/stack-copy.sh copy <--commit> --copies <home>/.local/share/mosaic-stack/copies`. The copy
`case2-unit.sh --copy` names has to exist before root renders the units (row 195 need 3).
5. `library` only: the persona import's `install` step from `--personas DIR` (D3).
6. Write `<configDir>/businesses/<id>.json` at 0600 with O_EXCL (D4).
7. `library` only: make `<configDir>/secrets/<business>/` at 0700, empty. The token is Jason's, step 4.
8. `mosaic business validate <id>`. If it refuses, remove the file step 6 wrote, then refuse.
9. Print the copy id and the next steps, which are root's 2c and Jason's 2d, 3 and 4.
4. `library` only: the persona import's `install` step from `--personas DIR` (D3).
5. Write `<configDir>/businesses/<id>.json` at 0600 with O_EXCL (D4).
6. `library` only: make `<configDir>/secrets/<business>/` at 0700, empty. The token is Jason's, design 10
step 4.
7. `mosaic business validate <id>`. If it refuses and step 5 created the file in this run, remove that
file, then refuse. A file that already matched on a re-run (D4) is never removed (N4).
8. Print the copy id and the next steps: root's 2c, Jason's 2d, 3 and 4, then the `--library-status`
check (D6b).
d1's step 3, `release.sh package` and `activate`, is gone (Q4a, webby-0085 B1, sage-2189). Package is a
`docker compose build`, and activate runs a pi Compose health check that mounts `PI_AUTH_FILE`
(compose.yaml:43). Neither fits 2a, which has no Docker and no pi credential, and the hub doesn't run
containers. The pinned harness comes from the copy's `npm ci` (Q3). The `CASE2_RELEASE` seam goes with
it.
It starts no unit, writes no unit file, doesn't write `/etc/mosaic-stack/dev.args` and doesn't run
`adopt`. These are my answers to row 195's needs 4 and 5 (gosalyn-0087), unchanged.
`adopt`. These are my answers to row 195's needs 4 and 5 (gosalyn-0087), unchanged. Row 195's W8 now
passes `--business-file` and has no release step.
### D3. Personas come from the staging dir
`library` requires `--personas DIR`, the staging dir from `case2-persona-import.mjs stage`. The install
runs `case2-persona-import.mjs install --staging DIR --config-dir <configDir> --business <id>` and reads
its two `instance` lines into `roles.bean.vars` and `roles.chet.vars`. Only those two lines go into the
business file. They hold a name and a digest, never text.
Unchanged from d1. `library` requires `--personas DIR`, the staging dir from `case2-persona-import.mjs
stage`. The install runs `case2-persona-import.mjs install --staging DIR --config-dir <configDir>
--business <id>` and reads its two `instance` lines into `roles.bean.vars` and `roles.chet.vars`. Only
those two lines go into the business file. They hold a name and a digest, never text.
This moves the persona import's stage part of design 10 step 6 ahead of 2b. That part is the read of
her Hermes profiles and Jason's check of the printed pairing. The install part stays as `<user>`, as
design 9.1 says. See Q1.
This moves the persona import's stage part of design 10 step 6 ahead of 2b: the read of her Hermes
profiles and Jason's check of the printed pairing. Webby agreed in webby-0085 (Q1).
### D4. Re-runs
Each step is safe to run again. bootstrap and the copy are idempotent. An existing business file that
matches the one the script would write passes. One that differs refuses
`business-file-differs`, and the script doesn't touch it. The persona import already refuses to
overwrite. On a re-run the install compares each existing persona file's sha256 with the staging
manifest, and refuses on a mismatch.
Unchanged from d1, plus N4. Each step is safe to run again. bootstrap and the copy are idempotent. An
existing business file that matches the one the script would write passes, and the script remembers it
didn't create it. One that differs refuses `business-file-differs`, and the script doesn't touch it.
The persona import already refuses to overwrite. On a re-run the install compares each existing
persona file's sha256 with the staging manifest, and refuses on a mismatch.
### D5. Inputs and seams
@@ -93,47 +110,75 @@ case2-install.sh --business ID --business-file FILE --commit SHA [--profile fact
```
`--home` defaults to `$HOME`, which is `/srv/mosaic` under `sudo -u mosaic -H`. `MOSAIC_CONFIG` is
honoured as bootstrap.sh honours it. Test seams follow case2-unit.sh's `CASE2_*` pattern:
- `CASE2_RELEASE` is the release script. The test uses a stub that logs its argv, because the real one
needs Docker.
- `CASE2_STACK_COPY` is the copy script. The test uses a stub that makes the copy dir.
honoured as bootstrap.sh honours it. One test seam, in case2-unit.sh's `CASE2_*` pattern:
`CASE2_STACK_COPY` is the copy script, and the test uses a stub that makes the copy dir.
Exit codes: 0 ok, 1 refused, 4 usage, as case2-unit.sh uses.
### D6. Precheck library checks
### D6. Precheck library checks, split in two (Q4b, sage-2189)
These are added to case2-precheck.sh under `--profile library` only. Each is a `refuse` line in the
script's style.
d1 ran `sync()` from the precheck. Webby's N1 and webby-0086 showed two faults. The precheck runs before
the units start, so there is no mirror yet (runtime.mjs:143-145). And a precheck `sync()` after the bus
is up would race the bus's timer, since `sync()` fetches outside the lock (sync.mjs:97-98). Sage split
the check in sage-2189, and the brief's "runs library.sync" becomes "reads the bus's recorded sync
state".
#### D6a. Before the units start, `--profile library`
These run where the precheck runs today, after 2c's render and before the start. They sync nothing and
read no mirror. Each is a `refuse` line in the script's style.
- The business file is owned by the uid, with no group or other bit (`business-file-mode`).
`businesses/.history/<id>/` is 0700 and its `changes.jsonl` is 0600, when present (`history-mode`).
- The persona dirs are 0700. Each creative role's persona file is regular, owned and 0600
(`persona-mode`), and its sha256 equals the role's `persona.sha256` (`persona-mismatch`). The output
prints names and digests, never text.
- `library.sync` calls the library package's `sync(context, { slot })` from a node block with the bus's
context, then `libraryStatus`. A sync error refuses with its code (`library-sync: <code>`). Each
R-problem prints as `ok library R<n> <path>`. The paths are her file names, and no text
prints. R-problems don't refuse, since the brief says the check lists them. See Q2.
- No `<dataRoot>/discord/*.json` exists (`discord-binding`).
The sync check needs the token from step 4. So the library precheck passes only after step 4, and the
host run sheet says so.
#### D6b. `case2-precheck.sh --business ID --library-status`, after the bus is up and after step 4
A new mode, run as `<user>` once the bus is up and Jason has placed the token. It changes nothing. A
node block builds the context the bus builds, `{ business, project, dataRoot }` for each library
project, with `dataRoot` from the config (N3, runtime.mjs:146-151, :162-164). For each one it:
- refuses `library-mirror-missing` when the mirror path doesn't exist. It checks this itself, so the
message says the bus hasn't made it, not `library-git`;
- calls `libraryStatus(context)` only, and refuses with the thrown code when that fails, for example
`library-git` for an unsafe mirror or `library-sync-busy` when the flock wait runs out;
- refuses `library-never-synced` when `lastFetch` is null;
- refuses `library-last-error <code>` when `lastError` is not null. The code is the fixed
`^[a-z-]{1,64}$` string, so it's safe to print. This also covers Webby's N2;
- prints each recorded R-problem as `problem R<n> <path>`, with the path through `JSON.stringify`, then
one `problems <count>` line (N5). Her file names are the only part that prints, never text.
R-problems don't refuse (Q2, agreed by Webby).
The bus syncs each library with a `user` every 60 s, so the first run after step 4 may need a minute,
or Jason's own `library.sync`. The run sheet says so.
### D7. host-run-sheet.md fix
`docs/guides/host-run-sheet.md:239` says "I2's install writes it" about `dev.args`. It becomes: root
`docs/guides/host-run-sheet.md:240` says "I2's install writes it" about `dev.args`. It becomes: root
writes it at the unit render, from the values the go names, and the install never writes it, because
`<user>` can't write `/etc` and root reads nothing `<user>` wrote. Sage asked in sage-1832 for this fix
to go in this build commit, with its path in the manifest. Section 9's `library-gitea` line (:270) is
"C2-13b's `library.sync` (not landed)". It becomes the precheck's `library-sync` line.
to go in this build commit, with its path in the manifest. Section 9's `library-gitea` line (:275) says
"C2-13b's `library.sync` (not landed)". It becomes the `--library-status` check (D6b). The install
section also gets a line that the copy's `npm ci` needs the npm registry from the host (Q3, webby-0085).
### D8. Tests, `packages/cli/tests/case2-install.test.mjs`
### D8. Design 10 text
Sage ruled that design 10 gets the same change when 226 lands, with the line in this row's manifest.
In agents/gosalyn/work/case2/DESIGN.md:
- step 2b drops `release.sh package` and `activate`;
- a new step after step 4 runs `case2-precheck.sh --business <business> --library-status`;
- the precheck's library text says it reads the bus's recorded sync state, not that it runs
`library.sync`.
### D9. Tests, `packages/cli/tests/case2-install.test.mjs`
These follow case2-unit.test.mjs: scratch HOME, MOSAIC_CONFIG, stub bin dir and seams, a local bare repo
cloned `--bare --no-local` as sync.test.mjs does, and the fixture's key never in output.
- Install `library`:
- writes the business file at 0600 with bean's and chet's persona vars;
- writes the persona files at 0600 in 0700 dirs and the empty slot dir at 0700;
- logs the release then copy calls in order;
- logs the copy call and no release call;
- starts nothing;
- passes on a re-run;
- refuses `business-file-differs` and leaves the file unchanged.
@@ -142,31 +187,47 @@ cloned `--bare --no-local` as sync.test.mjs does, and the fixture's key never in
- refuses a library project and `bus.tester` off.
- Install refuses: run as root, a business id mismatch, missing `--personas` under library, and a
`validate` refusal, after which no business file is left.
- Precheck library refusals, one case each, as the brief lists:
- a persona file at 0644;
- a digest mismatch;
- a business file at 0644;
- a sync failure, which is no token in the slot and a remote that doesn't resolve;
- a Discord binding.
- Precheck library pass: a bare repo with one R2 path prints its `R2` line and passes.
- N4: a re-run where the matching business file is already there and `validate` refuses leaves that
file in place, byte for byte.
- D6a refusals, one case each: a persona file at 0644, a digest mismatch, a business file at 0644, a
`changes.jsonl` at 0644, and a Discord binding. A clean library host passes D6a with no mirror.
- D6b, with the mirror made by `ensureMirror` and the state written by a real `sync()` against the local
bare repo, as the bus would:
- no mirror refuses `library-mirror-missing`;
- a fresh mirror refuses `library-never-synced`;
- a sync against a remote that doesn't resolve refuses `library-last-error` with its code;
- a synced bare repo with one R2 path prints its `problem` line and `problems 1`, and passes.
Every new refusal gets a mutant that removes it. The count goes in the slot request.
### D9. Files
### D10. Files
Owned by the brief: `scripts/case2-install.sh` (new), `scripts/case2-precheck.sh` (library checks
only) and `packages/cli/tests/case2-install.test.mjs` (new). Also `docs/guides/host-run-sheet.md` (D7,
sage-1832) and this BUILD.md. Before the slot request I'll grep for outside readers of
case2-precheck.sh, since case2-unit.test.mjs runs it.
Owned by the brief: `scripts/case2-install.sh` (new), `scripts/case2-precheck.sh` (library checks and
`--library-status` only) and `packages/cli/tests/case2-install.test.mjs` (new). Also
`docs/guides/host-run-sheet.md` (D7, sage-1832), agents/gosalyn/work/case2/DESIGN.md (D8, sage-2189)
and this BUILD.md. Before the slot request I'll grep for outside readers of case2-precheck.sh, since
case2-unit.test.mjs runs it.
## Questions
- Q1. Design 10 order. D3 puts the persona stage and Jason's pairing check before 2b. The other way
is for the install to write bean and chet without persona vars and a second run to add them after step
6. That needs a business file change after install, and D4 refuses that. I recommend D3.
- Q2. Should an R-problem refuse the library precheck? The brief says the check "lists" them, so D6
doesn't refuse. Her repo may well have R2 paths on day one, before she confirms the mapping in step 7.
- Q3. Design 10 step 2b names "the pinned harness" as part of the install. I read that as the pinned
pi from package.json. The copy installs it with `npm ci` from package-lock.json (stack-copy.sh:70),
so it isn't a separate install step. That `npm ci` needs the npm registry from the host. Does
anyone read it as more than that?
All three d1 questions are settled:
- Q1, persona stage before 2b: Webby agreed (webby-0085).
- Q2, R-problems don't refuse: Webby agreed.
- Q3, the pinned harness is the copy's `npm ci`: Webby agreed, and asked for the registry note in D7.
- Q4a and Q4b: Sage, sage-2189.
One choice for Webby to check: D6b's `problem` line carries the path through `JSON.stringify`. N5 asked
for a prefix and a count. I kept the path because Jason needs it to tell her which file to fix, and the
names are hers, not text.
## Changes from d1
- B1: release package and activate dropped, with the `CASE2_RELEASE` seam and its test (D2, D5).
- N1 and webby-0086: the sync is gone from the precheck. D6 splits into D6a before the start and D6b,
`--library-status`, after the bus is up and step 4 (sage-2189).
- N2: covered by D6b's `lastError` refusal.
- N3: D6b builds the context and slot as the bus does.
- N4: step 7 removes only a file this run created, with a test.
- N5: `problem` prefix and a `problems` count.
- N6: the run sheet lines are :240 and :275.
- New D8, the design 10 text change, and its path in D10.