diff --git a/agents/gosalyn/work/queue-226/BUILD.md b/agents/gosalyn/work/queue-226/BUILD.md index 408d1d3f7..f27e9b3ed 100644 --- a/agents/gosalyn/work/queue-226/BUILD.md +++ b/agents/gosalyn/work/queue-226/BUILD.md @@ -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..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 `/secrets//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 + `/secrets//library-gitea` (runtime.mjs:146-151, :162-164). - `case2-persona-import.mjs install --staging DIR --config-dir DIR --business ID` writes `/personas//.md` at 0600 in 0700 dirs, never overwriting, and prints `instance bean: persona.name Bean, persona.sha256 ` per persona. - A Discord binding is `/discord/.json` (packages/discord/src/binding.mjs:70-83). - `case2-unit.sh --copy ID` needs `/.local/share/mosaic-stack/copies/`, 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 /.local/share/mosaic-stack/copies`. The copy +3. `scripts/stack-copy.sh copy <--commit> --copies /.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 `/businesses/.json` at 0600 with O_EXCL (D4). -7. `library` only: make `/secrets//` at 0700, empty. The token is Jason's, step 4. -8. `mosaic business validate `. 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 `/businesses/.json` at 0600 with O_EXCL (D4). +6. `library` only: make `/secrets//` at 0700, empty. The token is Jason's, design 10 + step 4. +7. `mosaic business validate `. 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 --business ` 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 +--business ` 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 ``, 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//` 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: `). Each - R-problem prints as `ok library R `. 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 `/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 `` 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 ` 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 `, with the path through `JSON.stringify`, then + one `problems ` 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 `` can't write `/etc` and root reads nothing `` 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 --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.