Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62556184fb | ||
|
|
d572b61026 | ||
|
|
f46e66883f | ||
|
|
d372102882 | ||
|
|
b79ad5f2cb | ||
|
|
3fb00018e0 | ||
|
|
f7a77721a9 | ||
|
|
829e5f8340 | ||
|
|
abb352d27e |
No files matched your search
@@ -0,0 +1,191 @@
|
||||
# Row 251 build: T27 lists no identities right after `ssh-add -t 1` (Vikunja #453)
|
||||
|
||||
Brief `docs/plans/2026-10-21_t27-ssh-add-lifetime.md` (rev 2931), Mos ruling mos-1185 item 2, assigned in
|
||||
sage-2075. Owner Darkwing, reviewer Dewey, gate owner Sage. Branch `darkwing/row251` from 3b0aef995,
|
||||
claimed in-progress at rev 2938.
|
||||
|
||||
## Step 1: the failure names its cause (e5e4b2e5a)
|
||||
|
||||
T27's two lists now carry a message that prints only when they fail. It gives the time from addKey's
|
||||
start to its resolve and to each `ssh-add -l` call, the hrtime at each with how far into its second it
|
||||
fell, `/proc/uptime` at each, and whether the agent process and its socket were still there. The checks
|
||||
themselves did not change. The commit holds nothing else.
|
||||
|
||||
## Step 2: the cause
|
||||
|
||||
ssh-agent counts a key's lifetime in whole seconds of CLOCK_BOOTTIME, at the add and at the reap. I read
|
||||
OpenSSH 10.5p1, the version on dragon-lin (`ssh -V`: OpenSSH_10.5p1), at tag V_10_5_P1 (ssh-agent.c
|
||||
sha256 873691c4, misc.c sha256 ee467f43):
|
||||
|
||||
- `misc.c:1790-1798` and `:1828`: `monotime()` returns `ts.tv_sec` of `clock_gettime(CLOCK_BOOTTIME)`,
|
||||
falling back to MONOTONIC. The fraction of the second is dropped.
|
||||
- `ssh-agent.c:1315`: the `-t` constraint sets `*deathp = monotime() + seconds`. Lines 1410 and 1598
|
||||
do the same for the agent's default lifetime.
|
||||
- `ssh-agent.c:1064`: the reaper drops a key when `now >= id->death`, `now` being `monotime()` again.
|
||||
- `ssh-agent.c:2650`: the main loop runs `reaper()` after every poll, before it reads requests. A list
|
||||
that arrives after the boundary sees the key gone.
|
||||
|
||||
So a key added at second s plus a fraction f dies at s + N, and `-t N` keeps it N - f seconds, between
|
||||
N - 1 and N. With `-t 1`, an add late in a second leaves the key milliseconds to live. That matches
|
||||
Sage's probe in the brief: 8 of 80 `-t 1` adds missed, each one made at 0.9947-0.9973 of a monotonic
|
||||
second and finished at 0.998-0.0008, and none earlier in the second missed. It also matches proof 5's
|
||||
failure, where the first list ran 31.9 ms into the test and found no identities.
|
||||
|
||||
Node's hrtime is CLOCK_MONOTONIC. BOOTTIME differs from it by the time the host spent suspended. On
|
||||
dragon-lin the difference read 0.000001 s at 19:09Z, so a monotonic second boundary is a BOOTTIME one
|
||||
here.
|
||||
|
||||
`addKey` and the agent start are not the cause. `addKey` passes the lifetime through as `-t`
|
||||
(`host-run.mjs:90`), and the agent starts with no default lifetime (`:175`). Production adds the key
|
||||
with `timeout + 30` (`:180`), where losing up to a second makes no difference. The only caller that
|
||||
can't afford the lost fraction is T27, which asks for 1 s. The fix belongs in the test's lifetime choice,
|
||||
and `host-run.mjs` doesn't change.
|
||||
|
||||
The slot confirms this against T27 itself (below), not just from the source.
|
||||
|
||||
## Step 3: the fix (this commit)
|
||||
|
||||
T27 adds the key with `-t 2`. The agent then keeps it more than 1 s and at most 2 s. The first list
|
||||
runs milliseconds after the add, so it has over a second of margin even under load. The sleep before
|
||||
the second list grows from 2.5 s to 3.5 s, which keeps the old 1.5 s margin past the new 2 s ceiling.
|
||||
Both checks stay: listed after the add, gone after the lifetime. A comment above the add says why the
|
||||
lifetime is 2.
|
||||
|
||||
I considered `-t 3` with a 4.5 s sleep for a 2 s floor. One second of floor is far above anything the
|
||||
first list has needed (31.9 ms in proof 5), and the concurrent-suite run below tests it under load, so I
|
||||
took the smaller change.
|
||||
|
||||
## Base
|
||||
|
||||
The branch stays on 3b0aef995. By 19:15Z origin/refactor was at 6096b051b, after rows 66 and 243
|
||||
landed. `git diff 3b0aef995 6096b051b` touches neither `packages/bus/src/host-run.mjs` nor
|
||||
`packages/bus/tests/host-run.test.mjs`, so there's no rebase (sage-2084). Row 243 did change
|
||||
`packages/bus/tests/process.test.mjs`. My slot's bus suites run that file as it was at 3b0aef995, and the
|
||||
gate rerun on Sage's slot runs it as landed.
|
||||
|
||||
## Prediction for part A on e5e4b2e5a (written before the slot)
|
||||
|
||||
The hook starts `ssh-add -t 1` at the given fraction of a monotonic second. From the brief's probe, the
|
||||
agent stores the key about 3 ms later. The first list reaches the agent a few ms after addKey resolves:
|
||||
Node's exit event, the mark, and `ssh-add -l`'s own start. A red needs the add to land before the second
|
||||
boundary and the list to land after it.
|
||||
|
||||
- 0.990: green. The add and the list both land before the boundary.
|
||||
- 0.993, 0.995, 0.996, 0.997: red expected. The add lands before the boundary, the list after. At 0.993
|
||||
and 0.997 I'm less sure, because the list (at 0.993) or the add (at 0.997) can land right at the
|
||||
boundary. A green at either is in line with the cause, and so is a red.
|
||||
- 0.998: green. The add lands after the boundary, so the key gets almost a full second.
|
||||
|
||||
Every red should show "first list got" an answer with no SHA256 line. It should also show addKey
|
||||
resolving before a second boundary and the first list starting after it or within a few ms of it, with
|
||||
the agent alive and its socket present.
|
||||
|
||||
Under sage-2084's rule, on e5e4b2e5a only a red at 0.990 or 0.998 counts toward the stop, and two such
|
||||
reds stop the run. On the fix commit any red counts, and two stop it. In parts B to D, the first red stops
|
||||
the run.
|
||||
|
||||
## Slot plan
|
||||
|
||||
All scripts are in `/mnt/storage/scratch/darkwing-251-out/`, launched from `/`, each behind the busy
|
||||
checks and chained with `&&`, TMPDIR under `/mnt/storage/scratch/tmp`.
|
||||
|
||||
1. Confirmation. `align-hooks.mjs` is a load hook, outside the repo, that serves `host-run.mjs` with
|
||||
`addKey` wrapped to start `ssh-add` at a chosen fraction of a monotonic second. T27 runs unchanged
|
||||
under it, alone (`--test-name-pattern`). It runs at fractions 0.990, 0.993, 0.995, 0.996, 0.997 and
|
||||
0.998, three times each:
|
||||
- on e5e4b2e5a (step 1, `-t 1`). Expected: some first lists fail, and the step 1 message shows the
|
||||
add's resolve and the list on either side of a second boundary, with the agent alive and the
|
||||
socket present. These reds are the point of the run.
|
||||
- on the fix commit (`-t 2`). Expected: all 18 pass.
|
||||
2. `host-run.test.mjs` whole, once, on the fix commit.
|
||||
3. The bus suite whole, three times in a row.
|
||||
4. The bus suite whole in two clean worktrees at the same commit, started together.
|
||||
|
||||
## Slot results
|
||||
|
||||
Sage's CLEAR came in sage-2154 at 20:58:55Z. `slot-251.sh` ran parts A and B from 20:59:22Z to 21:01:33Z,
|
||||
on node v26.8.1 and OpenSSH_10.5p1, with TMPDIR `/mnt/storage/scratch/tmp/r251`. At the start, BOOTTIME
|
||||
minus MONOTONIC read 0.000001 s, then 0.000000 s twice (dewey-0769 N1). So the hrtime fractions below
|
||||
put the agent's second boundary in the right place.
|
||||
|
||||
### Part A: T27 alone under `align-hooks.mjs`, 3 runs per fraction
|
||||
|
||||
The fraction is when the hook started `ssh-add`. The hook logged each start within 0.0001 of its target.
|
||||
|
||||
| fraction | e5e4b2e5a (`-t 1`) | 68f2dfd4b (`-t 2`) |
|
||||
|---|---|---|
|
||||
| 0.990 | 3 pass | 3 pass |
|
||||
| 0.993 | 3 pass | 3 pass |
|
||||
| 0.995 | 3 red | 3 pass |
|
||||
| 0.996 | 3 red | 3 pass |
|
||||
| 0.997 | 3 pass | 3 pass |
|
||||
| 0.998 | 3 pass | 3 pass |
|
||||
|
||||
e5e4b2e5a went red 6 times out of 18, every time at a fraction the prediction named, so none of them
|
||||
counted toward the stop. 68f2dfd4b passed all 18. The fix runs used 68f2dfd4b, whose test blob is the same
|
||||
as c19b4167e's (280132c7).
|
||||
|
||||
Every red failed on the first list and got `"The agent has no identities.\n"`, with the agent alive and
|
||||
its socket present. The step 1 message gave these hrtime fractions. "addKey start" is the mark before the
|
||||
hook's wait, so its fraction is earlier than the ssh-add start.
|
||||
|
||||
| run | addKey resolved | first list mark |
|
||||
|---|---|---|
|
||||
| 0.995 rep 1 | 0.9992 (+317.6 ms) | 0.9993 |
|
||||
| 0.995 rep 2 | 0.9991 (+846.5 ms) | 0.9991 |
|
||||
| 0.995 rep 3 | 0.9988 (+845.0 ms) | 0.9988 |
|
||||
| 0.996 rep 1 | 0.9999 (+839.8 ms) | 1.0000 |
|
||||
| 0.996 rep 2 | 0.0004 (+845.0 ms) | 0.0004 |
|
||||
| 0.996 rep 3 | 0.0002 (+835.9 ms) | 0.0002 |
|
||||
|
||||
At 0.995 the add finished about 4 ms in, before the boundary, and the list mark came just before it. The
|
||||
`ssh-add -l` process then reached the agent after the boundary. At 0.996 the agent stored the key before
|
||||
the boundary, and addKey resolved just after it. In both cases the key's whole second ended between the
|
||||
add and the list, which is the cause step 2 gives. The two edge fractions came out green on the first
|
||||
build. At 0.993 the list still reached the agent before the boundary, and at 0.997 the add landed after
|
||||
it, so the key got almost a full second. The prediction allowed either result at those two.
|
||||
|
||||
### Part B: `host-run.test.mjs` whole on 68f2dfd4b
|
||||
|
||||
24 tests, 24 passed (21:01:18Z, 14.0 s).
|
||||
|
||||
### The stop before C1, and the resume
|
||||
|
||||
At 21:01:33Z the script stopped before C1, with exit 4 and no red. My own `busy.sh` matched two `sleep 60`
|
||||
processes, mine, in `darkwing-r251/packages/bus`. Part B left them. They come from T27 "the start sweep
|
||||
keeps a live bus dir and kills only a matching live process" (`host-run.test.mjs:528` and `:536`). Its
|
||||
`t.after` SIGKILLs the `/bin/sh -c 'sleep 60; :'` parent, but the `sleep` child lives on for up to 60 s.
|
||||
That code is outside this row's diff, so the row leaves it alone, and it's recorded below as a follow-up.
|
||||
Sage confirmed (sage-2159) that the right move is to resume only the scope that hadn't run.
|
||||
|
||||
`slot-251-cd.sh` runs C and D as `slot-251.sh` has them, with the same two busy checks. Before each check,
|
||||
it waits up to 75 s for those orphans to exit: my uid, my trees, a command line of exactly `sleep 60`.
|
||||
It logs them as it waits. Any other process still trips the checks.
|
||||
|
||||
### Parts C and D: the bus suite whole on 68f2dfd4b
|
||||
|
||||
| run | start | tests | pass | fail | time |
|
||||
|---|---|---|---|---|---|
|
||||
| C1 | 21:02:48Z | 598 | 598 | 0 | 21.6 s |
|
||||
| C2 | 21:04:06Z | 598 | 598 | 0 | 21.4 s |
|
||||
| C3 | 21:05:26Z | 598 | 598 | 0 | 22.1 s |
|
||||
| D1, darkwing-r251 | 21:06:44Z | 598 | 598 | 0 | 24.2 s |
|
||||
| D2, darkwing-r251b, TMPDIR `r251-b` | 21:06:44Z | 598 | 598 | 0 | 24.2 s |
|
||||
|
||||
The script waited 33 s before C2, 34 s before C3, 33 s before D and 32 s before the sweep. Each time it
|
||||
was waiting for that run's `sleep 60` orphans (two per suite, four after D), and nothing else. After the
|
||||
wait, the sweep of my trees, the out dir and TMPDIR found 0 processes left (21:08:04Z). Sage confirmed
|
||||
(sage-2159, sage-2160) that the short gitea-api and `node --check` runs they made during the slot weren't
|
||||
stops. None of my busy checks refused on any of them.
|
||||
|
||||
These runs used the bus files as they were at 3b0aef995. Since then, origin/refactor (666173292, then
|
||||
7247d80c7 and c3d88840c with row 196) has changed `broker.mjs`, `runtime.mjs` and `server.mjs`, which T27's
|
||||
file imports, along with other bus files. None of those commits touch this row's two paths or
|
||||
`host-run.mjs`, and `git merge-tree` of c3d88840c with 68f2dfd4b is clean. The gate rerun in
|
||||
a clean worktree on Sage's slot runs T27 against them.
|
||||
|
||||
### Follow-up, not in this row
|
||||
|
||||
T27's start-sweep test leaves the `sleep 60` child of each planted shell running for up to 60 s after
|
||||
the test. Killing the process group (`process.kill(-pid)`; both shells are spawned `detached`) or
|
||||
`exec sleep 60` in the shell would end the child along with its parent.
|
||||
@@ -0,0 +1,331 @@
|
||||
# Row 226 build notes, C2-13b install script and library precheck (#1727)
|
||||
|
||||
Brief: docs/plans/2026-10-20_case2-rows.md "C2-13b install script and library precheck" (:1073-1134 at
|
||||
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 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
|
||||
|
||||
Branch gosalyn/c2-13b from refactor d37210288. Rows 215, 221, 222 and 230 are done.
|
||||
|
||||
## Facts the design rests on
|
||||
|
||||
- No CLI writes a business file. `mosaic business validate <id>` checks one and exits 0 ok, 2 invalid
|
||||
(packages/business/src/cli.mjs). A business file needs `human`, `arbiters`, `projects`, `tracker`
|
||||
with a sync bot and a Vikunja credential ref, and `roles`, each role with its tracker bot when its
|
||||
definition uses Vikunja (business.mjs:23, :84-100, :131-149).
|
||||
- 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.
|
||||
- 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. 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
|
||||
|
||||
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`.
|
||||
- `library`: at least one library project, `bus.tester` not true, roles `bean` and `chet` with
|
||||
definition `companion`, and no `persona.*` var in the input, since D3 adds them.
|
||||
- `factory`: no library project, and `bus.tester` true. These are the precheck's rules at
|
||||
case2-precheck.sh:127-135, so the install refuses what the precheck would refuse.
|
||||
|
||||
### D2. Install steps, in order
|
||||
|
||||
1. Refuse to run as root.
|
||||
2. `scripts/bootstrap.sh`. It's idempotent and never rewrites an existing config.
|
||||
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).
|
||||
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. Row 195's W8 now
|
||||
passes `--business-file` and has no release step.
|
||||
|
||||
### D3. Personas come from the staging dir
|
||||
|
||||
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: the read of her Hermes
|
||||
profiles and Jason's check of the printed pairing. Webby agreed in webby-0085 (Q1).
|
||||
|
||||
### D4. Re-runs
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
case2-install.sh --business ID --business-file FILE --commit SHA [--profile factory|library]
|
||||
[--personas DIR] [--home DIR]
|
||||
```
|
||||
|
||||
`--home` defaults to `$HOME`, which is `/srv/mosaic` under `sudo -u mosaic -H`. `MOSAIC_CONFIG` is
|
||||
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, split in two (Q4b, sage-2189)
|
||||
|
||||
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.
|
||||
- No `<dataRoot>/discord/*.json` exists (`discord-binding`).
|
||||
|
||||
#### 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: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 (: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. 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 copy call and no release call;
|
||||
- starts nothing;
|
||||
- passes on a re-run;
|
||||
- refuses `business-file-differs` and leaves the file unchanged.
|
||||
- Install `factory`:
|
||||
- writes no library mapping, no persona file and no slot dir, which is the brief's case;
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
### D10. Files
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## Build
|
||||
|
||||
Webby approved d2 in webby-0087 (REVIEW-design-d2.md, webby/reviews 2f488b4ea). Nothing above this
|
||||
section changed after approval. This section records the build.
|
||||
|
||||
### D8 lands on case2-design
|
||||
|
||||
Sage ruled in sage-2195, after Webby's N1, that D8 goes on gosalyn/case2-design, because
|
||||
agents/gosalyn/work/case2/DESIGN.md isn't on refactor. The change is commit 624212842 there, pushed:
|
||||
step 2b drops `release.sh package` and `activate`, new step 4a runs `--library-status`, and the C2-13b
|
||||
row text says the precheck reads the bus's recorded sync state. DESIGN.md is not in this row's manifest.
|
||||
|
||||
The brief's lines docs/plans/2026-10-20_case2-rows.md:1093-1096 still name release package, activate and
|
||||
a library.sync fetch. Sage owns the brief and will amend those lines to match D8 in a doc-only commit
|
||||
after the re-pin window (sage-2205). They are not in this row's manifest.
|
||||
|
||||
### Webby's d2 notes
|
||||
|
||||
- N2: the `problem` line keeps the path. After `JSON.stringify` the precheck also escapes DEL, the C1
|
||||
controls U+0080-009F, U+200E and U+200F, U+2028 and U+2029, and the bidi controls U+202A-202E and
|
||||
U+2066-2069 as `\uXXXX`. Webby named C1 and bidi. I added the rest because `JSON.stringify` leaves
|
||||
them raw too, and each one can change how a terminal shows the line. The test path holds U+202E and
|
||||
U+0085, and the test checks that neither prints raw.
|
||||
- N3: `lastError` is checked before `lastFetch`. A first sync with no token records
|
||||
`library-credential-missing` and leaves `lastFetch` null, so it refuses `library-last-error`, not
|
||||
`library-never-synced`. The fresh-mirror test has `lastError` null and refuses `library-never-synced`.
|
||||
|
||||
### Choices made in the build
|
||||
|
||||
- The config dir. The persona import refuses a config dir with group or other bits, and bootstrap makes
|
||||
one with the default mode. So the install makes the config dir at 0700 when it's absent, before
|
||||
bootstrap, and refuses `config-dir-mode` when it exists with a group or other bit. It checks
|
||||
`secrets/` and `secrets/<id>/` the same way under library (`secrets-mode`, `slot-mode`).
|
||||
- `--home` must equal `$HOME` (`home-mismatch`). Every path comes from `$HOME` and `MOSAIC_CONFIG`, as
|
||||
bootstrap's does, so a different `--home` would only name a second home the script doesn't use.
|
||||
- A plan step runs before anything is written. It reads the input and the staging manifest, computes
|
||||
the persona vars from the manifest, and compares the result with what's on disk. It refuses every
|
||||
input rule, `business-file-differs` and `persona-differs` there, so a refused run writes nothing.
|
||||
After the import the script checks that the import's two `instance` lines carry the manifest's
|
||||
digests.
|
||||
- A re-run with both persona files present and matching skips the import, which would refuse to
|
||||
overwrite them. One matching file and one missing refuses `persona-differs`.
|
||||
- A diverged library prints one line under `--library-status`, naming the tip `library.sync --accept`
|
||||
would take. It doesn't refuse. The design doesn't list it as a refusal, and the bus still serves the
|
||||
old tip.
|
||||
- The precheck's library checks print `ok` lines for each check they pass, as the rest of the script
|
||||
does. A business file that's missing prints no extra line, since the existing business-file check
|
||||
already refuses it.
|
||||
- The business id must match the persona import's rule, `^[a-z0-9][a-z0-9-]{0,63}$`. That's narrower
|
||||
than the precheck's, which also takes `.` and `_`, because the persona dir uses the id.
|
||||
|
||||
### Outside readers
|
||||
|
||||
`git grep` over packages/*/tests/*, packages/*/src/* and scripts/test-*.sh finds case2-precheck.sh read
|
||||
by case2-unit.test.mjs, which runs it, and case1-accept.test.mjs, which names a copy's path but runs a
|
||||
fake. No test reads host-run-sheet.md. No test pins a hash of either script. case2-unit.test.mjs's
|
||||
precheck host has a 0600 business file and no persona vars, so the new D6a checks pass there.
|
||||
|
||||
### Mutants
|
||||
|
||||
24, in /mnt/storage/scratch/gosalyn-r226/mutate.py. Each replaces one exact string once. Line numbers
|
||||
are the candidate's. run.sh stops with BASE RED before any mutant unless the base run is rc=0 and
|
||||
fail 0.
|
||||
|
||||
| Mutant | Line | Check removed |
|
||||
|---|---|---|
|
||||
| I1 | install :73 | run-as-root |
|
||||
| I2 | install :74 | home-mismatch |
|
||||
| I3 | install :81 | config-dir-mode |
|
||||
| I4 | install :101 | business-id-mismatch |
|
||||
| I5 | install :107 | profile-library-business |
|
||||
| I6 | install :108 | bus-tester-off |
|
||||
| I7 | install :111 | bus-tester-on |
|
||||
| I8 | install :115 | persona-var-input |
|
||||
| I9 | install :158 | business-file-differs |
|
||||
| I10 | install :152 | persona-differs |
|
||||
| I11 | install :218 | N4, removes the file even when this run didn't write it |
|
||||
| I12 | install :218 | validate's removal of the file this run wrote |
|
||||
| P1 | precheck :229 | business-file-mode |
|
||||
| P2 | precheck :235 | history-mode for changes.jsonl |
|
||||
| P3 | precheck :246 | persona-mode for a persona file |
|
||||
| P4 | precheck :251 | persona-mismatch |
|
||||
| P5 | precheck :241 | persona-mode for a persona dir |
|
||||
| P6 | precheck :258 | discord-binding |
|
||||
| P7 | precheck :104 | library-mirror-missing |
|
||||
| P8 | precheck :107 | library-last-error |
|
||||
| P9 | precheck :107 | N3, the order swapped |
|
||||
| P10 | precheck :108 | library-never-synced |
|
||||
| P11 | precheck :92 | N2, the escape after stringify |
|
||||
| P12 | precheck :65 | --library-status refuses --profile |
|
||||
|
||||
`history-mode` for the history dir itself has no mutant and no test. The brief names only the
|
||||
`changes.jsonl` case.
|
||||
|
||||
### Tests
|
||||
|
||||
Not run yet. They wait for a slot.
|
||||
@@ -0,0 +1,90 @@
|
||||
# 10-24 window, notes for draft 1
|
||||
|
||||
Sage, 2026-10-21T19:40Z. These are working notes, not a draft. Draft 1 is cut from them once NEW is fixed.
|
||||
Draft 0 is `repin-1024-draft0.md`.
|
||||
|
||||
## Changes since draft 0
|
||||
|
||||
- **Phase 2 order (mos-1197 item 3).** Phase 2 now runs after this window's re-pin is verified, not on
|
||||
10-23 before it, and Jason picks the hour. So this window's OLD is the live pin, not a phase 2 copy, and
|
||||
draft 0's line 14-17 and Q2 are out of date.
|
||||
- **Console flags.** They stay as they are: no `--sso` and no `--send`. `--send` goes on with `--sso` in the
|
||||
phase 2 window (mos-1197 2a). P0 checks that the window's activate line carries neither flag.
|
||||
- **NEW (mos-1161).** NEW is the latest code landing when draft 1 is cut. At 19:40Z that is e54e05463 (row
|
||||
215). 194 hasn't landed, so P3 has no lifecycle step unless it lands with a full gate first.
|
||||
- **Row 66 live check (Vikunja #454, DEFERRED 3b0aef995).** This is the first re-pin whose tree includes
|
||||
2c38325ec. Add a step after the window is verified. One Claude Code managed session with a prompt that
|
||||
draws a known tell, then record the operator note on the RESULT, the next-turn notice and the
|
||||
`unslop.jsonl` counts. Backup first (Q173). Pi isn't run (#383).
|
||||
|
||||
## OLD, read 19:38Z from `stack-release.sh status mosaic-stack` (read only)
|
||||
|
||||
- Copy 4429a104f6d65ccf165f6c83b5d1dce3147dfde0, schema 3i, pm true, activation 20261020T220206Z-4429a104f6d6.
|
||||
- Console flags: `--port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username
|
||||
--allow-user jason.woltje --tester-port 7333`. P1 re-reads these on the day.
|
||||
- Check pass, unit flags match the pointer, 123 commits of drift.
|
||||
|
||||
## Schema
|
||||
|
||||
`git diff --name-only 4429a104f origin/refactor` shows only `packages/bus/schema.sql` and
|
||||
`migrations/3i-3j.sql` (row 234). Nothing since c771a36f4 adds a step, so the migration is still 3i to 3j.
|
||||
Recheck this at the cut. Row 206 (3k-3l) hasn't landed.
|
||||
|
||||
## Rows NEW carries after c771a36f4 (first-parent, code paths)
|
||||
|
||||
| Row | Commit | Live effect to check |
|
||||
|---|---|---|
|
||||
| 240 | c515273db | Installer I4: root host check, pins, boot unit. This window doesn't run it. |
|
||||
| 228 | 86249ac55 | ZAI_API_KEY joins the session env allowlist. The live config sets no ZAI key, so the env is unchanged. Check in P3. |
|
||||
| 238 | d49b1f2bd | sso-config takes `--instance` on named instances. Default instance, `--sso` off, so there's no effect. |
|
||||
| 212 | ee7b2f137 | New package packages/library. The live business has no library entry. |
|
||||
| 195 | b65014647 | scripts/case1-accept.sh, root only. Not run. |
|
||||
| 213 | 361ba2387 | Creative roles (roles/companion.*), persona file and block, launch-settings, bundle.mjs. Live roles are chief, pm, coder and reviewer. P3: the chief's and pm's bundles are unchanged apart from the new code path, and one pm reply. |
|
||||
| 231 | 29ce1e360 | Library reader fixes. No live library. |
|
||||
| 235 | b1fece6f0 | Machine grant row b: raise binding, executor, reconcile, revoke and sweep (broker, host-grant, credentials, runtime). The live business has no hosts. P3: bus start runs the grant reconcile and sweep, and they change nothing. Show the log lines. |
|
||||
| 221 | 300766450 | Persona import script, with a count-only dry run. Not run. yaml 2.9 joins package-lock, so P3 runs npm ci on the copy. |
|
||||
| 247 | 856c1826b | Persona import follow-up. Not run. |
|
||||
| 245 | 6d5545d44 | case1-accept F9 follow-up. Not run. |
|
||||
| 248 | 8a04f4aa4 | Test stubs only. |
|
||||
| 249 | 9de5a57ab | broker: hostLineLeft on a grant whose install never returned. No grants live. The A22b look-alike count in draft 0 already covers `hostLineLeft`. |
|
||||
| 197 | a36523713 | Test lane fixture. No live effect. |
|
||||
| 66 | 2c38325ec | Managed sessions run the unslop check, runner.mjs, and stack-copy.sh COPY_PATHS adds tools/unslop-hook. P3 checks that the copy has tools/unslop-hook. The live check is above. |
|
||||
| 243 | 6096b051b | Tests only. |
|
||||
| 215 | e54e05463 | Library entry kind, consumer skips, M9, bare mirror, library.file and library.status. The live business has no library entry. M9 refuses any organization write that changes `projects`, so P3 checks that the chief's or pm's usual organization writes don't touch projects. |
|
||||
| 250 | 1b6158ce2 | Conversation shim and K12 test. No live effect on its own. |
|
||||
| 222 | 0b33f5e9d | Fetch-only library sync: library.sync bus verb, a 60 s timer per library entry with a sync user, the askpass helper, and stack-copy.sh COPY_PATHS. The live business has no library entry, so the timer starts nothing. P3: the copy has packages/library/bin/askpass with its mode, and bus start logs no `library:` line. |
|
||||
| 196 | 7247d80c7 | Human boundary: SO_PEERCRED on the bus socket, the interpreter receipt in bus/peercred-starts.jsonl, and the Console's private tester socket in place of `--tester-port`. The section "If 196 lands before the cut" applies. P3: bus start writes a receipt line, and the Console starts with `--tester-socket`. |
|
||||
|
||||
Added 2026-10-21T20:30Z. NEW at this hour is 0b33f5e9d (row 222, landed 20:13Z), the latest code
|
||||
landing. PRD 1.9 (f48725cfc) and 1.10 are doc and test changes, but `packages/tasks/requirements.json`
|
||||
is in the copy, so `task.create` takes REQ-WEB-2 and REQ-INST-2 as ids after the re-pin. P3: requirements
|
||||
returns 70 ids at 1.10. 196 r6 (527b9a1e6, on f48725cfc) is in its gate, so the section below may apply.
|
||||
|
||||
Added 2026-10-21T21:02Z. 196 landed at 7247d80c7 (20:57Z), and c3d88840c after it is doc-only. NEW at
|
||||
this hour is 7247d80c7, so the section below applies.
|
||||
|
||||
## If 196 lands before the cut
|
||||
|
||||
196 retires the Console's `--tester-port` (webui cli.mjs:30, "activate explicitly with --tester-socket").
|
||||
The live Console runs `--tester-port 7333`, so a NEW that carries 196 changes the Console's command line.
|
||||
Draft 1 would then need:
|
||||
- the activate line with `--tester-socket` and its path;
|
||||
- the P2 unit diff showing only that change;
|
||||
- a P3 rehearsal that starts the Console with it.
|
||||
|
||||
bus.tester stays Jason's. The tester path changes how the test client reaches the Console, but not who can
|
||||
reach it from outside, so it stays Q173 work. I confirm that with Mos in draft 1. If 196 hasn't landed, NEW
|
||||
leaves it out and none of this applies.
|
||||
|
||||
mos-1198 item 4: until row 253 (#1758, case2-unit.sh and case1-accept.sh to `--tester-socket`) lands, no
|
||||
Case 1 or Case 2 host (.53, .54) takes a copy that carries 196. This window is dragon-lin only, so it isn't
|
||||
blocked, but any .53 or .54 step that names NEW waits on 253.
|
||||
|
||||
Row 250 landed 1b6158ce2 at 19:42Z (conversation shim and K12 test). It has no live effect on its own, but
|
||||
it is now the latest code landing if the cut were made today.
|
||||
|
||||
## Still open from draft 0
|
||||
|
||||
- The P-step evidence for rows 199, 210 and now 235 at bus start, from a P3 rehearsal run.
|
||||
- Recount the restore-guard table list from NEW (T46).
|
||||
- Q3 operator: Darkwing if free.
|
||||
@@ -0,0 +1,168 @@
|
||||
# 10-24 window: re-pin 4429a104f to 7247d80c7 (rows 190, 199, 210, 234 and on, row 196 last), with 3i-3j (draft 1)
|
||||
|
||||
Sage, 2026-10-21, draft 1, for mos-1145 under DECISIONS Q173. Mos checks this file at its commit before P0.
|
||||
Any change to this file or to NEW after that check means Mos checks it again (mos-0849/0850). The step layout
|
||||
follows `repin-4429a104f.md`. Where a step is unchanged, I name it and say what differs. Draft 0 is
|
||||
`repin-1024-draft0.md`, and the working notes are in `repin-1024-draft1-prep.md`.
|
||||
|
||||
## Who, and what nobody does
|
||||
|
||||
- Q3 below names the operator: Darkwing if they're free, otherwise me. Mos verifies each step's output before
|
||||
the next one, and runs `rehearse` (P3).
|
||||
- Jason has no step. Q173 covers a re-pin of this test system with a migration, a backup first and a STATE
|
||||
report after. Nothing is pruned.
|
||||
- Nobody acts as Jason. No `--by jason`, no human shell or tmux pane, and every bus read and write carries
|
||||
`--tester`. `bus.tester` stays as it is. If it isn't `true`, stop at P1.
|
||||
- No pi (P19 #383). No Save in the human Console, no `mosaic name`.
|
||||
- Outside reach doesn't change. There's no `--sso` and no `--send`. Both go on together in the Q175 phase 2
|
||||
window, which runs after this window is verified, when Jason starts it (mos-1195, mos-1197). The proof pair
|
||||
stays unset. The tunnel still forwards 7332 only (P4 and W10).
|
||||
- One Console flag does change. Row 196 retires the tester's TCP port. The live Console runs
|
||||
`--tester-port 7333`, and NEW refuses that flag. So W5 converts it explicitly with `--tester-socket`, and
|
||||
the tester listener becomes the 0600 socket `$DATA/console/tester.sock`. Only 127.0.0.1 could reach port
|
||||
7333, so nobody outside gains or loses reach. Mos ruled this Q173 work in mos-1211 item 2. The socket is
|
||||
narrower than loopback TCP, and Mos's acceptance in mos-1015 (on sage-1359) already accepted losing
|
||||
browser access to 7333. DECISIONS has no Jason ruling on 7333 (mos-1214 item 2).
|
||||
- Under mos-1198, .53 and .54 take no copy carrying 196 until row 253 lands. This window is dragon-lin only.
|
||||
|
||||
## Names
|
||||
|
||||
- OLD, live: `4429a104f6d65ccf165f6c83b5d1dce3147dfde0`, schema 3i, pointer `pm: true`, activation
|
||||
`20261020T220206Z-4429a104f6d6`. The Console runs `--port 7332 --public-host mosaic.woltje.com --auth-header
|
||||
X-authentik-username --allow-user jason.woltje --tester-port 7333`, read from `status` at 19:38Z. P1
|
||||
re-reads all of these on the day. I don't copy them from this file.
|
||||
- NEW: `7247d80c7af7ffa70fc61557406bf491cdf3bfc7`, the row 196 landing, the latest code landing when this
|
||||
draft was cut (mos-1161). It is 135 commits past OLD (`git rev-list --count 4429a104f..7247d80c7`). The commit
|
||||
after it, c3d88840c, is doc-only and doesn't move NEW. If 244 or 251 land before P0, NEW stays here unless
|
||||
Mos rules otherwise (Q1).
|
||||
- 194 hasn't landed, so P3 has no lifecycle step (mos-1161).
|
||||
- `REPO`, `P`, `WIN=agents/sage/work/window-repin-7247d80c7/` and `SNAP` are as in `repin-4429a104f.md`, with
|
||||
the sha checks at P0 and before S1, S2 and any D3.
|
||||
|
||||
## What NEW carries over OLD
|
||||
|
||||
This is draft 0's table, rows 201 through 234 plus PRD 1.7 and 1.8, unchanged, together with these rows from
|
||||
`git log --first-parent c771a36f4..7247d80c7`, limited to packages, scripts, roles, contracts, adapters,
|
||||
skills, tools, migrations and the package files:
|
||||
|
||||
| Row | Commit | Live effect to check |
|
||||
|---|---|---|
|
||||
| 240 | c515273db | Installer I4: root host check, pins and the boot unit. This window doesn't run them. |
|
||||
| 228 | 86249ac55 | ZAI_API_KEY joins the session env allowlist. The live config sets no ZAI key, so the env doesn't change. P3 checks this. |
|
||||
| 238 | d49b1f2bd | sso-config takes `--instance`. Default instance with `--sso` off, so no effect. |
|
||||
| 212 | ee7b2f137 | New package, packages/library. The live business has no library entry. |
|
||||
| 195 | b65014647 | scripts/case1-accept.sh, root only. Not run. |
|
||||
| 213 | 361ba2387 | Creative roles, persona file and block, launch-settings, bundle.mjs. P3 checks that the chief's and the pm's bundles are unchanged apart from the new code path, and gets one pm reply. |
|
||||
| 231 | 29ce1e360 | Library reader fixes. No live library. |
|
||||
| 235 | b1fece6f0 | Machine grant row b. The live business has no hosts. P3: bus start runs the grant reconcile and sweep, they change nothing, and the log lines are shown. |
|
||||
| 221, 247 | 300766450, 856c1826b | Persona import script and follow-up. Not run. yaml 2.9 joins package-lock, and W0's npm ci covers it. |
|
||||
| 245, 248, 249 | 6d5545d44, 8a04f4aa4, 9de5a57ab | case1-accept F9, test stubs, and `hostLineLeft` on a grant whose install never returned. No grants are live. The look-alike count below covers `hostLineLeft`. |
|
||||
| 197 | a36523713 | Test lane fixture. No live effect. |
|
||||
| 66 | 2c38325ec | Managed sessions run the unslop check. COPY_PATHS adds tools/unslop-hook, and P3 checks the copy has it. W12 is the live check. |
|
||||
| 243 | 6096b051b | Tests only. |
|
||||
| 215 | e54e05463 | Library entry kind, consumer skips, M9 and the bare mirror. M9 refuses an organization write that changes `projects`, so P3 checks that the chief's and the pm's usual organization writes don't touch it. |
|
||||
| 250 | 1b6158ce2 | Conversation shim and K12 test. No live effect on its own. |
|
||||
| 222 | 0b33f5e9d | library.sync verb, a 60 s timer per library entry, and the askpass helper in COPY_PATHS. No library entry, so the timer starts nothing. P3: the copy has `packages/library/bin/askpass` with its mode, and bus start logs no `library:` line. |
|
||||
| PRD 1.9, 1.10 | f48725cfc, add0f1aae | `packages/tasks/requirements.json` is in the copy. W9: the pm's requirements read answers version 1.10 with 70 ids. |
|
||||
| 196 | 7247d80c7 | The human boundary. See the next section. |
|
||||
|
||||
## Row 196 in this window
|
||||
|
||||
What the code does (`scripts/stack-release.sh:791-845`, `docs/TOOLS.md` "Console tester listener"):
|
||||
|
||||
- An inherited `--tester-port` with no `--tester-socket` refuses `tester-transport-migration` before anything
|
||||
stops. So `--tester-socket` goes on W3 and W5, and the plan's drop line must read
|
||||
`--tester-port 7333 (replaced by private Unix socket)`.
|
||||
- Before the stop, activate checks the socket path: `$DATA/console/tester.sock` must fit 107 bytes, and
|
||||
`console/` must be absent or a directory with no symlink, owned by the instance's uid, with no group or
|
||||
other bits. A failure refuses `tester-socket-path` before the stop. P1 reads `ls -ld $DATA/console`
|
||||
(expected absent).
|
||||
- Activate preflights the pinned root-owned system Python, records its identity, and checks the bus's fresh
|
||||
start receipt. Each bus start appends one line to `$DATA/bus/peercred-starts.jsonl`. P3's rehearsal must show
|
||||
the preflight passing on dragon-lin, and W9 reads the one new receipt line.
|
||||
- `rehearse` starts NEW's Console with `--tester-socket` under its scratch data root when the copy carries
|
||||
the socket marker (`stack-release.sh:2153-2155`). It doesn't run activate's path check from line 815, so
|
||||
that check first runs live at W3. P1's `ls -ld $DATA/console` plus the path length are its only preview.
|
||||
If the rehearsal Console fails to start on the socket, P3 stops and the log goes to Mos.
|
||||
- Direct browser access to the tester is gone, with Mos's acceptance in mos-1015. W10's browser check on
|
||||
127.0.0.1:7333 becomes `node packages/cli/src/tester-http.mjs GET /api/config` and `GET /api/bus/agents`, run from
|
||||
the NEW copy against the live data root. Each must answer 200 with the same tester flags OLD's 7333 gave.
|
||||
`mosaic ... --tester` still uses broker.sock.
|
||||
|
||||
Rollback is where 196 matters most. Without `--sso`, rollback takes the Console flags from the pointer, and
|
||||
after W5 the pointer carries `--tester-socket`. OLD's Console doesn't support the socket. So rollback to OLD
|
||||
drops the tester listener, the plan prints `--tester-socket (tester disabled; no TCP fallback)`, and port 7333
|
||||
doesn't come back. The test client then has no tester listener until a new activate. Q4 asks Mos how to
|
||||
handle that.
|
||||
|
||||
## Schema and backups
|
||||
|
||||
- `SCHEMA_VERSION` is 3i at OLD and 3j at NEW (`packages/bus/src/store.mjs:17`). The only step is
|
||||
`packages/bus/src/migrations/3i-3j.sql` (row 234, tables `hosts` and `host_grants`). Nothing after c771a36f4
|
||||
changes schema.sql or adds a step. `--plan` must say `migration: 3i -> 3j`.
|
||||
- Restore guard, recounted at NEW (T46, `packages/cli/tests/stack-release.test.mjs:913`). COUNTED is the
|
||||
same 11 tables (`stack-release.sh:404`). The untimed list has six: `host_grants`, `hosts`, `meta`,
|
||||
`message_threads`, `sqlite_sequence` and `task_comment_cursors`. Each write to `hosts` or `host_grants` goes
|
||||
with a `grant.*` event, which is counted. A D3 snapshot still covers them, as draft 0 says.
|
||||
- W5 takes the stopped store backup every activate takes (SHA256SUMS, SCHEMA 3i, STAMP) plus the rollback
|
||||
set. Before `--migrate` writes, I check that SCHEMA reads 3i and record the backup's SHA256SUMS hash. W0b
|
||||
copies the business file, as last time.
|
||||
- Grant look-alikes (row 234 A22b, gizmoduck-0107): P3, and again just before W5, on the backup copy. Count the
|
||||
`action.allowed` events whose body.action or body.operation starts with `grant.`, or whose body has
|
||||
`hostLineLeft`. Expect 0. Any nonzero count stops the window until I've read those rows.
|
||||
- Rollback after the migration isn't the plain rollback. OLD is 3i and the store is 3j, so going back needs
|
||||
`--restore <W5 activation>`, which loses the events written after W5. Mos decides any restore past W5.
|
||||
|
||||
## Before the window (nothing stopped)
|
||||
|
||||
| # | Who | Step |
|
||||
|---|-----|------|
|
||||
| P0 | Mos | The `$P` sha check, and this file's commit. |
|
||||
| P1 | Sage | `status`: pointer, activation, schema 3i, Console flags, unit flags match. The business file sha256 and `business-check.mjs` LOAD_OK for OLD now and NEW after W0. `ls -ld $DATA/console` (expected absent). `bus.tester` is `true`. |
|
||||
| W0 | Sage | `scripts/stack-copy.sh copy $NEW` in a gap between gate runs. The package-lock diff is empty. The copy has `tools/unslop-hook` and `packages/library/bin/askpass`, with the mode NEW's tree gives it. |
|
||||
| P2 | Sage | `console-check.sh $NEW` on a stub Console on a scratch port, once without and once with `--tester-socket` under a scratch data root. CONSOLE_PASS both. Render the Console unit for NEW from REPO's `webui-service.sh` and diff it against the live unit. The only difference allowed is `--tester-port 7333` becoming `--tester-socket`. Diff the bus unit too. It must match apart from the copy path. |
|
||||
| P3 | Mos | `stack-release.sh rehearse $NEW mosaic-stack --backup <latest live activation> --port <scratch>` with `--migrate` on the backup copy. It must show the 3j step, row counts before and after, the interpreter preflight passing, a receipt line, the grant reconcile and sweep changing nothing, no `library:` line, no publish or land decision open (rows 241, 242), the look-alike count 0, and one pm reply. As in mos-1107, Mos also starts NEW's Console by hand with the pointer's flags, with `--tester-socket` in place of `--tester-port`, and reads healthz, `/api/config` and the two tester-http reads. |
|
||||
| P4 | Sage | The tunnel read. |
|
||||
| P5 | Sage | The four service scripts are held at NEW (`git diff --quiet $NEW HEAD --` on them, and empty porcelain). If 244 or 251 lands and touches one of them, P5 fails and the window waits for Mos. |
|
||||
| P6 | Sage | The 7332 matrix (a)-(g) on OLD, saved as `$WIN/matrix-old.txt`, plus the two tester reads on 7333 with curl, for W10 to compare against. |
|
||||
|
||||
## The window
|
||||
|
||||
| # | Who | Step |
|
||||
|---|-----|------|
|
||||
| W0b | Sage | The business file copy. |
|
||||
| W1 | Sage | Idle check, the start stamp from `date -u`, then S1 with `$SNAP --held`. Open pm tasks from row 42: pm finishes or parks them before W5, or I record which piece crosses the window. |
|
||||
| W2 | Sage | `status`: pointer on OLD, `pm: true`, 3i, unit flags match. |
|
||||
| W3 | Sage | `stack-release.sh activate $NEW mosaic-stack --pm --migrate --tester-socket --plan`. It must show OLD to NEW, `migration: 3i -> 3j`, the backup line, the drop line for `--tester-port 7333`, and a Console line with OLD's other flags exactly: no `--send`, no `--sso`, no proof pair. Any other difference stops here. |
|
||||
| W4 | Sage | The unit reads, as in the 177-185 plan. |
|
||||
| W5 | Sage, on Mos's go | The same command without `--plan`. Exit 0, `result ok`, and every gate check passes, `pm_launched` included. Record the activation id, the backup path, STAMP and the backup SHA256SUMS sha256. Then `status`. |
|
||||
| W9 | Sage, after Mos verifies W5 | The 177-185 W9 list, plus: the webui journal since the W5 stamp (with `UTC`) has no `Sign-in:` line; `/proc/<Console pid>/cwd` names the NEW copy; `$DATA/console/tester.sock` is 0600 in a 0700 `console/`; `peercred-starts.jsonl` gained one line for this start; the bus journal has the grant reconcile and sweep lines and no `library:` line; the pm's requirements read gives 1.10 with 70 ids. |
|
||||
| W10 | Sage | The matrix again, as `$WIN/matrix-new.txt`, identical to `matrix-old.txt` apart from the lines the pin changes, each one named. The two tester reads through `tester-http.mjs` match P6's. Port 7333 has no listener (`ss -ltn`). Then P4 again. A write without a signed-in session must still be refused. That's Console POST through the main port, with no session. This is the check mos-1195 asks for before phase 2 is done, so I run it here too, as a baseline. |
|
||||
| W11 | Sage | STATE report in `$WIN/STATE.md`, posted on P19 386, 412 and 435 (J2, mos-1215), and on the Gitea issues for 190, 199, 210, 234 and 196. |
|
||||
| W12 | Sage, after W11 | Row 66's live check (Vikunja #454). One Claude Code managed session with a prompt that draws a known tell, then the operator note on the RESULT, the next-turn notice and the `unslop.jsonl` counts. Backup first (Q173). Pi isn't run (#383). |
|
||||
|
||||
## Stop conditions and rollback
|
||||
|
||||
The same as the 177-185 plan. Two failures in one window: stop and report (mos-0823).
|
||||
|
||||
Rollback before W5 writes is `rollback --plan`, then the real run. After the migration it needs `--restore
|
||||
<W5 activation>`, with Mos deciding. Both drop the tester listener (see Row 196 above). Keep every copy.
|
||||
Prune nothing.
|
||||
|
||||
## Questions for the plan check, answered (mos-1214 item 3)
|
||||
|
||||
- Q1: NEW stays 7247d80c7 unless a landing fixes something this window needs.
|
||||
- Q2: mos-1211 item 2 rules the `--tester-port 7333` to `--tester-socket` change Q173 work.
|
||||
- Q3: Darkwing operates if free, and I check. Otherwise I operate.
|
||||
- Q4: after a rollback, the tester listener stays off for the window's length, and the tester goes through
|
||||
broker.sock. A same-copy re-activation of OLD with `--tester-port 7333` goes to Mos only if a rollback
|
||||
holds past the window day.
|
||||
|
||||
## What Sage ran for this plan
|
||||
|
||||
Read only, 2026-10-21 between 20:40Z and 21:20Z: the path-limited first-parent log c771a36f4..7247d80c7,
|
||||
`git rev-list --count`, `SCHEMA_VERSION` at both ends, `git diff --name-only` on the schema and migrations,
|
||||
`stack-release.sh` lines 40-47, 125-158, 404, 791-845, 886-919 and 2140-2157, T46 in
|
||||
`stack-release.test.mjs:913`, `packages/cli/src/tester-http.mjs`, and TOOLS.md:1274-1310 on the tester
|
||||
listener. Nothing ran against
|
||||
live. OLD's values come from the 19:38Z `status` read, and P1 reads them again.
|
||||
@@ -1432,3 +1432,4 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-21T19:46:12Z | Dewey (Opus 5.5, 84fb346c) | row 222 r1 static review (megavolt-0095), c9aa9c2ee, manifest c2121b55 | APPROVE, comment 30138 on #1723, queue rev 2966; rulings: createObject=rename accepted, 5 s human timeout deferred with STATUS codes in the follow-up, credential.helper reset argv-only accepted; notes N1-N5; no tests run
|
||||
2026-10-21T19:49:37Z | Sage (Opus 5.5, 1ef1e4f8) | row 250 landing, row 196 r5 grant (mos-1198) and resume (mos-1200), F27 row 253 | 250 LANDED 1b6158ce2 (patch-id 29786caa, Filbert rev 2957, Gizmoduck slot green), #1755 closed c30139, Vikunja 450; 196 r5 91e3696ef hunk checked by Duckworth, webui 304/0; my queue add tripped Gander's busy check, not a stop (mos-1200), resume on 05e940117; row 253 #1758 brief 0cdead66c
|
||||
2026-10-21T20:46:03Z | Sage (Opus 5.5, 1ef1e4f8) | row 222 landing, PRD 1.9 and 1.10, Pocket ID design row 255, row 254 brief, 196 r6 grants | 222 LANDED 0b33f5e9d (megavolt-0098 gate, Dewey rev 2966, patch-id edb60b4f), #1723 closed c30147, done rev 2972; follow-up row 254 (#1759, after 196); PRD 1.9 f48725cfc (REQ-WEB-2, REQ-DEC-3 amended, mos-1202); 458 brief 7d08237e1 (#1760), row 255 rev 2976; PRD 1.10 add0f1aae REQ-INST-2 post-v1 (mos-1204, tasks 97/0, bus req 3/0); Vikunja 458 c478, 416 c479; queue-commit 21def2850 revs 2968-2976; 196 r6 gate gander-0066 2334/0, 38/38 killed; 244 granted sage-2147; 196 r6 resume granted sage-2145 after self-tee busy refusal (mos-1205). Miss: landed 222 before checking 196's manifest; 196 carried results voided. Class rule now (mos-1204, replaces the mos-1200 interface grep): before any landing push, compare every in-review and in-progress row's manifest against the landing's paths.
|
||||
2026-10-21T21:14Z | Sage (claude-opus-5-5, 1ef1e4f8) | row 196 landed 7247d80c7 and doc commit c3d88840c (#1694 closed, comment 30159); rows 253 and 254 briefed and launched (Gander #1758, Megavolt #1759); Honker 194 b4 rebase ruling (creative delete stays refused, Agents page hides Delete on the server predicate, actor-threaded delete deferred); queue revs 2977-2994 committed abb352d27 incl. Morgana 244, Gander 196 r6, Duckworth 196 m2, Darkwing 251 r2; 10-24 window draft 1 written (NEW 7247d80c7, --tester-socket, Mos mos-1211 agrees Q173) | landed and launched; draft 1 to Mos next
|
||||
@@ -234,10 +234,16 @@ When a run doesn't exit 0, send Mos the exit and the last lines of `/var/log/mos
|
||||
Precheck lines: none. Root's checkout at `<sha>` and the stack's user's clone: precheck line missing.
|
||||
After install, `stack-copy.sh check --units external` reads the copy the units run.
|
||||
|
||||
The stack's user's `scripts/case2-install.sh` (C2-13b, design 10 step 2b) makes the first copy with
|
||||
`stack-copy.sh copy <sha>`. The copy runs `npm ci` from `package-lock.json`, which installs the pinned
|
||||
harness, so the host needs to reach the npm registry during the install. The install runs no
|
||||
`release.sh package` or `activate`.
|
||||
|
||||
## 7. The args file
|
||||
|
||||
`/etc/mosaic-stack/dev.args` holds the `case2-unit.sh` arguments, one per line, with the serve flags after a
|
||||
line holding `--`. I2's install writes it; ops-01 doesn't write it by hand. It must be root's, with no group
|
||||
line holding `--`. Root writes it at the unit render, from the values the go names. The stack's user's
|
||||
install never writes it: that user can't write `/etc`, and root reads nothing that user wrote. It must be root's, with no group
|
||||
or other write (`chmod 0644` or tighter). Before `--` it takes only `--user`, `--home`, `--business`,
|
||||
`--copy` (exactly once), `--node`, `--profile`, `--claude` and `--pm`. `stack-upgrade.sh` changes only the
|
||||
`--copy` line, and only after confirm passes.
|
||||
@@ -272,7 +278,7 @@ file with `stat -c '%U %a %n' PATH`, never `cat`. `<business>` is the host's bus
|
||||
|---|---|---|---|---|---|
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/` | `claude-session.env`, one line `ZAI_API_KEY=...` | 0600 | mosaic | library | `<path> sets ZAI_API_KEY` |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/` | the Console's proof file, the path the args file's `--proof-file` names | 0600 or 0400 | mosaic | library | none. `case2-unit.sh` refuses at render. Precheck line missing. |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/<business>/` | `library-gitea` | 0600 | mosaic | library | C2-13b's `library.sync` (not landed) |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/<business>/` | `library-gitea` | 0600 | mosaic | library | `case2-precheck.sh --business <business> --library-status`, after the bus is up: `library-last-error library-credential-missing` until it's placed, then `library <project> last fetched ...` |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/agents/<business>/<agent>/` | one file per service | 0600, dirs 0700 | mosaic | both | none. Filled only by `mosaic credentials issue`, or Approve after #436. Never by hand. |
|
||||
|
||||
The installer key, `<configDir>/secrets/hosts/<business>/<hostId>/installer`, is not a slot anyone places.
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Brief: the human deletes a creative instance (REQ-COS-3, after row 194)
|
||||
|
||||
Sage, 2026-10-21, for mos-1214 item 5 and mos-1215 item 3. Owner Honker, reviewer Webby, gate owner Sage.
|
||||
Chained after row 194: `deleteRefusal` and the delete path land in 194, so this row builds on 194's
|
||||
landing. The row has to land before the v1 tag for 10-27. If it slips past that date, Sage tells Mos before
|
||||
194's v1 tag (mos-1215).
|
||||
|
||||
## Why
|
||||
|
||||
REQ-COS-3 (`docs/prd/mosaic-stack.md:342`) says "Only the user deletes an instance." Creative entries are
|
||||
instances: they sit in the business file's `instances`, and only the human's `instances.set` adds or changes
|
||||
one (C2-08 M10, row 213). Row 213 made every creative definition END_PROTECTED (`vocabulary.mjs:195`,
|
||||
`END_PROTECTED = THREAD_DEFINITIONS`). In row 194, `deleteRefusal` refuses END_PROTECTED for every actor
|
||||
(`organization-definition-refused`), so nobody can delete a creative instance. The human can only delist one
|
||||
with `launch: false`. This row lets the human delete one, which matches END_PROTECTED's intent: a protected
|
||||
end goes to the human (sage-0525).
|
||||
|
||||
## What this row does
|
||||
|
||||
1. **Human-only delete of a creative instance.** Pass the actor into `deleteRefusal`, and through every
|
||||
caller: the broker's check at the verb, the launcher pass's `set op delete`, the CLI and the Console.
|
||||
- A creative definition (`CREATIVE_DEFINITIONS`) whose actor is the business file's `human` passes this
|
||||
check and goes on to the remaining checks (references, kept, credentials).
|
||||
- Any other actor still gets a refusal. Use the code `creativeHumanOnly` gives
|
||||
(`organization-creative-human-only`), so the refusal says why.
|
||||
- Every other END_PROTECTED definition (the thread definitions that aren't creative, such as the chief
|
||||
of staff) still refuses `organization-definition-refused` for every actor, the human included.
|
||||
2. **Fire stays as it is.** `role.fire` and `instance.fire` keep refusing END_PROTECTED for every actor.
|
||||
REQ-COS-3's fire is the chief's action, and the chief can never hire a creative instance.
|
||||
3. **Agents page.** Row 194 hides Delete on `endProtected`. Offer Delete on a creative instance only on the
|
||||
human's page, from the same server predicate, and never on the tester listener. Add one browser test for
|
||||
each side.
|
||||
4. **Persona file.** Say in BUILD.md what happens to `<configDir>/personas/<business>/<instance>.md` on
|
||||
delete. The smallest choice is to leave it in place and record that. If you remove it, open it with
|
||||
`O_NOFOLLOW` as row 213 reads it, and add a test.
|
||||
5. **DEFERRED.** Close row 194's DEFERRED entry for human delete of a creative entry, and name this row in
|
||||
it.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to who may add or change a creative instance (M10), or to `role.hire`'s bounds.
|
||||
- No change to fire, veto or `launch.keep`.
|
||||
- No test touches `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, a slot file, .53 or .54.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves the manifest. On Sage's slot, every touched test file passes whole, along with the
|
||||
business, bus, cli and webui suites whole and every `scripts/test-*.sh`. Two mutants must be killed: one
|
||||
that lets a non-human actor delete a creative instance, and one that lets the human delete a non-creative
|
||||
END_PROTECTED instance.
|
||||
+6
-6
@@ -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 2976 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
Generated from `docs/plans/queue.json` revision 2994 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
|
||||
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
@@ -232,7 +232,7 @@ Generated from `docs/plans/queue.json` revision 2976 by `queue render`. Do not e
|
||||
| 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 | sage-1610 (honker-0049): a human add takes tracker {bot, botId}, required for a vikunja definition, refused otherwise, checked by checkBot and bot uniqueness; CLI and API only. Delete accepts a tracker bot when all credentials are store refs, and its result names the bot for removal by hand in Vikunja. No Vikunja account is created (Q188 untouched). Mutants: drop the requirement, refuse the bot on delete, skip checkBot on add. |
|
||||
| 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 | done | 190 done, 226 done, 233 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) | LANDED b65014647 (pushed 361ba2387 tip). Patch-id 656e43ff25de = 4ced5e63d..37171f65b; verify-commit ok, 5 paths. Combined gate with 213 on 38b83ea5c+paths 09:45:52-09:57:52Z: business 200/0 bus 542/0 harness 140/0 cli 663/0, all 10 scripts green, L1 killed. N1-N3 to DEFERRED next; follow-up darkwing/row195-n1 1e3c4767c. |
|
||||
| 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) | — |
|
||||
| 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 | done | — | 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) | Landed 7247d80c7 on 666173292: 39 manifest paths of 527b9a1e6 (CANDIDATE.sha256 537d30ae), patch-id 6f3777d593e6 matches f48725cfc..527b9a1e6 on the manifest, blob diff empty, tree matches merge-tree. Gate gander-0066 2334/0/1, all 10 shells, 38/38 mutants. Approved rev 2973 and rev 2986 (Duckworth, c30156). Overlap: 202 cli README and 194 Honker branches rebase onto it. BUILD-LOG and C1 DEFERRED follow doc-only. |
|
||||
| 197 | a second gate lane: two full gates at once on dragon-lin, design first (mos-0977 item 6) | herb; reviewers webby | #1695 | done | — | 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 | Landed a36523713 on refactor (base 5afff5490, pushed 18:40Z): Herb's c45ddeb6b cherry-picked. Range-diff = (only the cherry-pick line), patch-id 79c46b98c685 equals c45ddeb6b and candidate A d2de56bd2; 37 paths equal, marker test absent (mos-1174 check). Proof 6 PROOF_PASSED 18:40:06Z, sage-r197p6: 8 waves 26/26 steps, cb A 124 B 125, probe ok, no event or cleanup errors, final CLEAR/CLEAR. Webby approved r8 (rev 2867). |
|
||||
| 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 | done | — | 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 | Landed cb5fdb064 on refactor (base 76134dfde), pushed. Patch-id 828fa21a matches git diff 00d4cc1f6..86963d281 on the 37 manifest paths; docs/TOOLS.md 3-way with row 200, hunk unchanged. Manifest 35f900fd, Duckworth approve r1 29779. Gate on 9e1085816+patch: packages 2735/0, scripts 429/0; landed blobs equal the gate tree. |
|
||||
@@ -280,17 +280,17 @@ Generated from `docs/plans/queue.json` revision 2976 by `queue render`. Do not e
|
||||
| 241 | Row 237 follow-up: one decision lands once, a two-ticket broker land test and three mutants on the landing guards (megavolt-0069, sage-1651, P19 441) | megavolt; reviewers duckworth | #1747 | done | 237 done | Duckworth approves the manifest. The bus, seat and cli suites and every scripts/test-*.sh are green on Sage's gate, and the three mutants are killed. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: row 237 follow-up, one decision lands once, with tests at the broker | Landed 5d4a61776 on refactor, pushed (base e65a3ab4d). Blobs from 3e3ed303e on 2 paths; patch-id a1649f80 equals git diff f5753c0e8..3e3ed303e on those paths; ls-tree modes equal; manifest e8266864 verifies on the landed files. Sage slot on bbf3b1ee8: M1-M3 killed; gate 15 packages 2797/0, 10 script suites 429/0. Duckworth approve 29870 rev 2628. |
|
||||
| 242 | Row 237 follow-up N1: a retry after publish-timeout pushes once; hold the decision or kill and wait for the push, a slow-push retry test and a mutant (duckworth-0069, P19 440) | megavolt; reviewers duckworth | #1748 | done | 237 done | Duckworth approves the manifest. The bus and cli suites and every scripts/test-*.sh are green on Sage's gate, and the mutant is killed. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: row 237 follow-up N1, a retry after publish-timeout pushes once | Landed 5fb736b92 on refactor, pushed (base 96bac588f). Blobs from 635c10924 on 3 paths; patch-id ceb741ee equals git diff 8fede5fc6..635c10924 on those paths; ls-tree modes equal; manifest 1fbfab6c verifies on the landed files. Megavolt M1/M2 killed on Sage's slot; Sage gate 15 packages 2826/0, 10 script suites 429/0. Duckworth approve 29889 rev 2639. |
|
||||
| 243 | process.test.mjs startup token refusal sets its file mode (chmod), umask sweep of packages/*/tests | morgana; reviewers webby | #1754 | done | 197 done | Webby approves the manifest. The test file passes whole under umask 022 and under umask 077 on Sage's slot. (sage) | `docs/plans/2026-10-21_gate-env-rows.md` § The startup token refusal test sets its file mode | LANDED 6096b051b on 9a8c8860d, pushed. Patch-id f9d687dd1d41 equals a36523713..6581f750e; 5 paths, blobs and modes equal the candidate; no drift on those paths since a36523713. Gate: Webby r1 APPROVE rev 2939 (c30111); slot singles (morgana-0041): candidate 4 files whole green at umask 022 and 077, base 077 red only on the predicted tests. |
|
||||
| 244 | Gate runs use a fixture PI_AUTH_FILE (DEFERRED 059577886, Webby N4) | morgana; reviewers webby | #1757 | briefed | 197 done | Webby approves the manifest. The new test and every touched file are green on Sage's slot, along with test-task.sh and test-release.sh. (sage) | `docs/plans/2026-10-21_gate-env-rows.md` § Gate runs use a fixture pi auth file | — |
|
||||
| 244 | Gate runs use a fixture PI_AUTH_FILE (DEFERRED 059577886, Webby N4) | morgana; reviewers webby | #1757 | in-review, round 1, request posted | 197 done | Webby approves the manifest. The new test and every touched file are green on Sage's slot, along with test-task.sh and test-release.sh. (sage) | `docs/plans/2026-10-21_gate-env-rows.md` § Gate runs use a fixture pi auth file | — |
|
||||
| 245 | row 195 follow-up: Duckworth's N1-N3 on scripts/case1-accept.sh (root never runs an unchecked --node; the header lists what root runs; F6 refuses .. after the claude pin dir) | darkwing; reviewers duckworth | #1693; closes none | done | — | Duckworth approves the manifest. The cli suite and every scripts/test-*.sh are green on Sage's gate, and a mutant for each of N1 and N3 is killed. (sage) | `docs/plans/2026-10-20_hub-own-uid.md` § Row 195 follow-up: Duckworth's N1-N3 on the Case 1 acceptance script | LANDED 6d5545d44 on 856c1826b, pushed. Patch-id 36a621ac3966 equals 62ddc96cc..ef8b4439e. Gate: Duckworth r1 APPROVE rev 2849; N1 mutants 13-15 and N3 mutant 16 killed (sage-1961, darkwing-0107); Sage gate 12:24-12:35Z: case1-accept alone 23/0, cli 681/0, scripts/test-*.sh 429/0. Lands before row 246 as the brief asks. #1693 stays open for 246. |
|
||||
| 246 | row 195 window: the Case 1 acceptance run on 10.1.1.53 (carries row 195's done condition; needs a Jason or Mos go) | darkwing; reviewers duckworth | #1693 | queued | 226 done, 245 done | The window's acceptance run passes on .53, Duckworth reads its output, and Mos confirms. (sage) | `docs/plans/2026-10-20_hub-own-uid.md` § Row 195 window: the Case 1 acceptance run on 10.1.1.53 | — |
|
||||
| 247 | Case 2 C2-20b persona import follow-up: printed keys, the dry run fence check and the mkdir race | gosalyn; reviewers webby | #1749 | done | 221 done | Webby approves the manifest. The test file runs green on its own, and the three mutants are killed on Sage's slot. The cli suite and every scripts/test-*.sh are green on Sage's gate. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-20b persona import follow-up: printed keys, the dry run fence check and the mkdir race | LANDED 856c1826b on f8276c5eb, pushed. Patch-id 373ab2cc4c50 equals 6d43618a3's. Gate: Webby r1 APPROVE rev 2852; test file alone 14/0 and M1-M4 killed (sage-1971); Sage gate 12:10-12:21Z: file 14/0, cli 680/0, scripts/test-*.sh 429/0. M1/M2 at :248 not :237 (gosalyn-0111). |
|
||||
| 248 | row 235 follow-up: the grant-executor hold-add and hold-remove stubs stop when their test dir is gone (found by the 197 r8 proof) | sage; reviewers webby | #1750 | done | — | Webby approves the commit. On Sage's gate the whole grant-executor.test.mjs passes three times with no stub left, the bus suite is green, and a mutant that drops the dir check leaves a stub. (sage) | `docs/plans/2026-10-20_host-credentials.md` § Machine grant row b follow-up: the held stubs end with their test | LANDED 8a04f4aa4 on refactor, pushed. Temp-index landing of candidate 4d99c178b's one path onto bc579f7a8. No drift on the path since 540239b6b. Patch-id 84974d6b08d6 matches the candidate's. Gate per the brief: Webby APPROVE r1 rev 2883, Sage's gate 13:05-13:07Z (file 3x 37/0 no stub, bus 596/0, mutant left 2). |
|
||||
| 249 | row 235 follow-up: the grant undo records hostLineLeft when install never returned, since the remote add can outlive an abort or timeout (mos-1166) | gizmoduck; reviewers duckworth | #1751 | done | — | Duckworth approves the manifest. On Sage's gate the bus suite and the whole touched test files are green, and a mutant that drops the new catch branch is killed. (sage) | `docs/plans/2026-10-20_host-credentials.md` § Machine grant row b follow-up: the undo can't prove the remote add has ended | LANDED 9de5a57ab on refactor, pushed with 7b9737953 (DEFERRED N1). Range-diff: 8a04f4aa4..05595794b vs 8ca064158..9de5a57ab, same 3 paths, patch-id 99551b122e66 on both. Gate sage-2026 14:53Z: grant-executor whole 39/0, bus 598/0, no stub left, C1 (drop :1025) killed by G8 and both G23. Duckworth approve c30070. |
|
||||
| 250 | K12 (cohort.test.mjs) reads cgroup.events after cgroup.kill under load: print the shim's reason, then fix the cause at the source (Vikunja #450) | gizmoduck; reviewers filbert | #1755 | done | 197 done | reviewer approves the manifest; conversation and control-board suites whole on Sage's slot (sage) | `docs/plans/2026-10-21_k12-cohort-events.md` § Brief: K12 reads cgroup.events after cgroup.kill under load | LANDED 1b6158ce2 on refactor 19:42Z: candidate c1a858975 on 3b0aef995, manifest eca0b8e2, Filbert APPROVE rev 2957 (#1755 c30130). patch-id 29786caa = candidate; blob diff empty; tree 59f1e4781 = merge-tree. Gate on Sage slot sage-2109 (gizmoduck-0150): P1/P2/M1 as predicted, conversation whole 3x 183/183 plus concurrent pair, control-board 124/124. No drift on paths since base. |
|
||||
| 251 | T27 (host-run.test.mjs:453) lists no identities right after ssh-add -t 1: print timing and agent state, then fix the cause at the source (Vikunja #453) | darkwing; reviewers dewey | #1756 | in-review, round 1, request posted | 197 done | reviewer approves the manifest; bus suite whole on Sage's slot (sage) | `docs/plans/2026-10-21_t27-ssh-add-lifetime.md` § Brief: T27 lists no identities right after ssh-add -t 1 | mos-1185 item 2. If proof 6 fails outside 197's paths in a pair wave, this and row 250 go ahead of 197 (item 4). |
|
||||
| 251 | T27 (host-run.test.mjs:453) lists no identities right after ssh-add -t 1: print timing and agent state, then fix the cause at the source (Vikunja #453) | darkwing; reviewers dewey | #1756 | in-review, round 2, request posted | 197 done | reviewer approves the manifest; bus suite whole on Sage's slot (sage) | `docs/plans/2026-10-21_t27-ssh-add-lifetime.md` § Brief: T27 lists no identities right after ssh-add -t 1 | mos-1185 item 2. If proof 6 fails outside 197's paths in a pair wave, this and row 250 go ahead of 197 (item 4). |
|
||||
| 252 | Console inbox Approve and Deny for grant requests (#435, J2) | gander; reviewers webby | — | briefed | — | Webby approves DESIGN.md then manifest; webui, bus, cli suites and every test-*.sh green on Sage's gate; DESIGN mutants killed; REQ-WEB-2 PRD amendment landed first (sage) | `docs/plans/2026-10-21_console-inbox-approve.md` § Gate | mos-1193. Chained on row 196's candidate (Q177): design starts now on Gander's row 196 build; code rebases onto 196's landing. Vikunja #435. |
|
||||
| 253 | F27: case2-unit.sh and case1-accept.sh move to --tester-socket and refuse --tester-port with stack-release's migration wording; no .53/.54 copy with 196 until this lands (mos-1198 item 4) | gander; reviewers duckworth | #1758 | queued | 196 done | reviewer approves DESIGN.md then the manifest; cli and webui suites whole and every scripts/test-*.sh on Sage's slot; mutants per DESIGN.md; static check covers scripts and tests (sage) | `docs/plans/2026-10-21_tester-socket-scripts.md` § Brief: case2-unit.sh and case1-accept.sh move to --tester-socket (F27) | — |
|
||||
| 254 | Library sync follow-up: client timeout, STATUS codes, README (222 deferrals, #1759) | megavolt; reviewers dewey | — | queued | 196 done | Dewey approves the manifest; bus, library and webui suites whole and every scripts/test-*.sh green on Sage's slot; the timeout and STATUS mutants killed (sage) | `docs/plans/2026-10-21_library-sync-followup.md` § Gate | Dewey's deferrals from 222 approval c30138. Chained after 196 (human-cli channel proof). Issue #1759. |
|
||||
| 253 | F27: case2-unit.sh and case1-accept.sh move to --tester-socket and refuse --tester-port with stack-release's migration wording; no .53/.54 copy with 196 until this lands (mos-1198 item 4) | gander; reviewers duckworth | #1758 | briefed | 196 done | reviewer approves DESIGN.md then the manifest; cli and webui suites whole and every scripts/test-*.sh on Sage's slot; mutants per DESIGN.md; static check covers scripts and tests (sage) | `docs/plans/2026-10-21_tester-socket-scripts.md` § Brief: case2-unit.sh and case1-accept.sh move to --tester-socket (F27) | — |
|
||||
| 254 | Library sync follow-up: client timeout, STATUS codes, README (222 deferrals, #1759) | megavolt; reviewers dewey | #1759 | briefed | 196 done | Dewey approves the manifest; bus, library and webui suites whole and every scripts/test-*.sh green on Sage's slot; the timeout and STATUS mutants killed (sage) | `docs/plans/2026-10-21_library-sync-followup.md` § Gate | Dewey's deferrals from 222 approval c30138. Chained after 196 (human-cli channel proof). Issue #1759. |
|
||||
| 255 | Bundled Pocket ID for users with no OIDC provider, design (Q203, #1760) | launchpad; reviewers webby | — | queued | 252 done | Webby approves DESIGN.md; design only, build rows follow after Sage checks it against Q203, O6, REQ-WEB-2 and REQ-INST-1 (sage) | `docs/plans/2026-10-21_bundled-pocket-id.md` § Gate | Vikunja 458, Gitea #1760, mos-1202 item 2. REQ-INST-2 lands in PRD 1.10 as post-v1 (mos-1204). Design only: no install, no download, no .53/.54, dragon-lin keeps Authentik. |
|
||||
|
||||
<!-- mosaic-queue:end -->
|
||||
|
||||
+608
-20
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"canonicalRoot": "/mnt/storage/src/mosaic-stack",
|
||||
"revision": 2976,
|
||||
"revision": 2994,
|
||||
"rows": [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -20514,7 +20514,7 @@
|
||||
"closes": [
|
||||
1694
|
||||
],
|
||||
"state": "in-progress",
|
||||
"state": "done",
|
||||
"previousState": null,
|
||||
"required": false,
|
||||
"requiredSince": null,
|
||||
@@ -20584,18 +20584,69 @@
|
||||
"candidate": "d9fefc49ab1affb02d68b35b658772da4e82c724255611a7fe1d59b48b62006c"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": 2,
|
||||
"op": "gander-196-manifest-r6",
|
||||
"by": "gander",
|
||||
"at": "2026-10-21T20:51:08.099Z",
|
||||
"issue": 1694,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "537d30aef70b6cd19ab7283ce2a702d9ac55bb179c60d6a55f50b45a0e5b149d",
|
||||
"text": "0ad63c4f7a6cdd5b2587a0969fe7a95539cf1e4601f51fb33cc4df94a718b2b5 docs/TOOLS.md\n10c996d323ac46d45548f7748d40b246de93f8c5eacae9f951c6eeb5dcc62697 packages/bus/README.md\n60720461a7994011a9a30594715eb2c992be699cd61c656e77cbd609ad05328e packages/bus/fixtures/peer-client.mjs\n0c6d96abe4607cba0b02b0638a2fc217ff8135bac9a47761a0ef015fbe9d3079 packages/bus/src/human.mjs\nd5d98a9133a1b449acc48143fd559d890a470c6ea210b30b9b70e491e6cd855a packages/bus/src/interpreter.mjs\n29c88655e8b9aed8db604573c9213fd9f3c40ef35ff04f4eda8fbe8f1b8a32d7 packages/bus/src/peercred.mjs\nd1536a8f472fa5b3773adb10555529ed049ad63b647c0b44c00ce5f25ce09f85 packages/bus/src/peercred.py\n32ef39d80b523bdd586c29620d7a752c309c8e1aa70befc1414125468f3411dd packages/bus/src/principal.mjs\n190a4f56c44131f8a2325bc9c4a822556de432d1afd2e75ffe3168a171320bed packages/bus/src/runtime.mjs\n7be306c830aa98763b34b329995b40e9df443483fe20fcd69100a3acb6311bd6 packages/bus/src/server.mjs\ne99133c3c957a1e856e404343721add2430e4a0d8c75452517aef04942af56ab packages/bus/tests/channel-proof.test.mjs\nd19779b97e80ed3d62f58d0e798df46c23f45fa02b14fb4ac7a2ff4690a734e2 packages/bus/tests/interpreter.test.mjs\na39af877a2d9143cc8d0584af0d603ca7ab651db8787e5816579762f17295887 packages/bus/tests/peer-transport.test.mjs\n2c4c0bed41e546ce94535d8c4547b1cf5552a181183525dd1d52e2af1d64ccb6 packages/bus/tests/peercred.test.mjs\ncefbc0688d823c69196d94bb71467884acd639070721286d7e8e4e986c8240c1 packages/bus/tests/principal.test.mjs\n95c7acdb47f753d1e129adaa90b5937318d8d006ca47be9d92729fdae013776f packages/cli/README.md\n4bd7bdf0898844684ebf9444b110d2c5b6fa874b2681183e54f1f62f97823192 packages/cli/src/tester-http.mjs\na7cb20db2941b3b3abb09bcaa55eac92a1d420cf4ec79c6f83e4bb3235e04a2b packages/cli/tests/stack-release-fixture.mjs\n414d39e37d8e332761a6120ab728f67538e36082bf1f609eec7747bebdb434d4 packages/cli/tests/stack-release.test.mjs\nfbaa36e64129aee324341364bde831c379c098b97932cceadb7fcaa7909d4b20 packages/cli/tests/tester-http.test.mjs\n04b7c1165c4fa73137a72e8373d70fd7d96a5897b5511170e6d41908a7667e96 packages/webui/README.md\n934ab2f9b6bde72aafe7814758550c283eb25a4f3a23b284021b13718d67178f packages/webui/src/bus.mjs\n63dc2af0ff0c8f7c624268a78ffeede1593182a38d657a53e597338edf09226e packages/webui/src/cli.mjs\nda624f188c69e016ff82e1d1dcae1c579e68a57f0f25d773bb5d8bca56b80ee1 packages/webui/src/serve.mjs\n1c1fe0c795adac97f28502564a75f26d5666d9be5ae51169f7dd5bb7741fad16 packages/webui/src/tester-socket.mjs\n2f3ea92cbe1ca115301ee1e3b8fc55a206842606237446b700881ee57a1b18cb packages/webui/tests/browser.mjs\nf505b5fc635aa699556c237a92401b495943e45e25ebfadab945d64477e3e86e packages/webui/tests/library-cli.test.mjs\nedef361c4654ec010809c12c87c00197ec6e6b8337863068c4ffa144e291e300 packages/webui/tests/proof-cli.test.mjs\n5f34d699a1bce6f46f286def1a93fdd4d863d06bec6839677fae1041be7153dd packages/webui/tests/proof.test.mjs\n6457debc3d233a24643be00e4876e1129db664da2fbf5cc7168f3e371c46f313 packages/webui/tests/service.test.mjs\n3c1d2481eff2271c1c83f7ec2e9d616ac22600d032af23afc47f919c58f1b9ba packages/webui/tests/sso.test.mjs\n9215ff92df774ba1e83819b87f758b53c8f3d19c2824c916490165914f1cb9e8 packages/webui/tests/tester-browser-fixture.mjs\n89ffc5cbed1630a523080e8561d3678c1f781fb657199a5cd6cdb8189f082c73 packages/webui/tests/tester-browser.test.mjs\n23219434713fbd283a1e5cb1a373ed9d19d0963e171636dbf39bed3d60cd1316 packages/webui/tests/tester-cli.test.mjs\n7d4f3a59d21675807b1bfb92d40b7378134df450590bc6d42fa4103ab1343348 packages/webui/tests/tester-fixture.mjs\n7f0fc2daead11048cb86e0a74b6a223314785b1740a2927461d012b876a69f18 packages/webui/tests/tester-listener.test.mjs\nc01a942da1ad66a08a4e7c92d3c7e64f517530acfe5517f1e9895767d5ea3c0b packages/webui/tests/tester-socket.test.mjs\nb916a2793311d21e3ce7117e9b5e0768e6e33c841fa3aa39fb7ffc9c5974acd2 scripts/stack-release.sh\n6e7069cca9f7aef84c17f15611f2d7e0d09083a363c23da8dbd14d3ed29f0f40 scripts/webui-service.sh\n"
|
||||
},
|
||||
"request": "comment",
|
||||
"attempts": [
|
||||
{
|
||||
"op": "gander-196-manifest-r6",
|
||||
"by": "gander",
|
||||
"at": "2026-10-21T20:51:08.099Z",
|
||||
"state": "failed",
|
||||
"comment": null,
|
||||
"transport": {
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"resolutions": []
|
||||
},
|
||||
{
|
||||
"op": "sage-196-review-r2b",
|
||||
"by": "sage",
|
||||
"at": "2026-10-21T20:53:25.208Z",
|
||||
"state": "posted",
|
||||
"comment": 30155,
|
||||
"transport": {
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 30155,
|
||||
"detail": "created"
|
||||
},
|
||||
"resolutions": []
|
||||
}
|
||||
],
|
||||
"duplicateRisk": false,
|
||||
"receipts": [
|
||||
{
|
||||
"reviewer": "duckworth",
|
||||
"op": "duckworth-0098-record",
|
||||
"at": "2026-10-21T20:54:59.089Z",
|
||||
"verdict": "approve",
|
||||
"comment": 30156,
|
||||
"candidate": "537d30aef70b6cd19ab7283ce2a702d9ac55bb179c60d6a55f50b45a0e5b149d"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"claim": {
|
||||
"seat": "gander",
|
||||
"op": "gander-196-start"
|
||||
},
|
||||
"note": null,
|
||||
"claim": null,
|
||||
"note": "Landed 7247d80c7 on 666173292: 39 manifest paths of 527b9a1e6 (CANDIDATE.sha256 537d30ae), patch-id 6f3777d593e6 matches f48725cfc..527b9a1e6 on the manifest, blob diff empty, tree matches merge-tree. Gate gander-0066 2334/0/1, all 10 shells, 38/38 mutants. Approved rev 2973 and rev 2986 (Duckworth, c30156). Overlap: 202 cli README and 194 Honker branches rebase onto it. BUILD-LOG and C1 DEFERRED follow doc-only.",
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-20T19:01:13.621Z",
|
||||
"updatedAt": "2026-10-21T20:17:46.980Z",
|
||||
"updatedBy": "gander"
|
||||
"updatedAt": "2026-10-21T20:58:33.437Z",
|
||||
"updatedBy": "sage"
|
||||
},
|
||||
{
|
||||
"id": 197,
|
||||
@@ -25085,7 +25136,7 @@
|
||||
"closes": [
|
||||
1757
|
||||
],
|
||||
"state": "briefed",
|
||||
"state": "in-review",
|
||||
"previousState": null,
|
||||
"required": false,
|
||||
"requiredSince": null,
|
||||
@@ -25105,12 +25156,63 @@
|
||||
"reviewers": [
|
||||
"webby"
|
||||
],
|
||||
"review": null,
|
||||
"claim": null,
|
||||
"review": {
|
||||
"rounds": [
|
||||
{
|
||||
"n": 1,
|
||||
"op": "morgana-244-inreview-r1",
|
||||
"by": "morgana",
|
||||
"at": "2026-10-21T20:49:44.772Z",
|
||||
"issue": 1757,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "6e7e3e3c290fe5034cc8766b6dc7eeb3725a99f330837a357c66381d18be1b60",
|
||||
"text": "87e5deae3021116da0e78ce9c47bc2899dbcbc4d7207b8b293400d9b1549807c agents/morgana/work/queue-244/BUILD.md\n75d83e2cd7bc7173a0b8175600df819fb2bdf522d3930b265a78d4fcd31186d1 packages/cli/tests/gate-lane-auth.test.mjs\n5063d9458b2bf61ae85e8470f713f326a2ae1e19e6dd85f71ba531782b312fa8 packages/cli/tests/gate-lane-image.test.mjs\nbe706dff832257bd4781abeabd19faa4ac16edf9a4af0c96457e33cf3bb6deb3 scripts/test-release.sh\n10c4abb70bf9b83e617089b06a5436cc7fa6b866a176458b22128c3a15163f94 scripts/test-task.sh\nb488c7f275721e3e7b93d02a91345c0f5af99be0a5aed2e3f171d46ceca24521 scripts/testing/README.md\n8c5b08b136eda3964d8729ef646ff09f028c8348eded3bb6f79fb3fdd101bee7 scripts/testing/gate-lane.mjs\n10e87fcc9bce0cead8ff27af27cc6e0910820cd15da8e449cb5c07e379392e75 scripts/testing/lane-docker.mjs\n"
|
||||
},
|
||||
"request": "comment",
|
||||
"attempts": [
|
||||
{
|
||||
"op": "morgana-244-inreview-r1",
|
||||
"by": "morgana",
|
||||
"at": "2026-10-21T20:49:44.772Z",
|
||||
"state": "failed",
|
||||
"comment": null,
|
||||
"transport": {
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"resolutions": []
|
||||
},
|
||||
{
|
||||
"op": "sage-244-review-r1b",
|
||||
"by": "sage",
|
||||
"at": "2026-10-21T20:52:11.784Z",
|
||||
"state": "posted",
|
||||
"comment": 30154,
|
||||
"transport": {
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 30154,
|
||||
"detail": "created"
|
||||
},
|
||||
"resolutions": []
|
||||
}
|
||||
],
|
||||
"duplicateRisk": false,
|
||||
"receipts": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"claim": {
|
||||
"seat": "morgana",
|
||||
"op": "morgana-244-start"
|
||||
},
|
||||
"note": null,
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-21T09:15:58.616Z",
|
||||
"updatedAt": "2026-10-21T19:33:20.763Z",
|
||||
"updatedAt": "2026-10-21T20:52:44.248Z",
|
||||
"updatedBy": "sage"
|
||||
},
|
||||
{
|
||||
@@ -25709,6 +25811,41 @@
|
||||
],
|
||||
"duplicateRisk": false,
|
||||
"receipts": []
|
||||
},
|
||||
{
|
||||
"n": 2,
|
||||
"op": "darkwing-251-inreview-r2",
|
||||
"by": "darkwing",
|
||||
"at": "2026-10-21T21:10:40.372Z",
|
||||
"issue": 1756,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "f2bdd68461976b682df8ae0d7cf0e01d64eebf90162eafb1e3a505956c8ce278",
|
||||
"text": "e7d64f42fcf125d3a170cc00d749a5c6d0102f2130bd7c4bbe7f8c0ce8aef51c agents/darkwing/work/queue-251/BUILD.md\n280132c727bb3f9f392f6e95a6ff199efe947c4b40c1e8896640f75498cd6044 packages/bus/tests/host-run.test.mjs\n"
|
||||
},
|
||||
"request": "comment",
|
||||
"attempts": [
|
||||
{
|
||||
"op": "darkwing-251-inreview-r2",
|
||||
"by": "darkwing",
|
||||
"at": "2026-10-21T21:10:40.372Z",
|
||||
"state": "posted",
|
||||
"comment": 30164,
|
||||
"transport": null,
|
||||
"resolutions": [
|
||||
{
|
||||
"verb": "resolve",
|
||||
"op": "darkwing-251-resolve-r2",
|
||||
"by": "darkwing",
|
||||
"at": "2026-10-21T21:12:19.309Z",
|
||||
"comment": 30164,
|
||||
"reason": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"duplicateRisk": false,
|
||||
"receipts": []
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -25719,7 +25856,7 @@
|
||||
"note": "mos-1185 item 2. If proof 6 fails outside 197's paths in a pair wave, this and row 250 go ahead of 197 (item 4).",
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-21T17:13:07.643Z",
|
||||
"updatedAt": "2026-10-21T19:18:59.931Z",
|
||||
"updatedAt": "2026-10-21T21:12:19.309Z",
|
||||
"updatedBy": "darkwing"
|
||||
},
|
||||
{
|
||||
@@ -25761,7 +25898,7 @@
|
||||
"closes": [
|
||||
1758
|
||||
],
|
||||
"state": "queued",
|
||||
"state": "briefed",
|
||||
"previousState": null,
|
||||
"required": false,
|
||||
"requiredSince": null,
|
||||
@@ -25786,16 +25923,20 @@
|
||||
"note": null,
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-21T19:47:54.542Z",
|
||||
"updatedAt": "2026-10-21T19:47:54.542Z",
|
||||
"updatedAt": "2026-10-21T21:11:12.638Z",
|
||||
"updatedBy": "sage"
|
||||
},
|
||||
{
|
||||
"id": 254,
|
||||
"piece": "Library sync follow-up: client timeout, STATUS codes, README (222 deferrals, #1759)",
|
||||
"owner": "megavolt",
|
||||
"issues": [],
|
||||
"closes": [],
|
||||
"state": "queued",
|
||||
"issues": [
|
||||
1759
|
||||
],
|
||||
"closes": [
|
||||
1759
|
||||
],
|
||||
"state": "briefed",
|
||||
"previousState": null,
|
||||
"required": false,
|
||||
"requiredSince": null,
|
||||
@@ -25820,7 +25961,7 @@
|
||||
"note": "Dewey's deferrals from 222 approval c30138. Chained after 196 (human-cli channel proof). Issue #1759.",
|
||||
"blockedReason": null,
|
||||
"createdAt": "2026-10-21T20:17:15.497Z",
|
||||
"updatedAt": "2026-10-21T20:17:15.497Z",
|
||||
"updatedAt": "2026-10-21T21:11:45.908Z",
|
||||
"updatedBy": "sage"
|
||||
},
|
||||
{
|
||||
@@ -104262,6 +104403,453 @@
|
||||
"receipt": "ok sage-r458-add rev 2976 row 255 none→queued"
|
||||
},
|
||||
"viewSha": "9b2b48147dff353bc92e8f47eb3d9bc0f248b2b382d7148d1591f1c8437cce04"
|
||||
},
|
||||
{
|
||||
"rev": 2977,
|
||||
"op": "morgana-244-start",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 244,
|
||||
"to": "in-progress",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "morgana",
|
||||
"at": "2026-10-21T20:49:10.163Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 244,
|
||||
"from": "briefed",
|
||||
"to": "in-progress",
|
||||
"round": null,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok morgana-244-start rev 2977 row 244 briefed→in-progress"
|
||||
},
|
||||
"viewSha": "1923cb487ec169ba5eeb58cdaba354b3a0f061136fddd3b99fc67706af18a64a"
|
||||
},
|
||||
{
|
||||
"rev": 2978,
|
||||
"op": "morgana-244-inreview-r1",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 244,
|
||||
"to": "in-review",
|
||||
"reason": null,
|
||||
"candidate": "/mnt/storage/scratch/morgana-r244/candidate-manifest.sha256",
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "morgana",
|
||||
"at": "2026-10-21T20:49:44.772Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 244,
|
||||
"from": "in-progress",
|
||||
"to": "in-review",
|
||||
"round": 1,
|
||||
"issue": 1757,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "6e7e3e3c290fe5034cc8766b6dc7eeb3725a99f330837a357c66381d18be1b60",
|
||||
"text": "87e5deae3021116da0e78ce9c47bc2899dbcbc4d7207b8b293400d9b1549807c agents/morgana/work/queue-244/BUILD.md\n75d83e2cd7bc7173a0b8175600df819fb2bdf522d3930b265a78d4fcd31186d1 packages/cli/tests/gate-lane-auth.test.mjs\n5063d9458b2bf61ae85e8470f713f326a2ae1e19e6dd85f71ba531782b312fa8 packages/cli/tests/gate-lane-image.test.mjs\nbe706dff832257bd4781abeabd19faa4ac16edf9a4af0c96457e33cf3bb6deb3 scripts/test-release.sh\n10c4abb70bf9b83e617089b06a5436cc7fa6b866a176458b22128c3a15163f94 scripts/test-task.sh\nb488c7f275721e3e7b93d02a91345c0f5af99be0a5aed2e3f171d46ceca24521 scripts/testing/README.md\n8c5b08b136eda3964d8729ef646ff09f028c8348eded3bb6f79fb3fdd101bee7 scripts/testing/gate-lane.mjs\n10e87fcc9bce0cead8ff27af27cc6e0910820cd15da8e449cb5c07e379392e75 scripts/testing/lane-docker.mjs\n"
|
||||
},
|
||||
"receipt": "ok morgana-244-inreview-r1 rev 2978 row 244 in-progress→in-review round 1 on #1757; request morgana-244-inreview-r1 requesting"
|
||||
},
|
||||
"viewSha": "ffc26c20562fd80286b5d36f796d235f13c89fbc1b828b88187c1350de4abb0f"
|
||||
},
|
||||
{
|
||||
"rev": 2979,
|
||||
"op": "morgana-244-inreview-r1.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 244,
|
||||
"attempt": "morgana-244-inreview-r1",
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"by": "morgana",
|
||||
"at": "2026-10-21T20:50:16.649Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 244,
|
||||
"round": 1,
|
||||
"attempt": "morgana-244-inreview-r1",
|
||||
"state": "failed",
|
||||
"receipt": "ok morgana-244-inreview-r1.outcome rev 2979 row 244 round 1 request morgana-244-inreview-r1 failed"
|
||||
},
|
||||
"viewSha": "39f8cf4a5854d75ba63b0706d8bffd312fe51821ce712a3f7d1c6aa787fecff2"
|
||||
},
|
||||
{
|
||||
"rev": 2980,
|
||||
"op": "gander-196-manifest-r6",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 196,
|
||||
"to": "in-review",
|
||||
"reason": null,
|
||||
"candidate": "/home/jwoltje/.t3/worktrees/mosaic-stack/gander-row196/agents/gander/work/queue-196/CANDIDATE.sha256",
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "gander",
|
||||
"at": "2026-10-21T20:51:08.099Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 196,
|
||||
"from": "in-progress",
|
||||
"to": "in-review",
|
||||
"round": 2,
|
||||
"issue": 1694,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "537d30aef70b6cd19ab7283ce2a702d9ac55bb179c60d6a55f50b45a0e5b149d",
|
||||
"text": "0ad63c4f7a6cdd5b2587a0969fe7a95539cf1e4601f51fb33cc4df94a718b2b5 docs/TOOLS.md\n10c996d323ac46d45548f7748d40b246de93f8c5eacae9f951c6eeb5dcc62697 packages/bus/README.md\n60720461a7994011a9a30594715eb2c992be699cd61c656e77cbd609ad05328e packages/bus/fixtures/peer-client.mjs\n0c6d96abe4607cba0b02b0638a2fc217ff8135bac9a47761a0ef015fbe9d3079 packages/bus/src/human.mjs\nd5d98a9133a1b449acc48143fd559d890a470c6ea210b30b9b70e491e6cd855a packages/bus/src/interpreter.mjs\n29c88655e8b9aed8db604573c9213fd9f3c40ef35ff04f4eda8fbe8f1b8a32d7 packages/bus/src/peercred.mjs\nd1536a8f472fa5b3773adb10555529ed049ad63b647c0b44c00ce5f25ce09f85 packages/bus/src/peercred.py\n32ef39d80b523bdd586c29620d7a752c309c8e1aa70befc1414125468f3411dd packages/bus/src/principal.mjs\n190a4f56c44131f8a2325bc9c4a822556de432d1afd2e75ffe3168a171320bed packages/bus/src/runtime.mjs\n7be306c830aa98763b34b329995b40e9df443483fe20fcd69100a3acb6311bd6 packages/bus/src/server.mjs\ne99133c3c957a1e856e404343721add2430e4a0d8c75452517aef04942af56ab packages/bus/tests/channel-proof.test.mjs\nd19779b97e80ed3d62f58d0e798df46c23f45fa02b14fb4ac7a2ff4690a734e2 packages/bus/tests/interpreter.test.mjs\na39af877a2d9143cc8d0584af0d603ca7ab651db8787e5816579762f17295887 packages/bus/tests/peer-transport.test.mjs\n2c4c0bed41e546ce94535d8c4547b1cf5552a181183525dd1d52e2af1d64ccb6 packages/bus/tests/peercred.test.mjs\ncefbc0688d823c69196d94bb71467884acd639070721286d7e8e4e986c8240c1 packages/bus/tests/principal.test.mjs\n95c7acdb47f753d1e129adaa90b5937318d8d006ca47be9d92729fdae013776f packages/cli/README.md\n4bd7bdf0898844684ebf9444b110d2c5b6fa874b2681183e54f1f62f97823192 packages/cli/src/tester-http.mjs\na7cb20db2941b3b3abb09bcaa55eac92a1d420cf4ec79c6f83e4bb3235e04a2b packages/cli/tests/stack-release-fixture.mjs\n414d39e37d8e332761a6120ab728f67538e36082bf1f609eec7747bebdb434d4 packages/cli/tests/stack-release.test.mjs\nfbaa36e64129aee324341364bde831c379c098b97932cceadb7fcaa7909d4b20 packages/cli/tests/tester-http.test.mjs\n04b7c1165c4fa73137a72e8373d70fd7d96a5897b5511170e6d41908a7667e96 packages/webui/README.md\n934ab2f9b6bde72aafe7814758550c283eb25a4f3a23b284021b13718d67178f packages/webui/src/bus.mjs\n63dc2af0ff0c8f7c624268a78ffeede1593182a38d657a53e597338edf09226e packages/webui/src/cli.mjs\nda624f188c69e016ff82e1d1dcae1c579e68a57f0f25d773bb5d8bca56b80ee1 packages/webui/src/serve.mjs\n1c1fe0c795adac97f28502564a75f26d5666d9be5ae51169f7dd5bb7741fad16 packages/webui/src/tester-socket.mjs\n2f3ea92cbe1ca115301ee1e3b8fc55a206842606237446b700881ee57a1b18cb packages/webui/tests/browser.mjs\nf505b5fc635aa699556c237a92401b495943e45e25ebfadab945d64477e3e86e packages/webui/tests/library-cli.test.mjs\nedef361c4654ec010809c12c87c00197ec6e6b8337863068c4ffa144e291e300 packages/webui/tests/proof-cli.test.mjs\n5f34d699a1bce6f46f286def1a93fdd4d863d06bec6839677fae1041be7153dd packages/webui/tests/proof.test.mjs\n6457debc3d233a24643be00e4876e1129db664da2fbf5cc7168f3e371c46f313 packages/webui/tests/service.test.mjs\n3c1d2481eff2271c1c83f7ec2e9d616ac22600d032af23afc47f919c58f1b9ba packages/webui/tests/sso.test.mjs\n9215ff92df774ba1e83819b87f758b53c8f3d19c2824c916490165914f1cb9e8 packages/webui/tests/tester-browser-fixture.mjs\n89ffc5cbed1630a523080e8561d3678c1f781fb657199a5cd6cdb8189f082c73 packages/webui/tests/tester-browser.test.mjs\n23219434713fbd283a1e5cb1a373ed9d19d0963e171636dbf39bed3d60cd1316 packages/webui/tests/tester-cli.test.mjs\n7d4f3a59d21675807b1bfb92d40b7378134df450590bc6d42fa4103ab1343348 packages/webui/tests/tester-fixture.mjs\n7f0fc2daead11048cb86e0a74b6a223314785b1740a2927461d012b876a69f18 packages/webui/tests/tester-listener.test.mjs\nc01a942da1ad66a08a4e7c92d3c7e64f517530acfe5517f1e9895767d5ea3c0b packages/webui/tests/tester-socket.test.mjs\nb916a2793311d21e3ce7117e9b5e0768e6e33c841fa3aa39fb7ffc9c5974acd2 scripts/stack-release.sh\n6e7069cca9f7aef84c17f15611f2d7e0d09083a363c23da8dbd14d3ed29f0f40 scripts/webui-service.sh\n"
|
||||
},
|
||||
"receipt": "ok gander-196-manifest-r6 rev 2980 row 196 in-progress→in-review round 2 on #1694; request gander-196-manifest-r6 requesting"
|
||||
},
|
||||
"viewSha": "9f6ae73cc3c59e22f71ac340a7d09c5c9c4de226e3bf98f3bf45f33851a020a5"
|
||||
},
|
||||
{
|
||||
"rev": 2981,
|
||||
"op": "gander-196-manifest-r6.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 196,
|
||||
"attempt": "gander-196-manifest-r6",
|
||||
"outcome": "failed",
|
||||
"status": null,
|
||||
"comment": null,
|
||||
"detail": "pre-send: the credential or account check failed"
|
||||
},
|
||||
"by": "gander",
|
||||
"at": "2026-10-21T20:51:39.942Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 196,
|
||||
"round": 2,
|
||||
"attempt": "gander-196-manifest-r6",
|
||||
"state": "failed",
|
||||
"receipt": "ok gander-196-manifest-r6.outcome rev 2981 row 196 round 2 request gander-196-manifest-r6 failed"
|
||||
},
|
||||
"viewSha": "f79bfeade3f582818a8ff4e8cd34ee5f9a8f54828d1c95d8bd62d227b34338e6"
|
||||
},
|
||||
{
|
||||
"rev": 2982,
|
||||
"op": "sage-244-review-r1b",
|
||||
"verb": "review-request",
|
||||
"args": {
|
||||
"id": 244
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-21T20:52:11.784Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 244,
|
||||
"round": 1,
|
||||
"attempt": "sage-244-review-r1b",
|
||||
"state": "requesting",
|
||||
"receipt": "ok sage-244-review-r1b rev 2982 row 244 round 1 request sage-244-review-r1b requesting"
|
||||
},
|
||||
"viewSha": "88925213d32cb6b355fe6c54a293ff16f473a174aadb46b4aa1f196fabe36e1f"
|
||||
},
|
||||
{
|
||||
"rev": 2983,
|
||||
"op": "sage-244-review-r1b.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 244,
|
||||
"attempt": "sage-244-review-r1b",
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 30154,
|
||||
"detail": "created"
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-21T20:52:44.248Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 244,
|
||||
"round": 1,
|
||||
"attempt": "sage-244-review-r1b",
|
||||
"state": "posted",
|
||||
"receipt": "ok sage-244-review-r1b.outcome rev 2983 row 244 round 1 request sage-244-review-r1b posted comment 30154"
|
||||
},
|
||||
"viewSha": "4b3da809621219ffbff95bff6adf31f71440a496f04d5a3e506cf4bfb75eb444"
|
||||
},
|
||||
{
|
||||
"rev": 2984,
|
||||
"op": "sage-196-review-r2b",
|
||||
"verb": "review-request",
|
||||
"args": {
|
||||
"id": 196
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-21T20:53:25.208Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 196,
|
||||
"round": 2,
|
||||
"attempt": "sage-196-review-r2b",
|
||||
"state": "requesting",
|
||||
"receipt": "ok sage-196-review-r2b rev 2984 row 196 round 2 request sage-196-review-r2b requesting"
|
||||
},
|
||||
"viewSha": "c1d882f65b6ed95b007c78b8a532a80a731e34dcf70ce5b3c85eafb3832a6efd"
|
||||
},
|
||||
{
|
||||
"rev": 2985,
|
||||
"op": "sage-196-review-r2b.outcome",
|
||||
"verb": "review-outcome",
|
||||
"args": {
|
||||
"id": 196,
|
||||
"attempt": "sage-196-review-r2b",
|
||||
"outcome": "posted",
|
||||
"status": 201,
|
||||
"comment": 30155,
|
||||
"detail": "created"
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-21T20:53:57.304Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 196,
|
||||
"round": 2,
|
||||
"attempt": "sage-196-review-r2b",
|
||||
"state": "posted",
|
||||
"receipt": "ok sage-196-review-r2b.outcome rev 2985 row 196 round 2 request sage-196-review-r2b posted comment 30155"
|
||||
},
|
||||
"viewSha": "cd92dcbc6f2c8a57380369e9c57dd09cf6db128545ff11e77fa246d80bfe614a"
|
||||
},
|
||||
{
|
||||
"rev": 2986,
|
||||
"op": "duckworth-0098-record",
|
||||
"verb": "review-record",
|
||||
"args": {
|
||||
"id": 196,
|
||||
"verdict": "approve",
|
||||
"comment": 30156,
|
||||
"candidate": "537d30aef70b6cd19ab7283ce2a702d9ac55bb179c60d6a55f50b45a0e5b149d"
|
||||
},
|
||||
"by": "duckworth",
|
||||
"at": "2026-10-21T20:54:59.089Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 196,
|
||||
"round": 2,
|
||||
"verdict": "approve",
|
||||
"receipt": "ok duckworth-0098-record rev 2986 row 196 round 2 approve by duckworth (comment 30156)"
|
||||
},
|
||||
"viewSha": "204d0c2d64d27c6e3480f866cea0c2aa3657851fc56dd2ee2b3ecdd3cdb0ed1b"
|
||||
},
|
||||
{
|
||||
"rev": 2987,
|
||||
"op": "sage-196-landing-note",
|
||||
"verb": "note",
|
||||
"args": {
|
||||
"id": 196,
|
||||
"text": "Landed 7247d80c7 on 666173292: 39 manifest paths of 527b9a1e6 (CANDIDATE.sha256 537d30ae), patch-id 6f3777d593e6 matches f48725cfc..527b9a1e6 on the manifest, blob diff empty, tree matches merge-tree. Gate gander-0066 2334/0/1, all 10 shells, 38/38 mutants. Approved rev 2973 and rev 2986 (Duckworth, c30156). Overlap: 202 cli README and 194 Honker branches rebase onto it. BUILD-LOG and C1 DEFERRED follow doc-only."
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-21T20:58:02.224Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 196,
|
||||
"field": "note",
|
||||
"receipt": "ok sage-196-landing-note rev 2987 row 196 note"
|
||||
},
|
||||
"viewSha": "f6ee00477e8cb4bfea13eea5590c16b713eec9c485cc3c82da79cbbd53740473"
|
||||
},
|
||||
{
|
||||
"rev": 2988,
|
||||
"op": "sage-196-done",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 196,
|
||||
"to": "done",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-21T20:58:33.437Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 196,
|
||||
"from": "in-review",
|
||||
"to": "done",
|
||||
"round": 2,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok sage-196-done rev 2988 row 196 in-review→done round 2"
|
||||
},
|
||||
"viewSha": "785cd1b3d64c9fe685cef50753c9a04b5718ecc097d7199dfb9bc2cf27ae4361"
|
||||
},
|
||||
{
|
||||
"rev": 2989,
|
||||
"op": "sage-254-issue",
|
||||
"verb": "set",
|
||||
"args": {
|
||||
"id": 254,
|
||||
"field": "issues",
|
||||
"value": [
|
||||
1759
|
||||
],
|
||||
"reason": null
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-21T21:09:30.787Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 254,
|
||||
"field": "issues",
|
||||
"from": [],
|
||||
"to": [
|
||||
1759
|
||||
],
|
||||
"closes": {
|
||||
"from": [],
|
||||
"to": [
|
||||
1759
|
||||
],
|
||||
"narrowed": false
|
||||
},
|
||||
"receipt": "ok sage-254-issue rev 2989 row 254 issues: none→#1759; closes: none→#1759"
|
||||
},
|
||||
"viewSha": "209ae6f110d6eeed579b3f795719a05351dc6c7f37c151227d73f8bf89d2a53e"
|
||||
},
|
||||
{
|
||||
"rev": 2990,
|
||||
"op": "darkwing-251-inprogress-r2",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 251,
|
||||
"to": "in-progress",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "darkwing",
|
||||
"at": "2026-10-21T21:10:03.811Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 251,
|
||||
"from": "in-review",
|
||||
"to": "in-progress",
|
||||
"round": null,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok darkwing-251-inprogress-r2 rev 2990 row 251 in-review→in-progress"
|
||||
},
|
||||
"viewSha": "ee26d40e917bd52bf2770819fe5b5c81a2d3ea9546cb8c2113af215bd40e9519"
|
||||
},
|
||||
{
|
||||
"rev": 2991,
|
||||
"op": "darkwing-251-inreview-r2",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 251,
|
||||
"to": "in-review",
|
||||
"reason": null,
|
||||
"candidate": "/home/jwoltje/darkwing-scratch/r251/manifest-27f01ef63a6e9d2fcb7198ffc11ae9e389ab4da5.sha256",
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "darkwing",
|
||||
"at": "2026-10-21T21:10:40.372Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 251,
|
||||
"from": "in-progress",
|
||||
"to": "in-review",
|
||||
"round": 2,
|
||||
"issue": 1756,
|
||||
"candidate": {
|
||||
"kind": "manifest",
|
||||
"digest": "f2bdd68461976b682df8ae0d7cf0e01d64eebf90162eafb1e3a505956c8ce278",
|
||||
"text": "e7d64f42fcf125d3a170cc00d749a5c6d0102f2130bd7c4bbe7f8c0ce8aef51c agents/darkwing/work/queue-251/BUILD.md\n280132c727bb3f9f392f6e95a6ff199efe947c4b40c1e8896640f75498cd6044 packages/bus/tests/host-run.test.mjs\n"
|
||||
},
|
||||
"receipt": "ok darkwing-251-inreview-r2 rev 2991 row 251 in-progress→in-review round 2 on #1756; request darkwing-251-inreview-r2 requesting"
|
||||
},
|
||||
"viewSha": "f05f76b194c6b73232db4892921f4b56c310d5b493981287dd8a4217b23edec3"
|
||||
},
|
||||
{
|
||||
"rev": 2992,
|
||||
"op": "sage-253-briefed",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 253,
|
||||
"to": "briefed",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-21T21:11:12.638Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 253,
|
||||
"from": "queued",
|
||||
"to": "briefed",
|
||||
"round": null,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok sage-253-briefed rev 2992 row 253 queued→briefed"
|
||||
},
|
||||
"viewSha": "48947ebf2f45373f6443fbe62f43616b8c2dba7e3ed3d5134ae8e80383063658"
|
||||
},
|
||||
{
|
||||
"rev": 2993,
|
||||
"op": "sage-254-briefed",
|
||||
"verb": "move",
|
||||
"args": {
|
||||
"id": 254,
|
||||
"to": "briefed",
|
||||
"reason": null,
|
||||
"candidate": null,
|
||||
"evidence": null,
|
||||
"issue": null
|
||||
},
|
||||
"by": "sage",
|
||||
"at": "2026-10-21T21:11:45.908Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 254,
|
||||
"from": "queued",
|
||||
"to": "briefed",
|
||||
"round": null,
|
||||
"issue": null,
|
||||
"candidate": null,
|
||||
"receipt": "ok sage-254-briefed rev 2993 row 254 queued→briefed"
|
||||
},
|
||||
"viewSha": "ec736c1e91f42fc9e2141454630e1664000d2be2562c03052d04e08015e32af7"
|
||||
},
|
||||
{
|
||||
"rev": 2994,
|
||||
"op": "darkwing-251-resolve-r2",
|
||||
"verb": "review-resolve",
|
||||
"args": {
|
||||
"id": 251,
|
||||
"attempt": "darkwing-251-inreview-r2",
|
||||
"comment": 30164
|
||||
},
|
||||
"by": "darkwing",
|
||||
"at": "2026-10-21T21:12:19.309Z",
|
||||
"semantics": 2,
|
||||
"result": {
|
||||
"row": 251,
|
||||
"round": 2,
|
||||
"attempt": "darkwing-251-inreview-r2",
|
||||
"state": "posted",
|
||||
"receipt": "ok darkwing-251-resolve-r2 rev 2994 row 251 round 2 request darkwing-251-inreview-r2 requesting→posted comment 30164"
|
||||
},
|
||||
"viewSha": "ad44355179caf42d60dc72ac782bfb1abe147df011c93639df2a727f20c9832b"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -458,17 +458,36 @@ test('T27: ssh-add -t drops the key from an agent the test keeps alive', async (
|
||||
const agent = spawn('ssh-agent', ['-D', '-a', sock], { stdio: 'ignore' });
|
||||
t.after(() => agent.kill('SIGKILL'));
|
||||
assert.ok(await until(() => existsSync(sock)));
|
||||
await addKey(sock, readFileSync(join(dir, 'k'), 'utf8'), 1);
|
||||
const list = () => {
|
||||
// A failed list names its cause: when each step ran on the monotonic clock (hrtime) and on
|
||||
// /proc/uptime (CLOCK_BOOTTIME, which ssh-agent's lifetime counts in whole seconds), and
|
||||
// whether the agent and its socket were still there.
|
||||
const marks = [];
|
||||
const mark = (name) => marks.push({ name, ns: process.hrtime.bigint(), uptime: readFileSync('/proc/uptime', 'utf8').split(' ')[0] });
|
||||
const timing = () => {
|
||||
const at = (m) => `${m.name} +${(Number(m.ns - marks[0].ns) / 1e6).toFixed(1)} ms (hrtime ${(Number(m.ns % 1_000_000_000n) / 1e9).toFixed(4)} s into its second, uptime ${m.uptime})`;
|
||||
const alive = agent.exitCode === null && agent.signalCode === null;
|
||||
return `${marks.map(at).join('; ')}; agent ${alive ? 'alive' : `gone (${agent.exitCode ?? agent.signalCode})`}, socket ${existsSync(sock) ? 'present' : 'missing'}`;
|
||||
};
|
||||
// ssh-agent sets a key's death to the whole second of CLOCK_BOOTTIME at the add plus the
|
||||
// lifetime, and drops it once that whole second is reached. So `-t N` keeps a key between N - 1
|
||||
// and N seconds: `-t 1` added just before a second boundary was gone within milliseconds (#453).
|
||||
// `-t 2` keeps it at least 1 s, and the sleep waits past the 2 s ceiling.
|
||||
mark('addKey start');
|
||||
await addKey(sock, readFileSync(join(dir, 'k'), 'utf8'), 2);
|
||||
mark('addKey resolved');
|
||||
const list = (name) => {
|
||||
mark(name);
|
||||
try {
|
||||
return execFileSync('ssh-add', ['-l'], { env: { PATH: '/usr/bin:/bin', SSH_AUTH_SOCK: sock }, encoding: 'utf8' });
|
||||
} catch (e) {
|
||||
return e.stdout;
|
||||
}
|
||||
};
|
||||
assert.match(list(), /SHA256:/);
|
||||
await sleep(2500);
|
||||
assert.match(list(), /no identities/);
|
||||
const first = list('first list');
|
||||
assert.match(first, /SHA256:/, `first list got ${JSON.stringify(first)}: ${timing()}`);
|
||||
await sleep(3500);
|
||||
const second = list('second list');
|
||||
assert.match(second, /no identities/, `second list got ${JSON.stringify(second)}: ${timing()}`);
|
||||
});
|
||||
|
||||
// The test's own limit makes a hung addKey (M37a, M37b) fail in seconds, not at the batch timeout.
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
// Case 2 row C2-13b (#1727): scripts/case2-install.sh installs a hub as <user>, and
|
||||
// scripts/case2-precheck.sh's library checks and --library-status mode. Brief:
|
||||
// docs/plans/2026-10-20_case2-rows.md "C2-13b". Every case runs in a scratch home with its own
|
||||
// MOSAIC_CONFIG, a stub stack-copy and fake getent, sudo, id, node, bwrap and systemctl. The library
|
||||
// sync runs in-process against a local bare repo, as the bus would. Nothing here reads or writes
|
||||
// ~/.mosaic-dev, ~/.config/mosaic-dev, /etc or a network host, and no fixture value is a credential.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { businessDoc, writeJson } from "../../business/tests/helpers.mjs";
|
||||
import { BASE, MARKER, git, mappingEntry, repo } from "../../library/tests/helpers.mjs";
|
||||
import { ensureMirror, mirrorPath, sync } from "../../library/src/index.mjs";
|
||||
import { gitEnv } from "../../seat/src/checkout.mjs";
|
||||
import { SAFE } from "../../project/src/git.mjs";
|
||||
import { tmp } from "./helpers.mjs";
|
||||
|
||||
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
const INSTALL = join(REPO, "scripts", "case2-install.sh");
|
||||
const PRECHECK = join(REPO, "scripts", "case2-precheck.sh");
|
||||
const COMMIT = "c".repeat(40);
|
||||
const UID = process.getuid();
|
||||
const GID = process.getgid();
|
||||
// Made-up values; no output may hold them.
|
||||
const FIXTURE_KEY = "fixture-zai-key-8d41a7";
|
||||
const PERSONA_TEXT = { bean: "Bean is warm and brief. PERSONA-TEXT-bean-61f0\n", chet: "Chet is dry and exact. PERSONA-TEXT-chet-2b9e\n" };
|
||||
const sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
||||
const mode = (p) => statSync(p).mode & 0o777;
|
||||
|
||||
function script(file, text) {
|
||||
mkdirSync(dirname(file), { recursive: true });
|
||||
writeFileSync(file, `#!/usr/bin/env bash\n${text}\n`);
|
||||
chmodSync(file, 0o755);
|
||||
return file;
|
||||
}
|
||||
|
||||
// A scratch <user>: a home, an input business file, a persona staging dir and fakes.
|
||||
function world(t, { profile = "library" } = {}) {
|
||||
const root = tmp(t, "case2-install-");
|
||||
const home = join(root, "home");
|
||||
const configDir = join(home, ".config", "mosaic-dev");
|
||||
// claude-session.env is in place before the install, as 2d would leave it on a re-run.
|
||||
const secrets = join(configDir, "secrets");
|
||||
mkdirSync(secrets, { recursive: true, mode: 0o700 });
|
||||
chmodSync(configDir, 0o700);
|
||||
writeFileSync(join(secrets, "claude-session.env"), `ZAI_API_KEY=${FIXTURE_KEY}\n`, { mode: 0o600 });
|
||||
const doc = businessDoc(root);
|
||||
for (const r of Object.values(doc.roles)) r.vars = { ...r.vars, harness: "pi" };
|
||||
if (profile === "library") {
|
||||
doc.projects.books = { ...mappingEntry(), user: "writer" };
|
||||
doc.roles.bean = { definition: "companion", vars: { harness: "pi" } };
|
||||
doc.roles.chet = { definition: "companion", vars: { harness: "pi" } };
|
||||
} else doc.vars["bus.tester"] = true;
|
||||
const input = writeJson(join(root, "input", "acme.json"), doc);
|
||||
const staging = join(root, "staging");
|
||||
mkdirSync(staging, { mode: 0o700 });
|
||||
const personas = [];
|
||||
for (const [instance, name] of [["bean", "Bean"], ["chet", "Chet"]]) {
|
||||
writeFileSync(join(staging, `${instance}.md`), PERSONA_TEXT[instance], { mode: 0o600 });
|
||||
personas.push({ instance, name, bytes: Buffer.byteLength(PERSONA_TEXT[instance]), sha256: sha256(PERSONA_TEXT[instance]) });
|
||||
}
|
||||
writeFileSync(join(staging, "manifest.json"), `${JSON.stringify({ version: 1, personas }, null, 2)}\n`, { mode: 0o600 });
|
||||
const bin = join(root, "bin");
|
||||
const log = join(root, "calls.log");
|
||||
script(join(bin, "stack-copy"), `echo "stack-copy $*" >>'${log}'
|
||||
[ "$1" = copy ] && [ "$3" = --copies ] || exit 4
|
||||
mkdir -p "$4/$2/scripts" && : >"$4/$2/scripts/mosaic" && echo "copied: $4/$2"`);
|
||||
script(join(bin, "systemctl"), `echo "systemctl $*" >>'${log}'`);
|
||||
const me = spawnSync("id", ["-un"], { encoding: "utf8" }).stdout.trim();
|
||||
script(join(bin, "getent"), `[ "$1" = passwd ] && [ "$2" = '${me}' ] && echo '${me}:x:${UID}:${GID}::${home}:/bin/bash'`);
|
||||
script(join(bin, "sudo"), "exit 1");
|
||||
script(join(bin, "id"), `if [ "$1" = -G ]; then echo '${GID}'; elif [ "$1" = -u ] && [ -n "\${CASE2_TEST_UID:-}" ]; then echo "$CASE2_TEST_UID"; else exec /usr/bin/id "$@"; fi`);
|
||||
script(join(bin, "node"), `if [ "$1" = --version ]; then echo v24.3.0; exit 0; fi\nexec '${process.execPath}' "$@"`);
|
||||
script(join(bin, "bwrap"), "exit 0");
|
||||
const units = join(root, "units");
|
||||
mkdirSync(units);
|
||||
writeFileSync(join(units, "mosaic-webui.service"), "[Service]\nExecStart=/usr/bin/node /x/cli.mjs serve --port 443 --business acme\n");
|
||||
writeFileSync(join(units, "[email protected]"), "[Service]\nEnvironment=PATH=/usr/bin\n");
|
||||
const sys = join(root, "sys");
|
||||
mkdirSync(join(sys, "net", "ipv4"), { recursive: true });
|
||||
mkdirSync(join(sys, "kernel"), { recursive: true });
|
||||
writeFileSync(join(sys, "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
|
||||
writeFileSync(join(sys, "kernel", "apparmor_restrict_unprivileged_userns"), "1\n");
|
||||
return {
|
||||
root, home, configDir, secrets, input, staging, bin, log, units, sys, doc,
|
||||
config: join(configDir, "config.json"),
|
||||
business: join(configDir, "businesses", "acme.json"),
|
||||
personaDir: join(configDir, "personas", "acme"),
|
||||
slot: join(secrets, "acme"),
|
||||
dataRoot: join(home, ".mosaic-dev"),
|
||||
};
|
||||
}
|
||||
|
||||
function env(x, extra = {}) {
|
||||
return { PATH: `${x.bin}:/usr/bin:/bin`, HOME: x.home, MOSAIC_CONFIG: x.config, CASE2_STACK_COPY: join(x.bin, "stack-copy"), ...extra };
|
||||
}
|
||||
|
||||
function clean(out) {
|
||||
assert.doesNotMatch(out, new RegExp(FIXTURE_KEY), "the output holds the fixture key");
|
||||
assert.doesNotMatch(out, /PERSONA-TEXT/, "the output holds persona text");
|
||||
assert.doesNotMatch(out, new RegExp(MARKER), "the output holds her text");
|
||||
}
|
||||
|
||||
function install(x, { profile = "library", args = null, extra = {} } = {}) {
|
||||
const argv = args ?? ["--business", "acme", "--business-file", x.input, "--commit", COMMIT, "--profile", profile,
|
||||
...(profile === "library" ? ["--personas", x.staging] : [])];
|
||||
const r = spawnSync(INSTALL, argv, { encoding: "utf8", timeout: 120000, env: env(x, extra) });
|
||||
const out = r.stdout + r.stderr;
|
||||
clean(out);
|
||||
return { code: r.status, out };
|
||||
}
|
||||
|
||||
function precheck(x, { args = ["--business", "acme", "--profile", "library"] } = {}) {
|
||||
const r = spawnSync(PRECHECK, args, {
|
||||
encoding: "utf8",
|
||||
timeout: 60000,
|
||||
env: env(x, { CASE2_UNIT_DIR: x.units, CASE2_PROC_SYS: x.sys, CASE2_GETENT: join(x.bin, "getent") }),
|
||||
});
|
||||
const out = r.stdout + r.stderr;
|
||||
clean(out);
|
||||
return { code: r.status, out };
|
||||
}
|
||||
const status = (x) => precheck(x, { args: ["--business", "acme", "--library-status"] });
|
||||
|
||||
function refused(r, pattern) {
|
||||
assert.equal(r.code, 1, r.out);
|
||||
assert.match(r.out, pattern);
|
||||
}
|
||||
|
||||
const calls = (x) => (existsSync(x.log) ? readFileSync(x.log, "utf8") : "");
|
||||
|
||||
// ---- case2-install.sh ----
|
||||
|
||||
test("the library install writes the business file, personas and slot dir, makes the copy and starts nothing", (t) => {
|
||||
const x = world(t);
|
||||
const r = install(x);
|
||||
assert.equal(r.code, 0, r.out);
|
||||
assert.equal(mode(x.business), 0o600);
|
||||
const doc = JSON.parse(readFileSync(x.business, "utf8"));
|
||||
for (const [instance, name] of [["bean", "Bean"], ["chet", "Chet"]]) {
|
||||
assert.equal(doc.roles[instance].vars["persona.name"], name);
|
||||
assert.equal(doc.roles[instance].vars["persona.sha256"], sha256(PERSONA_TEXT[instance]));
|
||||
assert.equal(doc.roles[instance].vars.harness, "pi");
|
||||
const file = join(x.personaDir, `${instance}.md`);
|
||||
assert.equal(readFileSync(file, "utf8"), PERSONA_TEXT[instance]);
|
||||
assert.equal(mode(file), 0o600);
|
||||
}
|
||||
assert.equal(mode(join(x.configDir, "personas")), 0o700);
|
||||
assert.equal(mode(x.personaDir), 0o700);
|
||||
assert.equal(mode(x.slot), 0o700);
|
||||
assert.deepEqual(readdirSync(x.slot), []);
|
||||
assert.equal(calls(x), `stack-copy copy ${COMMIT} --copies ${join(x.home, ".local", "share", "mosaic-stack", "copies")}\n`);
|
||||
assert.doesNotMatch(r.out, /release/);
|
||||
assert.match(r.out, /business acme validates/);
|
||||
assert.match(r.out, /--library-status/);
|
||||
assert.ok(existsSync(x.config));
|
||||
});
|
||||
|
||||
test("a library re-run passes and leaves every file as it was", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const before = readFileSync(x.business);
|
||||
const r = install(x);
|
||||
assert.equal(r.code, 0, r.out);
|
||||
assert.match(r.out, /already matches/);
|
||||
assert.match(r.out, /personas already installed/);
|
||||
assert.deepEqual(readFileSync(x.business), before);
|
||||
});
|
||||
|
||||
test("a re-run refuses business-file-differs and leaves the file unchanged", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const doc = JSON.parse(readFileSync(x.business, "utf8"));
|
||||
doc.vars["tracker.pollSeconds"] = 120;
|
||||
writeJson(x.business, doc);
|
||||
const before = readFileSync(x.business);
|
||||
refused(install(x), /business-file-differs/);
|
||||
assert.deepEqual(readFileSync(x.business), before);
|
||||
});
|
||||
|
||||
test("a re-run refuses persona-differs when an installed persona isn't the staged one, and writes nothing", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
rmSync(x.business);
|
||||
writeFileSync(join(x.personaDir, "bean.md"), "Someone else. PERSONA-TEXT-other\n");
|
||||
refused(install(x), /persona-differs/);
|
||||
assert.equal(existsSync(x.business), false);
|
||||
});
|
||||
|
||||
test("the factory install writes no library mapping, persona or slot dir", (t) => {
|
||||
const x = world(t, { profile: "factory" });
|
||||
const r = install(x, { profile: "factory" });
|
||||
assert.equal(r.code, 0, r.out);
|
||||
const doc = JSON.parse(readFileSync(x.business, "utf8"));
|
||||
assert.equal(Object.values(doc.projects).some((p) => p.kind === "library"), false);
|
||||
assert.equal(existsSync(join(x.configDir, "personas")), false);
|
||||
assert.equal(existsSync(x.slot), false);
|
||||
assert.doesNotMatch(r.out, /--library-status/);
|
||||
});
|
||||
|
||||
test("the factory install refuses a library project and bus.tester off, before anything is written", (t) => {
|
||||
const x = world(t, { profile: "factory" });
|
||||
const doc = JSON.parse(readFileSync(x.input, "utf8"));
|
||||
doc.projects.books = { ...mappingEntry(), user: "writer" };
|
||||
writeJson(x.input, doc);
|
||||
refused(install(x, { profile: "factory" }), /profile-library-business/);
|
||||
delete doc.projects.books;
|
||||
doc.vars["bus.tester"] = false;
|
||||
writeJson(x.input, doc);
|
||||
refused(install(x, { profile: "factory" }), /bus-tester-off/);
|
||||
assert.equal(calls(x), "");
|
||||
assert.equal(existsSync(x.business), false);
|
||||
});
|
||||
|
||||
test("the library install refuses bus.tester on and a persona var in the input", (t) => {
|
||||
const x = world(t);
|
||||
const doc = JSON.parse(readFileSync(x.input, "utf8"));
|
||||
doc.vars["bus.tester"] = true;
|
||||
writeJson(x.input, doc);
|
||||
refused(install(x), /bus-tester-on/);
|
||||
doc.vars["bus.tester"] = false;
|
||||
doc.roles.bean.vars["persona.name"] = "Bean";
|
||||
writeJson(x.input, doc);
|
||||
refused(install(x), /persona-var-input/);
|
||||
assert.equal(calls(x), "");
|
||||
});
|
||||
|
||||
test("the install refuses root, a business id mismatch, another --home and a config dir with group bits", (t) => {
|
||||
const x = world(t);
|
||||
refused(install(x, { extra: { CASE2_TEST_UID: "0" } }), /run-as-root/);
|
||||
refused(install(x, { args: ["--business", "other", "--business-file", x.input, "--commit", COMMIT, "--personas", x.staging] }), /business-id-mismatch/);
|
||||
refused(install(x, { args: ["--business", "acme", "--business-file", x.input, "--commit", COMMIT, "--personas", x.staging, "--home", x.root] }), /home-mismatch/);
|
||||
chmodSync(x.configDir, 0o750);
|
||||
refused(install(x), /config-dir-mode/);
|
||||
assert.equal(calls(x), "");
|
||||
assert.equal(existsSync(x.business), false);
|
||||
});
|
||||
|
||||
test("the library install needs --personas", (t) => {
|
||||
const x = world(t);
|
||||
const r = install(x, { args: ["--business", "acme", "--business-file", x.input, "--commit", COMMIT] });
|
||||
assert.equal(r.code, 4, r.out);
|
||||
assert.match(r.out, /needs --personas/);
|
||||
});
|
||||
|
||||
test("a validate refusal removes the business file this run wrote", (t) => {
|
||||
const x = world(t);
|
||||
unlinkSync(x.doc.roles.pm.credentials.gitea.file);
|
||||
const r = install(x);
|
||||
refused(r, /validate: .*the business file this run wrote is removed/);
|
||||
assert.equal(existsSync(x.business), false);
|
||||
});
|
||||
|
||||
test("N4: a validate refusal on a re-run leaves the matching business file in place, byte for byte", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const before = readFileSync(x.business);
|
||||
unlinkSync(x.doc.roles.pm.credentials.gitea.file);
|
||||
const r = install(x);
|
||||
refused(r, /validate: /);
|
||||
assert.doesNotMatch(r.out, /is removed/);
|
||||
assert.deepEqual(readFileSync(x.business), before);
|
||||
});
|
||||
|
||||
// ---- case2-precheck.sh, the library profile (D6a) ----
|
||||
|
||||
test("a clean library host passes the precheck with no mirror yet", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const r = precheck(x);
|
||||
assert.equal(r.code, 0, r.out);
|
||||
assert.match(r.out, /ok {6}business file .* with no group or other bit/);
|
||||
assert.match(r.out, /ok {6}no history for acme yet/);
|
||||
assert.match(r.out, new RegExp(`ok {6}persona bean sha256 ${sha256(PERSONA_TEXT.bean)}`));
|
||||
assert.match(r.out, new RegExp(`ok {6}persona chet sha256 ${sha256(PERSONA_TEXT.chet)}`));
|
||||
assert.match(r.out, /ok {6}no Discord binding/);
|
||||
assert.equal(existsSync(mirrorPath({ business: { id: "acme", projects: { books: x.doc.projects.books } }, project: "books", dataRoot: x.dataRoot })), false);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a persona file at 0644", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
chmodSync(join(x.personaDir, "bean.md"), 0o644);
|
||||
refused(precheck(x), /REFUSED persona-mode: .*bean\.md must be .* 0600 file \(mode 644\)/);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a persona whose digest isn't persona.sha256", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
writeFileSync(join(x.personaDir, "chet.md"), "Not Chet. PERSONA-TEXT-other\n");
|
||||
refused(precheck(x), /REFUSED persona-mismatch: persona chet has sha256 [0-9a-f]{64}, and persona.sha256 is /);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a persona dir with group bits", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
chmodSync(x.personaDir, 0o750);
|
||||
refused(precheck(x), /REFUSED persona-mode: .*personas\/acme must be .* 0700 directory/);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a business file at 0644", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
chmodSync(x.business, 0o644);
|
||||
refused(precheck(x), /REFUSED business-file-mode: .*acme\.json .*\(mode 644\)/);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a changes.jsonl at 0644", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const history = join(x.configDir, "businesses", ".history", "acme");
|
||||
mkdirSync(history, { recursive: true, mode: 0o700 });
|
||||
writeFileSync(join(history, "changes.jsonl"), "", { mode: 0o644 });
|
||||
chmodSync(join(history, "changes.jsonl"), 0o644);
|
||||
refused(precheck(x), /REFUSED history-mode: .*changes\.jsonl must be .* 0600 file \(mode 644\)/);
|
||||
chmodSync(join(history, "changes.jsonl"), 0o600);
|
||||
assert.equal(precheck(x).code, 0);
|
||||
});
|
||||
|
||||
test("the library precheck refuses a Discord binding", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
mkdirSync(join(x.dataRoot, "discord"), { recursive: true });
|
||||
writeFileSync(join(x.dataRoot, "discord", "her.json"), "{}\n", { mode: 0o600 });
|
||||
refused(precheck(x), /REFUSED discord-binding: .*her\.json exists/);
|
||||
});
|
||||
|
||||
// ---- case2-precheck.sh --library-status (D6b) ----
|
||||
|
||||
// The context the bus builds for books (runtime.mjs:161-164), and a sync the way the bus's timer
|
||||
// runs it, against a local bare clone through the command parameter.
|
||||
function library(x) {
|
||||
const context = { business: { id: "acme", projects: { books: x.doc.projects.books } }, project: "books", dataRoot: x.dataRoot };
|
||||
mkdirSync(x.dataRoot, { recursive: true, mode: 0o700 });
|
||||
chmodSync(x.dataRoot, 0o700);
|
||||
const run = (bare, slot) => sync(context, {
|
||||
slot,
|
||||
command: (kind, url, { mirror, branch }) => ({
|
||||
argv: [...SAFE, "-c", "protocol.file.allow=always", "-c", "core.createObject=rename", "-C", mirror, "fetch", "--no-tags",
|
||||
"--no-write-fetch-head", "--end-of-options", bare, `+refs/heads/${branch}:refs/remotes/library/${branch}`],
|
||||
env: { ...gitEnv(), LC_ALL: "C", GIT_CEILING_DIRECTORIES: dirname(mirror) },
|
||||
}),
|
||||
});
|
||||
return { context, run };
|
||||
}
|
||||
|
||||
test("--library-status refuses library-mirror-missing before the bus has made the mirror", (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
refused(status(x), /REFUSED library-mirror-missing: library books has no mirror at .*the bus makes it when it starts/);
|
||||
});
|
||||
|
||||
test("--library-status refuses library-never-synced on a fresh mirror", async (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
await ensureMirror(library(x).context);
|
||||
const r = status(x);
|
||||
refused(r, /REFUSED library-never-synced: library books has no fetch yet/);
|
||||
assert.doesNotMatch(r.out, /library-last-error/);
|
||||
});
|
||||
|
||||
test("--library-status names a recorded lastError before the null lastFetch it leaves (N3)", async (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const lib = library(x);
|
||||
await lib.run("/nonexistent", join(x.slot, "library-gitea"));
|
||||
const r = status(x);
|
||||
refused(r, /REFUSED library-last-error: library books: library-credential-missing at \d{4}-\d\d-\d\dT/);
|
||||
assert.doesNotMatch(r.out, /library-never-synced/);
|
||||
});
|
||||
|
||||
test("--library-status prints each R-problem's path as an escaped JSON string, then the count, and passes", async (t) => {
|
||||
const x = world(t);
|
||||
assert.equal(install(x).code, 0);
|
||||
const odd = "OddName\u0085.md";
|
||||
const f = repo(t, { ...BASE, [odd]: `${MARKER}\n` });
|
||||
const bare = join(f.root, "remote.git");
|
||||
git(f.root, "clone", "-q", "--bare", "--no-local", f.repo, bare);
|
||||
writeFileSync(join(x.slot, "library-gitea"), "placeholder-not-a-token\n", { mode: 0o600 });
|
||||
const { state } = await library(x).run(bare, join(x.slot, "library-gitea"));
|
||||
assert.equal(state.lastError, null);
|
||||
const r = status(x);
|
||||
assert.equal(r.code, 0, r.out);
|
||||
assert.match(r.out, new RegExp(`ok {6}library books last fetched \\S+, serving ${f.commit}`));
|
||||
assert.match(r.out, /^problem {3}R2 "Inbox Note\.md"$/m);
|
||||
assert.match(r.out, /^problem {3}R\d+ "Odd\\u202eName\\u0085\.md"$/m);
|
||||
assert.doesNotMatch(r.out, /[\u0085]/);
|
||||
const lines = r.out.split("\n").filter((l) => l.startsWith("problem "));
|
||||
assert.equal(lines.length, state.problems.length);
|
||||
assert.match(r.out, new RegExp(`^problems ${state.problems.length}$`, "m"));
|
||||
assert.match(r.out, /case2-precheck: library status passed/);
|
||||
});
|
||||
|
||||
test("--library-status takes no --profile", (t) => {
|
||||
const x = world(t);
|
||||
const r = precheck(x, { args: ["--business", "acme", "--library-status", "--profile", "library"] });
|
||||
assert.equal(r.code, 4, r.out);
|
||||
});
|
||||
Executable
+228
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env bash
|
||||
# Case 2 row C2-13b (design 10 step 2b): a hub's install, run as <user>
|
||||
# after root's 2a and before root's 2c renders the units.
|
||||
#
|
||||
# case2-install.sh --business ID --business-file FILE --commit SHA
|
||||
# [--profile factory|library] [--personas DIR] [--home DIR]
|
||||
#
|
||||
# Steps, in order: refuse root; scripts/bootstrap.sh; stack-copy.sh copy
|
||||
# SHA, whose npm ci installs the pinned harness; for library, the persona
|
||||
# import's install from the staging DIR; the business file at 0600; for
|
||||
# library, the empty 0700 slot dir <configDir>/secrets/<id>/; `mosaic
|
||||
# business validate`; then the next steps. It runs no release package or
|
||||
# activate, starts no unit, writes no unit file and nothing under /etc, and
|
||||
# never writes the library token, which is Jason's step 4.
|
||||
#
|
||||
# FILE is the business document written for the host from the go. The
|
||||
# script adds only roles.bean.vars and roles.chet.vars persona.name and
|
||||
# persona.sha256, from the staging manifest, and checks the input against
|
||||
# the precheck's profile rules first: library needs a library project,
|
||||
# bus.tester off and bean and chet as companion roles with no persona var;
|
||||
# factory needs no library project and bus.tester on.
|
||||
#
|
||||
# Every refusal it can see before writing comes first, so a refused run
|
||||
# writes nothing. A re-run passes when the business file and personas on
|
||||
# disk match what it would write, and refuses business-file-differs or
|
||||
# persona-differs when they don't, touching neither. If validate refuses,
|
||||
# the business file goes only when this run created it.
|
||||
#
|
||||
# --home must be $HOME. The config dir is dirname of $MOSAIC_CONFIG, or
|
||||
# <home>/.config/mosaic-dev; the install makes it 0700 when absent and
|
||||
# refuses config-dir-mode when it has a group or other bit.
|
||||
#
|
||||
# Test seam, for packages/cli/tests/case2-install.test.mjs only:
|
||||
# CASE2_STACK_COPY the copy script, default scripts/stack-copy.sh
|
||||
#
|
||||
# Exit 0 ok, 1 refused, 4 usage.
|
||||
set -uo pipefail
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
STACK_COPY="${CASE2_STACK_COPY:-$REPO/scripts/stack-copy.sh}"
|
||||
|
||||
usage() { echo 'usage: case2-install.sh --business ID --business-file FILE --commit SHA [--profile factory|library] [--personas DIR] [--home DIR]' >&2; exit 4; }
|
||||
PROFILE=library BUSINESS='' BFILE='' COMMIT='' PERSONAS='' HOME_ARG="${HOME:-}"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--business) [ $# -ge 2 ] || usage; BUSINESS="$2"; shift 2 ;;
|
||||
--business-file) [ $# -ge 2 ] || usage; BFILE="$2"; shift 2 ;;
|
||||
--commit) [ $# -ge 2 ] || usage; COMMIT="$2"; shift 2 ;;
|
||||
--profile) [ $# -ge 2 ] || usage; PROFILE="$2"; shift 2 ;;
|
||||
--personas) [ $# -ge 2 ] || usage; PERSONAS="$2"; shift 2 ;;
|
||||
--home) [ $# -ge 2 ] || usage; HOME_ARG="$2"; shift 2 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$BUSINESS" ] && [ -n "$BFILE" ] && [ -n "$COMMIT" ] || usage
|
||||
case "$PROFILE" in factory|library) ;; *) echo "case2-install: --profile must be factory or library: $PROFILE" >&2; exit 4 ;; esac
|
||||
[[ "$BUSINESS" =~ ^[a-z0-9][a-z0-9-]{0,63}$ ]] || { echo "case2-install: --business must be a business id: $BUSINESS" >&2; exit 4; }
|
||||
[[ "$COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "case2-install: --commit must be a full 40-character commit id: $COMMIT" >&2; exit 4; }
|
||||
if [ "$PROFILE" = library ] && [ -z "$PERSONAS" ]; then echo 'case2-install: the library profile needs --personas DIR' >&2; exit 4; fi
|
||||
if [ "$PROFILE" = factory ] && [ -n "$PERSONAS" ]; then echo 'case2-install: the factory profile takes no --personas' >&2; exit 4; fi
|
||||
|
||||
say() { echo "case2-install: $1"; }
|
||||
refuse() { echo "case2-install: refused: $1" >&2; exit 1; }
|
||||
mode_of() { stat -c '%a' -- "$1"; }
|
||||
# An existing directory, not a link, owned by this uid, with no group or other bit.
|
||||
private_dir() { [ ! -L "$1" ] && [ -d "$1" ] && [ "$(stat -c '%u' -- "$1")" = "$(id -u)" ] && [ $((8#$(mode_of "$1") & 8#077)) -eq 0 ]; }
|
||||
# An absent path, or a private directory; anything else refuses CODE.
|
||||
absent_or_private() {
|
||||
if [ ! -e "$1" ] && [ ! -L "$1" ]; then return 0; fi
|
||||
private_dir "$1" || refuse "$2: $1 must be $(id -un)'s directory with no group or other bit"
|
||||
}
|
||||
|
||||
# 1. Never as root: root owns nothing under <home> here.
|
||||
[ "$(id -u)" != 0 ] || refuse 'run-as-root: run the install as <user>, not root'
|
||||
[ -n "${HOME:-}" ] && [ "$HOME_ARG" = "$HOME" ] || refuse "home-mismatch: --home $HOME_ARG is not \$HOME ${HOME:-unset}"
|
||||
CONFIG="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}"
|
||||
case "$CONFIG" in /*) ;; *) refuse "config-path: MOSAIC_CONFIG must be an absolute path: $CONFIG" ;; esac
|
||||
CONFIG_DIR="$(dirname -- "$CONFIG")"
|
||||
SECRETS="$CONFIG_DIR/secrets"
|
||||
SLOT="$SECRETS/$BUSINESS"
|
||||
COPIES="$HOME/.local/share/mosaic-stack/copies"
|
||||
absent_or_private "$CONFIG_DIR" config-dir-mode
|
||||
if [ "$PROFILE" = library ]; then
|
||||
absent_or_private "$SECRETS" secrets-mode
|
||||
absent_or_private "$SLOT" slot-mode
|
||||
fi
|
||||
|
||||
# The plan and the write share one helper. plan checks the input, the
|
||||
# staging manifest and what is on disk, and prints one decision per line;
|
||||
# write makes the business file with O_EXCL. Neither prints persona text.
|
||||
HELPER='
|
||||
import { closeSync, constants, fchmodSync, lstatSync, openSync, readFileSync, writeSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
const [verb, id, input, profile, staging, configDir] = process.argv.slice(1);
|
||||
const say = (...f) => console.log(f.join("\t"));
|
||||
const stop = (code, message) => { say("REFUSE", `${code}: ${message}`); process.exit(0); };
|
||||
const sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
||||
let doc;
|
||||
try { doc = JSON.parse(readFileSync(input, "utf8")); } catch (e) { stop("business-file-input", `cannot read ${input} as JSON (${e.code ?? "parse"})`); }
|
||||
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) stop("business-file-input", `${input} is not a JSON object`);
|
||||
if (doc.id !== id) stop("business-id-mismatch", `${input} has id ${JSON.stringify(doc.id)}, not ${id}`);
|
||||
const projects = doc.projects && typeof doc.projects === "object" ? doc.projects : {};
|
||||
const library = Object.keys(projects).filter((k) => projects[k]?.kind === "library");
|
||||
const tester = doc.vars?.["bus.tester"] === true;
|
||||
let personas = [];
|
||||
if (profile === "factory") {
|
||||
if (library.length) stop("profile-library-business", `the factory profile refuses library projects ${library.join(",")}`);
|
||||
if (!tester) stop("bus-tester-off", "the factory profile needs vars bus.tester true");
|
||||
} else {
|
||||
if (!library.length) stop("no-library-project", `${input} has no library project`);
|
||||
if (tester) stop("bus-tester-on", "the library profile needs bus.tester off");
|
||||
for (const instance of ["bean", "chet"]) {
|
||||
const role = doc.roles?.[instance];
|
||||
if (role?.definition !== "companion") stop("companion-roles", `roles.${instance} must have definition companion`);
|
||||
if (Object.keys(role.vars ?? {}).some((k) => k.startsWith("persona."))) stop("persona-var-input", `roles.${instance}.vars sets a persona var; the install adds them`);
|
||||
}
|
||||
let manifest;
|
||||
try { manifest = JSON.parse(readFileSync(join(staging, "manifest.json"), "utf8")); } catch (e) { stop("persona-manifest", `cannot read ${join(staging, "manifest.json")} (${e.code ?? "parse"})`); }
|
||||
const entries = manifest?.version === 1 && Array.isArray(manifest.personas) ? manifest.personas : [];
|
||||
const want = (instance) => entries.filter((m) => m?.instance === instance);
|
||||
for (const instance of ["bean", "chet"]) {
|
||||
const m = want(instance);
|
||||
if (entries.length !== 2 || m.length !== 1 || m[0].name !== instance[0].toUpperCase() + instance.slice(1) || !/^[0-9a-f]{64}$/.test(m[0].sha256 ?? ""))
|
||||
stop("persona-manifest", "the staging manifest does not list Bean and Chet once each with a sha256");
|
||||
personas.push(m[0]);
|
||||
}
|
||||
for (const m of personas) doc.roles[m.instance].vars = { ...(doc.roles[m.instance].vars ?? {}), "persona.name": m.name, "persona.sha256": m.sha256 };
|
||||
}
|
||||
const text = `${JSON.stringify(doc, null, 2)}\n`;
|
||||
const file = join(configDir, "businesses", `${id}.json`);
|
||||
if (verb === "write") {
|
||||
let fd;
|
||||
try { fd = openSync(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); }
|
||||
catch (e) { stop("business-file-raced", `${file} could not be made new (${e.code})`); }
|
||||
fchmodSync(fd, 0o600);
|
||||
writeSync(fd, text);
|
||||
closeSync(fd);
|
||||
say("WROTE", file);
|
||||
process.exit(0);
|
||||
}
|
||||
// plan: the personas on disk, then the business file on disk.
|
||||
if (profile === "library") {
|
||||
const dir = join(configDir, "personas", id);
|
||||
const seen = personas.map((m) => {
|
||||
const p = join(dir, `${m.instance}.md`);
|
||||
const s = lstatSync(p, { throwIfNoEntry: false });
|
||||
if (!s) return "absent";
|
||||
return s.isFile() && sha256(readFileSync(p)) === m.sha256 ? "same" : "differs";
|
||||
});
|
||||
if (seen.every((s) => s === "absent")) say("PERSONAS", "install");
|
||||
else if (seen.every((s) => s === "same")) say("PERSONAS", "present");
|
||||
else stop("persona-differs", `the persona files under ${dir} are not the staged pair; nothing written`);
|
||||
for (const m of personas) say("PERSONA", m.instance, m.name, m.sha256);
|
||||
}
|
||||
const s = lstatSync(file, { throwIfNoEntry: false });
|
||||
if (!s) say("BUSINESS", "new", file);
|
||||
else if (s.isFile() && !s.isSymbolicLink() && readFileSync(file, "utf8") === text) say("BUSINESS", "same", file);
|
||||
else stop("business-file-differs", `${file} exists and differs from the one this install writes; nothing written`);
|
||||
'
|
||||
helper() { (cd "$REPO" && node --input-type=module -e "$HELPER" "$1" "$BUSINESS" "$BFILE" "$PROFILE" "$PERSONAS" "$CONFIG_DIR" 2>&1); }
|
||||
field() { printf '%s\n' "$plan" | awk -F'\t' -v k="$1" '$1 == k { print $2 }'; }
|
||||
|
||||
plan="$(helper plan)" || refuse "plan: the check failed: $(printf '%s' "$plan" | head -n 1)"
|
||||
r="$(printf '%s\n' "$plan" | awk -F'\t' '$1 == "REFUSE" { print $2; exit }')"
|
||||
[ -z "$r" ] || refuse "$r"
|
||||
BUSINESS_STATE="$(field BUSINESS)"
|
||||
[ -n "$BUSINESS_STATE" ] || refuse "plan: no decision: $(printf '%s' "$plan" | head -n 1)"
|
||||
PERSONA_STATE="$(field PERSONAS)"
|
||||
|
||||
# 2. bootstrap: idempotent, and it never rewrites an existing config.
|
||||
if [ ! -e "$CONFIG_DIR" ]; then
|
||||
mkdir -p -- "$(dirname -- "$CONFIG_DIR")" && mkdir -m 700 -- "$CONFIG_DIR" || refuse "config-dir: cannot make $CONFIG_DIR"
|
||||
fi
|
||||
out="$("$REPO/scripts/bootstrap.sh" 2>&1)" || refuse "bootstrap: $(printf '%s' "$out" | tail -n 1)"
|
||||
say "config $CONFIG"
|
||||
|
||||
# 3. The stack copy row 195's units run from.
|
||||
out="$("$STACK_COPY" copy "$COMMIT" --copies "$COPIES" 2>&1)" || refuse "stack-copy: $(printf '%s' "$out" | tail -n 1)"
|
||||
[ -f "$COPIES/$COMMIT/scripts/mosaic" ] || refuse "stack-copy: no copy $COMMIT in $COPIES after the copy step"
|
||||
say "copy $COPIES/$COMMIT"
|
||||
|
||||
# 4. library: the personas, from the staging dir.
|
||||
if [ "$PROFILE" = library ]; then
|
||||
if [ "$PERSONA_STATE" = install ]; then
|
||||
out="$(node "$REPO/scripts/case2-persona-import.mjs" install --staging "$PERSONAS" --config-dir "$CONFIG_DIR" --business "$BUSINESS" 2>&1)" \
|
||||
|| refuse "persona-import: $(printf '%s' "$out" | tail -n 1)"
|
||||
while IFS=$'\t' read -r _ inst name sum; do
|
||||
printf '%s\n' "$out" | grep -qxF "instance $inst: persona.name $name, persona.sha256 $sum" \
|
||||
|| refuse "persona-import: the import did not report $inst with the manifest's sha256"
|
||||
done < <(printf '%s\n' "$plan" | awk -F'\t' '$1 == "PERSONA"')
|
||||
say "personas installed under $CONFIG_DIR/personas/$BUSINESS"
|
||||
else
|
||||
say "personas already installed under $CONFIG_DIR/personas/$BUSINESS"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 5. The business file, at 0600 and new, unless an equal one is there.
|
||||
CREATED=0
|
||||
if [ "$BUSINESS_STATE" = new ]; then
|
||||
mkdir -p -m 700 -- "$CONFIG_DIR/businesses" || refuse "business-file: cannot make $CONFIG_DIR/businesses"
|
||||
w="$(helper write)" || refuse "business-file: $(printf '%s' "$w" | head -n 1)"
|
||||
case "$w" in WROTE*) CREATED=1 ;; REFUSE*) refuse "${w#REFUSE }" ;; *) refuse "business-file: $(printf '%s' "$w" | head -n 1)" ;; esac
|
||||
say "business file $CONFIG_DIR/businesses/$BUSINESS.json written"
|
||||
else
|
||||
say "business file $CONFIG_DIR/businesses/$BUSINESS.json already matches"
|
||||
fi
|
||||
|
||||
# 6. library: the empty slot dir. The token is Jason's, design 10 step 4.
|
||||
if [ "$PROFILE" = library ]; then
|
||||
[ -e "$SECRETS" ] || mkdir -m 700 -- "$SECRETS" || refuse "slot: cannot make $SECRETS"
|
||||
[ -e "$SLOT" ] || mkdir -m 700 -- "$SLOT" || refuse "slot: cannot make $SLOT"
|
||||
private_dir "$SLOT" || refuse "slot-mode: $SLOT must be a 0700 directory"
|
||||
say "slot dir $SLOT"
|
||||
fi
|
||||
|
||||
# 7. validate, the business package's own schema.
|
||||
if ! out="$("$REPO/scripts/mosaic" business validate "$BUSINESS" 2>&1)"; then
|
||||
if [ "$CREATED" = 1 ]; then rm -f -- "$CONFIG_DIR/businesses/$BUSINESS.json"; fi
|
||||
refuse "validate: $(printf '%s' "$out" | tail -n 1)$( [ "$CREATED" = 1 ] && echo '; the business file this run wrote is removed')"
|
||||
fi
|
||||
say "business $BUSINESS validates"
|
||||
|
||||
# 8. What comes next. Root's steps run from root's own checkout.
|
||||
say "done. Next: root runs case2-unit.sh --copy $COMMIT --business $BUSINESS (design 10 step 2c), then 2d and 3."
|
||||
if [ "$PROFILE" = library ]; then
|
||||
say "Then Jason places the library token in $SLOT (step 4), and <user> runs case2-precheck.sh --business $BUSINESS --library-status (step 4a)."
|
||||
fi
|
||||
exit 0
|
||||
+124
-3
@@ -5,6 +5,7 @@
|
||||
# any one failed.
|
||||
#
|
||||
# case2-precheck.sh --business ID [--profile factory|library]
|
||||
# case2-precheck.sh --business ID --library-status
|
||||
#
|
||||
# Checks: the config and business file validate; `sudo -n true` fails and
|
||||
# `id -G` prints one group; bus.tester is off for library and on for factory,
|
||||
@@ -17,6 +18,22 @@
|
||||
# instance's provider is zai. It prints free memory and checks that HOME is
|
||||
# <home>. It never prints a value from claude-session.env.
|
||||
#
|
||||
# C2-13b adds to the library profile: the business file is <user>'s with no
|
||||
# group or other bit; businesses/.history/<id>/ is 0700 and its changes.jsonl
|
||||
# 0600, when present; the persona dirs are 0700 and each persona file is
|
||||
# <user>'s at 0600 with the sha256 its persona.sha256 names; and no Discord
|
||||
# binding exists under <dataRoot>/discord. These read no mirror and sync
|
||||
# nothing, since the bus makes the mirror when it starts.
|
||||
#
|
||||
# --library-status (design 10 step 4a) runs instead, once the bus is up and
|
||||
# the token is placed. It reads the bus's recorded sync state for each library
|
||||
# project through libraryStatus(), the call the bus's library.status makes,
|
||||
# and syncs nothing: sync() fetches outside the library lock, so a second
|
||||
# process would race the bus's timer. It refuses a missing mirror, a recorded
|
||||
# lastError and a null lastFetch, in that order, and prints each R-problem as a
|
||||
# `problem` line with its path as a JSON string, then a count. It prints no
|
||||
# text from her repo.
|
||||
#
|
||||
# Test seams, for packages/cli/tests/case2-unit.test.mjs only:
|
||||
# CASE2_UNIT_DIR the installed units, default /etc/systemd/system
|
||||
# CASE2_PROC_SYS default /proc/sys
|
||||
@@ -34,22 +51,86 @@ WEBUI_UNIT="$UNIT_DIR/mosaic-webui.service"
|
||||
# Claude Code's own prompt is many times this.
|
||||
MARKER_TOKEN_CEILING=2000
|
||||
|
||||
usage() { echo 'usage: case2-precheck.sh --business ID [--profile factory|library]' >&2; exit 4; }
|
||||
PROFILE=library BUSINESS=''
|
||||
usage() { echo 'usage: case2-precheck.sh --business ID [--profile factory|library] | --business ID --library-status' >&2; exit 4; }
|
||||
PROFILE=library PROFILE_SET=0 BUSINESS='' STATUS=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--profile) [ $# -ge 2 ] || usage; PROFILE="$2"; shift 2 ;;
|
||||
--profile) [ $# -ge 2 ] || usage; PROFILE="$2"; PROFILE_SET=1; shift 2 ;;
|
||||
--business) [ $# -ge 2 ] || usage; BUSINESS="$2"; shift 2 ;;
|
||||
--library-status) STATUS=1; shift ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$BUSINESS" ] || usage
|
||||
[ "$STATUS" = 0 ] || [ "$PROFILE_SET" = 0 ] || usage
|
||||
case "$PROFILE" in factory|library) ;; *) echo "case2-precheck: --profile must be factory or library: $PROFILE" >&2; exit 4 ;; esac
|
||||
[[ "$BUSINESS" =~ ^[a-z0-9][a-z0-9._-]{0,63}$ ]] || { echo "case2-precheck: --business must be a business id: $BUSINESS" >&2; exit 4; }
|
||||
|
||||
FAILED=0
|
||||
ok() { echo "ok $1"; }
|
||||
refuse() { echo "REFUSED $1"; FAILED=1; }
|
||||
mode_of() { stat -c '%a' -- "$1"; }
|
||||
# Regular, not a link, owned by this uid, with no group or other bit.
|
||||
private_file() { [ ! -L "$1" ] && [ -f "$1" ] && [ "$(stat -c '%u' -- "$1")" = "$(id -u)" ] && [ $((8#$(mode_of "$1") & 8#077)) -eq 0 ]; }
|
||||
# A directory, not a link, owned by this uid, at 0700.
|
||||
private_dir() { [ ! -L "$1" ] && [ -d "$1" ] && [ "$(stat -c '%u' -- "$1")" = "$(id -u)" ] && [ "$(mode_of "$1")" = 700 ]; }
|
||||
|
||||
# --library-status: the bus's recorded sync state, and nothing else.
|
||||
if [ "$STATUS" = 1 ]; then
|
||||
status="$(cd "$REPO" && node --input-type=module -e '
|
||||
import { lstatSync } from "node:fs";
|
||||
const [repo, id] = process.argv.slice(1);
|
||||
const { loadSystem, rolesDir } = await import(`${repo}/packages/cli/src/config.mjs`);
|
||||
const { loadBusiness } = await import(`${repo}/packages/business/src/index.mjs`);
|
||||
const { libraryStatus, mirrorPath } = await import(`${repo}/packages/library/src/index.mjs`);
|
||||
const say = (...f) => console.log(f.join("\t"));
|
||||
// A library error code is a fixed [a-z-] string; anything else prints as one name.
|
||||
const code = (e) => typeof e?.code === "string" && /^[a-z-]{1,64}$/.test(e.code) ? e.code : "library-status-failed";
|
||||
// JSON.stringify leaves DEL, C1 controls, the line and paragraph separators and the bidi controls
|
||||
// raw. They print as \uXXXX too, so a path from her repo cannot move the cursor or reorder a line.
|
||||
const RAW = /[\u007f-\u009f\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
|
||||
const quote = (s) => JSON.stringify(s).replace(RAW, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
||||
let system, b;
|
||||
try { system = loadSystem(); } catch (e) { say("BAD", `config: ${e.message.split("\n")[0]}`); process.exit(0); }
|
||||
try { b = loadBusiness(id, { rolesDir: rolesDir() }); } catch (e) { say("BAD", `business file: ${e.message.split("\n")[0]}`); process.exit(0); }
|
||||
const projects = Object.keys(b.projects).filter((p) => b.projects[p].kind === "library");
|
||||
if (!projects.length) say("BAD", `business ${id} has no library project`);
|
||||
for (const project of projects) {
|
||||
const context = { business: b, project, dataRoot: system.dataRoot };
|
||||
let mirror, s;
|
||||
try { mirror = mirrorPath(context); } catch (e) { say("ERROR", project, code(e)); continue; }
|
||||
let there;
|
||||
try { there = lstatSync(mirror, { throwIfNoEntry: false }); } catch (e) { say("ERROR", project, "library-git"); continue; }
|
||||
if (!there) { say("MISSING", project, mirror); continue; }
|
||||
try { s = await libraryStatus(context); } catch (e) { say("ERROR", project, code(e)); continue; }
|
||||
// lastError first: a first sync with no token records an error and leaves lastFetch null.
|
||||
if (s.lastError !== null) { say("LASTERROR", project, s.lastError.code, s.lastError.at); continue; }
|
||||
if (s.lastFetch === null) { say("NEVER", project); continue; }
|
||||
say("OK", project, s.lastFetch, s.served ?? "none");
|
||||
if (s.diverged !== null) say("DIVERGED", project, s.diverged.commit);
|
||||
for (const p of s.problems) say("PROBLEM", p.code, quote(p.path));
|
||||
say("PROBLEMS", project, s.problems.length);
|
||||
}
|
||||
' "$REPO" "$BUSINESS" 2>&1)" || true
|
||||
[ -n "$status" ] || refuse "library status: the check printed nothing"
|
||||
while IFS=$'\t' read -r kind a b c; do
|
||||
case "$kind" in
|
||||
BAD) refuse "$a" ;;
|
||||
ERROR) refuse "library-status: library $a: $b" ;;
|
||||
MISSING) refuse "library-mirror-missing: library $a has no mirror at $b; the bus makes it when it starts" ;;
|
||||
LASTERROR) refuse "library-last-error: library $a: $b at $c" ;;
|
||||
NEVER) refuse "library-never-synced: library $a has no fetch yet; wait for the bus's next sync, 60 s, or run library.sync" ;;
|
||||
OK) ok "library $a last fetched $b, serving $c" ;;
|
||||
DIVERGED) echo " library $a: a diverged tip $b waits for library.sync --accept" ;;
|
||||
PROBLEM) echo "problem $a $b" ;;
|
||||
PROBLEMS) echo "problems $b" ;;
|
||||
*) refuse "library status: $kind${a:+ $a}" ;;
|
||||
esac
|
||||
done <<<"$status"
|
||||
if [ "$FAILED" = 0 ]; then echo "case2-precheck: library status passed"; exit 0; fi
|
||||
echo "case2-precheck: refused"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# One setting's value from an installed unit: the last KEY= line in the file.
|
||||
unit_value() { [ -f "$1" ] && sed -n "s/^[[:space:]]*$2[[:space:]]*=[[:space:]]*//p" -- "$1" | tail -n 1; }
|
||||
@@ -100,6 +181,8 @@ let b;
|
||||
try { b = loadBusiness(id, { rolesDir: rolesDir() }); } catch (e) { say("BUSINESS", "bad", e.message.split("\n")[0]); process.exit(0); }
|
||||
say("BUSINESS", "ok", file);
|
||||
say("TESTER", b.vars["bus.tester"] === true ? "on" : "off");
|
||||
for (const [instance, role] of Object.entries(raw.roles ?? {}))
|
||||
if (typeof role?.vars?.["persona.sha256"] === "string") say("PERSONA", instance, role.vars["persona.sha256"]);
|
||||
const vars = systemVars(system);
|
||||
const models = b.launch?.models ?? null;
|
||||
for (const instance of Object.keys(b.roles)) {
|
||||
@@ -138,6 +221,44 @@ elif [ -n "$cfg" ] && [ "${cfg#CONFIG ok}" != "$cfg" ]; then
|
||||
esac
|
||||
fi
|
||||
|
||||
# C2-13b: the library files' modes, the persona digests and no Discord
|
||||
# binding (design 4). Names and digests print, never persona text.
|
||||
if [ "$PROFILE" = library ] && [ -n "$CONFIG_DIR" ]; then
|
||||
bfile="$CONFIG_DIR/businesses/$BUSINESS.json"
|
||||
if [ ! -e "$bfile" ] && [ ! -L "$bfile" ]; then :
|
||||
elif private_file "$bfile"; then ok "business file $bfile is $ME's with no group or other bit"
|
||||
else refuse "business-file-mode: $bfile must be $ME's regular file with no group or other bit (mode $(mode_of "$bfile"))"; fi
|
||||
hdir="$CONFIG_DIR/businesses/.history/$BUSINESS"
|
||||
if [ ! -e "$hdir" ] && [ ! -L "$hdir" ]; then ok "no history for $BUSINESS yet"
|
||||
elif ! private_dir "$hdir"; then refuse "history-mode: $hdir must be $ME's 0700 directory (mode $(mode_of "$hdir"))"
|
||||
elif [ ! -e "$hdir/changes.jsonl" ] && [ ! -L "$hdir/changes.jsonl" ]; then ok "history $hdir is 0700"
|
||||
elif private_file "$hdir/changes.jsonl" && [ "$(mode_of "$hdir/changes.jsonl")" = 600 ]; then ok "history $hdir is 0700 and changes.jsonl 0600"
|
||||
else refuse "history-mode: $hdir/changes.jsonl must be $ME's 0600 file (mode $(mode_of "$hdir/changes.jsonl"))"; fi
|
||||
personas="$(line PERSONA)"
|
||||
if [ -z "$personas" ]; then ok "no instance has persona vars"
|
||||
else
|
||||
for d in "$CONFIG_DIR/personas" "$CONFIG_DIR/personas/$BUSINESS"; do
|
||||
if private_dir "$d"; then ok "persona dir $d is 0700"
|
||||
else refuse "persona-mode: $d must be $ME's 0700 directory"; fi
|
||||
done
|
||||
while IFS=$'\t' read -r _ inst want; do
|
||||
pfile="$CONFIG_DIR/personas/$BUSINESS/$inst.md"
|
||||
if ! private_file "$pfile" || [ "$(mode_of "$pfile")" != 600 ]; then
|
||||
refuse "persona-mode: $pfile must be $ME's 0600 file$( [ -e "$pfile" ] && echo " (mode $(mode_of "$pfile"))")"
|
||||
continue
|
||||
fi
|
||||
have="$(sha256sum -- "$pfile" | cut -d' ' -f1)"
|
||||
if [ "$have" = "$want" ]; then ok "persona $inst sha256 $have"
|
||||
else refuse "persona-mismatch: persona $inst has sha256 $have, and persona.sha256 is $want"; fi
|
||||
done <<<"$personas"
|
||||
fi
|
||||
fi
|
||||
if [ "$PROFILE" = library ] && [ -n "$DATA_ROOT" ]; then
|
||||
binding="$(find "$DATA_ROOT/discord" -maxdepth 1 -name '*.json' -print -quit 2>/dev/null)"
|
||||
if [ -n "$binding" ]; then refuse "discord-binding: $binding exists; a library hub has no Discord binding"
|
||||
else ok "no Discord binding under $DATA_ROOT/discord"; fi
|
||||
fi
|
||||
|
||||
# No sudo, and one group.
|
||||
if sudo -n true >/dev/null 2>&1; then refuse "sudo -n true succeeds as $ME"; else ok "sudo -n true fails as $ME"; fi
|
||||
groups="$(id -G)"
|
||||
|
||||
Reference in new issue
Block a user