Compare commits
241
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af7c5bb094 | ||
|
|
6850e43908 | ||
|
|
9093999650 | ||
|
|
5c472db38d | ||
|
|
067c60ac0f | ||
|
|
cf5966469e | ||
|
|
66df77d113 | ||
|
|
023a600431 | ||
|
|
0006f03f47 | ||
|
|
10aa996fb0 | ||
|
|
97dc3fa1ea | ||
|
|
3405d8cae5 | ||
|
|
53f0445bbe | ||
|
|
c6ea27aaba | ||
|
|
61c17c3978 | ||
|
|
e1853039f5 | ||
|
|
9ffd417c70 | ||
|
|
439c3c5d35 | ||
|
|
192049c985 | ||
|
|
74d2d6da15 | ||
|
|
3c69ad053b | ||
|
|
14137ff639 | ||
|
|
0939c06827 | ||
|
|
37732a9bd2 | ||
|
|
333e34a484 | ||
|
|
d715fc7784 | ||
|
|
a019741396 | ||
|
|
f597910fa9 | ||
|
|
c1a13ab26a | ||
|
|
15cc360db0 | ||
|
|
914ac06855 | ||
|
|
13d066f75a | ||
|
|
9609c9e1ea | ||
|
|
c59b5652c6 | ||
|
|
63c351b284 | ||
|
|
1cd1bf31be | ||
|
|
0371fa9a09 | ||
|
|
da32114698 | ||
|
|
a2691a4b54 | ||
|
|
b7b518d6c3 | ||
|
|
761f3a3144 | ||
|
|
bc9a27981b | ||
|
|
05231348fb | ||
|
|
a989a70c1b | ||
|
|
03ee29bfcf | ||
|
|
d1154708ef | ||
|
|
6a8478a2d8 | ||
|
|
2c1fae5bee | ||
|
|
8306d6cd2b | ||
|
|
8978559b32 | ||
|
|
1f3b83f604 | ||
|
|
f2e66f1e29 | ||
|
|
1005f99b6a | ||
|
|
2364c521b2 | ||
|
|
23d43a930d | ||
|
|
c708442c4c | ||
|
|
cbf142b7f0 | ||
|
|
36842190b7 | ||
|
|
6b632d4ec9 | ||
|
|
0b3db9c22a | ||
|
|
ea022163d5 | ||
|
|
203d666f8c | ||
|
|
5fe8a03dc0 | ||
|
|
f12b790b4c | ||
|
|
7d550703e2 | ||
|
|
d83af73634 | ||
|
|
b6d604e96b | ||
|
|
dce214677f | ||
|
|
7e1b374d46 | ||
|
|
49ade06654 | ||
|
|
fb986ab2a9 | ||
|
|
b24c0f2ddb | ||
|
|
7d88908c53 | ||
|
|
daea42820a | ||
|
|
e906040957 | ||
|
|
911872a5ee | ||
|
|
16542b141e | ||
|
|
c18d267879 | ||
|
|
849e5bfb30 | ||
|
|
73ee45fa91 | ||
|
|
a6275ca3a8 | ||
|
|
6b4202fb6d | ||
|
|
7a2504991f | ||
|
|
0f1ebbab55 | ||
|
|
5fb0f88a23 | ||
|
|
43faeff461 | ||
|
|
dac7482bb7 | ||
|
|
00502a740d | ||
|
|
923c88e7dc | ||
|
|
9d3c9a3162 | ||
|
|
0603896bc6 | ||
|
|
edf7f504ef | ||
|
|
c226abfac7 | ||
|
|
a800944bce | ||
|
|
e08062d7c5 | ||
|
|
171480e6e8 | ||
|
|
32f05027cc | ||
|
|
6ef7be7330 | ||
|
|
cde575d283 | ||
|
|
471be47e5b | ||
|
|
e826c6dac8 | ||
|
|
78bd0d404b | ||
|
|
873992a9e2 | ||
|
|
39c821e7c0 | ||
|
|
34e1ab2bf5 | ||
|
|
057eebc46d | ||
|
|
55760b938b | ||
|
|
3cf210d00e | ||
|
|
1741bf720c | ||
|
|
d774c2626a | ||
|
|
de018df873 | ||
|
|
3dee8c4a73 | ||
|
|
0242780bac | ||
|
|
ad2be8ae45 | ||
|
|
a06e8f000a | ||
|
|
e2e7c60e56 | ||
|
|
14ad3baadc | ||
|
|
7703865349 | ||
|
|
8ca09fedf7 | ||
|
|
ef7ce90895 | ||
|
|
5c67029398 | ||
|
|
f0f9ff89c4 | ||
|
|
d3fa3a0ada | ||
|
|
fb8cabc649 | ||
|
|
c80230196d | ||
|
|
860e2012f5 | ||
|
|
9f3d724dff | ||
|
|
cb5700b617 | ||
|
|
2e91ddef65 | ||
|
|
88f7ebe0b6 | ||
|
|
e44598b48d | ||
|
|
7c00c83450 | ||
|
|
35130c26bb | ||
|
|
2f488b4ea7 | ||
|
|
660d25e656 | ||
|
|
15c09fd7b5 | ||
|
|
ebb3d83bf6 | ||
|
|
a11a9926cd | ||
|
|
5d8f0f4b54 | ||
|
|
ef2098c967 | ||
|
|
bb7c028852 | ||
|
|
de135fc029 | ||
|
|
ef63393ee4 | ||
|
|
bbca2935ba | ||
|
|
191048fbec | ||
|
|
4a3dc25763 | ||
|
|
bef9a62f4c | ||
|
|
a0a00630ea | ||
|
|
efd6c1f028 | ||
|
|
02bb0afb3a | ||
|
|
387f04dfda | ||
|
|
7a542c0ea2 | ||
|
|
858f773625 | ||
|
|
e10a6f1166 | ||
|
|
bba870ee9c | ||
|
|
2fb6815963 | ||
|
|
57da6fda51 | ||
|
|
361bd753fa | ||
|
|
69d5b39600 | ||
|
|
4e931c4f1f | ||
|
|
d2344ff8bf | ||
|
|
3ab3c010b1 | ||
|
|
deda9f48e8 | ||
|
|
433a95292e | ||
|
|
7a9a90a7a2 | ||
|
|
2f00563249 | ||
|
|
2a216fd169 | ||
|
|
928405649c | ||
|
|
2dfc1b9ad6 | ||
|
|
5bd27e804b | ||
|
|
449c3458ff | ||
|
|
82f50d1273 | ||
|
|
2beb49daeb | ||
|
|
d37be6ff29 | ||
|
|
a5643f9bd7 | ||
|
|
9705a6aaca | ||
|
|
1f404c973b | ||
|
|
b2793f2aa1 | ||
|
|
f942bd7d43 | ||
|
|
425900b036 | ||
|
|
85a72842c3 | ||
|
|
19bb3ae364 | ||
|
|
216302d65f | ||
|
|
0d0df1ecee | ||
|
|
dba33aaf60 | ||
|
|
0597cc1988 | ||
|
|
c7a672d622 | ||
|
|
04e78354a6 | ||
|
|
fcac66f7bf | ||
|
|
ca6b32d668 | ||
|
|
5a02ee3954 | ||
|
|
e1e64ff011 | ||
|
|
6d2e502728 | ||
|
|
c9d65664af | ||
|
|
18f5daaaef | ||
|
|
822bf1aa04 | ||
|
|
162ce46dbc | ||
|
|
dada066753 | ||
|
|
9afb947c73 | ||
|
|
8d53b93c78 | ||
|
|
365a763122 | ||
|
|
f9d218ab1e | ||
|
|
0599b119f4 | ||
|
|
b416d2ede9 | ||
|
|
5fc73a729c | ||
|
|
99772421ab | ||
|
|
da230ca489 | ||
|
|
4f2d1be68a | ||
|
|
d55d86bb04 | ||
|
|
c56ccf916e | ||
|
|
817e8f51b0 | ||
|
|
5d019d8768 | ||
|
|
59475fc5a8 | ||
|
|
1a268249e6 | ||
|
|
def2e79327 | ||
|
|
c30b36fffb | ||
|
|
9ae8c73acb | ||
|
|
8ecc7a239d | ||
|
|
7f57419b38 | ||
|
|
4ca8d6e949 | ||
|
|
22e5e825b0 | ||
|
|
e6dea74b22 | ||
|
|
cd2b377bbf | ||
|
|
ceccb0ef26 | ||
|
|
10ab8ea624 | ||
|
|
c539995cf4 | ||
|
|
92cc6011c8 | ||
|
|
7af1665748 | ||
|
|
2244e8bafc | ||
|
|
bde0fdec9f | ||
|
|
def677bc76 | ||
|
|
51f67f1f87 | ||
|
|
5b298bd00b | ||
|
|
565bc32c2f | ||
|
|
5140aa9e88 | ||
|
|
973cb5a872 | ||
|
|
b82ec93536 | ||
|
|
3560c450a6 | ||
|
|
4f02486d56 | ||
|
|
263ea8a9a3 | ||
|
|
6020bb43ac |
No files matched your search
+1
-17
@@ -75,8 +75,7 @@ session's workspace and its own process group. Same contract, plus:
|
||||
| `MOSAIC_POLICY_FILE`, `MOSAIC_TOOLS_FILE` | The bundle's gate policy and typed tools. |
|
||||
| `MOSAIC_TOOL_SOCKET` | The runner's tool socket, which the typed tools call. |
|
||||
| `MOSAIC_TURN_MARKER` | `pi`: the extension writes it at `agent_end`; a pi turn that exits 0 without it failed. |
|
||||
| `MOSAIC_EXTENSIONS` | `pi` only: extension files loaded with `-e`; a missing one exits 2. The pi module names two: the gate and typed tools extension, then the unslop check's `tools/unslop-hook/extension.ts` (row 66). |
|
||||
| `MOSAIC_UNSLOP_LOG` | The unslop check's JSONL log, `<run>/unslop.jsonl`, which the runner sets for both adapters and reads after each turn (row 66). |
|
||||
| `MOSAIC_EXTENSIONS` | `pi` only: extension files loaded with `-e`; a missing one exits 2. |
|
||||
| `MOSAIC_CLAUDE_SETTINGS`, `MOSAIC_CLAUDE_MCP_CONFIG` | `claude` only: the bundle's gate hook and MCP server; required. |
|
||||
| `MOSAIC_THINKING` | The route's thinking level (row 65), which the harness module's `levels()` already checked. `pi` passes `--thinking` (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`) and omits it when unset, as for `run-task.sh` workers. `claude` passes `--effort` (`low`, `medium`, `high`, `xhigh`, `max`) and requires it; unset exits 2. |
|
||||
| `MOSAIC_CLAUDE_BIN` | `claude` only: absolute path of the CLI to run, from the host's environment (the bus unit pins it). Unset or empty runs `claude` from `PATH`; a relative path or a non-executable file exits 2. |
|
||||
@@ -107,18 +106,3 @@ The cost is prompt size: a role's skill text is in every turn, where the
|
||||
other routes load a skill when it's used. The bundle caps a role's skills at
|
||||
64 KiB together, since `claude` passes `prompt.md` as one argument and Linux
|
||||
caps one argument at 128 KiB.
|
||||
|
||||
### The unslop check (row 66)
|
||||
|
||||
Both adapters run the mechanical writing check from `tools/unslop-hook`,
|
||||
driven by one `lists.json`. `pi` loads it as its second `-e` extension.
|
||||
`claude` runs `claude-hook.js` from the bundle's `settings.json`: the Stop
|
||||
hook checks the turn's replies, and the UserPromptSubmit hook adds the
|
||||
correction notice as context on the next turn when the last reply had
|
||||
tells. Each hook command ends `; exit 0`, so neither blocks. Both log to
|
||||
`MOSAIC_UNSLOP_LOG`, and the runner adds a note for the operator to the
|
||||
next RESULT it sends (`packages/harness/README.md`, the runner). The
|
||||
harness modules mount the three files read-only at their own paths, and
|
||||
the manifest records their digests. `MOSAIC_UNSLOP_HOOK` and `UNSLOP_LISTS`
|
||||
aren't in the session environment's allowlist, so a managed session can't
|
||||
switch the check off or swap its lists.
|
||||
@@ -0,0 +1,541 @@
|
||||
# Q175 design: the Console verifies SSO itself (Vikunja #412)
|
||||
|
||||
Author Sage, 2026-10-20, r3. Reviewer Filbert, and the review comes before any code. Line numbers are at
|
||||
refactor 4ca8d6e94 unless a line says otherwise. This replaces Part B (row 178 proxy proof, #385 comments 345
|
||||
and 353). #385 stays open until the Console stops trusting `X-authentik-username`.
|
||||
|
||||
## Changes from r2 (Filbert's r2 review, CHANGES)
|
||||
|
||||
- C1: the `console` key goes into the shared loader, scripts/mosaic-config.mjs, which today refuses any
|
||||
unknown top-level key (:113). Section 4 names the blast radius. Jason step 4 moves inside the phase 2
|
||||
window, after the pin and before the Console starts. The phase 2 rollback removes `console` first. A
|
||||
test-config.sh case covers it (test 25). Mos confirms the sequence before phase 1 (section 5).
|
||||
- C2: under `--sso` the page treats a 401 `sign-in required` as signed out and links to
|
||||
`/auth/login?return=<path>` (section 5 phase 1, test 24).
|
||||
- Notes: tests 7 and 15 are built so that only the record rule refuses; test 9a is an alg test; Q-B's two
|
||||
classes are defined, and test 23 adds a 200 HTML page; the phase 2 scratch Console uses a scratch config,
|
||||
a dummy slot, no `--send` and no `--tester-port`.
|
||||
- r3 fix (Filbert r3 approve): D1, the loader checks the slot reference's shape only, and the file checks
|
||||
run in the Console's `--sso` start; D2, the slot resolves from the config dir, and the scratch Console has
|
||||
a scratch dataRoot and an unreachable issuer (section 5).
|
||||
- Q-E: settled as Filbert ruled. The `subject-unknown` line keeps iss and sub, JSON-escaped and capped
|
||||
(section 1, tests 10 and 19).
|
||||
|
||||
## Changes from r1 (Filbert's r1 review, CHANGES)
|
||||
|
||||
- B1: the provider config moves to `~/.config/mosaic-dev/config.json` (section 4). Mos agreed (mos-0973). The
|
||||
Console prints provider ids, issuers and subject counts at start. The rehearse-strip follow-up is dropped.
|
||||
- B2: one return-path rule, `new URL(ret, publicOrigin).origin === publicOrigin` (section 1 step 7), with
|
||||
tests for `/\evil.example` and `/%09/evil.example`.
|
||||
- B3: refusals log a fixed code and the provider id, never the error or its cause (section 1).
|
||||
- B4: login records are capped at 32 (section 1 step 1), with a test.
|
||||
- B5: test 9 splits into a forged payload under a real kid and a kid missing from the JWKS, and a try/catch
|
||||
mutant joins the list. Test 2 no longer claims the signature mutant (section 9).
|
||||
- B6: the phase 2 forged-header check runs on a scratch Console, or Mos or Jason runs it on live (section 5).
|
||||
- Rulings: Q-B start degraded (section 4), Q-C only the public Host on 7332 (section 3), Q-D keep 1 h idle
|
||||
but polls don't count as activity (section 3).
|
||||
- Section 6: `startServer` throws on `tester && sso`, and `human` joins `tester` as a refused subject label.
|
||||
- The missing tests and nits N1-N5 are in sections 1, 3, 7 and 9.
|
||||
- Section 8: the human-cli statement stays as written (mos-0972). The recommendation is now Mos's ruling.
|
||||
|
||||
## The ruling this implements
|
||||
|
||||
Jason, DECISIONS Q175, verbatim: "Q1: yes. This should be a requirement for SSO usage. Whether we are using
|
||||
Pocket ID, Authentik, WorkOS, or another provider (we should support multiple), the system should use best
|
||||
practices for jwt or other methods of validation that do not rely on hacks to pass the auth." He added "Zero
|
||||
trust with verification" and confirmed five points (mos-0970):
|
||||
|
||||
1. The stack verifies every SSO login itself. It never takes identity from a proxy header or a shared secret.
|
||||
2. The Console is an OIDC relying party: discovery from the issuer URL, authorization code with PKCE, the ID
|
||||
token checked against the provider JWKS (signature, iss, aud, exp, nonce), then its own session cookie.
|
||||
3. Several providers can be configured side by side.
|
||||
4. Only asymmetric signing counts. HS256 is refused.
|
||||
5. A proxy-forwarded token such as `X-authentik-jwt` counts only through the same checks. It is optional and
|
||||
never the only way in.
|
||||
|
||||
## What exists today
|
||||
|
||||
The live Console runs from copies/490e2771d3d0 with `--public-host mosaic.woltje.com --auth-header
|
||||
X-authentik-username --allow-user jason.woltje --send --tester-port 7333`. Row 189 (#1687, P19 #413) adds
|
||||
`stack-release.sh activate --no-send`, and Mos's ruling mos-0972 takes `--send` off the live Console until
|
||||
phase 2.
|
||||
|
||||
- `previewOptions` (packages/webui/src/serve.mjs:205) builds `{ host, header, user }`.
|
||||
- A request is `named` when its Host equals the public name (:493). A named request passes sign-in when it
|
||||
carries exactly one auth header value equal to the user (:517-521). That header value is the whole identity.
|
||||
- Requests with a loopback Host on 7332 get the unauthenticated read path (:509-512). Writes need `named` (:526-546).
|
||||
- `proofOptions` (:217) is the row 178 shared-secret header. Q175 point 1 rules it out, so it retires with the
|
||||
auth header.
|
||||
- The tester listener (`--tester-port`, row 176) binds 127.0.0.1. Its writer is fixed as
|
||||
`{ tester: true, user: 'tester' }` (:522), it allows only the instance, route, name and chat writes (:523),
|
||||
and it refuses the auth and proof headers (:486-487). 7332 refuses `x-mosaic-tester` (:485).
|
||||
- The public path is Traefik, then the Authentik proxy outpost (provider mosaic-preview, HS256, empty JWKS),
|
||||
then chisel `R:0.0.0.0:7332:127.0.0.1:7332`, then the Console. Every public request reaches the Console from
|
||||
127.0.0.1 with whatever Host the outpost forwards.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. The Console becomes an OIDC relying party
|
||||
|
||||
New module `packages/webui/src/sso.mjs`, used only by the human listener (7332). Routes:
|
||||
|
||||
| Route | Method | What it does |
|
||||
|---|---|---|
|
||||
| `/auth/login` | GET | One provider configured: start its flow. Several: a plain page listing them, each a link to `/auth/login?provider=ID`. A provider whose discovery hasn't succeeded gets 503 (section 4). |
|
||||
| `/auth/callback` | GET | Finish the flow, create the session, 303 to the saved return path. |
|
||||
| `/auth/logout` | POST | Same-origin checks, delete the session, 303 to the provider's `end_session_endpoint`. |
|
||||
| `/auth/signed-out` | GET | Static "signed out" page with a sign-in link. |
|
||||
|
||||
Every URL the Console hands a provider is built from the configured `publicOrigin`: `redirect_uri` is
|
||||
`publicOrigin + '/auth/callback'` and `post_logout_redirect_uri` is `publicOrigin + '/auth/signed-out'`. No
|
||||
code path reads the request's Host to build them (N2).
|
||||
|
||||
The flow per provider:
|
||||
|
||||
1. Start. Generate a state, a nonce and a PKCE verifier (`generateRandomState`, `generateRandomNonce`,
|
||||
`generateRandomCodeVerifier`), and a challenge with `calculatePKCECodeChallenge` (S256 only). Put them, the
|
||||
provider id, the return path and an expiry 10 minutes out in a server-side login record keyed by a random
|
||||
id. The store holds at most 32 login records; a new one evicts the oldest (B4). Set `__Host-mosaic_login` to
|
||||
that id. 303 to `authorization_endpoint` with `response_type=code`, `scope=openid profile email`,
|
||||
`code_challenge_method=S256`, `redirect_uri`, `state` and `nonce`.
|
||||
2. Callback. Read exactly one `__Host-mosaic_login` cookie, then take and delete its record (single use, so a
|
||||
replayed callback finds nothing). Expire the cookie in the response. The provider comes from the record,
|
||||
never from the query, which stops a mix-up between providers. Two tabs signing in at once each set the
|
||||
cookie, so the later one wins and the earlier tab's callback finds no record and gets the 403 page with a
|
||||
sign-in link (N5). That costs one retry and keeps one cookie per browser.
|
||||
3. `validateAuthResponse(as, client, url, record.state)` checks state, refuses an error response, and checks
|
||||
the `iss` parameter when the provider advertises RFC 9207.
|
||||
4. `authorizationCodeGrantRequest(as, client, clientAuth, params, redirectUri, record.verifier)` returns a
|
||||
`Response`. Keep that object. `processAuthorizationCodeResponse(as, client, response, { expectedNonce:
|
||||
record.nonce, requireIdToken: true })` checks iss, aud (and azp when aud has several values), exp, iat and
|
||||
nonce, and checks `alg` against `client.id_token_signed_response_alg`. That field gets the provider's whole
|
||||
`algs` array, which the library's `checkSigningAlgorithm` accepts, so a provider listing RS256 and ES256
|
||||
takes either and nothing else (N1). Test 16 pins this.
|
||||
5. Signature. oauth4webapi does not verify the signature of an ID token that came straight from the token
|
||||
endpoint over TLS (OIDC Core 3.1.3.7 allows that, and the library docs for
|
||||
`validateApplicationLevelSignature` say so). Point 2 asks for the JWKS check, so the callback always calls
|
||||
`validateApplicationLevelSignature(as, response)` with the same `Response` object from step 4. It fetches
|
||||
the JWKS from the discovered `jwks_uri`, picks the key by `kty`, `kid`, `alg` and `use`, and refuses HS*
|
||||
outright. On a `kid` it doesn't have, it refetches the JWKS once if the cache is at least 60 s old, which
|
||||
covers key rotation (test 18). The call is not wrapped in a catch that continues; its throw ends the
|
||||
callback.
|
||||
6. Our own checks on top of the library: `header.alg` is in the provider's `algs`, the token's `iss` equals
|
||||
the configured issuer as a raw string compare, with no URL normalising (N3; the library's discovery check
|
||||
compares normalised hrefs, so this one is stricter), and `(iss, sub)` is in the provider's `subjects` list.
|
||||
7. Create the session (section 3), then 303 to the return path. The rule (B2): resolve
|
||||
`u = new URL(ret, publicOrigin)`, and use `u.pathname + u.search` only when
|
||||
`u.origin === publicOrigin`; otherwise use `/`. A missing or unparsable `ret` gives `/`.
|
||||
|
||||
Every refusal in steps 2-6 ends at a 403 page saying sign-in failed, with a link back to `/auth/login`. No
|
||||
step falls back to a header.
|
||||
|
||||
Logging (B3). A refusal logs one line: a fixed code from a closed list (`login-record-missing`,
|
||||
`state-mismatch`, `auth-error-response`, `grant-failed`, `id-token-invalid`, `signature-invalid`,
|
||||
`alg-refused`, `issuer-mismatch`, `subject-unknown`, `discovery-failed`, `jwks-failed`) and the provider id.
|
||||
It never logs the exception, its message or its `cause`. oauth4webapi's errors carry token bodies and claims
|
||||
in `cause`, so even a message can leak. The one extra field is on `subject-unknown`: it logs `iss` and `sub`,
|
||||
because Jason step 3 reads his `sub` from that line (Q-E, Filbert's ruling). Authentik's default subject mode
|
||||
is a hashed user id that its admin view doesn't show, so the line is the practical source. Neither value is a
|
||||
credential. Three conditions hold:
|
||||
|
||||
- Only a token that passed steps 3-6 up to the subject check reaches this line. Every earlier refusal logs
|
||||
its own code and the provider id only.
|
||||
- Both values are written with `JSON.stringify` and each is cut to 255 characters first. Any account at the
|
||||
provider can get a signed token, so a `sub` may hold a newline, and an unescaped one would let a sign-in
|
||||
forge a log line.
|
||||
- Test 10 uses a `sub` holding `\n` and a fake `refused sign-in` suffix and asserts one escaped line.
|
||||
|
||||
Test 19 asserts that no log line holds a token, code, state, nonce, verifier, client secret or session id.
|
||||
It exempts only the `iss` and `sub` fields of the `subject-unknown` line.
|
||||
|
||||
### 2. Library: oauth4webapi 3.8.8, pinned exactly
|
||||
|
||||
Mos's steer (mos-0971) was to prefer oauth4webapi over hand-rolled verification on `node:crypto` unless I
|
||||
could name a defect or a dependency cost that outweighs it. I found neither.
|
||||
|
||||
- MIT, no dependencies, one ESM file (build/index.js, 99,856 bytes). Tarball sha256 `bac088aecc7c...`,
|
||||
integrity `sha512-8N28E+a/oxfXW...` (the full values go in package-lock.json).
|
||||
- `supported()` covers PS/ES/RS 256/384/512, Ed25519/EdDSA and ML-DSA. No HS*. RSA keys under 2048 bits are
|
||||
refused.
|
||||
- It does discovery with an issuer equality check (`processDiscoveryResponse(expectedIssuer, ...)`), PKCE,
|
||||
state, the RFC 9207 `iss` parameter, nonce, and JWKS caching.
|
||||
- The one gap is the default skip of the token-endpoint signature check (section 1 step 5). Calling one
|
||||
exported function closes it, so it isn't a reason to hand-roll.
|
||||
|
||||
Hand-rolling would mean writing JOSE parsing, JWK import, key selection by `kid`, the claim checks and the
|
||||
discovery checks ourselves, with the same tests and more places to get it wrong.
|
||||
|
||||
Pin: `"oauth4webapi": "3.8.8"` in the root package.json `dependencies`, next to the pi pin, with the lockfile
|
||||
entry. Root is right because `scripts/stack-copy.sh:23-31` copies only the root package.json and lock and runs
|
||||
`npm ci` there (:55-56). packages/webui/src resolves the import from the copy's root node_modules. This is the
|
||||
webui's first runtime dependency. The root dependency rule (exact pins, never unversioned) applies to it.
|
||||
Upgrades are reviewed commits.
|
||||
|
||||
### 3. Sessions, cookies, Host and logout
|
||||
|
||||
- Session id: 32 random bytes, base64url. The store keys on its sha256 and holds no raw id, so a heap dump
|
||||
doesn't hold usable ids. Test 20 checks that the store's keys and values never contain the cookie value.
|
||||
- Cookie, exactly: `__Host-mosaic_session=<id>; Path=/; Secure; HttpOnly; SameSite=Lax`, no Domain, no
|
||||
Max-Age (a browser-session cookie). The `__Host-` prefix makes the browser refuse it unless it was set over
|
||||
https on this exact host. Test 21 asserts the whole Set-Cookie value, and a mutant drops `HttpOnly`.
|
||||
- Login cookie `__Host-mosaic_login`, the same attributes plus `Max-Age=600`. The login record also expires
|
||||
at 10 minutes server-side.
|
||||
- Lax, not Strict, because the callback is a top-level navigation from the provider's site, and a Strict
|
||||
cookie set there would be missing on the redirect to `/`. Lax still keeps the session off cross-site POSTs.
|
||||
The write routes keep their exact `Origin` and `Sec-Fetch-Site: same-origin` checks (:171, :250, :297, :340,
|
||||
:387, :425), so cross-site writes are refused twice.
|
||||
- Lifetime: 8 h absolute, 1 h idle, both server-side (Q-D, Filbert's ruling). The store is in memory with a
|
||||
cap of 64 sessions (oldest goes first), so a Console restart signs everyone out. That's acceptable for one
|
||||
human and keeps sessions off disk.
|
||||
- Activity (Q-D). Only a page load (a GET outside `/api/`) or a write (a POST) refreshes `seen`. The page's
|
||||
background reads don't: `refresh` re-reads the board every 10 s (packages/webui/src/public/app.js:346) and
|
||||
the 5 s timer at :301 redraws the age label. Without this rule an open tab would never go idle. Test 13
|
||||
covers it.
|
||||
- The session holds `{ provider, iss, sub, name, idToken, created, seen }`. `name` is the subject entry's
|
||||
label. It goes where `preview.user` goes today (send logs, settings, seat routes), so the downstream writer
|
||||
shape stays as it is. `idToken` is kept only as the logout hint and never leaves the server.
|
||||
- Host (Q-C, Filbert's ruling). Under `--sso`, 7332 serves only requests whose Host is the public host.
|
||||
Any other Host, loopback included, gets 403 `public Host required`, except `GET /healthz`. The loopback read
|
||||
path at :509-512 is gone under `--sso`. Today's readers of 127.0.0.1:7332 and where they go:
|
||||
- `scripts/webui-service.sh status` (:406), the stack-release gate (scripts/stack-release.sh:484) and the
|
||||
rehearse gate (:1369) read only `/healthz`, which stays open on loopback.
|
||||
- The webui README (:83) describes the loopback path. It changes in phase 1.
|
||||
- Agent reads of the board go to the tester listener on 7333, which already serves reads. I found no other
|
||||
code that reads 7332 on loopback. A person opening http://127.0.0.1:7332 in a local browser gets the 403
|
||||
and uses the public URL instead.
|
||||
Test 22 covers a loopback Host on `/`, `/api/board` and a write, and `/healthz` passing.
|
||||
- Every request on 7332 except `/healthz` and `/auth/*` needs a valid session. Without one, `/api/*` gets
|
||||
401 JSON and other GETs get a 303 to `/auth/login?return=<path>`.
|
||||
- Logout: `POST /auth/logout` with the same-origin checks. It deletes the session, expires the cookie, and
|
||||
303s to `end_session_endpoint?id_token_hint=...&post_logout_redirect_uri=<publicOrigin>/auth/signed-out&client_id=...`.
|
||||
If the provider has no `end_session_endpoint`, it goes straight to `/auth/signed-out`.
|
||||
- Limit: signing out at the provider does not end a Console session until idle or absolute expiry. Back-channel
|
||||
logout is a follow-up, not phase 1.
|
||||
|
||||
### 4. Provider config: shape and location
|
||||
|
||||
Location (B1, Filbert's ruling, Mos agreed in mos-0973): a `console.sso` key in
|
||||
`~/.config/mosaic-dev/config.json`. The reason is who can write it. The business file is writable by agents
|
||||
through `organizationWriter` (the tester verbs and the chief's `organization.set`), so a provider entry there
|
||||
could be changed by the agents the Console is meant to keep out. config.json is user-authored and never
|
||||
auto-written: `scripts/bootstrap.sh` creates it and nothing overwrites it. Adding the key is a Jason step
|
||||
(section 10).
|
||||
|
||||
The loader (C1). Every tool reads config.json through one loader, scripts/mosaic-config.mjs. Its `validate`
|
||||
refuses any top-level key other than `configVersion`, `environment`, `dataRoot` and `execution` with
|
||||
`unsupported configuration key` (:113). The business, seat, cli, discord, bootstrap, land-work, stack-copy and
|
||||
conductor-apply code all use it. So phase 1 changes the shared loader, not a Console-only reader:
|
||||
|
||||
- mosaic-config.mjs accepts an optional `console` key, and the strict `console.sso` shape validator (the
|
||||
config checks below, except the slot file checks) runs inside the loader. The fail-closed canon holds for
|
||||
every reader. The loader checks shape only and never touches a slot file: `clientSecret` must be
|
||||
`{ slot: <name> }` with the name matching `[a-z0-9-]{1,64}`. The file checks and the read run only in the
|
||||
Console's `--sso` start (D1), so a missing slot or a 0640 slot stops the Console, not the stack.
|
||||
- The blast radius is the whole stack. A typo in `console.sso` makes every tool refuse the config, not just
|
||||
the Console. That's the price of one loader, and the reason step 4 writes the key inside a window with a
|
||||
backup first.
|
||||
- The other direction breaks too. A copy older than phase 1 refuses a config that has the key. If the key
|
||||
went in before the phase 2 pin, the live copy (490e2771d3d0 or later) would refuse its own config on the
|
||||
next read, and the bus, seats and host would stop. After phase 2, a rollback to a pre-phase-1 copy fails
|
||||
the same way until the key is removed. This is the same trap as the plug-in kind before a rollback.
|
||||
- Files without the key still load, on old and new copies.
|
||||
|
||||
Test 25 in scripts/test-config.sh: a config with a valid `console.sso` loads, a malformed one fails with
|
||||
the bad key path named, and a config without `console` loads.
|
||||
|
||||
No secret value goes in this file. The client secret is a slot reference.
|
||||
|
||||
```json
|
||||
"console": {
|
||||
"sso": {
|
||||
"publicOrigin": "https://mosaic.woltje.com",
|
||||
"providers": [
|
||||
{
|
||||
"id": "authentik",
|
||||
"issuer": "https://auth.diversecanvas.com/application/o/mosaic-stack/",
|
||||
"clientId": "<from Jason>",
|
||||
"clientSecret": { "slot": "console-sso-authentik" },
|
||||
"algs": ["RS256"],
|
||||
"subjects": [ { "sub": "<from Jason>", "name": "jason.woltje" } ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
At start, `--sso` prints one line per provider: its id, its issuer and the number of subjects. No client id
|
||||
value beyond that, no secret, no `sub`.
|
||||
|
||||
The slot is `<configDir>/secrets/mosaic-stack/<slot>`, where `configDir` is the directory of the config file
|
||||
the loader read (`MOSAIC_CONFIG`, default `~/.config/mosaic-dev`), so a scratch config gets a scratch slot
|
||||
(D2). The Console's `--sso` start reads it. The Console refuses to start if
|
||||
the file is a symlink, isn't a regular file, isn't owned by the Console's uid, has any group or other mode
|
||||
bits (0640 refuses as well as 0644), or resolves under the repo or a stack copy. The value is never logged or
|
||||
put in a refusal message.
|
||||
|
||||
`stack-release.sh rehearse` starts its Console with `serve --port N --business B` and no `--sso`, so a
|
||||
rehearsed Console never reads `console.sso` and never uses the live redirect URI. No rehearse change is
|
||||
needed; r1's follow-up is dropped.
|
||||
|
||||
Config checks, run at start without the network. Any failure refuses the start, and the message names the
|
||||
provider and the check:
|
||||
|
||||
- `publicOrigin` is an https origin with no path, query or fragment.
|
||||
- Provider ids are unique and `[a-z0-9-]{1,32}`.
|
||||
- `issuer` is https with no query or fragment.
|
||||
- `algs` is non-empty and holds only asymmetric algs from the library's list. `HS256`, any `HS*` and `none`
|
||||
refuse.
|
||||
- `subjects` is non-empty, each `sub` is 1..255 visible characters, and no `name` is `tester` or `human`.
|
||||
- `clientSecret` is `{ slot: <name> }` with the name matching `[a-z0-9-]{1,64}` (shape only, in the loader).
|
||||
- In the Console's `--sso` start only, the slot file passes the file checks above.
|
||||
|
||||
Discovery checks, run per provider when its discovery succeeds:
|
||||
|
||||
- Discovery returns `issuer` exactly.
|
||||
- `id_token_signing_alg_values_supported` shares at least one alg with `algs`. mosaic-preview advertises HS256
|
||||
only, so it fails here.
|
||||
- `code_challenge_methods_supported` includes `S256`.
|
||||
- `jwks_uri` returns at least one key usable for one of `algs`. The empty mosaic-preview JWKS fails here too.
|
||||
|
||||
Unreachable provider (Q-B, Filbert's ruling). Config errors still refuse the start. Each fetch of the
|
||||
discovery document or the JWKS lands in one of two classes:
|
||||
|
||||
- Unreachable: a network error, a timeout, a status other than 200, or a body that doesn't parse as JSON. An
|
||||
Authentik maintenance page served as 200 HTML is unreachable, not refused, so it can't block a restart.
|
||||
- Refused: a JSON document that fails the issuer, alg, S256 or JWKS check.
|
||||
|
||||
An unreachable provider doesn't stop the Console. It starts, that provider's `/auth/login` gets 503
|
||||
`sign-in unavailable`, and a retry runs with backoff (5 s, doubling, capped at 5 minutes). `/healthz` stays
|
||||
200 and adds `sso: { <id>: "ready" | "unreachable" | "refused" }`, so the gates still pass and an operator
|
||||
can see the state. A refused provider at start refuses the start, because it's a configuration problem. On a
|
||||
later retry it stays disabled with the `discovery-failed` code until a restart. Sessions already made keep
|
||||
working while a provider is unreachable, since a session check needs no network. Test 23 covers the degraded
|
||||
start, a 200 HTML page, and the recovery.
|
||||
|
||||
Several providers: Pocket ID, WorkOS and other compliant OPs use the same entry shape. The login page lists
|
||||
them. Each entry has its own issuer, client, algs and subjects, and a session records which provider made it.
|
||||
|
||||
### 5. Retiring `--auth-header`, `--allow-user` and the proof header
|
||||
|
||||
1. **Phase 1 (code row).** Add `--sso` to `cli.mjs serve` and `webui-service.sh`. It reads `console.sso` from
|
||||
config.json. `--sso` cannot be combined with `--auth-header`, `--allow-user` or `--proof-*`. Under
|
||||
`--sso`, `named` means "has a session", and `writer` comes from the session. `--send` needs `--sso` or the
|
||||
old preview set. Phase 1 also carries the loader change and its test-config.sh case (section 4, test 25).
|
||||
Nothing live changes until the re-pin.
|
||||
|
||||
The page (C2). Today app.js signs out only on `res.type === 'opaqueredirect'` (packages/webui/src/public/
|
||||
app.js:111 and :305), which is the outpost's redirect. Under `--sso` the Console answers an `/api/*`
|
||||
request without a session with 401 `sign-in required`, so the page would show an error on every 10 s
|
||||
refresh and never offer a way back. Phase 1 makes both fetch paths treat a 401 whose body has
|
||||
`error: 'sign-in required'` as signed out, and the signed-out notice links to
|
||||
`/auth/login?return=<current path and query>`. Other 401s stay errors. The opaqueredirect branch stays
|
||||
until phase 3, because the preview flags still run behind the outpost until then. Test 24 covers it.
|
||||
|
||||
Before phase 1 starts, Mos confirms the config.json sequence in phase 2 (C1).
|
||||
2. **Phase 2 (Jason-gated window).** Re-pin to the phase 1 commit with `--sso --send --tester-port 7333`
|
||||
instead of the preview flags, after Jason steps 1 to 3. Backup first, STATE after (Q173). The order
|
||||
inside the window:
|
||||
1. Back up the store, the pointer, the unit and config.json.
|
||||
2. Pin the phase 1 copy with the Console stopped. The copy's loader accepts a config with or without
|
||||
`console`, so the bus and seats keep reading the old config.
|
||||
3. Straggler check (Mos's C1 condition, mos-0977): list every running process, unit and timer that
|
||||
still runs from the old copy and reads config.json. Each one is restarted on the new copy or shown
|
||||
not to re-read config. The old loader refuses the key, so a straggler would fail on its next read.
|
||||
Step 4 waits until the list is clean.
|
||||
4. Jason step 4: the `console.sso` key goes into config.json, written by Jason or by an agent on his go
|
||||
for that one key. The new loader reads it back (`scripts/mosaic-config.mjs` validate) before anything
|
||||
starts.
|
||||
5. Start the Console with `--sso --send --tester-port 7333`.
|
||||
Acceptance: Jason signs in through the Console flow, sends one message and signs out. The forged-header
|
||||
check (a request with `X-authentik-username` and no cookie gets 401) runs either on a scratch Console or
|
||||
on live by Mos or Jason (B6). No agent sends the auth header to the live 7332 (section 8). The scratch
|
||||
Console runs from the same copy as `serve --sso --port <scratch port>`, with `MOSAIC_CONFIG` pointing at a
|
||||
scratch config in a scratch dir. That config has a scratch `dataRoot`, and any business it serves comes
|
||||
from the scratch config dir; neither points at live. Its dummy client secret sits in the scratch slot
|
||||
(`<scratch configDir>/secrets/mosaic-stack/<slot>`). Its issuer is an unreachable loopback host, not
|
||||
Authentik, so the scratch Console sends nothing outbound (D2, mos-0971). It comes up in the degraded
|
||||
state (Q-B), and the 401 check needs no sign-in. It has no `--send` and no `--tester-port`.
|
||||
|
||||
Rollback for phase 2: remove the `console` key from config.json first (restore the step 1 backup), then
|
||||
roll the pointer back. An older copy's loader refuses a config holding the key (section 4), so the other
|
||||
order stops the bus and seats. The window plan lists that order, and the STATE check after a rollback
|
||||
reads config.json for the key's absence.
|
||||
3. **Phase 3 (code row).** Remove `previewOptions`, `proofOptions` and their tests. `cli.mjs` and
|
||||
`webui-service.sh` refuse the old flags with `--auth-header was retired by Q175; use --sso`. Close #385
|
||||
with the phase 2 STATE and the phase 3 commit.
|
||||
|
||||
The Authentik proxy outpost can stay in front. It still sends `X-authentik-*` headers, and the Console never
|
||||
reads them. A test proves that a request carrying them gets nothing without a session cookie.
|
||||
|
||||
### 6. The tester port and the N3 tester identity
|
||||
|
||||
The tester listener exists so agents can test as the tester and never as Jason (mos-0870 N3). Under SSO:
|
||||
|
||||
- It never mounts `/auth/*` (404), never reads a cookie, and refuses any request carrying `Authorization` or a
|
||||
`__Host-mosaic_session` / `__Host-mosaic_login` cookie (403 `tester listener takes no sign-in`). The same
|
||||
goes for `X-authentik-jwt` and the old auth and proof header names.
|
||||
- `startServer` throws when its options hold both `tester` and `sso`, so the SSO module can't be mounted on
|
||||
the tester listener by a wiring mistake. A unit test calls it with both.
|
||||
- Its writer stays the fixed `{ tester: true, user: 'tester' }`, and it allows only the row 175 writes, as
|
||||
today. No session or header can change it.
|
||||
- Its bus calls stay on `TESTER_CLI`. The bus checks the tester proof and refuses human verbs from it, and the
|
||||
broker refuses a business whose human is `tester` and any role named `tester`. So nothing that reaches 7333
|
||||
can become the human.
|
||||
- 7332 keeps refusing `x-mosaic-tester` for every Host (:485).
|
||||
- `subjects` validation refuses a `name` of `tester` or `human`.
|
||||
|
||||
### 7. Point 5, the proxy-forwarded token
|
||||
|
||||
Not in phase 1. Point 5 makes it optional and the code flow covers the ruling. If it's built later:
|
||||
|
||||
- Verify with jose (MIT, exact pin), against the JWKS from the configured issuer's own discovery. Never use
|
||||
the `X-authentik-meta-jwks` header, because it arrives with the request and the forger controls it.
|
||||
- Use the same alg allow list, iss, exp and subject list. `aud` is the proxy provider's client id, and the
|
||||
config check refuses it if it equals any code-flow `clientId`, so an ID token issued to the Console can't be
|
||||
replayed as a proxy token or the other way round (N4).
|
||||
- The mosaic-preview provider signs with HS256 and has an empty JWKS, so its `X-authentik-jwt` fails every
|
||||
check until Jason gives it an RSA or EC key (Jason step 1).
|
||||
|
||||
### 8. Interim exposure until phase 2
|
||||
|
||||
Today, with `--send` live, any same-uid process on dragon-lin can connect to 127.0.0.1:7332 with
|
||||
`Host: mosaic.woltje.com`, `X-authentik-username: jason.woltje`, the matching Origin and
|
||||
`Sec-Fetch-Site: same-origin`, and send as Jason. That includes agent seats. Through the tunnel, the outpost
|
||||
strips client copies of the header, so a remote forger needs local code execution first. Users on other uids
|
||||
can reach the loopback port too (P19 333).
|
||||
|
||||
Mos ruled (mos-0972) that the Console runs without `--send` until phase 2. Reads stay up, the header path
|
||||
stops carrying writes, and Jason sends piece 1 with `mosaic talk pm` instead. Row 189 (`stack-release.sh
|
||||
activate --no-send`, #1687) makes that change through the pointer so the unit and stack-active.json stay in
|
||||
step; the restart follows its landing. Until phase 2, no agent sends a request with the auth header to 7332.
|
||||
The tests use fixture servers.
|
||||
|
||||
To be clear about the limit: SSO protects Console ingress. It doesn't protect the bus's human boundary. The
|
||||
human proof is path-bound (#225), so a same-uid process can already run the tree's `human-cli.mjs` and act
|
||||
as the human without touching the Console. Q175 doesn't change that. P19 333 owns it.
|
||||
|
||||
### 9. Tests
|
||||
|
||||
All tests run against a fake OP in `packages/webui/tests/helpers/fake-oidc.mjs`. It's a loopback https
|
||||
server with a self-signed cert. Each test passes the library a `customFetch` that trusts only that cert, so
|
||||
production code needs no insecure-request flag. The fake OP has discovery, a JWKS, and authorization, token
|
||||
and end-session endpoints, with keys made by `node:crypto` (RSA 2048, P-256, and an HMAC secret for the
|
||||
forgeries). It can serve two issuers at once and rotate its keys. No test reaches a real provider. Each
|
||||
refusal test asserts the status, that no `__Host-mosaic_session` was set, and that the bus send stub was
|
||||
never called. Each test also passes once unmutated, as the control.
|
||||
|
||||
Refusals Mos named:
|
||||
|
||||
1. Forged header. `X-authentik-username: jason.woltje` with `Host: mosaic.woltje.com`, the right Origin and
|
||||
Fetch Metadata, and no cookie: 401 on `/api/bus/message`, `/api/settings` and `/api/config`, and a 303 to
|
||||
login on `/`.
|
||||
2. HS256 ID token. The fake OP signs the ID token with HS256, using the client secret as the key (the classic
|
||||
confusion) and `kid` set to a real RSA kid: refused at the callback. The library's alg check refuses it
|
||||
before any signature check, so this test proves the alg rule, not the signature check. Test 9 covers that.
|
||||
3. Wrong aud. A correctly signed RS256 token with `aud` set to another client id: refused. Also an `aud`
|
||||
array holding our client id and another, with no `azp`: refused.
|
||||
4. Expired token. A correctly signed token with `exp` 10 minutes ago (beyond the library's 30 s clock
|
||||
tolerance): refused.
|
||||
|
||||
More refusals:
|
||||
|
||||
5. Wrong `iss`, both a different issuer and a trailing-slash variant.
|
||||
6. Nonce mismatch or missing nonce.
|
||||
7. State mismatch, a missing login cookie, a replayed callback (record already taken), an expired login record,
|
||||
and two login cookies. For the replay, the fake OP is set to accept the same code twice, so the second
|
||||
callback reaches the record lookup and only the take-and-delete rule refuses it.
|
||||
8. A provider that demands no PKCE. The token request must carry `code_verifier`, and the fake OP refuses a
|
||||
wrong one.
|
||||
9. The signature check (B5), three cases: (a) `alg: none`; (b) an RS256 token whose header names a real kid in
|
||||
the JWKS, with a changed payload and the original signature; (c) an RS256 token signed by a key whose kid
|
||||
isn't in the JWKS, even after the refetch. (a) is refused by the alg check in step 4, so it's an alg test
|
||||
like test 2. Only (b) and (c) reach step 5, and (b) is the case only the signature check catches.
|
||||
10. A valid token for a `sub` not in `subjects`: 403 and one `subject-unknown` log line with the provider id,
|
||||
iss and sub only.
|
||||
11. Start refusals: `algs` holding `HS256` or `none`, HS-only discovery (the mosaic-preview shape), an empty
|
||||
JWKS, no S256, an issuer mismatch, a subject named `tester` or `human`, `--sso` together with
|
||||
`--auth-header`, and slot files that are a symlink, mode 0644, mode 0640, or under the repo. The owner
|
||||
check has no test: making a file owned by another uid needs root, which the suite doesn't have. The
|
||||
review reads that branch instead.
|
||||
12. Tester port: a valid session cookie (taken from a 7332 sign-in), `Authorization: Bearer <valid id token>`,
|
||||
and `X-authentik-jwt` each get 403. `/auth/login` on 7333 gets 404. `startServer` with `tester` and `sso`
|
||||
throws.
|
||||
13. Session: idle expiry, absolute expiry, a logout followed by reuse of the old cookie, and logout without
|
||||
same-origin (refused, session kept). Background `/api/` GETs every 10 s for over an hour don't keep the
|
||||
session alive; a page load does.
|
||||
14. Open redirect: `return=//evil.example`, `return=https://evil.example`, `return=/\evil.example` and
|
||||
`return=/%09/evil.example` all land on `/`, and `return=/board?x=1` lands there.
|
||||
15. Provider mix-up. Two providers, A and B. A login started for A whose callback carries B's `iss` and a
|
||||
code from B is refused, and a `provider=` query on the callback is ignored. B's code is issued against
|
||||
A-record's PKCE challenge, so the verifier matches and only the record rule (the callback belongs to A)
|
||||
refuses it.
|
||||
16. Several algs: a provider with `algs: ["RS256", "ES256"]` takes a token signed with either, and refuses
|
||||
PS256 even when the JWKS holds a usable PS256 key.
|
||||
17. Login record cap: 33 started logins leave 32 records, and the first login's callback finds no record.
|
||||
18. JWKS rotation: the fake OP rotates to a new key with a new kid. A sign-in after the rotation succeeds
|
||||
through the one refetch, and a token under the removed kid is refused.
|
||||
19. Logs: across every test in this file, captured log lines hold none of the tokens, codes, states, nonces,
|
||||
verifiers, the client secret or a session id that the test produced.
|
||||
20. The session store holds no raw session id (keys are sha256 values, and no value holds the cookie).
|
||||
21. Set-Cookie: the exact session and login cookie strings, including the expiry of the login cookie at
|
||||
callback.
|
||||
22. Host: under `--sso`, a loopback Host on `/`, `/api/board` and a write each get 403; `/healthz` on
|
||||
loopback gets 200.
|
||||
23. Degraded start: the fake OP is down at start; the Console starts, `/auth/login` is 503, `/healthz` reports
|
||||
`unreachable`; the OP comes up and the next retry makes sign-in work. A discovery URL that answers 200
|
||||
with an HTML page also counts as `unreachable` and the Console starts. A provider that answers with
|
||||
HS-only discovery at start refuses the start.
|
||||
24. Signed-out page (C2). A page test in the board-held.test.mjs style stubs fetch to answer 401
|
||||
`{ error: 'sign-in required' }`: the page shows the signed-out notice, its link is
|
||||
`/auth/login?return=<current path and query>`, and the refresh stops. A 401 with another error stays an
|
||||
error. The existing opaqueredirect case still signs out.
|
||||
25. Loader (C1), in scripts/test-config.sh: a config with a valid `console.sso` loads; a malformed one (an HS
|
||||
alg, a missing `slot` field, a slot name outside the pattern, an unknown key under `console`) fails and
|
||||
names the bad key path, and no case reads a slot file (the file cases stay in test 11); a config without
|
||||
`console` loads.
|
||||
|
||||
Positive paths: an RS256 and an ES256 provider each complete sign-in and one send, and two providers configured
|
||||
together each work with their own subjects.
|
||||
|
||||
Mutants for the gate, each killed by the named test:
|
||||
|
||||
- drop the `validateApplicationLevelSignature` call (9b);
|
||||
- wrap that call in a try/catch that continues (9b);
|
||||
- drop the `subjects` check (10);
|
||||
- accept HS* in `algs` (11);
|
||||
- take the provider from the query (15);
|
||||
- let the tester listener read cookies (12);
|
||||
- skip the login record delete (7);
|
||||
- drop `HttpOnly` from the session cookie (21);
|
||||
- refresh `seen` on every request (13);
|
||||
- build `redirect_uri` from the request Host (a test with a different Host on `/auth/login` checks the
|
||||
`redirect_uri` sent to the fake OP);
|
||||
- serve the loopback read path under `--sso` (22).
|
||||
|
||||
### 10. Jason-gated steps
|
||||
|
||||
1. Authentik mosaic-preview: give it an RSA or EC signing key or retire it, and confirm by an admin read that
|
||||
client_id `GEQ84kBbt2RG6fhYQGgZl2J9jgjw6wRJ3oSWxqZQ` belongs to it. (Ask B matched it only on scopes and
|
||||
redirect_uri.)
|
||||
2. Authentik mosaic-stack provider: register redirect URI `https://mosaic.woltje.com/auth/callback` and
|
||||
post-logout URI `https://mosaic.woltje.com/auth/signed-out`, keep RS256, and give the client id. Put the
|
||||
client secret into slot `console-sso-authentik` (or say an agent may create the slot file from a value he
|
||||
puts in place, per the Q173 secrets rule). Agents never see the value.
|
||||
3. His `sub` for that provider, or a go to read it from the Console's one `subject-unknown` log line on his
|
||||
first sign-in attempt.
|
||||
4. The `console.sso` entry in `~/.config/mosaic-dev/config.json`, inside the phase 2 window, after the pin
|
||||
and before the Console starts (section 5). He writes it, or he gives an agent the go to write that one
|
||||
key from the values in steps 2 and 3, with a backup of the file first. config.json is user-authored, so
|
||||
this isn't covered by Q173. Writing it earlier stops every tool on the live copy, whose loader refuses
|
||||
the key.
|
||||
5. Any Traefik or outpost change. None is needed while the outpost stays in front. Dropping the outpost
|
||||
later, so the Console is the only gate, changes who can reach the Console from outside and is his call.
|
||||
6. The phase 2 re-pin window go.
|
||||
|
||||
## Questions for review
|
||||
|
||||
Q-A to Q-E are settled (Filbert r1 and r2, Mos mos-0973). One open point, for Mos:
|
||||
|
||||
- The config.json sequence (C1): the key goes in only inside the phase 2 window, after the pin and before
|
||||
the Console starts, with a backup first, and a rollback removes it before the pointer moves (section 5).
|
||||
Mos confirms this before phase 1 starts.
|
||||
@@ -0,0 +1,147 @@
|
||||
# Row 163 (D2): `stack-release.sh rehearse` and `bus start --no-launch`
|
||||
|
||||
Gitea #1660. Brief: docs/plans/2026-10-18_stack-release.md, "Stack rehearsal on a scratch root
|
||||
(D2)", blob b1500d96ad27. Reviewer: Filbert. Rulings: mos-0961, mos-0962, mos-0963.
|
||||
Base: 4f02486d5 (origin/refactor). D2 lands after the Part B window (P19 385), because Part B's
|
||||
window file names the stack-release.sh blob (mos-0958, mos-0959).
|
||||
|
||||
## What it does
|
||||
|
||||
`rehearse ID BUSINESS --backup ACTIVATION|PATH --port N [--timeout S]` builds
|
||||
`<dataRoot>/rehearsals/<UTC second>-<copy 12>/` (0700) from a backup, runs a guard on the scratch
|
||||
config with the target copy's code, runs the migration checks when the schema differs, starts the
|
||||
target's `bus start BUSINESS --no-launch` and Console on the scratch root, gates them with D1's
|
||||
checks minus the PM's, checks that no session started, stops both, and compares the backup and the
|
||||
live config and business files with sums taken first. One PASS or FAIL line per check, then
|
||||
REHEARSAL_PASS (0) or REHEARSAL_FAIL (1). Usage exits 4. docs/TOOLS.md has the operator text.
|
||||
|
||||
`bus start --no-launch` (brief item 4): `start()` refuses `launches-disabled`, so every launch path
|
||||
spawns nothing; a session's launch request is refused and logged; `listen()` starts no thread
|
||||
ticker; `recover()` signals and ends nothing and logs what it left; the Registry is read-only, so
|
||||
sessions.json stays as an earlier host left it and `add()` refuses. The host-up line ends
|
||||
`, launches disabled`. `--pm` with it is a usage error. No unit passes it.
|
||||
|
||||
## Rulings carried
|
||||
|
||||
- mos-0961: `data-root-live` refuses a scratch dataRoot that resolves to the live data root;
|
||||
`sessions-live` refuses when a sessions.json in the scratch data root names a running pid. The run
|
||||
never deletes the file or retries. recover() under --no-launch is covered by the launcher test
|
||||
"bus start --no-launch leaves an earlier host's live session alone: no signal, no end,
|
||||
sessions.json as it was".
|
||||
- mos-0962: every tracker.* variable is stripped, business and role level; `business.diff` keeps the
|
||||
change; `tracker-configured` fails if bootConfig still sets trackers; notify.json is asserted to be
|
||||
exactly {"notifyVersion":1,"binding":null} at 0600.
|
||||
- mos-0963: every role credential reference and @sync's is moved to its own 0600 file under
|
||||
`<rehearsal>/placeholders/` holding `rehearsal-placeholder-token`; `credential-ref-live` fails for
|
||||
an env reference or a file whose real directory is not the placeholders directory. Test R9 leaves
|
||||
one role ref on a file outside placeholders and gets the FAIL; mutant K1 drops the check and R9
|
||||
fails.
|
||||
|
||||
## Outbound calls
|
||||
|
||||
The rehearsal reaches nothing off the host:
|
||||
|
||||
- reads: the live config.json, the live business file, the pointer, the backup, the copies
|
||||
directory, the target copy's roles/, git in the canonical checkout (`copy_ok`'s merge-base);
|
||||
- writes: only `<dataRoot>/rehearsals/<stamp>/`;
|
||||
- processes: the target's bus host (`--no-launch`) and Console (`serve --port N --business B`, no
|
||||
`--send`), both with `env -i HOME PATH MOSAIC_CONFIG=<scratch config>`;
|
||||
- network: 127.0.0.1:N for /healthz, and the host's sockets under the scratch data root.
|
||||
|
||||
No model call: launches are disabled and no session starts (`no-session`). No Vikunja: tracker.*
|
||||
is stripped and `tracker-configured` checks bootConfig. No Discord: notify binding null. No Gitea:
|
||||
every credential ref is a placeholder file, and the git socket serves only bound sessions, of
|
||||
which there are none.
|
||||
|
||||
`projects.stack.root` stays at the live checkout, /mnt/storage/src/mosaic-stack. The business
|
||||
file's projects are copied unchanged. Under --no-launch nothing makes a checkout from it, because
|
||||
checkouts are made only in `start()`, which refuses.
|
||||
|
||||
## Deviations from the brief
|
||||
|
||||
1. Item 1 says to copy "the business file, roles and organization file from the live config
|
||||
directory". Roles come from the target copy's `roles/`, as the live bus reads them (rolesDir is
|
||||
`<REPO>/roles` unless MOSAIC_ROLES_DIR is set, and `env -i` leaves it unset). The live config
|
||||
directory has no organization file; bootConfig's organization entry is the config dir itself,
|
||||
which here is the scratch config dir.
|
||||
2. The business file is rewritten, not copied (mos-0962, mos-0963), and `business.diff` records
|
||||
the change.
|
||||
3. The `tracker` key stays and `tracker.sync.credentials` becomes a placeholder: the business
|
||||
schema requires both, so stripping the key (mos-0962's wording) makes the loader refuse.
|
||||
bootConfig sets trackers only with tracker.baseUrl and a project's tracker.project, and both are
|
||||
stripped; `tracker-configured` checks the result. Authority: mos-0964 (2026-10-20T14:35:30Z),
|
||||
which accepts this and replaces the "@sync still present" clause of mos-0963 item 2:
|
||||
business.mjs:197 calls checkTracker on every load, and checkTracker requires tracker and
|
||||
tracker.sync (:57, :59) and checks sync.credentials (:63). @sync stays with its ref on a
|
||||
placeholder, and `credential-ref-live` covers it with the role refs. Evidence: R10 (a tracker
|
||||
still set fails `tracker-configured`), K2 (the check dropped, R10 fails), K10 (the tracker.*
|
||||
strip dropped, R1 fails on `tracker-configured`), and K9 (the @sync swap dropped, R1 fails on `credential-ref-live`).
|
||||
4. mosaic-config already refuses a scratch config that names the live data root lexically, because
|
||||
the rehearsal directory sits inside the live data root and a dataRoot may not contain the config
|
||||
dir. The guard's `data-root-live` catches the form that reaches the live root through a link
|
||||
(R11).
|
||||
5. Files owned: the brief lists stack-release.sh, its test, TOOLS.md and the packet. Item 4's flag
|
||||
adds packages/cli/src/cli.mjs, packages/cli/src/launcher.mjs, packages/seat/src/session.mjs and
|
||||
their tests (host.test.mjs, launcher.test.mjs, session.test.mjs).
|
||||
6. The brief's "free loopback port" is an operator-given `--port`, refused when reserved (7332,
|
||||
7333, the pointer's --port and --tester-port) or in use, never replaced by another port.
|
||||
7. Brief item 3's "in the foreground": the host runs as a child of the script for the length of
|
||||
the gate, then gets SIGTERM; it is not a unit and not detached.
|
||||
|
||||
## Tests
|
||||
|
||||
packages/cli/tests/stack-release.test.mjs, rehearse section (stand-in host, Console, migrate and
|
||||
Store in four fixture copies; real scripts/mosaic, mosaic-config, cli config and the business
|
||||
loader):
|
||||
|
||||
- R1 passes on the same schema: files, modes, placeholders, business.diff, the host's meta row in
|
||||
the scratch store and not in the backup, args logs, steps.log equal to the output, live files
|
||||
byte-identical.
|
||||
- R2 migration path 3g to 3h; R3 migrate fails.
|
||||
- R4 host fails, status fails, status names another pid, host launches, host keeps writer.lock.
|
||||
- R5 SIGTERM ignored: SIGKILL and FAIL host-stopped. R6 healthz. R7 copy-match.
|
||||
- R8 port in use, and 7332, 7333, the pointer's --port and --tester-port reserved.
|
||||
- R9 credential-ref-live (mos-0963). R10 tracker-configured (mos-0962). R11 data-root-live through
|
||||
a link and sessions-live on a running pid (mos-0961). R12 notify-off.
|
||||
- R13 old copy, backup missing, backup sums, no pointer, usage.
|
||||
|
||||
packages/cli/tests/launcher.test.mjs: recover() under --no-launch leaves an earlier host's live
|
||||
session alone; every launch is refused and logged; no thread tick (and the same host with
|
||||
launches runs it). packages/cli/tests/host.test.mjs: the host-up line, no sessions.json, --pm
|
||||
with --no-launch is usage, and no unit renders --no-launch. packages/seat/tests/session.test.mjs:
|
||||
a read-only Registry never writes and refuses add().
|
||||
|
||||
## Mutants
|
||||
|
||||
33, one per behaviour, each killed (mutants.py, mutants-run.log and mutants-rerun.log beside this file). The first run left K11
|
||||
(bus-status pid), K21 (scratch store linked to the backup), K27 (ticker under --no-launch) and
|
||||
K31 (cli drops launches); each got a test and the rerun killed all four. K21 now dies on
|
||||
FAIL backup-unchanged, the check it exists for.
|
||||
|
||||
## Round 2 (Filbert r1, comment 29445: F1-F3 changes, N1)
|
||||
|
||||
Filbert's six mutants M1-M6 survived the R tests. Each named a check that worked but that no test
|
||||
drove to FAIL. Round 2 adds those tests. stack-release.sh is unchanged (blob 0053f35d1360).
|
||||
|
||||
- F1 (M1, M2): R14. A new fixture fault, host-touches, has the host append to a file it lists.
|
||||
Three cases (the live config, the live business file, the backup's bus.sqlite) each give exactly
|
||||
one FAIL, live-config-unchanged or backup-unchanged, after the gate passed and the host stopped.
|
||||
- F2 (M3): R9 gains a pm gitea reference left on env REHEARSAL_GITEA_TOKEN, which fails
|
||||
credential-ref-live with "pm gitea is env REHEARSAL_GITEA_TOKEN" before the host starts.
|
||||
- F2 (M4): R11 gains a scratch config whose dataRoot is another existing directory ("resolves to
|
||||
X, not R/data") and one whose dataRoot is not there ("X does not exist"). Both fail
|
||||
data-root-scratch before the host starts.
|
||||
- F2 (M5): R4 gains host-launches-dir, a host that makes data/launches, which fails no-session
|
||||
with "found launches under".
|
||||
- F3 (M6): R15 sends TERM, INT and HUP to a rehearsal whose gate is still waiting on /healthz.
|
||||
Each gives FAIL signal, then console-stopped, host-stopped, host-released-store,
|
||||
backup-unchanged and live-config-unchanged all PASS, REHEARSAL_FAIL, exit 1, and the host's pid
|
||||
is gone. rehearse() gained an e.during hook that gets the child process.
|
||||
- N1: Registry.remove() wrote sessions.json under readOnly. #write() now returns at once when the
|
||||
registry is read-only, so the constructor, add() and remove() share one rule. The session test
|
||||
calls remove() on a read-only registry and checks that the file is unchanged.
|
||||
|
||||
Mutants K34-K41, one per finding and N1, are all killed (r2/mutants-run.log beside this file):
|
||||
K34 backup-unchanged always passes, K35 live-config-unchanged always passes, K36 env ref
|
||||
accepted, K37 data-root-scratch mismatch dropped, K38 missing root dropped, K39 launches dir
|
||||
ignored, K40 no INT/TERM/HUP trap, K41 read-only Registry writes.
|
||||
@@ -0,0 +1,10 @@
|
||||
04d72e5ef4d8d5fc85a4f11aa51331354696cc7cf66af2ed7540cf6cfc0153b9 agents/sage/work/queue-163/BUILD.md
|
||||
5bd1bc697cd56e57b00e60c0e80be72cd1043a55a6d2fc0b9e2ccdd0c5cf9802 docs/TOOLS.md
|
||||
52597d7fa1469510fcf8e6dfd63664ad4c492bc17039885aa839231798693058 packages/cli/src/cli.mjs
|
||||
5492ebb5aa3c9c85874e4c7de7b65fc37733e2db636690c11fe2d2fdf72c2dd0 packages/cli/src/launcher.mjs
|
||||
e9ce0e0f7368d3fb2e1edbd4ebc724513ec19d7bac0c993f8be0d9494427be07 packages/cli/tests/host.test.mjs
|
||||
42519898b5bcf84c21088647e161a86934d4fe351207f736b9b120b6e1c2b91b packages/cli/tests/launcher.test.mjs
|
||||
4dfeefa1bb3c11e8633d75df4f628497ebd427ebf8402ba5efc3e5c7fd030057 packages/cli/tests/stack-release.test.mjs
|
||||
5abdeab8bc6b63d291d0054124a4311a2260b590c68f758daa142a9deb2f4a72 packages/seat/src/session.mjs
|
||||
741883e399d77d429cc29a87beef6db9c51c1759669b025006f037001d101eb6 packages/seat/tests/session.test.mjs
|
||||
5bb3ac40102ce515f1acdcbc5670feb161ce40a193e94e2f8b104ebf98eddc38 scripts/stack-release.sh
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,28 @@
|
||||
base 580500699 dirty 0 DOCKER_HOST=unset
|
||||
pkg-bus rc=0 ℹ pass 451 ℹ fail 0 | ℹ duration_ms 3051.98676
|
||||
pkg-business rc=0 ℹ pass 149 ℹ fail 0 | ℹ duration_ms 1933.965446
|
||||
pkg-cli rc=0 ℹ pass 470 ℹ fail 0 | ℹ duration_ms 337885.04401
|
||||
pkg-control-board rc=0 ℹ pass 124 ℹ fail 0 | ℹ duration_ms 585.587135
|
||||
pkg-conversation rc=0 ℹ pass 182 ℹ fail 0 | ℹ duration_ms 33296.254589
|
||||
pkg-discord rc=0 ℹ pass 183 ℹ fail 0 | ℹ duration_ms 2624.332987
|
||||
pkg-harness rc=0 ℹ pass 133 ℹ fail 0 | ℹ duration_ms 11808.615207
|
||||
pkg-ledger rc=0 ℹ pass 89 ℹ fail 0 | ℹ duration_ms 12902.241615
|
||||
pkg-mosaic rc=0 ℹ pass 69 ℹ fail 0 | ℹ duration_ms 2051.760841
|
||||
pkg-plugins rc=0 ℹ pass 111 ℹ fail 0 | ℹ duration_ms 247.493374
|
||||
pkg-project rc=0 ℹ pass 40 ℹ fail 0 | ℹ duration_ms 1031.233595
|
||||
pkg-runs rc=0 ℹ pass 43 ℹ fail 0 | ℹ duration_ms 727.865986
|
||||
pkg-seat rc=0 ℹ pass 152 ℹ fail 0 | ℹ duration_ms 5858.066276
|
||||
pkg-tasks rc=0 ℹ pass 97 ℹ fail 0 | ℹ duration_ms 1147.333901
|
||||
pkg-webui rc=0 ℹ pass 269 ℹ fail 0 | ℹ duration_ms 46548.273469
|
||||
script-auth rc=0 | selftest: 15 passed, 0 failed
|
||||
script-conductor rc=0 | selftest: 17 passed, 0 failed
|
||||
script-config rc=0 | selftest: 24 passed, 0 failed
|
||||
script-discord rc=0 | discord suite: 78 passed, 0 failed
|
||||
script-extension-package rc=0 | extension package selftest: 18 passed, 0 failed
|
||||
script-foundation rc=0 | selftest: 44 passed, 0 failed
|
||||
script-land-work rc=0 | land-work selftests: 86 passed, 0 failed
|
||||
script-queue rc=0 ℹ pass 156 ℹ fail 0 | queue suite: 28 passed, 0 failed
|
||||
script-release rc=0 | selftest: 14 passed, 0 failed
|
||||
script-task rc=0 | selftest: 98 passed, 0 failed
|
||||
GATE_PASSED
|
||||
removed network sage-r163_default
|
||||
@@ -0,0 +1,5 @@
|
||||
KILLED K11 bus-status pid check dropped (rc 1)
|
||||
KILLED K21 scratch store links the backup (rc 1)
|
||||
KILLED K27 ticker runs under --no-launch (rc 1)
|
||||
KILLED K31 cli --no-launch not passed to the launcher (rc 1)
|
||||
ALL_KILLED
|
||||
@@ -0,0 +1,34 @@
|
||||
KILLED K1 credential-ref-live dropped (rc 1)
|
||||
KILLED K2 tracker-configured dropped (rc 1)
|
||||
KILLED K3 data-root-live dropped (rc 1)
|
||||
KILLED K4 sessions-live dropped (rc 1)
|
||||
KILLED K5 notify-off dropped (rc 1)
|
||||
KILLED K6 no-session dropped (rc 1)
|
||||
KILLED K7 copy-match dropped (rc 1)
|
||||
KILLED K8 role credential swap dropped (rc 1)
|
||||
KILLED K9 @sync swap dropped (rc 1)
|
||||
KILLED K10 business tracker strip dropped (rc 1)
|
||||
SURVIVED K11 bus-status pid check dropped (rc 0)
|
||||
KILLED K12 host-released-store dropped (rc 1)
|
||||
KILLED K13 SIGKILL counted as a pass (rc 1)
|
||||
KILLED K14 healthz always ok (rc 1)
|
||||
KILLED K15 port-reserved dropped (rc 1)
|
||||
KILLED K16 port-in-use dropped (rc 1)
|
||||
KILLED K17 backup-sums dropped (rc 1)
|
||||
KILLED K18 copy-without-no-launch dropped (rc 1)
|
||||
KILLED K19 migration skipped (rc 1)
|
||||
KILLED K20 migrate failure ignored (rc 1)
|
||||
SURVIVED K21 scratch store links the backup (rc 0)
|
||||
KILLED K22 scratch config keeps the live dataRoot (rc 1)
|
||||
KILLED K23 host started without --no-launch (rc 1)
|
||||
KILLED K24 start() launches under --no-launch (rc 1)
|
||||
KILLED K25 session launch request allowed under --no-launch (rc 1)
|
||||
KILLED K26 recover() ends sessions under --no-launch (rc 1)
|
||||
SURVIVED K27 ticker runs under --no-launch (rc 0)
|
||||
KILLED K28 registry writes under --no-launch (rc 1)
|
||||
KILLED K29 Registry readOnly writes anyway (rc 1)
|
||||
KILLED K30 Registry readOnly add() allowed (rc 1)
|
||||
SURVIVED K31 cli --no-launch not passed to the launcher (rc 0)
|
||||
KILLED K32 cli --pm with --no-launch allowed (rc 1)
|
||||
KILLED K33 cli host-up line drops launches disabled (rc 1)
|
||||
SOME_SURVIVED
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
# Row 163 mutants: each one drops or weakens one behaviour; the named tests must then fail.
|
||||
import hashlib, os, shutil, subprocess, sys, signal
|
||||
W = '/mnt/storage/scratch/sage-b163'
|
||||
OUT = os.environ.get('MUT_OUT', '/mnt/storage/scratch/sage-b163-gate/mutants')
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
SR = 'scripts/stack-release.sh'
|
||||
T = 'packages/cli/tests/stack-release.test.mjs'
|
||||
M = [
|
||||
('K1 credential-ref-live dropped', SR, 'if(f===null||phr===null||path.dirname(f)!==phr)fail("credential-ref-live"', 'if(false)fail("credential-ref-live"', T, '^R9 '),
|
||||
('K2 tracker-configured dropped', SR, 'if(config.trackers!==undefined)fail(', 'if(false)fail(', T, '^R10 '),
|
||||
('K3 data-root-live dropped', SR, 'if(root===real(live))fail(', 'if(false)fail(', T, '^R11 '),
|
||||
('K4 sessions-live dropped', SR, 'if(running.length)fail(', 'if(false)fail(', T, '^R11 '),
|
||||
('K5 notify-off dropped', SR, 'fail("notify-off",`${nf} is not {', 'void("notify-off",`${nf} is not {', T, '^R12 '),
|
||||
('K6 no-session dropped', SR, 'if [ -z "$found" ]; then r_pass no-session', 'if true; then r_pass no-session', T, '^R4 '),
|
||||
('K7 copy-match dropped', SR, ' R_GATE=copy-match\n', ' R_GATE=""; return 0\n', T, '^R7 '),
|
||||
('K8 role credential swap dropped', SR, 'strip(r?.vars);swap(name,r?.credentials)}', 'strip(r?.vars)}', T, '^R1 '),
|
||||
('K9 @sync swap dropped', SR, 'swap("@sync",b.tracker?.sync?.credentials);', '', T, '^R1 '),
|
||||
('K10 business tracker strip dropped', SR, '\nstrip(b.vars);\n', '\n', T, '^R1 '),
|
||||
('K11 bus-status pid check dropped', SR, 'String(s.host.pid)!==main?`host pid ${s.host.pid} is not ${main}, the host this run', 'false?`host pid ${s.host.pid} is not ${main}, the host this run', T, '^R4 '),
|
||||
('K12 host-released-store dropped', SR, 'if [ -e "$R/data/bus/writer.lock" ]; then', 'if false; then', T, '^R4 '),
|
||||
('K13 SIGKILL counted as a pass', SR, 'kill -KILL "$pid" 2>/dev/null || true; r_fail "$name-stopped"', 'kill -KILL "$pid" 2>/dev/null || true; r_pass "$name-stopped"', T, '^R5 '),
|
||||
('K14 healthz always ok', SR, '.then((r)=>process.exit(r.status===200?0:1),()=>process.exit(1));\' -- "$R_PORT"', '.then(()=>process.exit(0),()=>process.exit(0));\' -- "$R_PORT"', T, '^R6 '),
|
||||
('K15 port-reserved dropped', SR, '[ "$((10#$R_PORT))" != "$i" ] || r_abort port-reserved', 'true || r_abort port-reserved', T, '^R8 '),
|
||||
('K16 port-in-use dropped', SR, 'r_port_free "$R_PORT" || r_abort port-in-use', 'true || r_abort port-in-use', T, '^R8 '),
|
||||
('K17 backup-sums dropped', SR, 'sha256sum -c --quiet ../SHA256SUMS) >/dev/null 2>&1 ||', 'true) >/dev/null 2>&1 ||', T, '^R13 '),
|
||||
('K18 copy-without-no-launch dropped', SR, 'grep -qF -- \'"--no-launch"\' "$COPIES/$TO/packages/cli/src/cli.mjs" 2>/dev/null ||', 'true ||', T, '^R13 '),
|
||||
('K19 migration skipped', SR, 'if [ "$v" = "$T_SCHEMA" ]; then r_pass migration', 'if true; then r_pass migration', T, '^R2 '),
|
||||
('K20 migrate failure ignored', SR, '[ "$rc" = 0 ] && [ "$(tail -n 1 <<< "$out")" = "bus schema $T_SCHEMA ready" ] || r_abort migrate "', 'true || r_abort migrate "', T, '^R3 '),
|
||||
('K21 scratch store links the backup', SR, 'cp "$R_BK"/bus/* "$R/data/bus/" &&', 'ln -s "$R_BK"/bus/* "$R/data/bus/" &&', T, '^R1 '),
|
||||
('K22 scratch config keeps the live dataRoot', SR, 'c.dataRoot=data;', '', T, '^R1 '),
|
||||
('K23 host started without --no-launch', SR, '"$COPIES/$TO/scripts/mosaic" bus start "$BUSINESS" --no-launch >', '"$COPIES/$TO/scripts/mosaic" bus start "$BUSINESS" >', T, '^R1 '),
|
||||
('K24 start() launches under --no-launch', 'packages/cli/src/launcher.mjs', ' if (!launches) throw new Refusal("launches-disabled", "this host was started with --no-launch");\n', '', 'packages/cli/tests/launcher.test.mjs', None),
|
||||
('K25 session launch request allowed under --no-launch', 'packages/cli/src/launcher.mjs', ' if (!launches) {\n await host.op({ op: "refuse", cap, code: "launches-disabled" })', ' if (false) {\n await host.op({ op: "refuse", cap, code: "launches-disabled" })', 'packages/cli/tests/launcher.test.mjs', None),
|
||||
('K26 recover() ends sessions under --no-launch', 'packages/cli/src/launcher.mjs', ' if (!launches) {\n for (const s of leftover) log(', ' if (false) {\n for (const s of leftover) log(', 'packages/cli/tests/launcher.test.mjs', None),
|
||||
('K27 ticker runs under --no-launch', 'packages/cli/src/launcher.mjs', 'if (b.launch && launches) ticker', 'if (b.launch) ticker', 'packages/cli/tests/launcher.test.mjs', None),
|
||||
('K28 registry writes under --no-launch', 'packages/cli/src/launcher.mjs', 'new Registry(dataRoot, { readOnly: !launches })', 'new Registry(dataRoot)', 'packages/cli/tests/launcher.test.mjs', None),
|
||||
('K29 Registry readOnly writes anyway', 'packages/seat/src/session.mjs', ' if (!readOnly) this.#write();', ' this.#write();', 'packages/seat/tests/session.test.mjs', None),
|
||||
('K30 Registry readOnly add() allowed', 'packages/seat/src/session.mjs', ' if (this.#readOnly) throw new SeatError("this host keeps no sessions (--no-launch)", 2);\n', '', 'packages/seat/tests/session.test.mjs', None),
|
||||
('K31 cli --no-launch not passed to the launcher', 'packages/cli/src/cli.mjs', 'tracker: Boolean(boot.trackers?.[businessId]), launches, ', 'tracker: Boolean(boot.trackers?.[businessId]), ', 'packages/cli/tests/host.test.mjs', None),
|
||||
('K32 cli --pm with --no-launch allowed', 'packages/cli/src/cli.mjs', ' if (!launches && parsed.flags.has("--pm")) throw usage(', ' if (false) throw usage(', 'packages/cli/tests/host.test.mjs', None),
|
||||
('K33 cli host-up line drops launches disabled', 'packages/cli/src/cli.mjs', '${launches ? "" : ", launches disabled"}', '', 'packages/cli/tests/host.test.mjs', None),
|
||||
('K34 backup-unchanged always passes', SR, 'if [ "$(r_tree_sums "$R_BK")" = "$(cat "$R/backup.before")" ]; then r_pass backup-unchanged', 'if true; then r_pass backup-unchanged', T, '^R14 '),
|
||||
('K35 live-config-unchanged always passes', SR, 'if [ "$(sha256sum "$LIVE_CFG" "$LIVE_BIZ" 2>&1)" = "$(cat "$R/live.before")" ]; then', 'if true; then', T, '^R14 '),
|
||||
('K36 credential-ref-live accepts an env ref', SR, 'if(ref?.env!==undefined)fail("credential-ref-live"', 'if(false)fail("credential-ref-live"', T, '^R9 '),
|
||||
('K37 data-root-scratch mismatch dropped', SR, 'if(root!==real(scratch))fail(', 'if(false)fail(', T, '^R11 '),
|
||||
('K38 data-root-scratch missing root dropped', SR, 'if(root===null)fail("data-root-scratch"', 'if(false)fail("data-root-scratch"', T, '^R11 '),
|
||||
('K39 no-session ignores a launches dir', SR, '[ ! -e "$R/data/launches" ] || found+=" launches"', 'true || found+=" launches"', T, '^R4 '),
|
||||
('K40 no INT/TERM/HUP trap', SR, ' trap \'r_fail signal "the rehearsal was interrupted"; r_end\' INT TERM HUP\n', '', T, '^R15 '),
|
||||
('K41 read-only Registry writes', 'packages/seat/src/session.mjs', ' if (this.#readOnly) return;\n', '', 'packages/seat/tests/session.test.mjs', None),
|
||||
]
|
||||
sha = lambda p: hashlib.sha256(open(p, 'rb').read()).hexdigest()
|
||||
env = dict(os.environ, TMPDIR='/mnt/storage/scratch/sb163t')
|
||||
results = []
|
||||
ONLY = set(sys.argv[1:])
|
||||
for name, f, old, new, test, pat in M:
|
||||
if ONLY and name.split()[0] not in ONLY: continue
|
||||
p = os.path.join(W, f); bak = os.path.join(OUT, os.path.basename(f) + '.orig')
|
||||
shutil.copy2(p, bak)
|
||||
if sha(bak) != sha(p): sys.exit(f'backup of {p} failed')
|
||||
src = open(p).read()
|
||||
if src.count(old) != 1: sys.exit(f'{name}: pattern count {src.count(old)} in {f}')
|
||||
open(p, 'w').write(src.replace(old, new))
|
||||
cmd = ['node', '--test'] + (['--test-name-pattern', pat] if pat else []) + [test]
|
||||
log = os.path.join(OUT, name.split()[0] + '.log')
|
||||
try:
|
||||
with open(log, 'w') as lf:
|
||||
pr = subprocess.Popen(cmd, cwd=W, env=env, stdout=lf, stderr=subprocess.STDOUT, start_new_session=True)
|
||||
try: rc = pr.wait(timeout=600)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(pr.pid, signal.SIGKILL); pr.wait(); rc = 'timeout'
|
||||
finally:
|
||||
shutil.copy2(bak, p)
|
||||
if sha(bak) != sha(p): sys.exit(f'restore of {p} failed')
|
||||
killed = rc != 0
|
||||
results.append((name, killed, rc))
|
||||
print(f'{"KILLED" if killed else "SURVIVED"} {name} (rc {rc})', flush=True)
|
||||
print('ALL_KILLED' if all(k for _, k, _ in results) else 'SOME_SURVIVED')
|
||||
@@ -0,0 +1,48 @@
|
||||
✖ R14 a host that writes the live config, the live business file or the backup fails the end check by name (10205.967212ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 10820.998717
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/stack-release.test.mjs:2062:1
|
||||
✖ R14 a host that writes the live config, the live business file or the backup fails the end check by name (10205.967212ms)
|
||||
AssertionError [ERR_ASSERTION]: /mnt/storage/scratch/sb163t/sr-V3yNfh/data/backups/stack/20261020T000000Z-2a52abd7922e/bus/bus.sqlitePASS copy: 2a52abd7922ec2ce073c2b481b490e984630e9ce, schema 3g
|
||||
PASS port: 127.0.0.1:38065 is free
|
||||
PASS backup: /mnt/storage/scratch/sb163t/sr-V3yNfh/data/backups/stack/20261020T000000Z-2a52abd7922e matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/sb163t/sr-V3yNfh/data/rehearsals/20261020T150815Z-2a52abd7922e/data from the backup, config and business file under /mnt/storage/scratch/sb163t/sr-V3yNfh/data/rehearsals/20261020T150815Z-2a52abd7922e/config, business.diff beside them
|
||||
PASS data-root-live: /mnt/storage/scratch/sb163t/sr-V3yNfh/data/rehearsals/20261020T150815Z-2a52abd7922e/data is not the live data root
|
||||
PASS sessions-live: no running session named in the data root
|
||||
PASS data-root-scratch
|
||||
PASS notify-off: binding null, so no Discord bridge
|
||||
PASS tracker-configured: bootConfig sets no trackers
|
||||
PASS credential-ref-live: 9 reference(s), each a placeholder
|
||||
PASS migration: not needed: the backup is already 3g
|
||||
PASS gate: host-up, bus-status, console-up, healthz and copy-match on 2a52abd7922ec2ce073c2b481b490e984630e9ce
|
||||
PASS no-session: no sessions.json and no launches under /mnt/storage/scratch/sb163t/sr-V3yNfh/data/rehearsals/20261020T150815Z-2a52abd7922e/data
|
||||
PASS console-stopped: pid 3546679
|
||||
PASS host-stopped: pid 3546678
|
||||
PASS host-released-store
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_PASS
|
||||
rehearsal directory: /mnt/storage/scratch/sb163t/sr-V3yNfh/data/rehearsals/20261020T150815Z-2a52abd7922e
|
||||
|
||||
|
||||
0 !== 1
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/sage-b163/packages/cli/tests/stack-release.test.mjs:2066:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 0,
|
||||
expected: 1,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
✖ R14 a host that writes the live config, the live business file or the backup fails the end check by name (2394.880127ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3000.67202
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/stack-release.test.mjs:2062:1
|
||||
✖ R14 a host that writes the live config, the live business file or the backup fails the end check by name (2394.880127ms)
|
||||
AssertionError [ERR_ASSERTION]: /mnt/storage/scratch/sb163t/sr-VQM0Df/cfg/config.jsonPASS copy: bf4bac79a205e5010c48897456fc6e665a3cf9aa, schema 3g
|
||||
PASS port: 127.0.0.1:45755 is free
|
||||
PASS backup: /mnt/storage/scratch/sb163t/sr-VQM0Df/data/backups/stack/20261020T000000Z-bf4bac79a205 matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/sb163t/sr-VQM0Df/data/rehearsals/20261020T150820Z-bf4bac79a205/data from the backup, config and business file under /mnt/storage/scratch/sb163t/sr-VQM0Df/data/rehearsals/20261020T150820Z-bf4bac79a205/config, business.diff beside them
|
||||
PASS data-root-live: /mnt/storage/scratch/sb163t/sr-VQM0Df/data/rehearsals/20261020T150820Z-bf4bac79a205/data is not the live data root
|
||||
PASS sessions-live: no running session named in the data root
|
||||
PASS data-root-scratch
|
||||
PASS notify-off: binding null, so no Discord bridge
|
||||
PASS tracker-configured: bootConfig sets no trackers
|
||||
PASS credential-ref-live: 9 reference(s), each a placeholder
|
||||
PASS migration: not needed: the backup is already 3g
|
||||
PASS gate: host-up, bus-status, console-up, healthz and copy-match on bf4bac79a205e5010c48897456fc6e665a3cf9aa
|
||||
PASS no-session: no sessions.json and no launches under /mnt/storage/scratch/sb163t/sr-VQM0Df/data/rehearsals/20261020T150820Z-bf4bac79a205/data
|
||||
PASS console-stopped: pid 3547161
|
||||
PASS host-stopped: pid 3547160
|
||||
PASS host-released-store
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_PASS
|
||||
rehearsal directory: /mnt/storage/scratch/sb163t/sr-VQM0Df/data/rehearsals/20261020T150820Z-bf4bac79a205
|
||||
|
||||
|
||||
0 !== 1
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/sage-b163/packages/cli/tests/stack-release.test.mjs:2066:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 0,
|
||||
expected: 1,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
✖ R9 mos-0963: a role reference left outside placeholders/, or left on an env variable, fails credential-ref-live before the host starts (609.420853ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1216.913248
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/stack-release.test.mjs:1971:1
|
||||
✖ R9 mos-0963: a role reference left outside placeholders/, or left on an env variable, fails credential-ref-live before the host starts (609.420853ms)
|
||||
AssertionError [ERR_ASSERTION]: PASS copy: f2cd6e0875411f82c798b2a541604410cc77d2c7, schema 3g
|
||||
PASS port: 127.0.0.1:41709 is free
|
||||
PASS backup: /mnt/storage/scratch/sb163t/sr-TRrjEx/data/backups/stack/20261020T000000Z-f2cd6e087541 matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/sb163t/sr-TRrjEx/data/rehearsals/20261020T150823Z-f2cd6e087541/data from the backup, config and business file under /mnt/storage/scratch/sb163t/sr-TRrjEx/data/rehearsals/20261020T150823Z-f2cd6e087541/config, business.diff beside them
|
||||
PASS data-root-live: /mnt/storage/scratch/sb163t/sr-TRrjEx/data/rehearsals/20261020T150823Z-f2cd6e087541/data is not the live data root
|
||||
PASS sessions-live: no running session named in the data root
|
||||
PASS data-root-scratch
|
||||
PASS notify-off: binding null, so no Discord bridge
|
||||
PASS tracker-configured: bootConfig sets no trackers
|
||||
FAIL credential-ref-live: pm gitea is undefined, not a file in /mnt/storage/scratch/sb163t/sr-TRrjEx/data/rehearsals/20261020T150823Z-f2cd6e087541/placeholders
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/sb163t/sr-TRrjEx/data/rehearsals/20261020T150823Z-f2cd6e087541
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/sage-b163/packages/cli/tests/stack-release.test.mjs:1990:9)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
✖ R11 mos-0961: a scratch config on the live data root or on any root but the scratch one, or a sessions.json naming a running pid, refuses by name (605.865597ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1061.260755
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/stack-release.test.mjs:2006:1
|
||||
✖ R11 mos-0961: a scratch config on the live data root or on any root but the scratch one, or a sessions.json naming a running pid, refuses by name (605.865597ms)
|
||||
AssertionError [ERR_ASSERTION]: other-rootPASS copy: 236c2e27d28711316d96143968ae477b7fa7b7e7, schema 3g
|
||||
PASS port: 127.0.0.1:37319 is free
|
||||
PASS backup: /mnt/storage/scratch/sb163t/sr-aZQJrB/data/backups/stack/20261020T000000Z-236c2e27d287 matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/sb163t/sr-aZQJrB/data/rehearsals/20261020T150824Z-236c2e27d287/data from the backup, config and business file under /mnt/storage/scratch/sb163t/sr-aZQJrB/data/rehearsals/20261020T150824Z-236c2e27d287/config, business.diff beside them
|
||||
PASS data-root-live: /mnt/storage/scratch/sb163t/sr-aZQJrB/other-root is not the live data root
|
||||
PASS sessions-live: no running session named in the data root
|
||||
PASS data-root-scratch
|
||||
FAIL notify-off: /mnt/storage/scratch/sb163t/sr-aZQJrB/other-root/notify/acme/notify.json is missing or not JSON
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/sb163t/sr-aZQJrB/data/rehearsals/20261020T150824Z-236c2e27d287
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/sage-b163/packages/cli/tests/stack-release.test.mjs:2021:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
✖ R11 mos-0961: a scratch config on the live data root or on any root but the scratch one, or a sessions.json naming a running pid, refuses by name (916.097165ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1239.364874
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/stack-release.test.mjs:2006:1
|
||||
✖ R11 mos-0961: a scratch config on the live data root or on any root but the scratch one, or a sessions.json naming a running pid, refuses by name (916.097165ms)
|
||||
AssertionError [ERR_ASSERTION]: no-rootPASS copy: 998d46411d6c1d3168396f7bb5932e1e200a9263, schema 3g
|
||||
PASS port: 127.0.0.1:43579 is free
|
||||
PASS backup: /mnt/storage/scratch/sb163t/sr-v0EeC4/data/backups/stack/20261020T000000Z-998d46411d6c matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/sb163t/sr-v0EeC4/data/rehearsals/20261020T150825Z-998d46411d6c/data from the backup, config and business file under /mnt/storage/scratch/sb163t/sr-v0EeC4/data/rehearsals/20261020T150825Z-998d46411d6c/config, business.diff beside them
|
||||
PASS data-root-live: /mnt/storage/scratch/sb163t/sr-v0EeC4/no-root is not the live data root
|
||||
FAIL guard: The "path" argument must be of type string. Received null
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_FAIL
|
||||
rehearsal directory: /mnt/storage/scratch/sb163t/sr-v0EeC4/data/rehearsals/20261020T150825Z-998d46411d6c
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/sage-b163/packages/cli/tests/stack-release.test.mjs:2021:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: '==',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
✖ R4 a host that does not start, does not answer status, reports another pid, launches anyway, makes a launches directory or keeps its lock each fails by name (18892.096194ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 19506.873236
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/stack-release.test.mjs:1920:1
|
||||
✖ R4 a host that does not start, does not answer status, reports another pid, launches anyway, makes a launches directory or keeps its lock each fails by name (18892.096194ms)
|
||||
AssertionError [ERR_ASSERTION]: host-launches-dirPASS copy: 2cb8943db81ed8e914000449977627f0a9d42d00, schema 3g
|
||||
PASS port: 127.0.0.1:43729 is free
|
||||
PASS backup: /mnt/storage/scratch/sb163t/sr-D2Wt2k/data/backups/stack/20261020T000000Z-2cb8943db81e matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/sb163t/sr-D2Wt2k/data/rehearsals/20261020T150843Z-2cb8943db81e/data from the backup, config and business file under /mnt/storage/scratch/sb163t/sr-D2Wt2k/data/rehearsals/20261020T150843Z-2cb8943db81e/config, business.diff beside them
|
||||
PASS data-root-live: /mnt/storage/scratch/sb163t/sr-D2Wt2k/data/rehearsals/20261020T150843Z-2cb8943db81e/data is not the live data root
|
||||
PASS sessions-live: no running session named in the data root
|
||||
PASS data-root-scratch
|
||||
PASS notify-off: binding null, so no Discord bridge
|
||||
PASS tracker-configured: bootConfig sets no trackers
|
||||
PASS credential-ref-live: 9 reference(s), each a placeholder
|
||||
PASS migration: not needed: the backup is already 3g
|
||||
PASS gate: host-up, bus-status, console-up, healthz and copy-match on 2cb8943db81ed8e914000449977627f0a9d42d00
|
||||
PASS no-session: no sessions.json and no launches under /mnt/storage/scratch/sb163t/sr-D2Wt2k/data/rehearsals/20261020T150843Z-2cb8943db81e/data
|
||||
PASS console-stopped: pid 3550603
|
||||
PASS host-stopped: pid 3550602
|
||||
PASS host-released-store
|
||||
PASS backup-unchanged
|
||||
PASS live-config-unchanged
|
||||
REHEARSAL_PASS
|
||||
rehearsal directory: /mnt/storage/scratch/sb163t/sr-D2Wt2k/data/rehearsals/20261020T150843Z-2cb8943db81e
|
||||
|
||||
|
||||
0 !== 1
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/sage-b163/packages/cli/tests/stack-release.test.mjs:1924:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 0,
|
||||
expected: 1,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
✖ R15 a rehearsal stopped by TERM, INT or HUP fails signal and still stops the host and Console and runs the end checks (1289.845744ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1749.897862
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/cli/tests/stack-release.test.mjs:2073:1
|
||||
✖ R15 a rehearsal stopped by TERM, INT or HUP fails signal and still stops the host and Console and runs the end checks (1289.845744ms)
|
||||
AssertionError [ERR_ASSERTION]: SIGTERMPASS copy: ba3ad437eb120f80d3a1bdc438860968dd038962, schema 3g
|
||||
PASS port: 127.0.0.1:45847 is free
|
||||
PASS backup: /mnt/storage/scratch/sb163t/sr-Bin1lp/data/backups/stack/20261020T000000Z-ba3ad437eb12 matches its SHA256SUMS
|
||||
PASS scratch-root: /mnt/storage/scratch/sb163t/sr-Bin1lp/data/rehearsals/20261020T150846Z-ba3ad437eb12/data from the backup, config and business file under /mnt/storage/scratch/sb163t/sr-Bin1lp/data/rehearsals/20261020T150846Z-ba3ad437eb12/config, business.diff beside them
|
||||
PASS data-root-live: /mnt/storage/scratch/sb163t/sr-Bin1lp/data/rehearsals/20261020T150846Z-ba3ad437eb12/data is not the live data root
|
||||
PASS sessions-live: no running session named in the data root
|
||||
PASS data-root-scratch
|
||||
PASS notify-off: binding null, so no Discord bridge
|
||||
PASS tracker-configured: bootConfig sets no trackers
|
||||
PASS credential-ref-live: 9 reference(s), each a placeholder
|
||||
PASS migration: not needed: the backup is already 3g
|
||||
|
||||
|
||||
null !== 1
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/sage-b163/packages/cli/tests/stack-release.test.mjs:2079:10)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: null,
|
||||
expected: 1,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
✔ REQ-LIMIT-1: family substring matching is no longer exported (0.993829ms)
|
||||
✔ sessionEnv passes only the allowlist, the repo's bin on PATH, and the run id (0.643651ms)
|
||||
✔ sessionExit gives a runner's signal death back as the signal; a session credential is one whole set of nonempty variables (0.22563ms)
|
||||
✔ newRun: short ids, 0700 directories, and a refusal when the socket path won't fit (1.390202ms)
|
||||
✔ session file and launch log: 0600, the session file written once (0.890354ms)
|
||||
✔ endReason maps the runner's exit codes; a signal is killed (0.110674ms)
|
||||
✔ Registry mirrors to sessions.json; readSessions marks live entries; bad files refuse (1.151527ms)
|
||||
✖ a read-only Registry never writes sessions.json, refuses add() and leaves the file on remove() (1.357741ms)
|
||||
✔ REQ-THREAD-1 Registry.running keys a thread run by business, instance and thread (0.526184ms)
|
||||
✔ REQ-THREAD-2 REQ-LAUNCH-1 threadStore checks both segments before the join and refuses an unsafe directory (1.243195ms)
|
||||
✔ stopSession refuses an unknown run, reports a stale one, and won't signal a pid that isn't a runner (0.781332ms)
|
||||
✔ a session runs in its sandbox as pid 1 of its namespace, claims, answers, and stops on mosaic stop (192.679392ms)
|
||||
ℹ tests 12
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 266.973886
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/seat/tests/session.test.mjs:137:1
|
||||
✖ a read-only Registry never writes sessions.json, refuses add() and leaves the file on remove() (1.357741ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ '{\n "sessionsVersion": 1,\n "sessions": []\n}\n'
|
||||
- '{\n' +
|
||||
- ' "sessionsVersion": 1,\n' +
|
||||
- ' "sessions": [\n' +
|
||||
- ' {\n' +
|
||||
- ' "business": "acme",\n' +
|
||||
- ' "instance": "chief",\n' +
|
||||
- ' "run": "r1",\n' +
|
||||
- ' "runnerPid": 3551219,\n' +
|
||||
- ' "runnerStartTime": "61011768"\n' +
|
||||
- ' }\n' +
|
||||
- ' ]\n' +
|
||||
- '}\n'
|
||||
|
||||
at TestContext.<anonymous> (file:///mnt/storage/scratch/sage-b163/packages/seat/tests/session.test.mjs:142:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: '{\n "sessionsVersion": 1,\n "sessions": []\n}\n',
|
||||
expected: '{\n "sessionsVersion": 1,\n "sessions": [\n {\n "business": "acme",\n "instance": "chief",\n "run": "r1",\n "runnerPid": 3551219,\n "runnerStartTime": "61011768"\n }\n ]\n}\n',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
f04d12a963b3199420f1d9c41fbdae67faf46f710a805cb9358f1fb52f5faed9 agents/sage/work/queue-163/BUILD.md
|
||||
5bd1bc697cd56e57b00e60c0e80be72cd1043a55a6d2fc0b9e2ccdd0c5cf9802 docs/TOOLS.md
|
||||
52597d7fa1469510fcf8e6dfd63664ad4c492bc17039885aa839231798693058 packages/cli/src/cli.mjs
|
||||
5492ebb5aa3c9c85874e4c7de7b65fc37733e2db636690c11fe2d2fdf72c2dd0 packages/cli/src/launcher.mjs
|
||||
e9ce0e0f7368d3fb2e1edbd4ebc724513ec19d7bac0c993f8be0d9494427be07 packages/cli/tests/host.test.mjs
|
||||
42519898b5bcf84c21088647e161a86934d4fe351207f736b9b120b6e1c2b91b packages/cli/tests/launcher.test.mjs
|
||||
b01ff656962c6fdb5bc3554cfbb13645b7f25adae5f6cd9608af8de6d6091c6c packages/cli/tests/stack-release.test.mjs
|
||||
448138d12404346989a6751a01c747718cef7d2bec19a0639a29fa6a8b74a82d packages/seat/src/session.mjs
|
||||
cd6277c4e7155a7c93d09f5bc7cca1130708a35a6b75aa02d99af8360d6338b0 packages/seat/tests/session.test.mjs
|
||||
5bb3ac40102ce515f1acdcbc5670feb161ce40a193e94e2f8b104ebf98eddc38 scripts/stack-release.sh
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,28 @@
|
||||
base 2a5f3a3a0 dirty 0 DOCKER_HOST=unset
|
||||
pkg-bus rc=0 ℹ pass 451 ℹ fail 0 | ℹ duration_ms 2970.304395
|
||||
pkg-business rc=0 ℹ pass 149 ℹ fail 0 | ℹ duration_ms 1953.76274
|
||||
pkg-cli rc=0 ℹ pass 472 ℹ fail 0 | ℹ duration_ms 367258.38335
|
||||
pkg-control-board rc=0 ℹ pass 124 ℹ fail 0 | ℹ duration_ms 585.394845
|
||||
pkg-conversation rc=0 ℹ pass 182 ℹ fail 0 | ℹ duration_ms 33402.754981
|
||||
pkg-discord rc=0 ℹ pass 183 ℹ fail 0 | ℹ duration_ms 2610.108892
|
||||
pkg-harness rc=0 ℹ pass 133 ℹ fail 0 | ℹ duration_ms 11612.167166
|
||||
pkg-ledger rc=0 ℹ pass 89 ℹ fail 0 | ℹ duration_ms 12928.162376
|
||||
pkg-mosaic rc=0 ℹ pass 69 ℹ fail 0 | ℹ duration_ms 2092.404214
|
||||
pkg-plugins rc=0 ℹ pass 111 ℹ fail 0 | ℹ duration_ms 250.471164
|
||||
pkg-project rc=0 ℹ pass 40 ℹ fail 0 | ℹ duration_ms 1069.844547
|
||||
pkg-runs rc=0 ℹ pass 43 ℹ fail 0 | ℹ duration_ms 732.731695
|
||||
pkg-seat rc=0 ℹ pass 152 ℹ fail 0 | ℹ duration_ms 5889.377731
|
||||
pkg-tasks rc=0 ℹ pass 97 ℹ fail 0 | ℹ duration_ms 1159.016661
|
||||
pkg-webui rc=0 ℹ pass 269 ℹ fail 0 | ℹ duration_ms 54984.81726
|
||||
script-auth rc=0 | selftest: 15 passed, 0 failed
|
||||
script-conductor rc=0 | selftest: 17 passed, 0 failed
|
||||
script-config rc=0 | selftest: 24 passed, 0 failed
|
||||
script-discord rc=0 | discord suite: 78 passed, 0 failed
|
||||
script-extension-package rc=0 | extension package selftest: 18 passed, 0 failed
|
||||
script-foundation rc=0 | selftest: 44 passed, 0 failed
|
||||
script-land-work rc=0 | land-work selftests: 86 passed, 0 failed
|
||||
script-queue rc=0 ℹ pass 156 ℹ fail 0 | queue suite: 28 passed, 0 failed
|
||||
script-release rc=0 | selftest: 14 passed, 0 failed
|
||||
script-task rc=0 | selftest: 98 passed, 0 failed
|
||||
GATE_PASSED
|
||||
removed network sage-r163_default
|
||||
@@ -0,0 +1,9 @@
|
||||
KILLED K34 backup-unchanged always passes (rc 1)
|
||||
KILLED K35 live-config-unchanged always passes (rc 1)
|
||||
KILLED K36 credential-ref-live accepts an env ref (rc 1)
|
||||
KILLED K37 data-root-scratch mismatch dropped (rc 1)
|
||||
KILLED K38 data-root-scratch missing root dropped (rc 1)
|
||||
KILLED K39 no-session ignores a launches dir (rc 1)
|
||||
KILLED K40 no INT/TERM/HUP trap (rc 1)
|
||||
KILLED K41 read-only Registry writes (rc 1)
|
||||
ALL_KILLED
|
||||
@@ -0,0 +1,7 @@
|
||||
70dbc89c0282fa063514317262d8926d16b8b8aaab4a1e47379303918495fc2c agents/sage/work/queue-189/BUILD.md
|
||||
4bd216cce2b40b39998725daa8989f6e3acb3ccd81a7017f6b09026af3965584 agents/sage/work/queue-189/mutants-run.log
|
||||
ddbdddf03abf4aa91aa62f52e468be7af3c6b2f5d7bec2ee99b11e315471f18c agents/sage/work/queue-189/mutants.sh
|
||||
5d7b2da84d994f4f93d1212e349b4e4c19c5c00780e6cfcde91edb3ca08c014e docs/plans/2026-10-20_activate-no-send.md
|
||||
38e410c4cb731390f96747646eb7f6caa7a587fca9024ccbe901aab5ac19c7cf docs/TOOLS.md
|
||||
fe430969cc669515e348fced1a22cbe839a12023f7ac67cfba905b7afd29872f packages/cli/tests/stack-release.test.mjs
|
||||
16f77305e558f4fb953accc6d09f1f7b408aba363851d8ca79216ffce41bed9f scripts/stack-release.sh
|
||||
@@ -1,149 +0,0 @@
|
||||
# Row 66: managed sessions run the unslop check on both harnesses
|
||||
|
||||
Sage, 2026-10-20. Brief: `docs/plans/2026-10-11_managed-session-config.md`, section "Managed
|
||||
sessions run the unslop check on both harnesses", blob 21ae25a328d4. Issue #1559. Reviewers:
|
||||
Dewey and Filbert. Built in the worktree `/mnt/storage/scratch/sage-b66` (branch `sage/row66`)
|
||||
on base 5487ed39d.
|
||||
|
||||
Candidate: see the commit line in the review request. `candidate-manifest.sha256` in this
|
||||
directory has the sha256 of each changed file at the candidate.
|
||||
|
||||
## What changed
|
||||
|
||||
**Wiring.** Both harness modules mount `tools/unslop-hook/lists.json`, `unslop-check.js` and
|
||||
the harness's own entry (pi `extension.ts`, Claude Code `claude-hook.js`) read-only at their
|
||||
own paths, and the bundle manifest lists the three in `sources` with their digests. Pi loads
|
||||
`extension.ts` as the second entry of `MOSAIC_EXTENSIONS`, after the gate extension. Claude
|
||||
Code's `settings.json` gains a Stop hook and a UserPromptSubmit hook, each
|
||||
`timeout -k 2 10 '<node>' '<dir>claude-hook.js' stop|prompt; exit 0`, hook timeout 20, no
|
||||
matcher. Neither can block a reply or a prompt.
|
||||
|
||||
**claude-hook.js** (new). `stop` checks each assistant message with text since the turn's
|
||||
prompt and logs `checked` or `flagged`. `prompt` checks the last reply and, when it has tells,
|
||||
prints the extension's notice, word for word, as `additionalContext`, once per reply (it reads
|
||||
the log for an earlier `notice_injected` with the same entry id). A broken lists.json logs
|
||||
`broken_lists`; any other fault logs `hook_failed`; the exit is always 0.
|
||||
|
||||
**The operator channel.** The runner sets `MOSAIC_UNSLOP_LOG` to `<run>/unslop.jsonl` after the
|
||||
module's variables, so a module can't point it elsewhere. After the landing turn and after each message turn it reads what the
|
||||
log gained (whole lines only, from an offset). The next RESULT it sends ends with a note. The
|
||||
whole note, both parts and the blank line before them, is at most 600 characters (r2):
|
||||
|
||||
- `[unslop: N reply|replies of this session flagged since the last note: <details>. The session gets a correction notice on its next turn.]`
|
||||
- `[unslop: the check is broken, so this session's replies go unchecked: <reason>]`, once a run.
|
||||
|
||||
That RESULT is what `mosaic talk` and `mosaic follow` print. A turn that sends no reply (an
|
||||
incoming RESULT, the landing turn) keeps its note for the next one. `note()` doesn't change
|
||||
state and `sent()` runs only after `message.send` succeeds, so a failed send keeps the note.
|
||||
`clip()` caps the body at 32000 characters plus its 33-character cut line before the note, so the
|
||||
sum is at most 32633, under the bus limit of 32768. The broken reason is cut at 300 where the
|
||||
runner takes it, from the log or from its own read failure, so the broken part is at most 371
|
||||
characters and the flagged part is cut to what is left.
|
||||
|
||||
**Counts.** `unslopCounts(path)` reads the log after the session ended:
|
||||
`{checked, flagged, patterns: {<source>: n}, notices, broken}`. A missing log gives zeros.
|
||||
Findings now carry `source`, the entry's pattern id (`ms-unslop:7`), so `patterns` is per id.
|
||||
|
||||
**The stack copy (r2).** `scripts/stack-copy.sh` `COPY_PATHS` gains `tools/unslop-hook`. Both
|
||||
modules join their files under `REPO`, and a session launched from a pinned copy has
|
||||
`REPO = copies/<sha>`. Without it every managed session from a copy past this row would fail at
|
||||
bundle build with ENOENT (Dewey P1, comment 29440).
|
||||
|
||||
## Round 2
|
||||
|
||||
Round 1 (candidate 8ec03b45a, manifest e68c6bce5ec4) got changes from Filbert (29439) and Dewey
|
||||
(29440). Round 2 carries:
|
||||
|
||||
- F1 (both): a note with both parts wasn't bounded. A 300-character reason, a long flagged part
|
||||
and a clipped answer came to 32934 (Filbert) or 33004 (Dewey), over 32768, and the broker
|
||||
refused the send. `note()` now gives the flagged part only the room the broken part leaves, and
|
||||
`fresh()` cuts every reason at 300, the runner's own read-failure reason included. Tests: the
|
||||
runner test with an answer of 40000 characters, a 400-character `hook_failed` reason and 30
|
||||
flagged replies (note ≤ 600, body ≤ 32768, cut line at 32000), and a unit check that both
|
||||
parts come to exactly 600 and that a read failure on a 250-character path is cut.
|
||||
- F2 (Filbert; Dewey's M3): no test failed a send through `main()`. The new runner test has the
|
||||
broker refuse the first RESULT. The note then rides the next RESULT. One correction to the
|
||||
F1 consequence as written: the broker marks a received message delivered, so the refused
|
||||
message isn't received again in that run. It stays unread and its sender gets no reply; the
|
||||
turn doesn't rerun.
|
||||
- P1 (Dewey): `COPY_PATHS` gains `tools/unslop-hook`. The copy fixtures in
|
||||
`stack-copy.test.mjs` and `pinned-copy.test.mjs` copy `tools/unslop-hook` too, and the
|
||||
pinned-copy top-level list gains `tools`. A new `stack-copy.test.mjs` test makes a copy, loads
|
||||
both harness modules from it, and checks that the adapter, the six unslop paths and the mounts
|
||||
are under the copy and exist.
|
||||
- N1 (Filbert): the two session-test comments say `<dir>/unslop.jsonl`.
|
||||
- N2 (Filbert), doc-only follow-up: the brief's `<run>/session/unslop.jsonl` (:463) and "print
|
||||
mode notifies nobody" (:477) are stale. The brief is Jason's text for this row; the deviation
|
||||
below records the path.
|
||||
- Dewey's notes, no change: the Claude Code Stop timing (finding 3 below) gets a look in the
|
||||
landing session; `patterns` keys per entry source, which is the brief's "per pattern".
|
||||
|
||||
r2 mutants, each killed by its intended test (`mutants-r2.log`):
|
||||
|
||||
| Mutant | Killed by |
|
||||
|---|---|
|
||||
| K1 the r1 `note()` cut | the clipped-answer runner test (the send is refused and it times out) and the unit check |
|
||||
| K2 `fresh()` doesn't cut the reason | the unit check |
|
||||
| K3 `sent()` before `message.send` | the refused-send runner test |
|
||||
| K4 `COPY_PATHS` without `tools/unslop-hook` | the copy test |
|
||||
|
||||
## Ship list against the brief
|
||||
|
||||
| Brief item | Where |
|
||||
|---|---|
|
||||
| A reply with a tell gets a correction notice on the next turn, both harnesses | `pi-session.test.mjs`, `claude-session.test.mjs`, row 66 four-turn tests: notices per request `[0, 1, 1, 2]`, notice text matched in the second request |
|
||||
| A clean reply gets none | same tests: turn 2 is clean and turn 3's request carries no new notice |
|
||||
| Broken lists.json fails closed: no clean verdict, operator told once, session alive | the row 66 broken-lists tests per harness: checked 0, flagged 0, first note broken, second note empty, the session answers both turns |
|
||||
| Counts readable after the session ends | `unslopCounts` on each session's log: `{checked: 4, flagged: 2, notices: 2, broken: null}`, patterns non-empty |
|
||||
| Interface 1 and the export list unchanged | no harness module export added or removed; the runner gains `unslopLog`, `unslopCounts`, `unslopNotes` |
|
||||
|
||||
## Findings made while building
|
||||
|
||||
1. **extension.ts read the wrong reply.** Pi 0.85.1 `getBranch()` returns entries root first
|
||||
(`session-manager.js:958-967` reverses the leaf-to-root walk). `before_agent_start` took the
|
||||
first assistant entry it met, which was the session's first reply, every turn. In the
|
||||
four-turn test the old code gave notices `[0, 1, 2]` over three turns (turn 3 re-noticed
|
||||
turn 1); walking from the end gives `[0, 1, 1]`. Fixed in `extension.ts`; this also affects
|
||||
the fleet copy under `~/.mosaic/tools`, which I didn't touch.
|
||||
2. **claude-hook replies() merged replies.** Grouping by `message.id` across the transcript
|
||||
merged two replies that shared an id (the test API answers `msg_mock` for all). It now groups
|
||||
adjacent assistant lines only, keyed by the first line's uuid. Before the fix turn 4 got no
|
||||
notice (`[0, 1, 1, 1]`).
|
||||
3. **Claude Code Stop timing.** On the first turn of a new session the transcript didn't yet
|
||||
hold the reply when Stop ran, so the hook checked `last_assistant_message` with an empty
|
||||
`entryId`. On resumed turns the reply was in the transcript. The verdict is the same either
|
||||
way; only the entry id differs.
|
||||
4. The finding detail format is `banned word "delve" x1`.
|
||||
|
||||
## Deviations
|
||||
|
||||
- **The log path is `<run>/unslop.jsonl`, not the brief's `<run>/session/unslop.jsonl`.** A
|
||||
thread run's session directory is the thread's store, and
|
||||
`packages/cli/tests/thread-launch.test.mjs:179` pins that a thread run keeps no per-run
|
||||
`session/`. My first build made `<run>/session` for the log and failed that test in the gate
|
||||
(452/453). The brief's other line, "`unslop.jsonl` in the run directory", still holds, and each
|
||||
run keeps its own log.
|
||||
|
||||
- **`packages/seat/tests/sandbox.test.mjs`** isn't an owned file. Its pin
|
||||
`binds(args).length === list.length - 1` broke because the modules now mount three more files.
|
||||
It is now `- 4`, and the test asserts the three unslop read-only binds by name.
|
||||
- **`scripts/stack-copy.sh`** and the two copy tests aren't owned files. The copy change is P1
|
||||
above. PB3 of the Part B window (`repin-partB-proof.md`) holds `scripts/stack-copy.sh` still
|
||||
until B7, so this row lands after the Part B window has run.
|
||||
- `MOSAIC_EXTENSIONS` is set through the pi module's `env`, not in the runner.
|
||||
- `unslop-check.js` gained the `source` field on findings (needed for per-pattern counts).
|
||||
|
||||
## Limits
|
||||
|
||||
- The log is in the run directory, which the session can write. A session could forge or erase
|
||||
its own counts. They are the session's report, not host evidence.
|
||||
- The pi extension's dedupe is in memory: after a restart, or when a turn's final message is
|
||||
tool-only, the same earlier reply can get a second notice.
|
||||
- `hook_failed`, `skipped_broken` and `broken_lists` all count as broken for the operator note.
|
||||
- No blocking (Q84 option B), no list changes, no skill changes: out of scope by the brief.
|
||||
|
||||
## Gate
|
||||
|
||||
Recorded with the candidate in the review request: test-unslop-check, the harness, plugins,
|
||||
seat and cli suites, and every `scripts/test-*.sh`. At landing Sage runs one managed session per
|
||||
harness and attaches the notice, the correction and the counts.
|
||||
@@ -0,0 +1,23 @@
|
||||
b99e7f4bf58ac68c8f5fa58eb7b90c0f85b15ca9a80fd093f6aaeafeac931608 adapters/README.md
|
||||
c32d2a3276db92433d4e3c4f4b45fab048d9a46119d9f69cead6a11cfacfeadb agents/sage/work/queue-66/BUILD.md
|
||||
8777fac3acb89f3af4ac0db2e7c44e309408a8ab8d1b0a0942c81c54ad295fa9 docs/TOOLS.md
|
||||
ea11398dc5751a0f9415079e7c03f413e97f5443f6bd0fc1fc2319ec646c20cb packages/cli/tests/stack-copy.test.mjs
|
||||
b027dc74583217b4151173ecbabfcfbfc7b108ac6b7a8e93dd21a2834051714f packages/harness/README.md
|
||||
bfb2156c609eeaf87c9e209c562576a5ef47535eaeee61d841bb0bd75a76a69d packages/harness/src/runner.mjs
|
||||
0a27f2c8de7b9191f9c0d4a209331c63ff7630923e669838b41715924b0a64b0 packages/harness/tests/bundle.test.mjs
|
||||
81d0352f2dcd41eb2e2ef962365d9b97515d7e8d9668a35bb9ce9dce4f1451ea packages/harness/tests/claude-session.test.mjs
|
||||
b16f33442cefd5b5ae2af85edab33250ee6b31025e437edf0e4eba9fe0afa793 packages/harness/tests/fixtures/fake-adapter.mjs
|
||||
1e187a2e23a197640d139e6975dfaf83dd7e337c360d128833678dc60f3e2046 packages/harness/tests/pi-session.test.mjs
|
||||
37ca6ad6467573658c1ac6807b6cd00e3f9da289107fb689190a1872bff9b89c packages/harness/tests/runner.test.mjs
|
||||
04c24058f37260c9192d5036c8cf28b50faba8e3e3bf57f198d233485d0d57e6 packages/plugins/harness/claude-code/index.mjs
|
||||
2c1252e8e8db3ba4551bf1e34156ddf4e933cc664fc157105bef9a2705d6ce3b packages/plugins/harness/pi/index.mjs
|
||||
7e0bea27d4360a39874fa64b3506a85f83888613a285ec0b087d74bf22b76421 packages/plugins/README.md
|
||||
ec4ec9502ee8e766e3abd04d9f724c0d8ff4ef114394a0f76d5320c2dc715c5c packages/plugins/tests/harness.test.mjs
|
||||
f6141dac9a8c61573e4198480fed85e093992ee9c19fb55e9bc2752381b7b07a packages/seat/tests/sandbox.test.mjs
|
||||
e60bf54c23e04436a982174348857458039a22e1769fc9ad4db8d4f9b57f2e86 packages/webui/tests/pinned-copy.test.mjs
|
||||
3ba3dbd9ff8dd68f0aa85bac92a8960af10166130bbc16df74fd7340776bb3b1 scripts/stack-copy.sh
|
||||
7ee6ed6e6ac55b1348d40a358372e66c86fdebacd90001d447040133e1187882 tools/unslop-hook/claude-hook.js
|
||||
ce1dfb11d0a8e8efd3d6dae67cc259c2e7443a62c7416bdedd4c71306d8c6df1 tools/unslop-hook/extension.ts
|
||||
f07d67806f40763b4be87d31c2976448b5cfc34d7328519fae2d8313b1ca3e20 tools/unslop-hook/README.md
|
||||
dbe3d5c9ae654d2401e3c496a6b03757a438e4a551fd8e460df8c82902bb80a7 tools/unslop-hook/test-unslop-check.js
|
||||
76be78178a0703b9d20a7df2278ecb13f429dea90cea45180f22302e267ea25e tools/unslop-hook/unslop-check.js
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,31 @@
|
||||
base 8ec03b45a dirty 11 DOCKER_HOST=unset
|
||||
unslop rc=0 | 28/28 passed
|
||||
pkg-bus rc=0 ℹ pass 451 ℹ fail 0 | ℹ duration_ms 3039.978
|
||||
pkg-business rc=0 ℹ pass 149 ℹ fail 0 | ℹ duration_ms 2119.713943
|
||||
pkg-cli rc=0 ℹ pass 454 ℹ fail 0 | ℹ duration_ms 294959.155129
|
||||
pkg-control-board rc=0 ℹ pass 124 ℹ fail 0 | ℹ duration_ms 590.346847
|
||||
pkg-conversation rc=0 ℹ pass 182 ℹ fail 0 | ℹ duration_ms 33735.620305
|
||||
pkg-discord rc=0 ℹ pass 183 ℹ fail 0 | ℹ duration_ms 2607.173768
|
||||
pkg-harness rc=0 ℹ pass 142 ℹ fail 0 | ℹ duration_ms 11398.667142
|
||||
pkg-ledger rc=0 ℹ pass 89 ℹ fail 0 | ℹ duration_ms 12920.093134
|
||||
pkg-mosaic rc=0 ℹ pass 69 ℹ fail 0 | ℹ duration_ms 2060.508354
|
||||
pkg-plugins rc=0 ℹ pass 111 ℹ fail 0 | ℹ duration_ms 245.928975
|
||||
pkg-project rc=0 ℹ pass 40 ℹ fail 0 | ℹ duration_ms 1052.940331
|
||||
pkg-runs rc=0 ℹ pass 43 ℹ fail 0 | ℹ duration_ms 741.129125
|
||||
pkg-seat rc=0 ℹ pass 151 ℹ fail 0 | ℹ duration_ms 5795.650378
|
||||
pkg-tasks rc=0 ℹ pass 97 ℹ fail 0 | ℹ duration_ms 1148.870662
|
||||
pkg-webui rc=0 ℹ pass 269 ℹ fail 0 | ℹ duration_ms 55714.69432
|
||||
script-auth rc=0 | selftest: 15 passed, 0 failed
|
||||
script-conductor rc=0 | selftest: 17 passed, 0 failed
|
||||
script-config rc=0 | selftest: 24 passed, 0 failed
|
||||
script-discord rc=0 | discord suite: 78 passed, 0 failed
|
||||
script-extension-package rc=0 | extension package selftest: 18 passed, 0 failed
|
||||
script-foundation rc=0 | selftest: 44 passed, 0 failed
|
||||
script-land-work rc=0 | land-work selftests: 86 passed, 0 failed
|
||||
script-queue rc=0 ℹ pass 156 ℹ fail 0 | queue suite: 28 passed, 0 failed
|
||||
script-release rc=0 | selftest: 14 passed, 0 failed
|
||||
script-task rc=0 | selftest: 98 passed, 0 failed
|
||||
GATE_PASSED
|
||||
removed network sage-b66_default
|
||||
exit=0
|
||||
2026-10-20T13:41:45Z
|
||||
@@ -0,0 +1,5 @@
|
||||
K1-note-r1 rc=1 KILLED ℹ pass 37 ℹ fail 2
|
||||
K2-reason-uncut rc=1 KILLED ℹ pass 38 ℹ fail 1
|
||||
K3-sent-first rc=1 KILLED ℹ pass 38 ℹ fail 1
|
||||
K4-no-copy-path rc=1 KILLED ℹ pass 12 ℹ fail 1
|
||||
MUTANTS_DONE
|
||||
@@ -0,0 +1,10 @@
|
||||
packages/harness/tests/runner.test.mjs rc=0 ℹ pass 39 ℹ fail 0
|
||||
packages/harness/tests/pi-session.test.mjs rc=0 ℹ pass 10 ℹ fail 0
|
||||
packages/harness/tests/claude-session.test.mjs rc=0 ℹ pass 10 ℹ fail 0
|
||||
packages/cli/tests/stack-copy.test.mjs rc=0 ℹ pass 13 ℹ fail 0
|
||||
packages/webui/tests/pinned-copy.test.mjs rc=0 ℹ pass 10 ℹ fail 0
|
||||
packages/cli/tests/stack-release.test.mjs rc=0 ℹ pass 151 ℹ fail 0
|
||||
packages/cli/tests/thread-launch.test.mjs rc=0 ℹ pass 11 ℹ fail 0
|
||||
packages/seat/tests/sandbox.test.mjs rc=0 ℹ pass 10 ℹ fail 0
|
||||
unslop rc=0 28/28 passed
|
||||
SINGLES_DONE
|
||||
@@ -117,3 +117,82 @@ d. Before piece 1 opens, I check on the live pin which role takes a piece
|
||||
|
||||
Mos passes this to Jason when piece 1 opens, not before. Lead decisions 94,
|
||||
114 and 115 stand.
|
||||
|
||||
## 2026-10-20T12:54Z trigger (a) met; check (d), the part an agent can read
|
||||
|
||||
Trigger (a): the Part A window put 490e2771d3d0 live (pm true, schema 3i,
|
||||
proof off), checked by Mos (mos-0928). That is the first window verified
|
||||
since mos-0855, and it carries 3i, so piece 1 may open.
|
||||
|
||||
Check (d), what I can read from this seat at 12:54Z:
|
||||
- `scripts/mosaic launches list` prints one run, r34520a581318,
|
||||
mosaic-stack/chief, claude-code claude-opus-5-5, by jason, since
|
||||
2026-10-20T07:58:02Z, running.
|
||||
- The bus, the Console (7332) and that run's runner all run from
|
||||
copies/490e2771d3d0, and the bus runs `bus start mosaic-stack --pm`.
|
||||
- What I can't check: that a human message to the PM gets answered. Every
|
||||
other `scripts/mosaic` verb refuses an agent run ("this runs only from a
|
||||
human shell"), and the tester switch is Jason's. That half needs a human
|
||||
send, or Mos's Part A STATE evidence if it holds one.
|
||||
|
||||
Rule (c) still applies: Part B (P19 385) is a window, so if Jason gives J1
|
||||
while a piece is open, Part B waits for that piece to close.
|
||||
|
||||
## 2026-10-20T12:55Z mos-0960: the first message to pm is a reachability check
|
||||
|
||||
Mos accepted the correction (no date) and agreed trigger (a) is met. Check
|
||||
(d) item 3 is not settled: the Part A window (W9) tested only the tester
|
||||
thread d0ba7a66, and S1 had no human thread, so no talk or follow answer
|
||||
from pm exists on 490e2771d3d0. Mos read the live business file: launch.by
|
||||
is chief, and pm is in launch.instances with cto, assistant, coder and
|
||||
reviewer, with no threadIdleSeconds. Mos's reading of cli.mjs:345 (not a
|
||||
test) is that a talk to pm tells the chief to launch pm.
|
||||
|
||||
Ruling:
|
||||
- Jason's first message to pm is a reachability check, not a piece, like
|
||||
6b5b7aef and a8df1e12 on 10-12.
|
||||
- If pm answers, his next real Mosaic Stack request is piece 1.
|
||||
- If pm doesn't answer, I file the defect and piece 1 doesn't open.
|
||||
- Rule (c) stands. Mos recommends to Jason that J1 and Part B go before
|
||||
piece 1 if he gives both together, so the pieces run on the final Console
|
||||
auth.
|
||||
|
||||
Mos tells Jason in the morning summary.
|
||||
|
||||
## 2026-10-20T17:48Z correction: on a pinned host Jason uses plain `mosaic` (Mos mos-0968)
|
||||
|
||||
This supersedes the 10-12 line at 18:56Z, "Until row 85 lands, Jason uses
|
||||
the full path, `/mnt/storage/src/mosaic-stack/scripts/mosaic talk pm
|
||||
"..."`". Row 85 landed, and the host, bus and Console now run from the
|
||||
pinned copy 490e2771d3d0. The bus takes human commands only from the tree
|
||||
it runs from. The canonical checkout is not that tree.
|
||||
|
||||
Jason ran the full-path command Mos gave him from that line. It refused
|
||||
with human-required: "the bus accepts these commands only from the tree it
|
||||
runs from; on a pinned host run `mosaic` on PATH". ~/.npm-global/bin/mosaic
|
||||
now execs copies/490e2771d3d0/scripts/mosaic, which I read at 17:48Z, so
|
||||
Jason uses plain `mosaic talk pm "..."` or the Console. Friction is on
|
||||
Vikunja #205, comment 369.
|
||||
|
||||
Nothing else changes. The refused command never reached pm, so the
|
||||
reachability check (mos-0960) has not happened and piece 1 is not open.
|
||||
|
||||
## 2026-10-20T17:54Z S7 reachability pass (Mos mos-0969)
|
||||
|
||||
Jason ran the plain PATH command from the 17:48Z entry. His output, as Mos
|
||||
relayed it in mos-0969:
|
||||
|
||||
```
|
||||
[jwoltje@dragon-lin winA]$ mosaic talk pm "Can you hear me?"
|
||||
sent 51cebff7 to pm
|
||||
pm isn't running, chief has been told
|
||||
pm (RESULT, re 51cebff7) cff1e9ad:
|
||||
Yes, I'm getting your messages. I haven't changed anything on the tracker. What do you need?
|
||||
```
|
||||
|
||||
pm was not running when 51cebff7 arrived. The chief launched it, and pm
|
||||
answered with cff1e9ad. Under mos-0960 this is the reachability check, not
|
||||
a piece. It passed, so the "pm doesn't answer" branch does not apply and
|
||||
no defect is filed.
|
||||
|
||||
Piece 1 is Jason's next real Mosaic Stack request to pm. It is not open yet.
|
||||
@@ -0,0 +1,56 @@
|
||||
// Sage, 2026-10-20: row 163 (D2, #1660) round 2 approved. One comment on P19 337 (D2), per mos-0882 (every
|
||||
// landing or decision gets a Vikunja comment). Sage's own key (bot user 13); the sync key reads back. Comments only:
|
||||
// no task edits, labels, assignees or bucket moves. Checks the task's title before posting. Prints no secret.
|
||||
// Usage: node comment-337-r163-approved.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 337, title: /^D2: stack rehearsal on a scratch root/, body: [
|
||||
"Row 163 (Gitea #1660) landed and pushed. Landing commit 8ecc7a239 (Sage; Filbert approve 29447), queue revs 2235-2236 (landing note and done) in 9ae8c73ac, range 4ca8d6e94..9ae8c73ac on refactor. Patch-id b8d5f8a37c24 matches candidate c6f4f4c12596. #1660 is closed with comment 29450.",
|
||||
"Sage gate passed at 18:16Z on eaa0f2666: cli 472/472 and every package and scripts/test-*.sh green. The pushed commit is a cherry-pick of eaa0f2666 onto 7f57419b3, which adds only the Q175 design doc, and no test reads that file. Mos dropped the Part B wait when Q175 replaced Part B (mos-0971).",
|
||||
"Not live. The live stack still runs copies/490e2771d3d0. Rehearse and bus start --no-launch go live at the next re-pin, where the Q173 backup and STATE report apply.",
|
||||
] },
|
||||
{ id: 412, title: /^Console SSO: zero-trust OIDC verification/, body: [
|
||||
"Design r1 is committed: agents/sage/work/q175-sso/DESIGN.md in 7f57419b3 (blob 91e5b1b5e737), pushed in 9ae8c73ac. The Console becomes an OIDC relying party on oauth4webapi 3.8.8, pinned exactly. It always calls validateApplicationLevelSignature, because the library skips the signature check on a token-endpoint ID token. Identity is an (iss, sub) pair, and only asymmetric algs are accepted.",
|
||||
"Filbert's r1 review asks for changes in the doc before any code (B1-B6). B1 moves the provider config to config.json, because agents have write paths into the business file. B2 closes an open-redirect hole. B3 keeps the library's error causes out of logs. B4 caps login records. B5 fixes the signature mutant test. B6 removes a live forged-header probe from the phase 2 acceptance. Their rulings: Q-B start degraded when the IdP is unreachable, Q-C yes (7332 accepts only the public Host under --sso), Q-D keep idle expiry but don't refresh it on poll GETs. r2 follows.",
|
||||
"Interim (mos-0972): the Console runs without --send until phase 2. stack-release.sh has no way to drop --send that keeps stack-active.json and the unit in step, so a small tool row comes first, through review.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sage, 2026-10-20: row 163 (D2, #1660) round 2 approved. One comment on P19 337 (D2), per mos-0882 (every
|
||||
// landing or decision gets a Vikunja comment). Sage's own key (bot user 13); the sync key reads back. Comments only:
|
||||
// no task edits, labels, assignees or bucket moves. Checks the task's title before posting. Prints no secret.
|
||||
// Usage: node comment-337-r163-approved.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 337, title: /^D2: stack rehearsal on a scratch root/, body: [
|
||||
"Row 163 (Gitea #1660) round 2 is approved by Filbert (comment 29447, queue rev 2234, committed cd2b377bb). Candidate 2c090eefd on code 2a5f3a3a0, base 4f02486d5, manifest digest c6f4f4c12596. Filbert's own run: 249 of 249 single-file tests pass, and their seven mutants (M1-M6 from round 1, plus one for the read-only registry) are all killed.",
|
||||
"Not landed yet. Mos's landing order holds (mos-0965, mos-0967): the candidate moves scripts/stack-release.sh off blob 2586a2a37cd2, which the Part B A2 check rests on, so D2 lands only after the Part B window closes (B7). If refactor moves past 4f02486d5 on any manifest path or otherwise before then, the full gate runs again on the new base before landing.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,51 @@
|
||||
// Sage, 2026-10-20: row 163 (D2, #1660) in review, round 2. One comment on P19 337 (D2), per mos-0882 (every
|
||||
// landing or decision gets a Vikunja comment). Sage's own key (bot user 13); the sync key reads back. Comments only:
|
||||
// no task edits, labels, assignees or bucket moves. Checks the task's title before posting. Prints no secret.
|
||||
// Usage: node comment-337-r163-inreview-2.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 337, title: /^D2: stack rehearsal on a scratch root/, body: [
|
||||
"Row 163 (Gitea #1660) is in review, round 2, with Filbert as reviewer (request comment 29446, queue revs 2231-2233, committed c539995cf). Round 1 got changes from Filbert (comment 29445, rev 2230): six mutants survived because no test drove those checks to fail, plus a non-blocking note that a read-only session registry still wrote on remove().",
|
||||
"Round 2 adds the tests: R14 (a host that writes the live config, business file or backup fails live-config-unchanged or backup-unchanged), R15 (TERM, INT and HUP during the gate fail signal and still stop the host and Console and run the end checks), an env-ref case in R9, other-root and missing-root cases in R11, and a launches directory case in R4. The read-only registry now never writes its file. stack-release.sh is unchanged from round 1.",
|
||||
"Candidate 2c090eefd on code 2a5f3a3a0, base 4f02486d5; manifest digest c6f4f4c12596, 10 files, +1024/-13 (round 1 to round 2: 4 files, +85/-7). The gate passed on 2a5f3a3a0; mutants K34-K41 are all killed. Mos checked the manifest before the request (mos-0967). Packet: agents/sage/work/queue-163/r2/ in refactor 7af166574. Landing order is unchanged: after the Part B window closes (B7), with a new gate if the base moves.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,51 @@
|
||||
// Sage, 2026-10-20: row 163 (D2, #1660) in review, round 1. One comment on P19 337 (D2), per mos-0882 (every
|
||||
// landing or decision gets a Vikunja comment). Sage's own key (bot user 13); the sync key reads back. Comments only:
|
||||
// no task edits, labels, assignees or bucket moves. Checks the task's title before posting. Prints no secret.
|
||||
// Usage: node comment-337-r163-inreview.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 337, title: /^D2: stack rehearsal on a scratch root/, body: [
|
||||
"Row 163 (Gitea #1660) is in review, round 1, with Filbert as reviewer (request comment 29444, queue revs 2228-2229, committed def677bc7). Candidate 4be3f6aec on 580500699, base 4f02486d5; manifest digest 430212299beb, 10 files, +947/-14. It adds `stack-release.sh rehearse`, which runs the D2 rehearsal on a scratch root from a backup, and `bus start --no-launch`, a host that launches no sessions, runs no thread tick and writes no sessions.json.",
|
||||
"The gate passed on 580500699 (all package and script suites; Docker network removed, 0 containers left). Mutants K1-K33 are all killed; four survived the first run (K11, K21, K27, K31) and each got a test. Packet: agents/sage/work/queue-163/ in refactor 51f67f1f8.",
|
||||
"Mos accepted deviations 1-7 in mos-0965; deviation 3 (the scratch business keeps its tracker key, with @sync on a placeholder ref) rests on mos-0964, since business.mjs:197 refuses a file without it. Landing order (mos-0965): the candidate moves stack-release.sh off blob 2586a2a37cd2, which the Part B A2 check rests on, so D2 lands only after the Part B window closes (B7). If the base moves before then, the gate runs again.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,56 @@
|
||||
// Sage, 2026-10-20: files the activate --no-send row (mos-0972, mos-0973) as one P19 task, owner Sage, no due date.
|
||||
// Sage's own key (bot user 13); the sync key reads back. No labels, assignees, due dates or bucket moves. Refuses if
|
||||
// the title exists. Prints no secret.
|
||||
// Usage: node file-no-send-row.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const SIG = "Created by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (sig, xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(sig)}</p>`;
|
||||
const T = {
|
||||
title: "stack-release.sh activate --no-send: drop the Console's --send and keep the pointer in step (Q175 interim, mos-0972)",
|
||||
description: ps(SIG, [
|
||||
"Mos ruled (mos-0972) that the Console runs without --send until Q175 phase 2 (P19 #412), because with --send live any same-uid process can forge X-authentik-username on 127.0.0.1:7332 and send as Jason.",
|
||||
"stack-release.sh has no path that drops --send and keeps ~/.mosaic-dev/state/stack-active.json and the unit in step: console_for_target (:658-687) carries the pointer's flags, check_target (:247-249) allows a same-copy activate only for the proof pair, and adopt refuses once a pointer exists. A hand reinstall makes flags_drift refuse every later activate, pm-on and rollback.",
|
||||
"The row adds activate --no-send, modeled on row 187's --no-proof. It drops --send from the pointer and the unit. A same-copy activate is allowed when the pointer holds --send. Rollback and pm-on carry the pointer as they do now. There is no re-enable flag. Brief: docs/plans/2026-10-20_activate-no-send.md. Owner Sage, reviewer Dewey, gate the cli suite plus every scripts/test-*.sh (mos-0973 go).",
|
||||
"After landing: a same-copy activate on 490e2771d3d0 with --no-send under Q173, with the plan output sent to Mos first, a backup first and a STATE report after. Then row 66 lands.",
|
||||
]),
|
||||
};
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
async function existing() {
|
||||
const out = [];
|
||||
for (let page = 1; page < 20; page++) {
|
||||
const r = await api("GET", `/api/v2/projects/${PROJECT}/tasks?per_page=50&page=${page}`, undefined, sync);
|
||||
if (r.status !== 200) throw new Error(`list tasks: ${r.status}`);
|
||||
out.push(...list(r.json));
|
||||
if (list(r.json).length < 50) return out;
|
||||
}
|
||||
throw new Error("more than 19 pages");
|
||||
}
|
||||
const rec = { at: new Date().toISOString(), project: PROJECT, created: null, problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
if ((await existing()).some((t) => t.title === T.title)) throw new Error("title exists; refusing");
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
const r = await api("PUT", `/api/v1/projects/${PROJECT}/tasks`, { title: T.title, description: T.description }, key);
|
||||
rec.created = { status: r.status, id: r.json?.id ?? null };
|
||||
if (r.status >= 300 || !Number.isSafeInteger(r.json?.id)) rec.problems.push(`create: ${r.status} ${r.json?.message ?? ""}`);
|
||||
else {
|
||||
const back = await api("GET", `/api/v2/tasks/${r.json.id}`, undefined, sync);
|
||||
Object.assign(rec.created, { readBack: back.status, done: back.json?.done, inverse: `mark done: POST /api/v1/tasks/${r.json.id} with the whole task, done true (sage key); delete is Jason's or ops-01's` });
|
||||
if (back.status !== 200 || back.json?.title !== T.title || back.json?.done) rec.problems.push("create read-back differs");
|
||||
}
|
||||
save();
|
||||
console.log(JSON.stringify(rec));
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"at": "2026-10-20T18:21:15.323Z",
|
||||
"project": 19,
|
||||
"created": {
|
||||
"status": 201,
|
||||
"id": 413,
|
||||
"readBack": 200,
|
||||
"done": false,
|
||||
"inverse": "mark done: POST /api/v1/tasks/413 with the whole task, done true (sage key); delete is Jason's or ops-01's"
|
||||
},
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"at": "2026-10-20T15:41:54.066Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 337,
|
||||
"status": 201,
|
||||
"id": 368,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"at": "2026-10-20T15:26:21.265Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 337,
|
||||
"status": 201,
|
||||
"id": 367,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"at": "2026-10-20T14:49:31.177Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 337,
|
||||
"status": 201,
|
||||
"id": 366,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"at": "2026-10-20T18:19:36.544Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 337,
|
||||
"status": 201,
|
||||
"id": 372,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 412,
|
||||
"status": 201,
|
||||
"id": 373,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
# Webby review: C2-01c brief, playground levels (mos-1240, sage-2218), round d1
|
||||
|
||||
Webby, 2026-10-21T22:38Z (from `date -u`). Author: Gosalyn (gosalyn-0130). Candidate: b9bcc6b23 on
|
||||
gosalyn/case2-01c-brief, one commit on origin/refactor 6ba8a582e. The rows file blob is 978990319.
|
||||
Diff: `git diff 6ba8a582e b9bcc6b23`, +103/-5. It's a brief, so there's no queue record.
|
||||
|
||||
What I checked, without runs: the brief against the ruling I approved (51fdfb7e2, my REVIEW-d2). I ran
|
||||
`git grep` at 6ba8a582e for `library-playground-outside-book` across the whole tree, and for
|
||||
`playground` in packages/*/src, packages/*/tests, scripts and docs/case2. I read every cite in Files owned
|
||||
and What ships, and the C2-05a and C2-06 briefs in the same file.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1. The gate's grep can't pass as written. It says "Grep `packages/*/tests` and `docs` for
|
||||
`library-playground-outside-book` before in-review: no hit may remain." Under `docs`, at b9bcc6b23:
|
||||
- docs/plans/2026-10-20_case2-rows.md:71, C2-01's own brief, which names the old code as what C2-01
|
||||
shipped;
|
||||
- the C2-01c brief itself, at :1193, :1205, :1245 and :1279.
|
||||
Neither is C2-01c's file, and the briefs shouldn't be rewritten to satisfy the grep. Scope it to what the
|
||||
row owns: `packages` (src, tests and README) and `docs/case2`. Those are the hits a builder can actually
|
||||
clear: mapping.mjs:84, README.md:44, mapping.test.mjs:16-20 and :72, business.test.mjs:25 and
|
||||
SURVEY-RUN.md:114. The agents/ hits (herb queue-215 patch, sage queue-218 BUILD.md) are history and stay
|
||||
out of the grep too.
|
||||
|
||||
## Your three questions
|
||||
|
||||
1. Files owned. My whole-tree grep agrees with yours. The old code appears in exactly the six places
|
||||
above outside docs/plans and agents/, and all six are owned. I also read the other playground
|
||||
fixtures: tests/fixtures/mapping.json (book level only), reader.test.mjs:18 and :134-139,
|
||||
survey.test.mjs:73, render.test.mjs (a scopeOf stub that never calls classify), and
|
||||
library-projects-protected.test.mjs:15 and :37. None uses a prefix outside a book, so none changes.
|
||||
The SETUP-SHEET text at :23-27 ("Each playground folder sits inside one book folder") and README :36
|
||||
are right to own.
|
||||
2. C2-05a and C2-06. I agree with both. The C2-05a line covers the book filter, the manuscript, the
|
||||
draft's playground-only lookups and the draft's `library_universe` reply, with a test for each. The
|
||||
C2-06 brief (:365-410) only sends the guard to design 5.2. Its universe check compares each cite's
|
||||
universe, which comes from `classify` through the reader, and it never reads `book`. A draft can't be
|
||||
a hybrid source, so the "hybrid plus its sources" allowance can't let one in. It needs no line.
|
||||
3. Tests. One case per level, the `.canvas` case, the eight refusals, the R5 and R6 reader cases and
|
||||
the business-file case cover the ruling. Two cases are missing. Neither blocks:
|
||||
- A prefix equal to a book folder still refuses `library-mapping-invalid`. The refusals list in What
|
||||
ships names it, but the test list doesn't. mapping.test.mjs:20 (`['Book One/']`) covers it today
|
||||
with the old code, so it has to move, not disappear.
|
||||
- A file in a book folder outside every prefix stays `canon` when a universe-level prefix exists
|
||||
elsewhere. That's the one case for the new order.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Say what `checkMapping` returns for a draft. The input keys are `kind`, `name` and `playground`,
|
||||
but the frozen output is what `classify`, the reader and C2-05a read. Today's `classify` does
|
||||
`entry.books.find(...)` on every entry (scope.mjs:11). If the output leaves `books` out, any reader that
|
||||
keeps that shape throws. I'd return `{kind: 'draft', name, books: [], playground}`, so `books` is always
|
||||
a list. Add one test asserting the shape.
|
||||
|
||||
N2. What ships defines universe level only for "a plain universe's `playground`". The ruling says a
|
||||
prefix in `universes.<u>.playground` outside every book folder, which includes a hybrid. A hybrid
|
||||
keeps `playground` in its key list (mapping.mjs:70), and it may have `books: []`, which leaves it no
|
||||
book-level prefix to have. Say whether a hybrid can have a universe-level prefix. I read the ruling as
|
||||
yes. If so, one test.
|
||||
|
||||
## Checked and fine
|
||||
|
||||
- The cites hold at 6ba8a582e: mapping.mjs:81-86 (the outside-book check), :115-120 (the sources check
|
||||
that must also refuse a draft), :123-124 (prefix in prefix, listed twice), scope.mjs:5-7
|
||||
(the comment) and :10-20 (`classify`), README.md:36 and :44-45, SETUP-SHEET.md:23-27,
|
||||
SURVEY-RUN.md:114 and business.test.mjs:24-25.
|
||||
- The refusal codes match the ruling. `library-playground-other-universe` goes to Sage on the new-codes
|
||||
line.
|
||||
- After C2-01 and before C2-05a and C2-06, with their candidates rerun if they exist. The reviewer is
|
||||
me, and Sage picks the Opus seat.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Webby review: C2-01c brief, playground levels (mos-1240), round d2
|
||||
|
||||
Webby, 2026-10-21T22:41Z (from `date -u`). Author: Gosalyn (gosalyn-0132). Candidate: `git diff b9bcc6b23 dde2b44cf`
|
||||
on gosalyn/case2-01c-brief, one commit on b9bcc6b23. The rows file blob is a6ac627c8. I checked it
|
||||
against my d1 (REVIEW-d1.md, e44598b48). It's a brief round, so there's no queue record.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- B1. The gate grep covers `packages` (src, tests and README.md) and `docs/case2`. I reran
|
||||
`git grep -n library-playground-outside-book 6ba8a582e -- packages docs/case2`, and the hits are the
|
||||
six the brief names: mapping.mjs:84, README.md:44, mapping.test.mjs:16-20 and :72,
|
||||
business.test.mjs:25, SURVEY-RUN.md:114. All six are owned.
|
||||
- N1. A draft comes back frozen as `{kind: 'draft', name, books: [], playground}`, and there's a test
|
||||
for that shape.
|
||||
- N2. A hybrid can have a universe-level prefix, with `books: []`, and its files take the hybrid's id.
|
||||
The universe-level test runs in a plain universe and in a hybrid.
|
||||
- Test gaps. A prefix equal to a book folder has its own refusal case, and mapping.test.mjs:20 moves to
|
||||
`library-mapping-invalid`. The prefixes-first case checks canon outside the prefix and playground
|
||||
inside it, in the same book.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
None. Two edited lines in What ships and Tests run past the file's wrap width. That's cosmetic.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Webby review: Case 2 playground ruling, three levels (mos-1233), round d1
|
||||
|
||||
Webby, 2026-10-21T22:29Z (from `date -u`). Author: Gosalyn (gosalyn-0122). For mos-1233, Vikunja 464.
|
||||
Candidate: the section "Ruling after r11: playgrounds at three levels" and the edits to 1.1 and 1.2 in
|
||||
agents/gosalyn/work/case2/DESIGN.md, `git diff 624212842 7a7c00464` on gosalyn/case2-design. It's a design
|
||||
round, so there's no queue record.
|
||||
|
||||
What I checked, without runs: the diff against TARGET.md:92-95 (Mos's STATE repo, read only), REQ-LIB-2
|
||||
and REQ-LIB-3 as written in PRD 1.12 r1 (origin/ludwig/prd-1.12 docs/prd/mosaic-stack.md:1103-1120 and the
|
||||
level note at :1487-1490), design 1.3, 1.4, 3 and 5.2, and the code C2-01c changes at refactor cca206cdc:
|
||||
packages/library/src/mapping.mjs and scope.mjs, and the mapping tests.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1. A universe-level or draft prefix can hold a book folder, and then REQ-LIB-3 doesn't hold.
|
||||
- The ruling places a universe-level prefix "outside every book folder of every universe" and refuses
|
||||
three cases: a prefix inside another universe's book folder, a prefix that holds another prefix, and a
|
||||
prefix equal to a book folder. Nothing refuses a prefix that holds a book folder. Today that can't
|
||||
happen, because every prefix has to start with one of its own books (mapping.mjs:82-85). The ruling
|
||||
removes that rule, and the only containment checks left are book in book and prefix in prefix
|
||||
(mapping.mjs:122-123).
|
||||
- The new scope rule checks prefixes first. Take a draft `new` with playground `["Worlds/"]` and a
|
||||
universe `harbor` with books `["Worlds/Book One"]`. Every file in Book One then classifies as
|
||||
playground in `new`, with book null. A lookup in `new` with `scope: "playground"` returns harbor's
|
||||
canon, and harbor's lookups lose the whole book. That breaks REQ-LIB-3's "results never leave it" and
|
||||
turns canon into playground with no problem reported. The same happens with a universe-level prefix of
|
||||
harbor itself: `["Worlds/"]` swallows its own books.
|
||||
- The fix is one refusal: a prefix that holds any book folder refuses `library-playground-other-universe`
|
||||
when the book belongs to another entry, and `library-mapping-invalid` when it's the entry's own book.
|
||||
Add a test for each in C2-01c. With that in place, I agree REQ-LIB-3 holds as written. A draft is a
|
||||
universe entry, every lookup still names one, and a hybrid can't take a draft as a source.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The 1.2 table's `unmapped` row still ends "anything outside a book folder". After the ruling, a `.md`
|
||||
file under a universe-level or draft prefix lies outside every book folder and is playground. Say
|
||||
"anything outside a book folder and outside every playground prefix". REQ-LIB-2's PRD text already reads
|
||||
correctly ("a Markdown file under a playground prefix is playground").
|
||||
|
||||
N2. C2-01c has to say what `kind` may be. Today a non-hybrid entry can't carry `kind` at all (the `only`
|
||||
list at mapping.mjs:70). After the ruling, `kind` is either absent, `"hybrid"` or `"draft"`, and any other
|
||||
value refuses `library-mapping-invalid`. A draft's allowed keys are `kind`, `name` and `playground`. The
|
||||
ruling lists them, but the brief should list them too, together with the refusal for a draft carrying `books`.
|
||||
|
||||
N3. `classify` finds the book first and the prefix second (scope.mjs:10-18), and the survey and reader
|
||||
call it (survey.mjs:66, reader.mjs:182). C2-01c rewrites it to check prefixes first. The brief should name
|
||||
every reader of `universe.books` and of `classify(...).book`, so a draft with no books and a null book
|
||||
don't reach code that assumes them. C2-05a, C2-06 and the survey's per-book counts are the likely ones.
|
||||
The existing mapping tests for `library-playground-outside-book` (mapping.test.mjs:16-20) change code
|
||||
too, so they're C2-01c's files.
|
||||
|
||||
N4. Section 8's row list has no C2-01c line, and the rows file has no brief for it. Sage briefs the row.
|
||||
The ruling's last paragraph is what that brief should copy.
|
||||
|
||||
## Checked and fine
|
||||
|
||||
- TARGET.md:92-95. Book level is unchanged. Universe level covers "for books in a universe" outside any
|
||||
one book. The draft covers "where new universes can be created". The PRD level note (:1487-1490) can
|
||||
drop its caveat once this lands.
|
||||
- REQ-LIB-2. Scope still comes from the path alone. A playground at any level is never canon. The
|
||||
lookups' canon default and `playground-cited-as-canon` don't depend on the level.
|
||||
- `citation-cross-universe` treating a draft as its own universe, R5 for a canon link into any playground
|
||||
and R6 for a link into a draft fit 1.3's table as written.
|
||||
- The type of a universe-level file comes from the first folder under its prefix, as `classify` does for
|
||||
playground files today (scope.mjs:14-17). So `library_find`'s `type?` keeps working there.
|
||||
- `library_manuscript` needs a book, so it never reads book-null files. That matches the PRD, which
|
||||
names the manuscript only per book.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Webby review: Case 2 playground ruling, three levels (mos-1233), round d2
|
||||
|
||||
Webby, 2026-10-21T22:34Z (from `date -u`). Author: Gosalyn (gosalyn-0125). Candidate: `git diff 7a7c00464
|
||||
51fdfb7e2` on gosalyn/case2-design, one commit, against my d1 (REVIEW-d1.md, 35130c26b). It's a design
|
||||
round, so there's no queue record.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- B1. The ruling and the 1.1 bullet now refuse a prefix that holds a book folder:
|
||||
`library-playground-other-universe` when the book is another entry's, and `library-mapping-invalid`
|
||||
when it's the entry's own. Section 8's C2-01c line asks for a test of each. My d1 example, a draft
|
||||
`Worlds/` over harbor's `Worlds/Book One`, now refuses. With that, REQ-LIB-3 holds as written. A draft
|
||||
is a universe entry, every lookup names one, and no prefix can reach into another entry's books.
|
||||
- N1. The 1.2 `unmapped` row ends "and outside every playground prefix".
|
||||
- N2. `kind` is absent, `"hybrid"` or `"draft"`, and any other value refuses. A draft takes `kind`,
|
||||
`name` and `playground` with at least one prefix.
|
||||
- N3. I checked the reader list at cca206cdc. mapping.mjs:73-78 builds `books` and `bookOwner`.
|
||||
reader.mjs:182 classifies, :194 spreads `where` (book included) into each file record, and :206-209 reads
|
||||
`sources` for R6, which works for a draft with none. survey.mjs:66 and mirror.mjs:246 read only
|
||||
`scope`. The unlanded C2-05a and C2-06 parts are named, and so are mapping.test.mjs:16-20.
|
||||
- N4. Section 8 has the C2-01c row, after C2-01, owner Opus, with the test gate.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
None.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Case 2 Q219 idea caps (design 2852348d2, C2-06 brief 3d381b0a0), Webby
|
||||
|
||||
Request gosalyn-0136. Ruling Q219 (DECISIONS.md:2157 in the Mos STATE repo, read only), mos-1299,
|
||||
Vikunja 465. Git and reading only. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
## What I checked
|
||||
|
||||
- 2852348d2 has parent 51fdfb7e2, the tip of origin/gosalyn/case2-design. It touches only DESIGN.md,
|
||||
and the base blob is 0cf1bef29, the same blob as on origin/refactor aca16404c. The new blob is 897ba113b.
|
||||
- 3d381b0a0 has parent aca16404c, which is origin/refactor. It touches only
|
||||
docs/plans/2026-10-20_case2-rows.md, and the new blob is 7e0abdbe2.
|
||||
- Neither commit has a co-author line.
|
||||
- Q219 changes ideas from 5 of 200 to 3 of 120 and leaves the rest of M5 alone. It drops the residual
|
||||
from 1,000 to 360 and says Jason's acceptance is enough. The design matches each point:
|
||||
- the new ruling section (:117-124)
|
||||
- the M5 row (:901)
|
||||
- the residual paragraph (:908-914)
|
||||
- 10 step 8 (:1418-1419)
|
||||
B5 (:418-420) keeps the r1 text as history and adds a pointer to Q219.
|
||||
- In the new DESIGN.md, the only "200" left beside an idea limit is B5's history line. The other
|
||||
hits are the 200 KB page store (:946). The arithmetic holds: 3 x 120 = 360, inside the 1,200 total.
|
||||
- The brief's What ships names both limits (:395). Its Tests line names the refusals (4 ideas, a
|
||||
121-character idea), and the general rule still accepts at each boundary. Out of scope (:404) now
|
||||
says the residual is closed in Q219. In the new rows file, no idea limit says 200 or 5.
|
||||
- The design's section 8 C2-06 row has no residual text, so leaving it unchanged is correct.
|
||||
|
||||
## Notes (non-blocking)
|
||||
|
||||
N1. docs/case2/SETUP-SHEET.md:115-117 on origin/refactor still tells Melanie "at most 5 ideas of up to
|
||||
200 characters each" and "You or Jason can ask for lower limits". After Q219 both are stale. This is
|
||||
a doc change outside these two commits (decision 137, so it doesn't block). It needs a home. The
|
||||
simplest one is a line in C2-06's What ships and its manifest, so the sheet and the guard land
|
||||
together.
|
||||
|
||||
N2. Rows file :398 runs to 143 characters. The paragraph around it wraps at about 105. Rewrap it
|
||||
next time the brief is touched.
|
||||
|
||||
N3. Both commits are unpushed (Gizmoduck's slot). This review covers the commits by sha. If a
|
||||
rebase changes either one, the review doesn't carry over until I compare the new commit with
|
||||
this one.
|
||||
|
||||
## Follow-up 6aecdb675 (gosalyn-0138): APPROVE
|
||||
|
||||
6aecdb675 has parent 3d381b0a0 and no rebase, so the approval of 2852348d2 and 3d381b0a0 stands. It
|
||||
touches only the rows file (blob 15cad9112) and has no co-author line.
|
||||
|
||||
- N1 taken. Files owned adds the sheet's reply limits paragraph only. What ships says what the sheet
|
||||
must say (3 ideas of up to 120 characters) and drops the lower-limits offer. No test in packages or
|
||||
scripts reads SETUP-SHEET.md.
|
||||
- N2 taken. Lines 379-412 are all 110 characters or shorter.
|
||||
|
||||
N4 (non-blocking). The new cite says `:114-117` at aca16404c. At aca16404c, line 114 is blank and the
|
||||
paragraph runs from :115 to :118, ending with "No cited answer" instead. "The reply limits paragraph
|
||||
only" still names it without doubt, so this doesn't block. Fix it next time the brief is touched.
|
||||
|
||||
## Cite fix 33ac28398 (gosalyn-0140): APPROVE
|
||||
|
||||
33ac28398 has parent 6aecdb675 and no rebase. It changes one line, the sheet cite `:114-117` to
|
||||
`:115-118` (rows blob 4b25e76ec). N4 is closed, and the approvals above stand.
|
||||
@@ -0,0 +1,131 @@
|
||||
# Case 2 Q222 reply limits as settings (design df29550c9, C2-06 brief 467be26e3), Webby
|
||||
|
||||
Request gosalyn-0143. Ruling Q222 (mos-1302): "Things should NOT be hardcoded". Mos asked for two
|
||||
checks: no path lets an agent session change a limit, and no bad value switches the guard off. I read
|
||||
with git against origin/refactor aca16404c. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES (R1-R3), design and brief
|
||||
|
||||
The structure is right. The settings live in a top-level key outside `projects`. A human-only verb
|
||||
writes them, outside `ORGANIZATION_KEYS` and `TESTER_ORGANIZATION_VERBS`. A writer fence covers
|
||||
every other path, and each key falls back on its own. The three gaps below are in the text the build
|
||||
will follow.
|
||||
|
||||
## What I checked
|
||||
|
||||
- df29550c9 is a child of 2852348d2 and touches only DESIGN.md (blob 05de030ad). 467be26e3 is a child
|
||||
of 33ac28398 and touches only the rows file (blob 10a818f44). Neither has a co-author line.
|
||||
- Agent paths to a write, at aca16404c:
|
||||
- `organization.set` maps to `limits.set` with actor `<definition>.<role>`, keys limited to
|
||||
`ORGANIZATION_KEYS` (broker.mjs:219, :4205-4213).
|
||||
- The chief's hire and fire use actor `<definition>.<role>` (:4306).
|
||||
- The tester uses actor `"tester"` (:4201), gated by `TESTER_ORGANIZATION_VERBS` (:139, :4158).
|
||||
- Credential verbs (credential-verbs.mjs:27-50) clone the file document.
|
||||
- The launcher's delete uses actor `r.by`, the human who asked (:3892).
|
||||
- Every setter builds `next` with `structuredClone` of the file's document: instance-settings.mjs
|
||||
:226/:291/:327, launch-settings.mjs:180/:288, plugin-settings.mjs:57, credential-verbs.mjs:27/:47.
|
||||
So none of them changes `replyLimits` today, and the M11 fence won't refuse an ordinary agent write.
|
||||
The design's claim holds.
|
||||
- M9 is already in `organizationWriter.write` (organization.mjs:191-201). It compares `document` and
|
||||
then `written`, the JSON round-trip, against `old.projects`. `old` is the on-disk file.
|
||||
- `validateBusinessDocument` (business.mjs:229) rejects unknown top keys, so `replyLimits` has to join
|
||||
`TOP_KEYS`. With any value accepted, a bad hand edit can't stop her business from loading. I agree
|
||||
with that choice.
|
||||
- The ceilings make sense as validity bounds. Every key resolves to an integer within a range, so no
|
||||
value reads as "no limit". Replacing the whole object, with a missing key meaning its default, is
|
||||
fine. It matches how `plugins.set` takes its whole value.
|
||||
|
||||
## Required changes
|
||||
|
||||
R1. Say which `human` the fence compares against: the on-disk file's (`old.human` in `write`), never
|
||||
the incoming document's. The design says "unless `actor` equals the file's `human`". A direct `write`
|
||||
call is in M11's scope. Compared against `document.human`, a write that sets `human` to its own actor
|
||||
and changes `replyLimits` would pass. Add a test: a direct `write` that changes both `human` (to the
|
||||
actor) and `replyLimits` refuses `organization-reply-limits-human-only`, and the file and
|
||||
`changes.jsonl` are unchanged. Put the same test in the brief's M11 list.
|
||||
|
||||
R2. Say what the guard does when the business file can't be read or parsed at check time: missing,
|
||||
unreadable, not JSON, or a document that isn't an object. The fallback paragraph covers a bad
|
||||
`replyLimits` value, not a failed read. The guard must refuse the reply in that case, or check it at
|
||||
the defaults. It must never skip the check. Name the outcome and its code, and add a test to the
|
||||
brief: with the file unreadable, an over-limit reply refuses and `messages` is unchanged.
|
||||
|
||||
R3. Name booleans, arrays and objects in the fallback list next to `null`, a string and a fraction,
|
||||
and test them. JavaScript comparisons coerce: `true >= 1`, and `[7] <= 1000` with `[7]` read as 7. An
|
||||
implementation that checks ranges with `<` and `>` and no type check would let these through. Say
|
||||
"an integer by `Number.isInteger`" or something equally exact, and add `true`, `[7]`, `{}` and `"5"`
|
||||
to the brief's invalid-value tests.
|
||||
|
||||
## Notes (non-blocking)
|
||||
|
||||
N1. M9 checks both `document` and `written` (organization.mjs:193 and :200). Write M11 the same way
|
||||
and say so, so a value with a `toJSON` from a direct caller can't differ between the check and the
|
||||
disk. Agent sessions can't send such a value through the broker, so this doesn't block.
|
||||
|
||||
N2. The guard should read each key with `Object.hasOwn`. `library.limits.set` should refuse unknown
|
||||
keys with `invalid-request`, the way `keys()` does for the other verbs. The design says unknown keys
|
||||
are ignored when read. For the verb it says only that a value outside the table refuses.
|
||||
|
||||
N3. Line cites drift against aca16404c:
|
||||
- `TOP_KEYS` is business.mjs:24, not :21.
|
||||
- `write` is organization.mjs:180, not :177.
|
||||
- `ORGANIZATION_KEYS` is broker.mjs:219 and `TESTER_ORGANIZATION_VERBS` is :139, not :103 and :145.
|
||||
Fix these in the design and the brief.
|
||||
|
||||
N4. "After: C2-04". M9 is already in `write` at aca16404c, so C2-04's writer part has landed. If
|
||||
C2-04 is done, the dependency is met. If parts of it are still open, say which.
|
||||
|
||||
N5. At the ceilings, a human-set value can make the residual very large (up to 100,000 characters
|
||||
of item text) and can make the correction check permissive (`correctionPercent` 100). The guard
|
||||
stays on, since cites are still required, and Q222 gives the choice to the human. It's worth one line
|
||||
in the setup sheet and in `library.status`: the residual is `ideasPerReply` times `ideaLength`.
|
||||
|
||||
N6. The tester's actor is the literal `"tester"` and a role's is `<definition>.<role>`.
|
||||
`ID_PATTERN` (vocabulary.mjs:199) allows both `tester` and dotted ids as the business `human`. So a
|
||||
human id of `tester` or `assistant.bean` would equal an agent actor, and the M11 compare would pass
|
||||
for that agent. The human picks the id, so this is unlikely. Still, the fence could also require
|
||||
that the write came from a human verb, or the validator could refuse a `human` id that collides
|
||||
with those actor shapes.
|
||||
|
||||
## After mos-1305
|
||||
|
||||
Mos accepted four of Gosalyn's choices: the ceilings as validity bounds, 0 as the strict value for
|
||||
`ideasPerReply` and both correction keys, After C2-04, and the verb in limits.mjs. None of R1-R3
|
||||
conflicts with them. All three are about the settings mechanism (the M11 fence, the read at check
|
||||
time, the per-key fallback), which carries into the rework after Q30, so they stand. N4 is withdrawn,
|
||||
since Mos accepted After C2-04. Findings on the item format itself wait for the Q30 rework.
|
||||
|
||||
## Round 2 (gosalyn-0150): APPROVE, design f164dcef8 and brief 51cc0445c
|
||||
|
||||
f164dcef8 is a child of df29550c9 and touches only DESIGN.md (blob 6c5a36eb7). 51cc0445c is a child
|
||||
of 467be26e3 and touches only the rows file (blob 8f350730f, +26/-14). Neither has a co-author line.
|
||||
Both are local, so this approval binds to these two shas. A rebase needs a compare before it carries.
|
||||
|
||||
- R1 met. The fence compares against the on-disk `old.replyLimits` and `old.human`, never
|
||||
`document.human`. It also refuses `tester` and `<definition>.<role>` for any role in `old.roles`.
|
||||
The brief adds the test for a write that changes both `human` and `replyLimits`, plus a mutant that
|
||||
compares `document.human`.
|
||||
- R2 met. A missing, unreadable, non-JSON or non-object file checks at the defaults and
|
||||
`library.status` shows `reply-limits-unreadable`. The brief tests an over-limit reply with the file
|
||||
unreadable, `messages` unchanged. R2 allowed refuse or defaults; defaults is fine.
|
||||
- R3 met. Keys are read by `Object.hasOwn` and used only if `Number.isInteger` and in range. The list
|
||||
names booleans, strings, arrays, objects, fractions and `NaN`, and the brief tests `true`, `[7]`, `{}`,
|
||||
`"5"` and an array as `replyLimits`.
|
||||
- N1 (both `document` and `written`), N2 (unknown key refuses `invalid-request`), N5 (setup sheet line
|
||||
and the residual in `library.status`) and N6 (agent actor shapes) are taken.
|
||||
- N3 cites check at aca16404c: business.mjs:24, organization.mjs:180 and :191-193/:199-200, broker.mjs
|
||||
:139, :219, :419 and :570.
|
||||
|
||||
## Notes (non-blocking)
|
||||
|
||||
N7. broker.mjs:327 (`checkBusiness`, row 177) already refuses a business whose `human` is `tester`, so
|
||||
that half of N6 was closed before this round. A dotted human id equal to a role actor is still legal.
|
||||
With the new rule, that human can't change the limits at all, since their own verb's actor matches
|
||||
the refused shape. That fails closed, so it doesn't block. The build should name the code that verb
|
||||
returns in that case, and the cleaner fix is still a validator refusal of such an id.
|
||||
|
||||
N8. The brief's dotted-human test says "that role's write refuses". `organization.set` can't carry
|
||||
`replyLimits` (it isn't in `ORGANIZATION_KEYS`), so the test has to be a direct `write` with the role's
|
||||
actor. Say so when the brief is next touched, or the test may pass for the wrong reason.
|
||||
|
||||
N9. Rows file :383 runs to 115 characters. Rewrap when the brief is next touched.
|
||||
@@ -0,0 +1,305 @@
|
||||
# PRD 1.17 draft d2, Case 2 story files are DOCX (Vikunja 570), Webby
|
||||
|
||||
Request: Sage sage-3098 (00:50:47Z 10-23), reviewer named in mos-1367 B5.
|
||||
Draft: agents/sage/work/prd-1-17/DRAFT-d2.md, sha256 02af5bbfd27d54bf7afc917fe573c310e9059e1bf50af2fa7d7ac1c893788b1d
|
||||
(matched before reading). Base: docs/prd/mosaic-stack.md blob a6db9b4a13d4 at 482e865ac.
|
||||
Sources: /mnt/storage/src/jason-master-restart/DECISIONS.md Q235-Q237 (lines 2355-2400), read only.
|
||||
|
||||
## Verdict: CHANGES (B1, B2, B3)
|
||||
|
||||
## What I checked
|
||||
|
||||
- Quotes. Every "Replace" and "Add after/before" anchor appears exactly once in the 1.16 text with lines
|
||||
joined: REQ-LIB-2 scope sentence (base :1225-1230), "Lookups return canon..." (:1230-1231), "so an agent
|
||||
can't cite..." (:1231-1232), REQ-LIB-4 "The user commits and pushes..." (:1252-1253), REQ-LIB-5 "Each
|
||||
answer names the commit it read." (:1262-1263), REQ-LIB-6 "A reply may cite lines..." (:1283-1284),
|
||||
"the broker refuses such a write before any git call." (:1276), "(Design 1.1" (:1208), and the 1.11 MVP
|
||||
line quoted in the notes.
|
||||
- Ids. requirements.json at 482e865ac has 85 ids and requirements.test.mjs:21 pins 85. One new id makes 86.
|
||||
The test's version pin (:24, '1.16') also moves, and the draft's file list covers it.
|
||||
- B1 condition (mos-1367). REQ-LIB-8 bullet 1 (draft :132-134) says it plainly: the converted file leaves
|
||||
the Gitea-mirrored repository and its DOCX lives in the stack's writing store, not in the repository
|
||||
the user pushes or opens in Obsidian. REQ-LIB-4's replacement (:100-104) says the same. Met.
|
||||
- B2 condition. Draft :135-140: each import has its own fidelity report, the human's confirmation of it, a
|
||||
manifest naming the source file and commit, and one commit. Met, apart from B3 below.
|
||||
- P1-P6 against REQ-LIB-8:
|
||||
- P1: bullet text :127-129. P2: :135-140 (convert, mark, move to the archive folder), with O4's
|
||||
archived scope in REQ-LIB-2 keeping it out of canon. P3: :143-145. P4: :129-131 and the format map
|
||||
:146-147. P5: :141-142 and :151. P6: :148-151, and the 1.17 note :161-164 for the 10-27 release.
|
||||
- All six are present.
|
||||
- Text beyond P1-P6. These trace to the landed O2 design D10/D12 or to Mos's O1-O4 rulings, not to
|
||||
P1-P6. Each is cited in the draft, so I don't count them as overreach, but Mos and Jason should know
|
||||
they are in the text Jason says yes to:
|
||||
- REQ-LIB-4: each accepted editor save is one commit by the human in the writing store (D12).
|
||||
- REQ-LIB-5: a DOCX is read only from a committed version, never unsaved text.
|
||||
- REQ-LIB-6: "A DOCX citation never calls a paragraph a line."
|
||||
- REQ-LIB-8 :148-150: the import opens only once agents can read committed DOCX "and cite them".
|
||||
P6 says only "once agents can read DOCX chapters". Citing is a stricter gate than P6.
|
||||
- The Risks line on zip diffs.
|
||||
- REQ-LIB-8 vs REQ-LIB-6 supporting writes. No conflict in the writes themselves. The archive folder
|
||||
may not overlap another prefix (draft :85-87), so it can't sit inside a supporting folder, and REQ-LIB-6
|
||||
gains "the archive folder is read-only to it". Supporting files stay Markdown in the user's repository.
|
||||
- REQ-LIB-8 vs REQ-WAIT-2/3. No conflict. The import is a human verb with no role grant and no agent
|
||||
request path (O3), so it isn't a vocabulary action and REQ-WAIT-3 doesn't reach it. REQ-WAIT-3's
|
||||
`library.write` paragraph (base :719-724) already says it "never writes a story file", which still
|
||||
holds for a DOCX in the writing store. If 571 later makes the import a vocabulary action, REQ-WAIT-3
|
||||
makes it a hard wait and REQ-WAIT-2 keeps it out of every role file, as O3 says.
|
||||
|
||||
## Required changes
|
||||
|
||||
- B1. A playground file can become canon. REQ-LIB-2's replacement (draft :83-84) makes "a story file's
|
||||
DOCX document in the writing store" canon for its book, with no other scope for a DOCX. REQ-LIB-8
|
||||
(:135) lets the import take any "Markdown story file", and P1 makes "chapters and other story files"
|
||||
DOCX. Nothing stops the import from taking a Markdown file under a playground prefix. Its DOCX would
|
||||
then come back as canon, which breaks REQ-LIB-2's own title, "Playgrounds are never confused with
|
||||
canon" (base :1211). The fix can be either of these:
|
||||
- The import takes only a canon file, a Markdown file in a book folder outside every playground and
|
||||
supporting prefix, and playground text stays Markdown for now.
|
||||
- A DOCX keeps the scope, universe and book of the file it came from.
|
||||
The first is smaller and leaves the playground question to 571.
|
||||
- B2. Archived files have no universe. REQ-LIB-1's addition (draft :69-70) names "one archive folder" for
|
||||
the whole mapping, and REQ-LIB-2's addition (:85-87) says it may not overlap a book folder. A file
|
||||
moved there loses the path that gave it a universe and book. REQ-LIB-3 (base :1237) says "Every lookup
|
||||
names one universe, and its results never leave it". An archived lookup then either returns nothing or
|
||||
returns files from every universe, and the second breaks REQ-LIB-3. Two ways to fix it:
|
||||
- The archive folder is set per universe (or per book).
|
||||
- The converted mark records the source path, and an archived file takes its universe and book from
|
||||
that mark.
|
||||
Either way the text should say which, since O4's continuity use (citing history) needs the book.
|
||||
- B3. The archive move has no named committer. The import makes two changes in two places:
|
||||
- the DOCX commit in the writing store;
|
||||
- the mark and move of the Markdown file in the user's repository.
|
||||
REQ-LIB-4 today says the user commits with their own tools and the bus only mirrors that repository
|
||||
with a read-only token, fast-forward only (base :1252-1259). REQ-LIB-6 ties every agent write to the
|
||||
agent's own account. The draft doesn't say whose account commits the archive move in the user's
|
||||
repository, or how "changes nothing when any step fails" (:140) holds when one step is a commit to a
|
||||
repository the stack doesn't own. Say who commits it. The human's own account, as with REQ-LIB-4's
|
||||
saves, fits P5. Also say what "changes nothing" covers. For example, the writing-store commit happens
|
||||
only after the move is on the remote and in the mirror, or the reverse. The mechanics can stay with 571.
|
||||
|
||||
## Notes (non-blocking)
|
||||
|
||||
- N1. REQ-LIB-2's new rule (draft :80-81) says "a file under the archive folder is archived". Every other
|
||||
scope in the same sentence is "a Markdown file". A non-Markdown file the user drops in the archive
|
||||
folder would become archived and returnable. "a Markdown file under the archive folder" matches the
|
||||
other scopes.
|
||||
- N2. The Settled points intro (:16) says Mos took the first choice in each, but only O1 and O2 carry
|
||||
"Ruled yes in B1/B2". O3 and O4 still read as recommendations. Add "Ruled yes in B3" and "in B4" so the
|
||||
record doesn't need mos-1367 to read.
|
||||
- N3. REQ-LIB-1's version tag (:70-71) doesn't say where it goes. The 1.15 tag sits inside the closing
|
||||
parenthesis, "Version 1.15: supporting folders.)", so the 1.17 tag presumably follows it there.
|
||||
- N4. P5 names "Melanie's real books", and REQ-LIB-8 :150-151 says "a real user's books". That reads
|
||||
broader, which is the safer direction. I'm flagging it only so Mos sees the wording changed.
|
||||
- N5. If mos-1368's impact map changes the text, I'll review the delta against d2.
|
||||
- N6. The 1.15 and 1.16 notes keep the old text of every requirement they rewrote, for example "The 1.14
|
||||
text of REQ-LIB-6 was: ..." (base :1735). The d2 notes don't keep the 1.16 text of the REQ-LIB-2, -4, -5
|
||||
and -6 sentences they replace. Adding those lines follows the convention and lets Jason read the change
|
||||
without the diff.
|
||||
|
||||
## Round 2: draft d3 (sage-3099, 00:54:40Z 10-23): APPROVE
|
||||
|
||||
Draft: DRAFT-d3.md, sha256 f6dd3c28743bb34c5cde10a2d7db7a51d684f8ad2c91a95fe6440835f631cf4d (matched). I read
|
||||
the full d2-to-d3 diff. Lines below are d3 lines.
|
||||
|
||||
- B1 met. REQ-LIB-8 :162-165: only a canon Markdown file converts. The import refuses playground, supporting
|
||||
and archived files and a converted one. REQ-LIB-2 :111-112 makes a DOCX canon only for the universe and
|
||||
book of the canon file it came from. A playground file can no longer come back as canon.
|
||||
- B2 met. REQ-LIB-2 :110-111 and REQ-LIB-8 :170-171: the move keeps the old path under the archive folder, and
|
||||
the archived file takes its universe and book from that path. Scope still comes from the path alone, and
|
||||
REQ-LIB-3 holds. The 1.17 note :219-220 says so.
|
||||
- B3 met. :172-176: the mark and move are one commit authored by the human in the user's repository. The
|
||||
DOCX is canon only once both commits exist, and the Markdown stays canon at its old path until then. A
|
||||
failed import leaves no returned DOCX and no moved Markdown.
|
||||
- N1, N2, N3 and N6 are met. The three version-tag anchors are each unique in the 1.16 text: "Version 1.15:
|
||||
supporting folders.)" with lines joined, "Version 1.15: supporting.)" at base :1236, and "Version 1.15.)"
|
||||
at base :1293. The quoted 1.16 sentences in the notes (:205-215) match the text they replace.
|
||||
- N4 is kept with a reason in C-section. That's Mos's call.
|
||||
|
||||
### Notes (non-blocking, round 2)
|
||||
|
||||
- N7. "Pushed the way REQ-LIB-4's Console saves are" (:172-173). In d3, the only Console saves REQ-LIB-4
|
||||
mentions are the DOCX saves to the writing store, which are never pushed to the user's repository. The
|
||||
path meant is presumably REQ-LIB-4's existing "Bus writes and pushes come in later rows (C2-02,
|
||||
C2-03b)" (base :1259, and the 1.15 note at base :1762-1763 on the user's Console saves). Naming those
|
||||
rows would remove the ambiguity.
|
||||
- N8. "No Markdown moved" on failure (:175-176) only holds if the store commit comes before the push of the
|
||||
move. Once the move is pushed it can't be withdrawn without a new commit, because history isn't
|
||||
rewritten. That ordering is 571's to state.
|
||||
- N9. C1 narrows P1. Jason's P1 makes "chapters and other story files" DOCX, and d3 keeps playground story
|
||||
text Markdown. The 1.17 note :217-218 says this openly, so Jason's yes covers it. Mos should make sure it
|
||||
is visible when the text goes to him.
|
||||
|
||||
## Round 3: draft d5 (sage-3117, 01:31:00Z 10-23): CHANGES (B1, B2, B3)
|
||||
|
||||
Draft: DRAFT-d5.md, sha256 0dc49e5b4b84de0e43d365c1ef87d3e6300dba7e986d5dc0bb4acc50ad4e900b (matched; DRAFT.md is
|
||||
the same blob), 458 lines. Lines below are d5 lines. Base anchors are from 482e865ac with lines joined. Sources read
|
||||
only: DECISIONS Q226 (d183246, :2253-2273) and the impact map (d183246, projects/mosaic-mvp/case2-docx-impact-map.md).
|
||||
|
||||
### Anchors and titles
|
||||
|
||||
- Each new "Replace" and "becomes" anchor appears once in the 1.16 text, with one exception, N10.
|
||||
- The three changed titles (:348-353) are 120, 116 and 117 characters, one line each, inside the
|
||||
requirements.test.mjs title rules.
|
||||
- The six roles/*.md contracts each hold "Every item cites the lines of the user's material it is about" (:311-313).
|
||||
|
||||
### Sage's five checks
|
||||
|
||||
1. REQ-LIB-2 opening "A Markdown file's scope comes from its path alone" (:95). Right call. A DOCX takes its scope
|
||||
from its source (:103-107), so "a file's" would be false. A non-Markdown file in the archive folder stays
|
||||
unmapped, which closes my old N1.
|
||||
2. Exclusion by id (:232-233). Needed: a DOCX started in the editor has no old path to exclude. REQ-LIB-3's own text
|
||||
(base :1246-1247) says "an exclusion keeps a source path out", so see N11.
|
||||
3. Autosave (:263-265). F41 says "All changes are instantly committed and available to the agents", and Mos's
|
||||
reading is "Each save is a commit at once, and agents read it at their next lookup" (DECISIONS :2257, :2268).
|
||||
Saves while the human types fit "instantly". The human-set intervals are Sage's addition (:418) and appear in
|
||||
the text Jason approves, but the J list (:426-432) doesn't carry them. See N12. REQ-LIB-5's "a lookup that
|
||||
starts after a save is acknowledged returns that save or a later one" (:148-149) matches "available to the
|
||||
agents".
|
||||
4. Unmarked folders not taken (:421). Agreed. REQ-LIB-2 leaves an unmapped file unmapped and never returned, so a
|
||||
DOCX made from one would have no scope to keep. The impact map's Q18 put the narrow or wide reading to Jason, and
|
||||
d5 marks the choice as item 4 (:443), so he sees it.
|
||||
5. A6's view not taken (:422). Acceptable only with Jason's yes. The impact map says "Jason: yes only if the
|
||||
amendment drops that view" (A6). J-b (:427) asks it. See N13.
|
||||
|
||||
## Required changes
|
||||
|
||||
- B1. REQ-LIB-8 and REQ-LIB-9 have no titles. requirements.test.mjs:20 needs one entry per `**REQ-` id, each with a
|
||||
title of at most 120 characters, and Jason says yes to the exact text, so the titles belong in it. Fix text for
|
||||
the :346-354 note, after the REQ-LIB-4 title:
|
||||
"REQ-LIB-8 is titled 'Story files become DOCX through a human-only, one-way import that archives the Markdown
|
||||
under its old path' and REQ-LIB-9 'The Console's DOCX editor: only a human saves, each save is one commit, and
|
||||
the editor code is vendored and pinned'." These are 106 and 114 characters. Also add "and titles of REQ-LIB-8
|
||||
and REQ-LIB-9" to the file list at :451.
|
||||
- B2. The old-path rule covers canon only, but d5 converts playground files too. REQ-LIB-2 (:97-99) excepts a
|
||||
converted file's old path only inside the canon clause, and REQ-LIB-8 (:226-227) says only "is not canon". D1
|
||||
makes playground files convertible (:189, :201). After one is converted, a Markdown file the user puts back at
|
||||
its old path under the playground prefix is returned as playground, next to the playground DOCX that replaced
|
||||
it. Agents then see two current versions of one story file, which is what D5's rule exists to stop. Fix text:
|
||||
- REQ-LIB-2, replace "one under a playground prefix at any level is playground, any other Markdown file in a
|
||||
book folder is canon unless its path is a converted file's old path (REQ-LIB-8), and everything else" with
|
||||
"a Markdown file at a converted file's old path is unmapped (REQ-LIB-8), one under a playground prefix at any
|
||||
level is playground, any other Markdown file in a book folder is canon, and everything else".
|
||||
- REQ-LIB-8 :226, "is not canon" becomes "is neither canon nor playground".
|
||||
- B3. The 1.12 note misstates Q226 (:285-289). It says "Q226 later brought C2-02, C2-03b, C2-11a and C2-11b into
|
||||
the 10-27 slice". Q226's F42 reading names three: "C2-02, C2-03b and C2-11b move into the 10-27 slice"
|
||||
(DECISIONS :2269), and its F41 reading says "The editor stays an add-on that doesn't gate 10-27 (Q195)"
|
||||
(:2268). C2-11a isn't in Q226. The impact map's A57 says of C2-11a and C2-11b "Neither gates 10-27,
|
||||
ROWS:1655", which disagrees with F42 for C2-11b. Fix: name only C2-02, C2-03b and C2-11b with Q226 F42 as the
|
||||
source, and either cite the ruling that added C2-11a or drop it. If the queue's current gating differs from
|
||||
Q226, say which record wins. Jason reads this note as history, so it has to match his record.
|
||||
|
||||
## Notes (non-blocking, round 3)
|
||||
|
||||
- N10. "The Console shows each cite's scope." appears twice in the 1.16 text: in REQ-LIB-2 (base :1233) and in the
|
||||
1.14 note quoting the old REQ-LIB-2 (base :1706). The replacement at :117 should say "in REQ-LIB-2", so the 1.14
|
||||
note, which is history, stays unchanged.
|
||||
- N11. :342 says REQ-LIB-3 is unchanged, but REQ-LIB-8's by-id exclusion adds a form that REQ-LIB-3's "an
|
||||
exclusion keeps a source path out" doesn't have. Say "REQ-LIB-3's text is unchanged; REQ-LIB-8 adds an
|
||||
exclusion by DOCX id", or add the clause to REQ-LIB-3 itself.
|
||||
- N12. Add a J item for the autosave intervals. F41 says "instantly", and "at intervals only a human changes" is
|
||||
new text. J-e covers versioning, not timing.
|
||||
- N13. J-b should quote what it narrows. REQ-LIB-5 keeps its "Connections are always visible" tag (:161-162), while
|
||||
its new text says no graph shows a DOCX file's links (:158-159). Q226 asked for "another way to handle the
|
||||
visual brain map". J-b would be clearer to Jason if it named both.
|
||||
- N14. REQ-LIB-4's new title "agents read Markdown only there" can be read as "agents read only Markdown there".
|
||||
"agents read the user's Markdown only there" makes the title 128 characters, over the limit. "and agents read
|
||||
Markdown from it alone" makes it exactly 120. Sage's call.
|
||||
- N7-N9 of round 2: N8 is settled by D3's order bullet (:218-223). N7 is met at :215-216 (C2-02, C2-03b). N9 no
|
||||
longer applies, since d5 converts playground story files too (:189).
|
||||
|
||||
## Round 4: draft d6 (sage-3119, 01:36:38Z 10-23): APPROVE
|
||||
|
||||
Draft: DRAFT-d6.md, sha256 886b93c571f87bfbb2f27ee3113b6800a32d8f89196d95ace33566031bee380a (matched; DRAFT.md is
|
||||
the same blob), 475 lines. I read the whole d5-to-d6 diff. Nothing outside the listed fixes changed.
|
||||
|
||||
- B1 met. Both titles are in the note (:359) and the file list (:468). Sage's REQ-LIB-8 title is 113 characters,
|
||||
and I accept it over mine: it covers D2's story files started in the editor, which mine left out.
|
||||
- B2 met. REQ-LIB-2 (:97-100) puts the old-path rule ahead of the playground and canon clauses, so it covers both.
|
||||
REQ-LIB-8 (:227-228) says "neither canon nor playground".
|
||||
- B3 met. The note (:287-291) names F42's three rows, matching DECISIONS :2269, and says F42 wins over records
|
||||
that differ. The 1.12 note it cites does list C2-11a among rows that don't gate 10-27 (base :1637).
|
||||
- N10 met (:118). The sentence still appears twice in the base, and "In REQ-LIB-2 only" scopes it.
|
||||
- N11, N12 (J-h, :449) and N13 (J-b, :443) met.
|
||||
- N14 not taken, and Sage cites mos-1372 D11. That's Mos's call.
|
||||
|
||||
## Round 5: draft d8 (sage-3378, 11:13:06Z 10-23)
|
||||
|
||||
Read: DRAFT-d8.md first as sha e9b01d44e32fe63b (634 lines), then as regenerated under mos-1446 (sage-3383, 11:17:01Z), sha 343cff6ff8fdfdb8 (638 lines, same as DRAFT.md). d7-to-d8.diff sha d8be352ad71e8172. Its body equals `diff -u` of d7 and d8, and only the header's file names and times differ. Mos's fix list mos-1443 sha 4c1b30d4. Line numbers below are for 343cff6f. Doc-only, no slot used, no node run.
|
||||
|
||||
Verdict: APPROVE. No B items.
|
||||
|
||||
Checked:
|
||||
- Placement. Every Replace and Delete block of the fix list is gone from d8 or kept inside its With text. Every With and Insert block appears once. J-i to J-l follow J-h in order. M1's sentences sit after "Bus writes and pushes come in later rows (C2-02, C2-03b)." The M30 placement notes are in for REQ-LIB-8, REQ-LIB-9 and the Risks lines. The inline fixes M36a (D9), M45a ("Review order (mos-1367 B5):") and M45c (D10) are in, and their old forms are gone.
|
||||
- The d8 changes list has one entry for each of R1 to R14 and M1 to M45, plus the landing swap line and the file list.
|
||||
- Landing swap. b99ca86b9 is on origin/refactor ("docs: align editor conversion boundary and host restore with PRD d8"). M15 (source line) and M16 (571 list lead) cite it. J-b keeps 767bb62c7. The Connections view text in writing-editor.md is the same at both commits, so J-b's line numbers hold.
|
||||
- mos-1446. The source line (:70-87) quotes Q235 in full. I checked it word for word against jason-master-restart DECISIONS.md:2357, read only. It keeps "creating writing" and drops only the leading "Q36:", which the line's "answering Q36" covers. The quote rewraps to 82 columns. The M15 line (:500) says "in full (mos-1446)". Rerunning my check on 343cff6f, every With block is found except M15 (changed by this ruling) and M16 (found once 767bb62c7 is swapped for b99ca86b9).
|
||||
- Open defaults with Mos: M10 has no J-m, M15 quotes Q235 in full under mos-1446, and M40 has both sentences in REQ-LIB-6 ("No creative agent starts or undoes a conversion ..." and "An agent may still suggest a conversion to the human in chat.").
|
||||
- R3's five d9 edits are not applied. REQ-LIB-5's notes still say "until a Console view of their links exists", and the Risks line says the same.
|
||||
- Section 5.1 against origin/refactor. packages/tasks/tests/requirements.test.mjs:21 pins 85 ids and :77 expects `requirements[85].id: REQ-ROLE-1 twice`. packages/bus/tests/requirements.test.mjs:48 pins length 85. REQ-LIB-7 and REQ-INST-1 are adjacent in packages/tasks/requirements.json:86-87, so the new ids fit between them. `GATED_ONLY` is at packages/business/src/vocabulary.mjs:101, as the 571 list item names it.
|
||||
- Stale d7 text against the new text. No "agents read only", "W4b" (outside the M4 history line), "three things", "acknowledged" or "WebUI view" is left in the PRD text. "path alone" appears only in the Replace instruction (DRAFT-d8.md:106) and the kept 1.16 quote (:402). REQ-LIB-4, REQ-LIB-5, REQ-LIB-8, J-h and J-j agree on read timing: a save is read at the next lookup after the editor reports it committed.
|
||||
|
||||
Non-blocking:
|
||||
- N15. DRAFT-d8.md:559, the D7 landing line, still says the impact map's item 2 adds "no agent credential reaches it". R8 changed Item 2 (:620) and REQ-LIB-4 (:156) to "no agent can reach it with any credential". The pointer quotes text that no longer exists. Suggest quoting the R8 form.
|
||||
- N16. DRAFT-d8.md:538, the d7 changes line, says "d6's 'no agent credential reaches it' holds". That was true of d7. As history it can stay, but a short "(R8 replaces it in d8)" saves Jason reading two answers.
|
||||
- N17. Rewrap. The PRD text still has three lines over 82 columns: :107 (83, REQ-LIB-2), :276 (83) and :277 (95, the REQ-LIB-8 old-path bullet). All three came in with d6 B2 and are in d7 at :91, :230 and :231. d8 fixed the other d7 overlong lines. Not blocking because landing rewraps the quoted text anyway.
|
||||
- N18. No round of mine covers d6 to d7 (mos-1379 on Fenton's 571 B2). I read its text as carried in d8: REQ-LIB-5's tracked-changes, comments and tracked-deletions sentences with the Q222 tag (:179-181, :197), and the J-d sentence (:603). They agree with the 571 design's settings and with J-d's "Markdown has neither". I accept them.
|
||||
|
||||
## Round 6: draft d9 (sage-3498, 15:25:43Z 10-23)
|
||||
|
||||
Read with git show from Sage's local commit 4863e2caf, which is unpushed. DRAFT-d9.md is sha 7dfc52fd602405a1, 647 lines, and DRAFT.md is the same file. d8-to-d9.diff is sha 7cf3431492251a1b, and its body equals `diff -u` of d8 (343cff6f) and d9. Also read: Q239 at jason-master-restart 7ee5ab7 DECISIONS.md:2406 (read only), and R3 and M22 in Mos's fix list mos-1443 (d8-fixes.md:13-20 and :393-410). The 1.16 base is docs/prd/mosaic-stack.md blob a6db9b4a at 482e865ac. Line numbers below are for d9. Doc-only, no slot used, no node run.
|
||||
|
||||
Verdict: CHANGES, one B item.
|
||||
|
||||
Sage's three questions:
|
||||
- Items 2 and 3 stay inside Q239's "nothing else changes". Without item 2, REQ-LIB-5's note (:417-418) and the graph Risks line (:456-457) would still say the view is later work, which contradicts the new REQ-LIB-5 sentence (:194-195). Item 3 only records the answer. R3 itself lists the note, the Risks line and the A6 line as d9 edits, so none of them is new scope. None should come out. B1 is about their wording, not their presence.
|
||||
- Line widths. No PRD-text line is over 82 columns. :137 (101) and :192 (83) are instruction lines, both already in d8. N17's two rewraps keep the words: the whitespace-normalized text is unchanged.
|
||||
- Replace quotes. Every Replace, "Add, after" and "becomes" quote in the Text changes section occurs once in the 1.16 base, normalized for whitespace. The one exception is "The Console shows each cite's scope.", which occurs twice (REQ-LIB-2 and the 1.14 note at base :1706). d9 scopes it with "In REQ-LIB-2 only", as N10 asked. The insertion anchors "Trusted mode lets an agent", "### Installer" and "## Milestones" each occur once.
|
||||
|
||||
Checked as well: N15 and N16 are in at :568 and :547, worded as I asked. Nothing outside the listed places changed.
|
||||
|
||||
Blocking:
|
||||
- B1. d9 doesn't use the edits Mos already ruled for a yes to J-b, and doesn't say why. mos-1443 R3 (d8-fixes.md:13-20) says "If Jason says yes, d9 takes five edits", with exact text. d9 departs from three of them:
|
||||
1. REQ-LIB-5 (:194-195). R3.1 adds "The Console's Connections view lists the current links and backlinks of each story file the user may see, in both formats, with each one's scope and format." d9 instead replaces "and no graph shows them yet" with "The Console shows a converted chapter's links out and in, in a Connections view." That is a narrower requirement. It covers converted chapters only, and it drops each link's scope and format and the "story file the user may see" bound. Q239's reading ("a Console view that shows a converted chapter's connections") fits either text. A builder would get a different view from each, so the choice of text belongs to Mos, not to the draft.
|
||||
2. The Risks line (:456-457). R3.2 is "...show in the Console's editor and its Connections view, and no graph shows them yet." d9 ends "..., not in Obsidian." That's close in meaning, but it is still not the ruled text.
|
||||
3. The A6 line. R3.3 moves it from "Not taken" up to A6 under "What d5 adds". d9 leaves it under Not taken (:605) and adds a sentence, so a line about a view that is now taken still sits under "Not taken".
|
||||
R3.5 matches in substance at :418. Fix: either take R3.1 to R3.3 as written, or keep d9's text and add a line under "d9 changes" saying it departs from R3 and why, so Mos rules on it at his J-b check. R3.4 is design work (Gyro, writing-editor.md and docx-citations.md at 767bb62c7). A note in "d9 changes" that it follows would keep it from being lost.
|
||||
|
||||
Non-blocking:
|
||||
- N19. J-b (:610) still says "His call whether 1.17 requires that view. As written, it does not." and "no graph or list shows them" ahead of the answer. Both are false of d9. Suggest putting them in the past ("As d8 was written, it did not") or ending J-b's question before the answer.
|
||||
- N20. J-b's "the import does not wait for the view (mos-1443 R3)" states as settled what R3 calls "my default for Mos to confirm". Q239 doesn't address it. Either cite it as Mos's default, or have Mos confirm it at the J-b check and cite that.
|
||||
- N21. The new REQ-LIB-5 sentence rests on Q239, but REQ-LIB-5's 1.17 tag (:198) cites only Q222, and the Status and source lines name Q235 to Q237. Adding "Q239" to the tag would give the requirement its source. It's Mos's call, given Q239's "nothing else changes".
|
||||
|
||||
## Round 7: draft d9 r2 (sage-3502, 15:36:19Z 10-23)
|
||||
|
||||
Read with git show from Sage's local commit 8b4201055, which is unpushed and sits on 4863e2caf. DRAFT-d9.md is sha b17ce3867c4dfea7, 650 lines, and DRAFT.md is the same file. d8-to-d9.diff is sha 130ac61133f12324, and its body equals `diff -u` of d8 and d9 r2. I diffed d9 r1 (7dfc52fd) against r2: the changes are the header, :194-197, :420-421, :459, the d9 changes lines (:476-477), the A6 move (:588 in, old :605 out) and J-b (:613). Line numbers are r2's.
|
||||
|
||||
Verdict: APPROVE. No B items.
|
||||
|
||||
- B1 met. R3.1, R3.2 and R3.5's sentences each occur once in d9 r2, word for word with d8-fixes.md:15-19, normalized for whitespace. R3.3: the A6 line now sits as a sub-bullet under A6 in "What d5 adds" (:588), and "Not taken" keeps only the item 4 line. The d9 changes line names R3.4 as Gyro's design edit, with its anchors.
|
||||
- N19 met. J-b's question is in the past tense ("As d8 was written, it did not.").
|
||||
- N20 met. J-b cites the import not waiting for the view as Mos's R3 default, for him to confirm.
|
||||
- N21 goes to Mos with the sha, which I accept.
|
||||
- No contradiction found. REQ-LIB-8's four opening conditions don't name the view, which fits the R3 default. REQ-LIB-5's Markdown sentence (Obsidian graph) and the view's "in both formats" don't conflict.
|
||||
- Widths. No PRD-text line is over 82 columns. :137 and :192 are the same instruction lines as before.
|
||||
- The Replace quotes didn't change between r1 and r2, so round 6's result holds: each quote occurs once in base a6db9b4a, apart from the scoped REQ-LIB-2 one.
|
||||
|
||||
## Round 8, d9 r3 (4607e15b8, DRAFT-d9.md sha 02f28decad7f946f)
|
||||
|
||||
Scope under mos-1470: Mos's two edits only. Static read with `git show`.
|
||||
|
||||
- Parent of 4607e15b8 is 8b4201055, so round 7's record keeps its sha.
|
||||
`git diff 8b4201055 4607e15b8` touches DRAFT-d9.md, DRAFT.md and d8-to-d9.diff.
|
||||
DRAFT.md has the same content as DRAFT-d9.md (02f28dec). DRAFT-d8.md doesn't change.
|
||||
- Edit 1 (N20): the end of J-b reads "Q239 doesn't address it, and Mos confirmed it
|
||||
at his J-b check (mos-1470)." That is the only change to J-b.
|
||||
- Edit 2 (N21): the REQ-LIB-5 version tag at :199-201 adds "and Q239 for the
|
||||
Connections view". Line widths are 79, 78 and 23, all under 82. The old quote is
|
||||
unchanged from round 7.
|
||||
- d9 changes: the J-b yes line now ends "Beyond these and N21, nothing else in the
|
||||
PRD text changes.", the N19/N20 line records the confirmation, and a new N21 line
|
||||
follows. These are not PRD text.
|
||||
- d8-to-d9.diff (ca6c40f9eb077ce6): with the two header lines skipped, its body
|
||||
equals `diff -u DRAFT-d8.md DRAFT-d9.md` at 4607e15b8.
|
||||
- Mos's text comes from Sage's relay in sage-3505. I haven't read mos-1470 itself.
|
||||
|
||||
Verdict: APPROVE. No new N items.
|
||||
@@ -0,0 +1,143 @@
|
||||
# PRD 1.15, REQ-LIB-6 rewrite (594add438), Webby
|
||||
|
||||
Request gosalyn-0168 (17:23Z 10-22), for mos-1320 item 8. Commit 594add438 on gosalyn/prd-req-lib-6,
|
||||
parent aca16404c, local, no co-author line. Three files: docs/prd/mosaic-stack.md (+105/-35 with the
|
||||
others), packages/tasks/requirements.json and the version pin in requirements.test.mjs. Mos accepted the
|
||||
REQ-LIB-1 and REQ-LIB-2 widening in mos-1322 and named this review the gate. Read with git and against
|
||||
Mos's STATE repo (read only). I ran nothing.
|
||||
|
||||
## Verdict: CHANGES (B1)
|
||||
|
||||
One sentence in REQ-LIB-6 says more than the design does. Everything else is right.
|
||||
|
||||
## What I checked
|
||||
|
||||
- The quotes. Q223's two passages are verbatim from DECISIONS.md:2212. "Q30: yes." and the F43 line
|
||||
"citations should be requireable. Tunable setting." are Jason's words in the Q226 record (:2253,
|
||||
:2261). Mos's reading of F43 is verbatim from the bullet at :2270. TARGET.md:101 is "Agents NEVER write
|
||||
prose for the user".
|
||||
- REQ-LIB-6 against design b68d79663 and Q223/Q226: agents never write story, they write only `.md`
|
||||
files under a supporting prefix a human marks (design :215-218, :797-802), every other path refuses
|
||||
before git, each write is one commit by the agent's own Gitea account with a trailer, she sees a
|
||||
notice and can revert, chat is free text, every present citation is checked, `requireCitation` is
|
||||
human-only and off by default.
|
||||
- REQ-LIB-2. The scope order matches the design's table (:823): supporting first, so a folder in both
|
||||
lists is supporting, then playground, then canon. Lookups default to canon. The canon-cite rule is the
|
||||
only claim left once Q223 removed the item kinds, which is Mos's reading too.
|
||||
- The residual replacement matches the design's: a supporting doc can hold any text, the path check in
|
||||
the bus is the only path limit, and the agent's token can write any path.
|
||||
- The Version 1.15 note keeps the old REQ-LIB-6 and residual texts verbatim and lists REQ-LIB-3, 4, 5
|
||||
and 7 as unchanged. LIB-4's reading (writes in C2-02 and C2-03b, the fetch read-only through the
|
||||
chief's grant) is right.
|
||||
- requirements.json: version 1.15, three titles changed, all within 120 characters (117, 112, 119). The
|
||||
count stays 85. The test's pin at :24 moves to 1.15, and the status regex at :23 matches the new
|
||||
status line.
|
||||
|
||||
## Required change
|
||||
|
||||
B1. REQ-LIB-6 says `requireCitation` "makes every reply carry a citation". The design keeps the
|
||||
runner's fixed texts passing when it's true (design :245 and :1286-1287, and webby-0151 R1 added the
|
||||
too-long text): no answer, the turn failed, and the answer was too long to send. A test written from
|
||||
this sentence would refuse those notices. Say "makes every reply carry a citation, except the runner's
|
||||
fixed notices", or name them. The 1.14 text had the same exception, so the 1.15 text shouldn't lose it.
|
||||
|
||||
## Notes (non-blocking)
|
||||
|
||||
N1. REQ-LIB-1's rewrap leaves "organization file" alone on a line (prd.md, the second line of the
|
||||
item). Refill the paragraph.
|
||||
|
||||
N2. REQ-LIB-2's "follow the same placement rules" is fine at PRD level. The design also refuses a
|
||||
supporting prefix that holds another supporting prefix, and names `library-supporting-other-universe`.
|
||||
The design is where those live, and the parenthetical already points to it.
|
||||
|
||||
## Round 2: 6742cf5ad (gosalyn-0173, 17:33Z, parent 594add438)
|
||||
|
||||
Only docs/prd/mosaic-stack.md, +6/-4, local, no co-author line. Read with git, nothing run.
|
||||
|
||||
Verdict: APPROVE.
|
||||
|
||||
- B1 met. `requireCitation` now excepts the runner's fixed notices and names all four: no answer, a
|
||||
failed turn, no answer passing the citation check, and the too-long text. Those are the four bodies the
|
||||
design's guard accepts after webby-0151 R1.
|
||||
- N1 met. "organization file" rejoins its sentence. The refill leaves "is refused. So" short on its line,
|
||||
which is cosmetic.
|
||||
|
||||
The gate is `packages/tasks/tests/requirements.test.mjs` and the `scripts/test-*.sh` that read the PRD or
|
||||
requirements.json, on Sage's run.
|
||||
|
||||
## Round 3: row 282 candidate 6b5b06819 on 94c33a43c, and Scrooge's row 285 blocker (18:40Z)
|
||||
|
||||
The rebase is clean: range-diff shows both commits as `=` (1841f7d4e, 6b5b06819), and none of the three
|
||||
paths moved between aca16404c and 94c33a43c. But Scrooge's row 285 report (scrooge-285-routing-q1-webby)
|
||||
shows a gap I missed in both earlier rounds, so the approval does not carry. Read with git, nothing run.
|
||||
|
||||
Verdict on 6b5b06819: CHANGES (B2).
|
||||
|
||||
B2. 1.15 gives creative agents a write (REQ-LIB-6, prd.md:1253-1260), and design f6e3e7f20:259 and the
|
||||
C2-08b brief (883f78f14 rows :1821-1822) put the new action `library.write` in each creative role's
|
||||
`withinRole`. PRD 1.15 never says that action is routed:
|
||||
- REQ-WAIT-3 (prd.md:698-707): a new action is a hard wait "until a PRD version says it isn't", and the
|
||||
paragraph after it lists each routed addition by version. 1.15 adds no line there.
|
||||
- REQ-WAIT-2 (:692-697): no role file may grant a hard-wait action. The list changes only "under a new
|
||||
PRD version that Jason approves".
|
||||
- Code agrees. vocabulary.mjs:62-66 and broker.mjs:51-52 add an action to ROUTED only by the reviewed
|
||||
commit that names it routed. Scrooge cites role.mjs:43 as the refusal.
|
||||
So C2-08b cannot be built to its brief under 1.15 as written. The fix belongs in this PRD row, the
|
||||
version that creates the write. Add a REQ-WAIT-3 line in the existing pattern, for example:
|
||||
"`library.write` was added as a hard wait and routes under REQ-LIB-6 since version 1.15." Also name the
|
||||
bounds that make it routable: creative sessions only, supporting `.md` paths only, a live chief grant,
|
||||
one commit per write with the notice and revert. Then record it in the 1.15 version note.
|
||||
REQ-WAIT-2 makes this Jason's approval, not mine, and Mos's mos-1322 acceptance covered the LIB-1/LIB-2
|
||||
widening, not a hard-waits change. Mos or Jason has to say yes to the routing sentence before it lands.
|
||||
|
||||
My miss: in rounds 1 and 2 I checked REQ-LIB-6 against Q223/Q226 and the design, but never against the
|
||||
REQ-WAIT rules that govern any new action. The design and brief rounds (webby-0151, -0155) missed it too.
|
||||
|
||||
## Round 4: Mos ruling mos-1328 (18:42Z), and the routing text in 759a18d85 (gosalyn-0181)
|
||||
|
||||
Mos ruled that routing `library.write` needs Jason's explicit yes. Q223 and Q226 say what agents may do,
|
||||
but they don't approve a version text. So 1.15 lands as I approved it, and a separate 1.16 carries only
|
||||
the routing. Row 285 goes `--after` the 1.16 row.
|
||||
|
||||
- Row 282 on 6b5b06819: APPROVE again. webby-0160 carries, and B2 moves to 1.16 by Mos's ruling.
|
||||
The rebase checks in round 3 still hold.
|
||||
- 759a18d85 (parent 6b5b06819, PRD only, +22/-3, no co-author line, every new line within 75
|
||||
characters, `git diff --check` clean): it does not go into 282. The text is right, and I'd approve it
|
||||
as the 1.16 content. The REQ-WAIT-3 paragraph (prd.md:711) matches design f6e3e7f20:259-260: creative
|
||||
definitions holding the action within-role, supporting `.md` paths only, a live Gitea grant on the
|
||||
library project, one commit per write with the trailer, the notice and revert. The REQ-LIB-6 sentence
|
||||
points to those bounds without restating them.
|
||||
- For the 1.16 row: say "since version 1.16" in both places, add a 1.16 header and note in place of the
|
||||
1.15 edits, bump requirements.json to 1.16, and move the test pin at requirements.test.mjs:24 to 1.16.
|
||||
I review that text, then Mos puts it to Jason.
|
||||
|
||||
## PRD 1.16 (row #1796), 482e865ac on 6b5b06819 (gosalyn-0184): APPROVE
|
||||
|
||||
I reviewed the diff 6b5b06819..482e865ac only. It touches three files (+35/-6): the PRD (blob a6db9b4a1),
|
||||
requirements.json (version only) and requirements.test.mjs:24 (pin only). No co-author line,
|
||||
`git diff --check` is clean, and the only added line over 75 characters is the test pin. Read with git,
|
||||
nothing run.
|
||||
|
||||
- The REQ-WAIT-3 paragraph (prd.md:719-725) follows the 1.7 and 1.8 routing paragraphs and gives the
|
||||
bounds of design f6e3e7f20:259-260. "Six creative roles" matches `CREATIVE_DEFINITIONS`
|
||||
(packages/business/src/vocabulary.mjs:207 at 94c33a43c).
|
||||
- Whose grant gates a write: Gosalyn's text says the writing agent's own grant. That matches design
|
||||
:259-260 (`library-write-no-grant`), :1396 and :1702, and REQ-LIB-6's "by that agent's own Gitea
|
||||
account". The chief's grant covers only the fetch. If Mos meant the chief's grant, that changes the
|
||||
approved design, so it's Mos's call, not a PRD edit. I agree with the text as written.
|
||||
- The REQ-LIB-6 sentence (:1280-1282) names the action and points back to REQ-WAIT-3. It doesn't restate
|
||||
the bounds.
|
||||
- The header, the 1.16 source line and the 1.16 note are accurate. The note says ROUTED is added only by
|
||||
C2-08b (row 285), after this version, which matches mos-1328.
|
||||
- The gate is the same three files as 282, because the version pin moved.
|
||||
|
||||
### Approved blob check (mos-1354, 22:25Z 10-22)
|
||||
|
||||
Jason approved PRD 1.16 ("Q32: yes", DECISIONS Q229) as blob a6db9b4a13d42c816371afa60c90efae74bbc0c1. `git rev-parse 482e865ac:docs/prd/mosaic-stack.md` returns exactly that blob, so the one I approved is the one Jason approved. mos-1354 calls the file docs/prd/prd.md, but at 482e865ac the PRD is docs/prd/mosaic-stack.md. The blob id is what binds. 482e865ac isn't on origin/refactor yet, which still has blob 717c66fc. At landing, check that the landed docs/prd/mosaic-stack.md blob is a6db9b4a. A mismatch goes to Sage and Mos.
|
||||
|
||||
### Round 1 verdict on 482e865acb7a8d881e1e2cc0a80141fb872cd688 (sage-3049, request c30846, rev 3354): APPROVE
|
||||
|
||||
- The candidate is 482e865ac, the commit I approved earlier in this file. Its parent is 6b5b06819 and its tree is unchanged.
|
||||
- `482e865ac:docs/prd/mosaic-stack.md` is blob a6db9b4a13d42c816371afa60c90efae74bbc0c1, which is the text Jason approved (Q229).
|
||||
- No base drift. The three paths have the same blobs at 6b5b06819 and on origin/refactor f0e3dcdf4: PRD 717c66fc, requirements.json a079888e, requirements.test.mjs 759b011f. A landing on refactor therefore produces blob a6db9b4a. Row 282 landed as 647424442, not 6b5b06819, but it carries the same three blobs.
|
||||
- No co-author line, and `git diff --check` is clean. Read with git; nothing run. The gate (requirements.test and the tasks suite) runs on Sage's CLEAR.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Case 2 Q226 round 2 (design b68d79663, briefs d4d503db9), Webby
|
||||
|
||||
Request gosalyn-0165 (17:19Z 10-22), for webby-0151. Design b68d79663 on gosalyn/case2-design (parent
|
||||
f164dcef8, DESIGN.md blob 61394054e) and briefs d4d503db9 on gosalyn/case2-06-idea-caps (parent
|
||||
51cc0445c, rows blob 5254e360c). Both are amended and local. I read the deltas from af3349f00 and
|
||||
965dec0ee with git, against my last-fetched origin/refactor aca16404c. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES (R4)
|
||||
|
||||
R1, R2, N1-N5 and the mos-1320 items are met. R3 is met in its wording, but the "ever existed" check
|
||||
has no named source, and the obvious one loses the record on revoke. That's R4, one paragraph and one
|
||||
test line in C2-03b and design 7.2. The PRD REQ-LIB-6 row brief is approved as written.
|
||||
|
||||
## Round 1 items
|
||||
|
||||
- R1 met. A `request-too-large` on a JSON body gets one retry turn naming the limit, then the fixed text
|
||||
`(the answer was too long to send)`. The guard accepts four bodies when `requireCitation` is true.
|
||||
Design 5.2 and the C2-06 brief agree, and the runner test stores the fixed text for a cited reply over
|
||||
the limit.
|
||||
- R2 met. An unreadable business file resolves `requireCitation` to true, `library.status` shows
|
||||
`reply-limits-unreadable`, and the fixed texts still pass. It's in the Q222 paragraph, the ruling
|
||||
fallback, M5 and C2-06's settings and tests. A bad hand-edited value still reads as off, which is right.
|
||||
- R3 wording met. The fallback applies only while no chief grant record has ever existed, in any state.
|
||||
After that a revoked or `held` grant, or a fired chief, refuses `library-fetch-no-grant`. A write, a
|
||||
revert and a save never fall back. The tests cover a valid slot and a mode-0000 slot. Step 4b deletes
|
||||
the slot and revokes its token, and design 7.2, 10 and the brief agree. See R4 for the source.
|
||||
- N1 met. A revert and her save are authored by `editor` and push with `library-human`.
|
||||
`library-mapping-invalid` and `library-human-missing` refuse before any git call.
|
||||
- N2 met. Design 5.2 cites runner.mjs:571-574, :572 and :577-583.
|
||||
- N3 met. M3 names packages/harness/src/tools.mjs and the `raise_decision` skip at :241.
|
||||
- N4 and mos-1320 item 5. C2-21 has both record-only Gitea checks.
|
||||
- N5 met. One line in C2-08b.
|
||||
|
||||
## mos-1320 and sage-2833
|
||||
|
||||
- Item 2. The C2-21 brief and the design keep the `#giteaRole` comment citing mos-1320
|
||||
(broker.mjs:2118-2121 at aca16404c). The `host-creative-role` refusals stay: the grant writer at
|
||||
broker.mjs:1096, `host.run` at :1244, and the step 1 recheck. Each has a named existing test:
|
||||
grant-host.test.mjs:125 and grant-executor.test.mjs:1035 and :1046. I checked all five lines.
|
||||
- sage-2833. C2-05b is back as row 224, built at 23816cf28. The new C2-05c (reviewer Dewey) owns the
|
||||
supporting scope and blob, and C2-02 now runs after C2-05c. The scope enum pin at
|
||||
library-tools.test.mjs:49 in 23816cf28 is `['canon', 'playground']`, so C2-05c changing it is right.
|
||||
- Item 8. The PRD row brief is below.
|
||||
|
||||
## Required change
|
||||
|
||||
R4. Name what the "ever existed" check reads, and make it survive a revoke. At aca16404c a revoke
|
||||
deletes the `gitea_grants` row (broker.mjs:2785), and so does `grant-lost` (:2609). The table has no
|
||||
state column (schema.sql:469-480). A check written as "is there a `gitea_grants` row for the chief"
|
||||
finds nothing after a revoke or a fire, and the fetch falls back to slot `library-gitea`. That's the hole
|
||||
R3 closed. The `events` table is append-only (schema.sql:131-132), and grant events carry `method:
|
||||
'gitea'` and `agent` under `GRANT_EVIDENCE` (broker.mjs:246, :2366, :2469). Say:
|
||||
|
||||
- The check reads grant evidence events, not `gitea_grants`. Any `GRANT_EVIDENCE` event with `method`
|
||||
`gitea` and `agent` = the chief ends the fallback for good.
|
||||
- Drop "on the library project", or name where the project is read after a revoke. `grant.applied` and
|
||||
`grant.revoked` carry no `projects` (broker.mjs:2469, :2786-2789), and the row that has them is gone.
|
||||
Dropping it fails safe: a chief granted on any project loses the slot.
|
||||
- The revoked and fired test cases go through the broker's real revoke and fire paths, so the row is
|
||||
actually deleted when the fetch runs. A fixture that leaves the row in place passes either way.
|
||||
|
||||
## PRD REQ-LIB-6 row brief: approved
|
||||
|
||||
The cites hold at aca16404c: REQ-LIB-6 at docs/prd/mosaic-stack.md:1230, the title at
|
||||
packages/tasks/requirements.json:85, and the `['docs/prd/mosaic-stack.md', '1.14']` pin at
|
||||
requirements.test.mjs:24. The count pin at :21 stays 85 because the id stays. The owned files cover the
|
||||
PRD's `Status: approved, version` line, which :23 matches. Landing before C2-01d and C2-02 is right,
|
||||
since both cite the new text. When the PRD commit comes, I check the new REQ-LIB-6 against Q223 and
|
||||
Q226, the `:1594` residual, the list of unchanged REQ-LIB ids, and the version against whatever landed
|
||||
last.
|
||||
|
||||
## Notes (non-blocking)
|
||||
|
||||
N6. The briefs now order a docs row first (item 0) and C2-05c before C2-02. When Sage adds the queue
|
||||
rows, the `After` fields should match that order, so `queue next` doesn't offer C2-02 early.
|
||||
|
||||
## Round 3: design f6e3e7f20, briefs 883f78f14 (gosalyn-0171, 17:30Z)
|
||||
|
||||
f6e3e7f20 has parent b68d79663 and touches only DESIGN.md (+13/-8). 883f78f14 has parent d4d503db9 and
|
||||
touches only the rows file (+21/-12). Neither has a co-author line. Both are local. Read with git, nothing
|
||||
run.
|
||||
|
||||
Verdict: APPROVE. R4 is met.
|
||||
|
||||
- The check reads the append-only events table for any `GRANT_EVIDENCE` event with `method` `gitea` and
|
||||
`agent` the chief, of any action, and never `gitea_grants`. The design's ruling paragraph, 7.2 and
|
||||
steps 4 and 4b say the same thing as the C2-03b bullet. The cites hold at aca16404c: broker.mjs:246,
|
||||
:2469, :2609, :2785 and :2787, and schema.sql:131-132 and :469-480.
|
||||
- "On the library project" is gone, and the brief tests a chief grant on another project ending the
|
||||
fallback.
|
||||
- The revoked and fired cases go through `grants.revoke` and `instance.fire`, and each asserts the row is
|
||||
gone before the fetch.
|
||||
- The `held` case has an event to find. `held` is a credential-file state (gitea-grant.mjs:400, :414-425)
|
||||
that follows a commit `grant.step` or an unknown token. Both come after gitea grant events (broker.mjs
|
||||
:2366, :2236), so a held chief never falls back to the slot.
|
||||
|
||||
No new notes. N6 (the After order for Sage) still stands.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Case 2 Q226 rework (design af3349f00, briefs 965dec0ee), Webby
|
||||
|
||||
Request gosalyn-0161 (17:07Z 10-22), for mos-1309. Design af3349f00 on gosalyn/case2-design and briefs
|
||||
965dec0ee on gosalyn/case2-06-idea-caps, both local. I read with git against my last-fetched
|
||||
origin/refactor aca16404c. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES (R1-R3)
|
||||
|
||||
The rework follows mos-1309. The shared bot is gone from the write path, the Q222 fence carries over to
|
||||
the two new keys, every write and save keeps the stale-base refusal, and M12 runs before any git call.
|
||||
The three gaps are a silent drop in the runner plan, one setting that fails open, and the fetch fallback
|
||||
Gosalyn asked me to question.
|
||||
|
||||
## What I checked
|
||||
|
||||
- af3349f00 has parent f164dcef8 and touches only DESIGN.md (blob 83bef1bd1, +387/-150). 965dec0ee has
|
||||
parent 51cc0445c and touches only the rows file (blob f3ee8ce29, +677/-126). Neither has a co-author
|
||||
line. Both are local, so this review binds to these shas, and a rebase needs a compare.
|
||||
- Gosalyn's three code findings hold at aca16404c:
|
||||
1. `#giteaRole` is broker.mjs:2122-2135. :2126 refuses every `CREATIVE_DEFINITIONS` role with
|
||||
`gitea-not-declared`, and the comment at :2121 cites mos-1163.
|
||||
2. `clip` is runner.mjs:130-131 (`BODY_MAX` 32000 at :56). :572 holds both fixed texts and :573 sends
|
||||
`clip(body) + unslop.note()`. A JSON body would lose its shape either way, so the library-business
|
||||
rule is needed.
|
||||
3. The runner's tool socket refuses past 65536 at :294. The bus server's `LIMIT` is 65536 (:6), with
|
||||
the save-verb exception at :9, :47-48 and :62. The client checks at client.mjs:19.
|
||||
- The Q222 fence carries. M11 is unchanged: the on-disk `old.human`, the actor-shape refusal, both
|
||||
`document` and `written`, and the `document.human` mutant. Keys are read by `Object.hasOwn`,
|
||||
`requireCitation` only as a `boolean`, and `citationsPerReply` only as an integer from 1 to 1,000. The
|
||||
invalid-value list names `"true"`, `1`, `null`, `[true]` and `{}` for the boolean.
|
||||
- My Q222 notes: N7 is taken (the writer's code is named, no validator change, said why). N8 is taken (a
|
||||
direct `write` with the role as actor). N9 is taken (C2-06's section is within 110 characters).
|
||||
- Writes have no shared-credential path. A write needs a live, not `held`, seat grant
|
||||
(`library-write-no-grant`), the helper answers one named seat's user and file, and it refuses another
|
||||
seat's user. `identity` and `publish` aren't in `ORGANIZATION_KEYS` (broker.mjs:219) or `INSTANCE_KEYS`
|
||||
(:222), so no agent can give itself a grant target.
|
||||
- Stale-base holds on all three commit paths: the agent write, the revert (base is the commit's own
|
||||
blob) and her save. The race retry re-applies only when the blob at the new tip still equals `base`.
|
||||
- The commit message carries only the path and the trailer. M12 refuses control characters, so a path
|
||||
can't inject a second trailer line.
|
||||
- The revert choice is right. A revert is her act, and authoring it as her `editor` with her own token
|
||||
means it still works after the agent's grant is revoked or the seat is fired.
|
||||
|
||||
## Required changes
|
||||
|
||||
R1. Say what she sees when a cited reply is too long to send. The brief removes the clip for a JSON body
|
||||
and says a body over the 64 KiB request limit "fails the send as any failed send does today". Today that
|
||||
is the catch at runner.mjs:577-583: it logs and skips `message.read`. She gets no answer and no notice.
|
||||
The retry doesn't help, since it runs only on a code from the 5.2 table, and `request-too-large` comes
|
||||
from the client (client.mjs:19-20) before the broker. Removing the clip makes this more likely: 32,000
|
||||
characters of non-ASCII text with JSON escaping can already pass 64 KiB. Pick one outcome and test it.
|
||||
The simplest is to treat `request-too-large` on a JSON body like a table refusal: one retry turn naming
|
||||
the limit, then a fixed text such as `(the answer was too long to send)`. That text then joins the bodies
|
||||
the guard accepts when `requireCitation` is true. Put it in 5.2 and the C2-06 brief, with a runner test
|
||||
that a cited reply over the limit stores the fixed text.
|
||||
|
||||
R2. An unreadable business file turns `requireCitation` off. Under Q222 the defaults were the strict
|
||||
values, so "check at the defaults" failed safe. `requireCitation` defaults to `false`, the loose value. If
|
||||
she sets it to `true` and the file can't be read at check time, an uncited reply passes. Mos's check for
|
||||
this round was that an unreadable file fails safe. For the unreadable case only, resolve
|
||||
`requireCitation` to `true` (or refuse the reply) and keep `library.status` showing
|
||||
`reply-limits-unreadable`. The fixed texts still pass, so she still hears that something went wrong. Add
|
||||
the test to C2-06: with the file unreadable, an uncited reply refuses `citation-missing` and `messages` is
|
||||
unchanged. A bad hand-edited value reading as off is fine. That's a human edit, Jason's default, and
|
||||
`library.status` names the key.
|
||||
|
||||
R3. Narrow the C2-03b fetch fallback to slot `library-gitea`. The brief falls back "only while the chief
|
||||
has no live read grant". That also covers a chief whose grant was revoked or `held`, or a fired chief. A
|
||||
revoke would then quietly move the fetch onto the old shared token instead of stopping it, and the
|
||||
shared credential the ruling removes stays live with no end date. The design (10 step 4) says C2-03b
|
||||
retires the slot, and the brief keeps it, so the two disagree. Say:
|
||||
- The fallback applies only when no chief grant record exists at all. Once one has existed, a revoked or
|
||||
`held` grant refuses the fetch with a named code and `library.status` shows it.
|
||||
- The fallback never applies to a push. Add a test: a write with no live seat grant refuses
|
||||
`library-write-no-grant`, and the slot file is never opened (a read trace, or the slot at mode 0000).
|
||||
- Name the step that removes the slot after C2-21 lands, and make the two documents agree.
|
||||
|
||||
## Notes (non-blocking)
|
||||
|
||||
N1. The design's write steps say "The author and committer are the seat's `identity`", and the revert
|
||||
and her save follow "the same steps". Only the C2-02 brief says a revert is authored by her `editor` and
|
||||
pushes with `library-human`. Say it in the design too. Also name the code for a revert or save when
|
||||
`editor` or slot `library-human` is missing. The brief names `library-mapping-invalid` for a save
|
||||
without `editor`, and nothing for a missing slot.
|
||||
|
||||
N2. Design 5.2 still cites `runner.mjs:449` for the fixed texts and `:451-456` for the dropped answer. At
|
||||
aca16404c those lines are `founderCheck` and `role.claim`. The texts are at :572 and the catch is at
|
||||
:577-583. The C2-06 brief has the right lines. Fix the design.
|
||||
|
||||
N3. M3 says `tools.mjs:225-280` "adds those to every session". At aca16404c `raise_decision` is already
|
||||
skipped for creative definitions (packages/harness/src/tools.mjs:241). The rest of the list is right.
|
||||
Also name the path, `packages/harness/src/tools.mjs`.
|
||||
|
||||
N4. Gitea branch protection can block force-push for collaborators. That would back D6 at the remote
|
||||
for the seat tokens, which "can write every path". It doesn't limit paths, so the residual stands. Worth
|
||||
one line next to the protected-file-patterns sentence for C2-21's live check.
|
||||
|
||||
N5. Adding the gitea credential to `roles/chief-of-staff.json` reaches every business that uses the
|
||||
role, Case 1 included. A grant still needs `identity` and `publish` in the business file, which only the
|
||||
human sets, and a human-approved `credential.mint`. So nothing widens without her or Jason. C2-08b's
|
||||
brief could say that in one line, so the reviewer of that row doesn't have to rebuild the argument.
|
||||
|
||||
## After mos-1320 (cc, 17:09Z)
|
||||
|
||||
Mos ruled on Gosalyn's points in gosalyn-0162. None of R1-R3 conflicts with those rulings. R1 and R2
|
||||
are about the runner and the guard. R3 is about the fetch's slot `library-gitea`, which mos-1320 doesn't
|
||||
address. Item 3 covers slot `library-human` for her saves, and I agree with it.
|
||||
|
||||
- Item 5 asks for a record-only check of protected file patterns on the scratch Gitea. N4 fits the same
|
||||
run: whether branch protection refuses a seat user's force-push while her normal push passes.
|
||||
Record-only, like item 5.
|
||||
- Next round I check item 2 in the C2-21 brief: the comment at broker.mjs:2120 cites mos-1320, and the
|
||||
host-grant refusals (`host-creative-role` in the grant writer, the step 1 recheck and `host.run`) stay
|
||||
as they are, each with a test or a named existing one.
|
||||
- Item 8, the REQ-LIB-6 docs row, I review when Gosalyn sends it.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Webby review: Case 2 design r1 (#416)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Independent reviewer, replacing Dewey (Mos, mos-0980).
|
||||
Subject: `agents/gosalyn/work/case2/DESIGN.md`, blob fba567f71dc1, at e997f3b76 and on
|
||||
origin/gosalyn/case2-design 88222f3f3 (same blob). Author: Gosalyn. Static review only. I ran no
|
||||
tests and changed no product code.
|
||||
|
||||
Code base: `git archive 5d019d876`. `git diff --stat 5d019d876 9ae8c73ac -- packages adapters roles scripts`
|
||||
is empty, so every line below also holds at origin/refactor 9ae8c73ac. Paths are relative to `packages/`
|
||||
unless they start with `adapters/` or `roles/`.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Twelve blockers. Section 6's no-schema claim holds. Section 5's write-side guardrails hold. The
|
||||
message-side guardrails have two channels the design misses (B3, B4), and the smallest slice can't
|
||||
run as written (B1, B2).
|
||||
|
||||
## Citations checked
|
||||
|
||||
These hold as cited: git.mjs:8 (`protocol.allow=never` in SAFE), git.mjs:44 (any `remote.*` key
|
||||
refuses), git.mjs:55-56; project index.mjs:12, :31-84, :97, :195-234, :209, :248-261, :256-258;
|
||||
paths.mjs:7, :14-15, :34-48 (`absolute(project.root)` at :41 throws on a missing root);
|
||||
business.mjs:186-187; gate.mjs:130-167 (typed by name at :134, built-ins at :137-138);
|
||||
claude-gate.mjs:13; tools.mjs:6-7, :95-102, :186, :211-220, :271-280; vocabulary.mjs:92-97, :131;
|
||||
role.mjs:19-28, :55-66; broker.mjs:290-291, :642-650, :1378-1430, :1434-1435, :1867, :2331-2347,
|
||||
:2374-2399; schema.sql:240-250, :296-306, :310; runner.mjs:446-456; seat session.mjs:59-64;
|
||||
adapters/claude/adapter.sh:47-53; launcher.mjs:135, :236-238, :353; webui bus.js:885, :919.
|
||||
|
||||
One miss: the `ls-tree` and `cat-file` reads that 1.3 cites at read.mjs:67-86 are in `blob()` at
|
||||
read.mjs:19-26 (N1).
|
||||
|
||||
## Section 6: "no schema or trigger change"
|
||||
|
||||
It holds. `threads` has no definition column or check (schema.sql:240-250).
|
||||
`message_threads_match` (:296-299) and `delivery_same_thread` (:300-306) check one thread's
|
||||
instance, and a room is one thread per instance, so each insert in `room.send` passes as written.
|
||||
`thread_one_per_run` allows one held thread per run, which matches one session per agent. The only
|
||||
constraint the design must name is `UNIQUE (project, name)` (:249). `room.open` names each thread
|
||||
after its instance, so an existing thread with that name refuses (N9).
|
||||
|
||||
The single-instance rule is code, not schema: broker.mjs:291, :2332-2334, launcher.mjs:135 and
|
||||
:236-237. Changing it is C2-09's job, which is why B1 matters.
|
||||
|
||||
## Jason's three guardrails
|
||||
|
||||
| Guardrail | Enforced in code? |
|
||||
|---|---|
|
||||
| Agents never write prose for the user | Into the library: yes. Gate (gate.mjs:137-138), typed tools (no `write_record` without `record.write`, tools.mjs:211) and human-only verbs make an agent write impossible. In messages: only the form is bounded (M5), and two channels skip M5 (B3, B4). The 400-character limit is stated in 5.1, but the stated residual is too small (B5) |
|
||||
| Agents always refer to the source material | Structural once B6 and B7 are fixed. The quote-in-lines check at a reachable commit is real verification |
|
||||
| Tool use is guarded | Yes: gate, typed tool list, broker re-check (tools.mjs:6-7) and `#checkMessaging` (broker.mjs:1867). N6 closes a role-file gap |
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. The smallest slice gives Melanie no way to chat with a creative agent. `#threaded` is true only
|
||||
for definition `assistant` (broker.mjs:291). `thread.create` refuses `thread-instance-ambiguous`
|
||||
with more than one (:2334). The launcher refuses `thread-not-allowed` for any other definition
|
||||
(launcher.mjs:135, :237). C2-08 adds the `THREAD_DEFINITIONS` constant, but C2-09 is the row that
|
||||
makes the broker and launcher read it and adds `instance` to `thread.create`, and C2-09 is outside
|
||||
the slice (DESIGN 9). The Console's thread creation must pass `instance` too. Fix: put C2-09's
|
||||
broker, launcher and `thread.create` parts in the slice. The Console chat must pick the instance.
|
||||
`room.*` can stay out.
|
||||
|
||||
B2. In the slice, Melanie reads raw JSON and can't see sync failures. Replies are JSON bodies (5.2),
|
||||
and only C2-10 renders them. 7.2 step 2 says "The Console shows them with file and line", but the
|
||||
library pages are C2-11b, outside the slice. A bad Obsidian push silently stops agents seeing new
|
||||
work. Fix: the slice needs a minimal cited-item renderer in the existing chat view and a sync status
|
||||
line (last valid commit, problems, `unpushed`).
|
||||
|
||||
B3. `raise_decision` is a second prose channel to the human. Every agent gets it with free-text
|
||||
`question` and option `text` (tools.mjs:221, comment at :5-6). An uncited raise goes to the human.
|
||||
M3's "no write tool exists" is true, but M5 covers only `message.send`. Fix: either refuse
|
||||
`decision.raise` from creative definitions in the broker and drop the tool from their typed set, or
|
||||
put the raise text through the same guard. Test both paths.
|
||||
|
||||
B4. The Console conversation view shows the model's raw text, including text M5 refused.
|
||||
webui serve.mjs:559-569 proxies `/api/conversation` to the control board, and the reader emits
|
||||
assistant text blocks (conversation/src/events.mjs:58, :82). A refused prose answer stays in the
|
||||
session file and is readable. Fix: leave creative sessions out of the catalogue on a library
|
||||
instance, or show stored messages only. Test that a creative session isn't listed.
|
||||
|
||||
B5. The stated limit understates what M5 lets through. A reply holds 20 items of 400 characters,
|
||||
which is 8,000 characters. An `idea` item's text is free, and any real quote of any line satisfies
|
||||
the cite. That is a full scene per reply, not "a one-sentence suggestion" (5.1). Jason wrote
|
||||
"NEVER", and Q176 says the system enforces it. Fix: add a total-characters cap per reply and a lower
|
||||
cap for `idea` items (for example 200 characters and at most 5 items). State the residual with
|
||||
those numbers. Mos takes the residual to Jason or Melanie for acceptance, because it is their rule.
|
||||
|
||||
B6. A `correction` isn't bound to the source. M5 limits the edit distance between `from` and `to`,
|
||||
but nothing requires `from` to be text in the cited lines. An agent can invent both. Fix: `from`
|
||||
must equal one cite's `quote`, after the same whitespace normalization, inside that cite's lines.
|
||||
Test that an invented `from` is refused.
|
||||
|
||||
B7. `citation-unfetched` has nothing to check against. The web trail stores the URL
|
||||
(broker.mjs:1521) and a summary of status, bytes and redirects (bus/src/web.mjs:46), not the page
|
||||
text. "The quote occurs in the fetched text" needs that text. Also, Claude's built-in WebSearch and
|
||||
WebFetch are on when `policy.webBuiltin` is set (gate.mjs:28-32), and those fetches never reach the
|
||||
broker. Fix: say where fetched text is kept (bounded, per run, or a new table, and if a table then
|
||||
say so). M1 must refuse the built-in web source for creative definitions, so `research` uses only
|
||||
the broker's tools.
|
||||
|
||||
B8. Remote content is a new untrusted input, and the S-rules don't check tree entries. Today only
|
||||
the bus writes record repos. Under 7.2, Obsidian or anyone with push to the Gitea repo writes too.
|
||||
A fast-forward checks out whatever the commit holds. No rule refuses symlinks (120000), gitlinks
|
||||
(160000), executables or nested `.gitattributes` and `.gitmodules`. Separately, S1 refuses the
|
||||
`.gitignore` that C2-12 commits, and Obsidian puts new notes and `.trash/` at the vault root by
|
||||
default. Fix: add a rule that every entry is a 100644 blob or a tree, and refuse those names at any
|
||||
depth. Allow `.gitignore` at the top. Commit Obsidian's new-file location and trash settings in
|
||||
`app.json`. Test each case.
|
||||
|
||||
B9. The fail-closed state in 7.2 strands Melanie's writes. If remote main is ahead and invalid, the
|
||||
bus stays on the old commit (step 2). The next bus write commits on that base. The push can't
|
||||
fast-forward, so the commit is `unpushed`, then `library-diverged`, with no merge until C2-17.
|
||||
S5 makes this likely: Obsidian users create `[[stub]]` links to notes that don't exist yet, so one
|
||||
dangling link in the playground blocks the whole sync. Fix: refuse library writes while remote main
|
||||
is ahead and invalid (`library-remote-invalid`). Apply S5 to canon only, or allow unresolved links
|
||||
in `playground/`. Test both.
|
||||
|
||||
B10. The 7.2 gate can't catch the obvious mutant. Tests use the file protocol through a test-only
|
||||
argument, so the https path, `GIT_ASKPASS` and the protocol override are never run. A mutant that
|
||||
sets `protocol.allow=always` passes them all. `git()` passes only `gitEnv()` plus the identity
|
||||
argument (git.mjs:10-12, seat checkout.mjs:79-89), so sync needs its own env entry for the helper.
|
||||
`GIT_TERMINAL_PROMPT=0` (checkout.mjs:85) doesn't stop askpass, which is fine. Fix: one function
|
||||
builds the production fetch and push argv and env. A test asserts that the argv holds SAFE, then
|
||||
exactly `-c protocol.https.allow=always`, and no other `protocol.*` key. Another asserts that
|
||||
`GIT_ASKPASS` names a helper outside the repo that reads the slot file. Keep the file-protocol
|
||||
switch out of any path that a business file or a bus verb can reach.
|
||||
|
||||
B11. An optional `root` breaks more than the overlap loop. `ensureCheckouts` clones every entry's
|
||||
root (seat checkout.mjs:310, :319). With no root it calls `clone(project, undefined, ...)`, so "a
|
||||
project with no root gives agents no clone" (2.1) is false today. `projectFilePath(entry.root)`
|
||||
(business/src/project.mjs:9-10) throws on undefined, and it is called at business/src/cli.mjs:72,
|
||||
:78, :118, resolve.mjs:44 and instance-settings.mjs:142. Fix: C2-04 lists these consumers, each
|
||||
skips a library project explicitly, and each has a test.
|
||||
|
||||
B12. Scope and crossovers can also come through front-matter fields, and in the slice a hybrid
|
||||
rests on convention. S2 and S3 speak of links only. `extends:`, `sources`, and `exclude` and `pin`
|
||||
paths are references too, and a canon book entity with `extends: playground/...` would bring
|
||||
playground text into canon lookups labelled canon. `library_manuscript` has no `scope`, so it must
|
||||
label or refuse a playground book. In the slice there is no crossover dialog (C2-11b), so a hybrid
|
||||
is a hand-written `universe.json` pushed from Obsidian, and S6 doesn't require `sanctioned`. Fix:
|
||||
put every path field under S2 and S3. Make S6 require `sanctioned`. State that in the slice a hybrid
|
||||
is Melanie's hand edit, and that only her account and the bus account can write to the Gitea repo.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Cite read.mjs:19-26 for the blob reads, not :67-86.
|
||||
N2. `END_PROTECTED` is `[THREAD_DEFINITION]` (vocabulary.mjs:135). Say whether creative
|
||||
definitions become end-protected when `THREAD_DEFINITIONS` replaces it.
|
||||
N3. The runner sends `(the session gave no answer)` and `The turn failed: ...` (runner.mjs:449).
|
||||
The guard will refuse both, so Melanie never learns a turn failed. Add a fixed turn-failed notice,
|
||||
and test that any notice other than the fixed bodies is refused.
|
||||
N4. Promotion moves several files. `commitFile` commits one path (index.mjs:219). C2-02 needs a
|
||||
multi-path commit.
|
||||
N5. Ids allow capitals (paths.mjs:7). Two universes that differ only by case collide on a
|
||||
case-insensitive file system. Enforce lowercase in `checkLibrary`.
|
||||
N6. A role with no `messaging` key may message anyone (role.mjs:50). M1 must require `messaging`
|
||||
for creative definitions.
|
||||
N7. C2-05b's sync-before-read needs C2-03, which its row doesn't list. C2-03 reads the remote URL
|
||||
that C2-04 adds to the business file, so either pass the URL in or order C2-04 first.
|
||||
N8. C2-06 must run the guard through both the runner's RESULT path and `send_message`, with a
|
||||
non-creative sender in a library business. Otherwise a mutant keyed on `CREATIVE_DEFINITIONS`
|
||||
passes, against 5.2's "role choice doesn't matter".
|
||||
N9. Name the refusal when `room.open` meets an existing thread with an instance's name
|
||||
(schema.sql:249).
|
||||
|
||||
## Rows and the date
|
||||
|
||||
The rows are small and most gates name the right refusal. B6, B10 and N8 are the gates that miss
|
||||
their obvious mutant. The listed slice (C2-01 to C2-08, C2-12, C2-13) does not let Melanie start.
|
||||
She can write in Obsidian, but she can't reach an agent (B1) or read its answers (B2). With
|
||||
C2-09's broker part and a minimal renderer added, it can be done by 10-27 only if T1 row 1 (row
|
||||
190) lands by about 10-24, as section 9 says. Reaching the Console from her machine also needs SSO
|
||||
(Q175).
|
||||
@@ -0,0 +1,74 @@
|
||||
# Webby review: Case 2 design r10 (#416), two system units
|
||||
|
||||
Webby, 2026-10-20T21:01Z (from `date -u`). Author: Gosalyn (gosalyn-0045). Commit 4281b0064 on
|
||||
gosalyn/case2-design, DESIGN blob 4a1860e698e0. Diff `git diff 51006710a 4281b0064`, one commit. I
|
||||
read both unit templates, scripts/webui-service.sh, webui `cli.mjs serve`, scripts/mosaic and decisions 62,
|
||||
94 and 102 on origin/refactor. Static review.
|
||||
|
||||
## Verdict: CHANGES (B1, B2, B3)
|
||||
|
||||
The fix is the right one. Two units from the existing templates match how the stack runs today, and C2-10b
|
||||
needs the Console. One copy for both units keeps the human proof working, since the bus accepts only its
|
||||
own human-cli.mjs path (webui-service.sh:266-273). Making `%h` literal is correct for a system unit. The new
|
||||
refusal cases are good.
|
||||
|
||||
B1. The Console's boot start is Jason's call, not mine or Mos's, and the design should default to the strict
|
||||
reading until he rules. Decision 94 (lead-decisions:2016-2023) says the process that starts the Console
|
||||
holds human authority, and that a start through systemd gives a clean ancestry the proof accepts, "the
|
||||
detachment gap decision 62 names". Decision 102 lets one named seat, Mos, start it. It calls that a
|
||||
tradeoff Jason accepted, because "the human proof now rests on Jason's grant to one named seat" (:2253). A
|
||||
boot-enabled system unit hands that start to PID 1 on every reboot, with no named starter. That extends a
|
||||
human-authority boundary, so under AGENTS.md it needs an owner decision. Decision 94 also accepts the
|
||||
local-forgery limit only "for this PoC on dragon-lin under Jason's user ... this decision doesn't carry the
|
||||
limit there" (:2058-2062). On .54 any process that reaches the Console port can send as Melanie, and
|
||||
`melanie`'s Hermes processes and sudo are on that host until #423 step 5. Nobody has accepted that limit
|
||||
for .54.
|
||||
|
||||
Fix: render the Console unit with no `[Install]` section, so a human or Mos starts it at step 9 and after
|
||||
each boot. That fails closed and is cheap to reverse, because adding `[Install]` later is one re-render.
|
||||
Put two questions to Jason through Mos: may a boot start stand in for the human's start on .54, and does he
|
||||
accept the local-forgery limit there? The bus unit can stay boot-enabled, since it holds no human
|
||||
capability.
|
||||
|
||||
B2. `@PATH@`, `@NODE@` and `@FLAGS@` have no render rule. Both templates set
|
||||
`Environment=PATH=@PATH@` (mosaic-bus.service.in:15, mosaic-webui.service.in:25). The bus `ExecStart` is
|
||||
scripts/mosaic, a bash script that runs `exec node` from PATH (scripts/mosaic:22-40). So the bus's node is
|
||||
whatever PATH the render writes, not `--node`. Row 195 found v22 on .53, and the precheck checks
|
||||
`node --version` on `<user>`'s shell PATH, not the unit's. Fix: say `@PATH@` is `dirname(--node)` followed
|
||||
by `/usr/local/bin:/usr/bin:/bin`, as webui-service.sh:246-248 does, and `@NODE@` is `--node`. Say
|
||||
`@FLAGS@` is the checked serve flags, and refuse any `@[A-Z_]+@` left in either render, not only `%h`. Add
|
||||
a test that the bus unit's PATH starts with the `--node` directory.
|
||||
|
||||
B3. The hardening lines are never listed. In r9 they lived in the new template that C2-13 owned. r10 drops
|
||||
that template, and the render "adds ... the hardening lines", but neither the design nor the brief says
|
||||
which ones. The test "a template with a hardening line removed" has nothing to remove: the templates carry
|
||||
only `NoNewPrivileges=yes`. Some common lines break sessions. `RestrictNamespaces=`, `PrivateUsers=` or a
|
||||
`SystemCallFilter=` without the namespace calls stops bwrap, and then no role starts. The bwrap precheck
|
||||
runs in `<user>`'s shell, outside the unit's sandbox, so it can't catch that. Fix: list the exact lines in
|
||||
the brief (for example `ProtectHome=yes`, `ProtectSystem=strict` with `ReadWritePaths=<home>`,
|
||||
`PrivateTmp=yes`, `NoNewPrivileges=yes`), say none of them restricts user namespaces, and add a step 9
|
||||
check that one session starts under the bus unit.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Both templates already end with `[Install]` `WantedBy=default.target` (bus :41-42, webui :39-40). The
|
||||
render should replace that line with `multi-user.target`, not add a second one. Under B1 it removes the
|
||||
Console's section.
|
||||
|
||||
N2. The render adds no `After=` or `Wants=`, so at boot the two units start in parallel. That is harmless
|
||||
today, because serve calls the bus per request (webui cli.mjs, `busCaller`) and starts without it. But the
|
||||
stated order applies only to a manual step 9. Either add `After=mosaic-bus@<business>.service` to the
|
||||
Console unit, or say that boot order doesn't matter.
|
||||
|
||||
N3. "The serve flags pass scripts/webui-service.sh's own flag checks" can't be done by calling that
|
||||
script. `serve_flags` (:146-207) is internal. Its proof-file check compares the owner to `id -u` (:201),
|
||||
which is root under case2-unit.sh. `render` finds node with `command -v` (:237), not `--node`. And
|
||||
webui-service.sh isn't in the files owned. Say whether case2-unit.sh copies the checks, with tests, or
|
||||
whether the row may move `serve_flags` into a sourced file. Also state which serve flags Case 2 uses.
|
||||
|
||||
N4. With a copy, webui-service.sh passes `--checkout <canonical>` (:251). Her host has no canonical
|
||||
checkout, so the queue read runs from the copy, and the queue store refuses that. Say that the Console unit
|
||||
gets no `--checkout`, and that the queue panel isn't part of Case 2.
|
||||
|
||||
N5. A re-pin must move both units to the same copy, or the human proof fails (webui-service.sh:266-273).
|
||||
Have case2-unit.sh refuse a render whose two units name different copy ids, and test it.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Webby review: Case 2 design r11 (#416), Console without [Install], render rules, 9.3
|
||||
|
||||
Webby, 2026-10-20T21:14Z (from `date -u`). Author: Gosalyn (gosalyn-0048, gosalyn-0051). Two commits on
|
||||
gosalyn/case2-design, reviewed together as r11:
|
||||
- fb20a61b4: DESIGN blob 49eafb5a5666, rows blob edbc493fb092.
|
||||
- eac92d956: rows only, rows blob 3a86d5f048dc. This is the one to re-pin.
|
||||
|
||||
Diffs: `git diff 4281b0064 fb20a61b4` and `git diff fb20a61b4 eac92d956`. I read B1 against mos-1097 and
|
||||
the port squat against mos-1100. I read the cited code at fb20a61b4. Its packages and scripts differ
|
||||
from origin/refactor only in scripts/stack-release.sh, which none of these citations use. This is a
|
||||
static review. I ran nothing on any host.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed, per mos-1097. The Console unit renders with no `[Install]`, the script enables only the bus
|
||||
unit, and step 9 makes Jason the one who starts it, after each boot too. The tradeoff after an unplanned
|
||||
reboot is written down. Q185 (jason-master-restart DECISIONS.md:1573) does put him in the homelab for
|
||||
1012 reboots. Mos asked for two facts, and 9.3 gives both with citations:
|
||||
- Who shares the Console's uid: the bus host and its children, the Console and its `human-cli.mjs`
|
||||
children (webui bus.mjs:1-3), every session inside bwrap, and the admin steps.
|
||||
- What a bwrap session can reach. The port, yes. sandbox.mjs:67 has no `--unshare-net`, and :29-31 says
|
||||
the network isn't confined. The proof file, no. The binds at :101-116 are all under the run directory,
|
||||
the workspace, the system paths or the sockets, and :21-22 says there's no `~/.config`. The one
|
||||
harness mount, claude-code's `mounts()` (plugins/harness/claude-code/index.mjs:73-77), is only the
|
||||
pinned binary.
|
||||
|
||||
The 401/403 reading of serve.mjs holds. The proof check at :495-503 comes before any auth-header read, a
|
||||
proof header with a loopback Host gets 403 at :497, and every write route needs `named` (:526, :532,
|
||||
:537, :542, :546).
|
||||
|
||||
B2 is fixed. I listed every token in both templates. The bus has `@REPO@`, `@PATH@`, `@CLAUDE_ENV@`,
|
||||
`@PM_FLAGS@`, `%h` and `%i`. The Console has `@REPO@`, `@PATH@`, `@NODE@` and `@FLAGS@`. Every `@` token
|
||||
and `%h` has a rule, and `%i` is left alone, which is right for a system unit. The leftover refusal is
|
||||
`@[A-Z_]+@` or `%h`, so it doesn't catch `%i`. The `@PATH@` rule fixes the case I missed in r10. With
|
||||
`--node /usr/bin/node`, webui-service.sh:246-248 would let a node in /usr/local/bin win, and there's a
|
||||
test for that case.
|
||||
|
||||
B3 is fixed. I checked the four lines against the sandbox:
|
||||
- Every read-write bind (workspace, run directory, thread store) and every socket is under the data
|
||||
root, inside `<home>`: config.mjs:19, git-socket.mjs:1, launcher.mjs:5. So `ReadWritePaths=<home>`
|
||||
covers them.
|
||||
- Nothing uses `/run/user`, so `ProtectHome=yes` takes away nothing the bus needs.
|
||||
- No line restricts namespaces, and the refusal for `RestrictNamespaces=`, `PrivateUsers=` and
|
||||
`SystemCallFilter=` has a test.
|
||||
- The step 9 session start is the real check, and it waits on #383 as stated.
|
||||
|
||||
N1-N5 from r10 are all in the C2-13a brief, with tests.
|
||||
|
||||
The port squat and its fix are right. A session inside bwrap holds capabilities only in its own user
|
||||
namespace, which doesn't own the host's network namespace, so it can't bind below the port floor. cli.mjs
|
||||
:27 takes ports 0..65535, so serve needs no change. eac92d956 adds both mos-1100 conditions:
|
||||
- The precheck refuses a floor at or below the port.
|
||||
- The bind test builds a real session argv with `binds()` (sandbox.mjs:101) and `sandboxArgv()` (:162).
|
||||
It expects `EACCES` below the floor, runs a control that binds successfully, and fails rather than
|
||||
skips on a host with a low floor.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. A correction to my own r10 B1. I cited the dragon-lin forgery acceptance as decision 94. It is
|
||||
decision 95 (lead-decisions.md:2048, the text at :2058-2062). r11 9.3 has it right. Decision 94
|
||||
(:2003) is the one about Jason's start, which r11's "Changes from r10" also has right.
|
||||
|
||||
N2. The bind test's control binds "a free port of 1024 or above". On a host whose floor is above 1024,
|
||||
that control fails for the wrong reason. Pick the control port at or above the floor the test just
|
||||
read, and the low port below it. It's one line in the brief.
|
||||
|
||||
N3. The precheck reads the Console's `--port` from the installed unit, so it can only pass after step 2c.
|
||||
Say what it does when no Console unit is installed, presumably refuse with a named error, add a case for
|
||||
it, and say in section 10 that the precheck runs after 2c.
|
||||
|
||||
N4. `AmbientCapabilities=` carries across exec, so each `human-cli.mjs` child the Console starts also
|
||||
holds `CAP_NET_BIND_SERVICE`. That's harmless today, because they run reviewed code and start no
|
||||
session. Add one sentence to 9.3 so that a later Console child that starts something doesn't inherit the
|
||||
capability unnoticed.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Webby review: Case 2 design r2 (#416, creative writing hub)
|
||||
|
||||
Webby, 2026-10-20T19:28Z (from `date -u`). Independent reviewer. Author: Gosalyn.
|
||||
Subject: `agents/gosalyn/work/case2/DESIGN.md` at c1913fb07 on gosalyn/case2-design, blob
|
||||
05a79cbca624, sha256 bda7a717...4055 (verified). Code read at 9ae8c73ac, where the citations point.
|
||||
Static review. I ran nothing. I read the whole file. The r1 items I don't name below read as fixed.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Four blockers. Three are in the persona path, which is new in r2. The data model rewrite onto her
|
||||
real repo is the right call, and the 1.3 claim mostly holds (see "The three questions").
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. A top-level playground has no universe. 1.1 (:169-170) suggests "one at the top", but 1.2
|
||||
(:191) gives a file the universe whose `books` list holds its book folder. A file under a top-level
|
||||
prefix has no book folder, so `library_find` (universe required) can't return it, and the cite
|
||||
check's one-universe rule (:464) has nothing to compare. Fix: put `playground` inside each universe
|
||||
entry, or refuse a prefix that isn't inside a book folder. Also give playground the `.md` condition
|
||||
canon has (:187). As written, a `.canvas` under a playground prefix is playground and citable.
|
||||
|
||||
B2. Where `persona.text` lives, and who can read it. Instance entries take inline `vars` only
|
||||
(business.mjs:89 closes the keys). Nothing lets an entry point at a file, so "a 0600 file that the
|
||||
business file entry takes" (:665, :722-723) has no mechanism. Inline, the text is read by more than
|
||||
the design says:
|
||||
- The tester's `instances` read is allowed (broker.mjs:103) and returns every instance's vars
|
||||
whole (instance-settings.mjs:58-63). That breaks "No agent sees the persona text" (:724) on any
|
||||
host with `bus.tester` on (vars.mjs, `bus.tester`).
|
||||
- Every organization write logs old and new values of each changed path in
|
||||
`businesses/.history/<id>/changes.jsonl` (organization.mjs:70-80, :201-204).
|
||||
Fix: say the text sits inline in the business file, or add a file-reference var with its own
|
||||
checks. Then either refuse persona keys in the tester's reads, or make C2-13's precheck refuse
|
||||
`bus.tester` on `<host>`. State the mode of the business file and `.history`. Test it in C2-08.
|
||||
|
||||
B3. Name the places `THREAD_DEFINITION` must stay `assistant`. Section 4 (:404-406) replaces it
|
||||
"wherever it means may hold a thread" and keeps only tools.mjs:186. launch-settings.mjs:140, :186
|
||||
and :191 also use it. They let `organization.set`'s `assistants` key set an assistant's whole vars,
|
||||
which the chief of staff holds (`ORGANIZATION_KEYS`, broker.mjs:145, :1266-1271, Q169). Read as "may hold a thread", that
|
||||
lets the chief of staff rewrite Bean's `persona.text` and model. Fix: list those three lines as
|
||||
staying `assistant`, and add a C2-08 test that `assistants` with `bean` refuses
|
||||
`organization-not-assistant`.
|
||||
The same change has a second effect the design doesn't state. With `END_PROTECTED` set to
|
||||
`THREAD_DEFINITIONS` (:407-408), `instances.set` refuses to add or change a creative instance
|
||||
(instance-settings.mjs:23, :109), and `definitions()` marks all six not accepted (:52). So her model
|
||||
choice for Bean, a persona edit and adding a specialist later are all hand edits of the business
|
||||
file. 4 (:394) and 10 step 7 read as if she can pick specialists later. Say how that happens.
|
||||
|
||||
B4. Name the persona block's exact place. `prompt()` is the contract (bundle.mjs:177), then
|
||||
`## This session` (:179) with the session lines. The last of those says "end each turn with the
|
||||
answer itself" (:188), then the end_run and review-loop lines follow. "After the contract" can
|
||||
mean before `## This session` or at the very end, and the C2-08 snapshot depends on which. I'd put
|
||||
it between the contract and `## This session`, so the broker's facts come after the persona. Fence
|
||||
it the way skills are (`SKILL_END`, :107, :170), and refuse persona text that holds the end marker
|
||||
or a line starting `## `. Hermes text is 8,000 characters that nobody here has read, so it can't be
|
||||
allowed to forge a section. The snapshot then pins the order: contract, persona, session.
|
||||
|
||||
## The three questions
|
||||
|
||||
1.3, reporting is enough. For the agent-side guarantee, yes. Scope comes from the path alone, and
|
||||
the cite check (:463) refuses a playground cite on `fact`, `finding` and `correction` whatever her
|
||||
files contain. None of R1-R7 changes a scope. Holding back her head would add nothing to that
|
||||
check. Two limits belong in the text (N1, N2).
|
||||
|
||||
M9. It holds today. Every organization setter builds its document from the file and changes only
|
||||
its own keys (broker.mjs:1261-1269, instance-settings.mjs:93-121, launch-settings.mjs:182-191), so
|
||||
no verb can change `projects` now. M9 guards against a future setter. Place it where that setter
|
||||
can't skip it (N3).
|
||||
|
||||
Persona order. See B4.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. `library_get` takes only `path` (:346), so its `links_out` and `links_in` are the one place a
|
||||
lookup can name a file in another universe or an unmapped file. Say both lists drop those targets
|
||||
(R6, R2), and add a C2-05a test.
|
||||
N2. Scope is the current mapping applied to the path. A note she moves into a book folder is canon
|
||||
on her next push, and a mapping edit re-scopes old commits for the cite check. Both are right, and
|
||||
both are hers. Put the first in the setup sheet (C2-12).
|
||||
N3. Put M9 in `organizationWriter.write` (organization.mjs:177), comparing `canonicalJson` of old
|
||||
and new `projects`, hybrid entries included. Since no verb can build the change, the C2-04 test
|
||||
calls `writer.write` directly with a changed playground prefix, and one with a changed hybrid.
|
||||
N4. Obsidian resolves a shortest-path link across the whole vault, which is `~/src` with her other
|
||||
repos in it. The reader sees one repo. A base name unique in the books repo but not in the vault
|
||||
can resolve to a different file than Obsidian shows her. Count those in the survey's link-form
|
||||
counts if the survey can see it, or note it in the setup sheet.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Webby review: Case 2 design r3 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:43Z (from `date -u`). Author: Gosalyn. Subject: `agents/gosalyn/work/case2/DESIGN.md` at
|
||||
2958acb81 on gosalyn/case2-design, blob 60bc7d682e0d, sha256 f368c27d...c325 (verified). Read as the diff from
|
||||
c1913fb07 (r2, my REVIEW at 162ce46db), with the code at 9ae8c73ac. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
My r2 B1, B2 and B4 are fixed, and so are N1 to N4. B3 is fixed for the three paths it names, but it misses a
|
||||
fourth (B1 below).
|
||||
|
||||
- r2 B1. Playground prefixes now sit inside a universe and must start with one of its book folders, or the
|
||||
file refuses with `library-playground-outside-book`. A `.canvas` under a prefix is unmapped. 1.2's universe
|
||||
rule then holds for playground files.
|
||||
- r2 B2. The text moves to `<configDir>/personas/<business>/<instance>.md`, and the vars hold only the name and
|
||||
the digest. I checked the modes against the code. `durable` (organization.mjs:59-61) and `log` (:138-141)
|
||||
open with 0600 and O_NOFOLLOW. `log` refuses a group or other bit. `directory` (:39, :45) makes 0700 and
|
||||
refuses a loose one. The `instances` read (instance-settings.mjs:61-63) copies vars whole, so it now carries
|
||||
only the digest. `previous.json` (organization.mjs:197-198) holds the old business file, which has no text
|
||||
either.
|
||||
- r2 B3. launch-settings.mjs:140, :186 and :191 stay `THREAD_DEFINITION`. `organization.set`'s `instances` key
|
||||
refuses a creative instance for every actor. `instances.set` gets the human-only creative branch.
|
||||
- r2 B4. The fence follows `SKILL_END` (bundle.mjs:107, :170). The block sits between `contract.trimEnd()`
|
||||
(:177) and "## This session" (:179). The snapshot pins that order.
|
||||
- The correction is right. broker.mjs:147-149 says the chief of staff or a human-resolved decision ends an
|
||||
`END_PROTECTED` instance.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. `routes.set` lets the tester change a creative instance's harness, model and thinking. It is in
|
||||
`TESTER_ORGANIZATION_VERBS` (broker.mjs:103), and the gate at :1259 lets a tester through. `setRoutes`
|
||||
(launch-settings.mjs:240-272) has no protected-definition check. It replaces the route vars of any named
|
||||
instance, then writes at :277. So the design's 4 is wrong in two places:
|
||||
- DESIGN.md:474-493 lists every path to a creative instance and leaves this one out.
|
||||
- DESIGN.md:493 says that with `bus.tester` on, the tester "could change no creative instance".
|
||||
|
||||
M10 (DESIGN.md:563) has no test for it. `bus.tester` is off on her business and C2-13 refuses it, so this is
|
||||
not reachable there today. But the design claims the rule holds even if testing were on, and on any library
|
||||
business that turns testing on, it doesn't.
|
||||
|
||||
Fix: in C2-08, have `setRoutes` refuse a `CREATIVE_DEFINITIONS` instance from actor `tester` with
|
||||
`organization-creative-human-only`, as the `instances.set` branch does. The human keeps `routes.set`, which is
|
||||
the Console's routing table. Add the path to the list at 474, fix 493, and add the case to M10's tests: the
|
||||
tester's `routes.set` on `bean` refuses, the file is unchanged, and the human's still works.
|
||||
|
||||
## Your two questions
|
||||
|
||||
Keying the creative branch on actor `"tester"` is enough. broker.mjs:1272 sets the actor to `"tester"` only
|
||||
for a tester session. The business load check refuses a human id of `tester` (broker.mjs:245) and an instance
|
||||
of that name (:248). instance-settings.mjs:86 refuses one too. So the string can't name the human, and the
|
||||
writer is the only place that knows the definition, because it reads the document. A broker-side check would
|
||||
have to read the file a second time to learn the definition. The one thing to add: make C2-08's tester case go
|
||||
through the broker with a bound tester session, not just `set(..., "tester")`, so a change to the actor line at
|
||||
:1272 fails the test. The same goes for B1's case.
|
||||
|
||||
The `# `/`## ` refusal is safe, because it fails closed, but it may refuse her real personas. The fence alone
|
||||
stops a section escape, because nothing after `</persona-file>` belongs to the persona. The heading rule
|
||||
guards against a model reading a `## ` line inside the fence as a new section, which is reasonable. I don't
|
||||
know what her Hermes persona files look like. If they use Markdown headings, C2-20 stops at the fence check
|
||||
(DESIGN.md row C2-20) and the design names no next step. See N2.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The chief of staff can change which model a creative instance runs without touching the instance.
|
||||
`organization.set` takes `models` (broker.mjs:145, ORGANIZATION_KEYS), and launch-settings.mjs:169 replaces
|
||||
the whole `launch.models` table. If `bean`'s `model` var is a key the chief of staff rewrites, Bean runs the new
|
||||
model. That matches what 4 promises, since the instance and the persona stay unchanged, but mos-1033's "only
|
||||
the human" reads wider. Either say in 4 that the models table is shared and the chief of staff may retune it,
|
||||
or refuse a `models` change to a key a creative instance names. Mos's call, so I'd put it to him.
|
||||
N2. For C2-20, count the lines starting `# ` or `## ` in each persona and print the count with the byte count,
|
||||
before Jason's go. Then state the remedy in 9.1. That could be Melanie or Jason editing the text, or a recorded
|
||||
rewrite to `### `. Any rewrite needs her say, because the text is hers.
|
||||
N3. "Only Bean's or Chet's own session sees it" (DESIGN.md:866) needs a qualifier. `buildBundle` writes
|
||||
prompt.md, persona included, once per run into the run directory at 0600 (bundle.mjs:241-244, :265). Run
|
||||
directories are write-once and stay until `prune`. The claude adapter also passes prompt.md as one argument
|
||||
(bundle.mjs:104-105), and that argument is visible in `/proc/<pid>/cmdline` to other local users. No verb reads
|
||||
either copy, and on her VM the only other user is `melanie`, whose personas they are. So this doesn't change the
|
||||
design's safety. The text should name both copies, and the C2-13 precheck should check the run root's mode
|
||||
with the rest.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Webby review: Case 2 design r4 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:58Z (from `date -u`). Author: Gosalyn. Subject: `agents/gosalyn/work/case2/DESIGN.md` at
|
||||
5fc296e70 on gosalyn/case2-design, blob 7c62c40cbbb5, sha256 d06d3053...ab0a (verified). Read as the diff from
|
||||
r3 (2958acb81, my REVIEW at fcac66f7b), with the code at 9ae8c73ac. Static review. I ran nothing, apart from
|
||||
reading pi 0.85.1's resource-loader.js in the canonical checkout's node_modules.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
My r3 B1 is fixed, and so are N1, N2 and N3.
|
||||
|
||||
- r3 B1. `routes.set` is now on the list (DESIGN.md:549-552), and :493 is fixed (now :565). The one rule keys
|
||||
on the business file's `human`. I checked the actor lines: broker.mjs:1272 sets `tester` or the human id,
|
||||
and :1278 sets `<definition>.<instance>` for a role. M10 has the tester's `routes.set` on `bean` refusing
|
||||
and the human's working, and the tester cases go through a bound tester session (:567-570).
|
||||
- N3. The adapter lines are right (claude adapter.sh:79, :82; pi :106, :121). I read pi 0.85.1's
|
||||
`resolvePromptInput` (dist/core/resource-loader.js:17-31). It reads the value as a file when
|
||||
`existsSync` is true, and on a read error it warns and returns the path itself. See N1 below for the case
|
||||
where the file doesn't exist.
|
||||
- N2. The dry run prints counts only, and the rule changes before the import if a count is nonzero. That's
|
||||
what I asked for.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. The human does reach the `models` write, through `limits.set`. DESIGN.md:29-30 and :545-548 say that
|
||||
`organization.set` takes only an agent session, "so no verb writes `launch.models` for the human today", and
|
||||
that Jason edits it by hand. The first half is true. The conclusion isn't:
|
||||
|
||||
- `limits.set` is a human verb (broker.mjs:1248). It takes the same `ORGANIZATION_KEYS`, `models` included
|
||||
(:145, launch-settings.mjs:16).
|
||||
- `organization.set` is `limits.set` for a role (broker.mjs:1249-1251). Both reach the same `set()`
|
||||
(:1284, launch-settings.mjs:150-169), and the `models` replace is at :169.
|
||||
- So the human's `limits.set { models }` replaces `launch.models` today, with actor = her id. The tester
|
||||
can't, because `limits.set` isn't in `TESTER_ORGANIZATION_VERBS` (broker.mjs:103).
|
||||
|
||||
The one actor rule still gives the right behaviour here, since the human passes and the chief of staff
|
||||
refuses. But section 4 is the map of who can change a creative instance, and it states this path wrongly.
|
||||
M10 also has no test showing the `models` check keys on the actor rather than on the verb. If someone builds
|
||||
it as "refuse for every actor", the way the `instances` key at :539-541 works, her own model retune refuses,
|
||||
and no test catches it.
|
||||
|
||||
Fix:
|
||||
- In both places, say that the human writes `launch.models` with `limits.set` and the chief of staff with
|
||||
`organization.set`, both through `set()`, and that the check refuses unless the actor is the human.
|
||||
- Add to M10 and C2-08: the human's `limits.set` change to Bean's `launch.models` key works, next to the chief
|
||||
of staff's change that refuses.
|
||||
- :541 already says the human's `instances: {bean: ...}` refuses. That case also goes through `limits.set`.
|
||||
Name the verb there too, so the test is written against it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Pi also uses the path as the prompt when the file doesn't exist. Then `existsSync` is false and
|
||||
`resolvePromptInput` returns the input with no warning (resource-loader.js:21, :30). So the adapter's check
|
||||
has to cover existence as well as readability. `[ -r "$f" ]` does both. Add a missing file to C2-08's pi
|
||||
adapter test, next to the unreadable one.
|
||||
|
||||
N2. The claude adapter test can only show what the adapter puts in argv. Whether claude 2.1.304 reads
|
||||
`--system-prompt-file` as a full replacement, the way `--system-prompt` works today, rests on its `--help`
|
||||
until C2-13. Have the C2-13 precheck run one prompt through the host's claude with a marker system prompt and
|
||||
check the reply, not only that the flag is listed. If it acts as an append, the contract and gate text would
|
||||
sit on top of claude's default prompt, which changes what the session is told.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Webby review: Case 2 design r5 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:54Z (from `date -u`). Author: Gosalyn. Subject: `agents/gosalyn/work/case2/DESIGN.md` at
|
||||
e1635308b on gosalyn/case2-design (parent 5fc296e70), blob fd5df75ce635, sha256 9ba4f965...2d22d (verified).
|
||||
Read as the diff from r4 (my REVIEW at 0597cc198), with the code at 9ae8c73ac. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- r4 B1 is fixed. Section 4 (DESIGN.md:555-557) says `limits.set` and `organization.set` take the same
|
||||
keys and reach the same `set()`, and that the check keys on the actor. I re-read
|
||||
packages/bus/src/broker.mjs:1248-1251 and :1284 at 9ae8c73ac, and they match. The `instances` case
|
||||
(:565-566) names both verbs. The `models` case (:568-573) now refuses unless the actor is the human, and
|
||||
its test has her `limits.set` change to Bean's key working next to the chief's refusal. M10 (:665) and
|
||||
C2-08 (:899) carry the same case. The r3 Changes entry (:44-45) records the correction rather than
|
||||
deleting the old claim. No stale "agent-only" or "by hand" claim is left (grep).
|
||||
- r4 N1 is fixed. :535-537 covers the missing file, the check is `[ -r ]`, and C2-08 tests a missing and
|
||||
an unreadable prompt file.
|
||||
- r4 N2 is fixed. C2-13 (:905) runs one marker prompt through the host's claude with
|
||||
`--system-prompt-file` and checks that it replaces the default prompt.
|
||||
|
||||
No new findings. Section 10 step 1 stays on hold per mos-1050. That isn't mine to clear.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Webby review: Case 2 design r6, section 10 step 1 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:59Z (from `date -u`). Author: Gosalyn. Subject: `git diff e1635308b 1508fd175` of
|
||||
`agents/gosalyn/work/case2/DESIGN.md` (r5 blob fd5df75ce635, approved in webby-0021, to r6 blob 80893dd4cff1).
|
||||
Static review. I checked the cites in jason-master-restart at 1c378b4 (read only): DECISIONS.md Q183 and
|
||||
projects/mosaic-mvp/MELANIE-HOST-RECON.md:27-39. Both say what r6 says they say.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
The Q183 reading is right. #421 is closed, FED R8 records Jason's acceptance, in-VM tightening is
|
||||
allowed and is #423, and each VM gets a `mosaic` user without sudo. Section 9's isolation paragraph
|
||||
(DESIGN.md:961-965) states the residual as it is now, with the recon cite.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Step 1 has lost the owner of the root steps on .54. Until r5, ops-01's plan section 3 (#421) created
|
||||
`mosaic`, `/srv/mosaic` at 0700, and the unit (DESIGN.md:947-950 still cites it). That plan stopped. r6
|
||||
step 1 (:1017) says "the install creates the `mosaic` user". But step 2 (:1023) runs the install "as
|
||||
`<user>`", and C2-13 runs as `<user>`. A user can't create itself, and `mosaic` without sudo can't
|
||||
write a system unit `mosaic-stack.service` (:948). So nothing names who does the root part, with which
|
||||
access, or under whose go. The only root paths into .54 today are pve root and `melanie`'s sudo
|
||||
(MELANIE-HOST-RECON.md:31, :35-36), and using either one is an act on Melanie's machine.
|
||||
|
||||
Fix: list the root steps as their own item: create `mosaic` (system user, no sudo, no groups, no keys),
|
||||
create `/srv/mosaic` at 0700 owned by it, and install and enable the unit. Name who runs them (Jason, or
|
||||
ops-01 under his go), and through which path (melanie's sudo or pve). Say that this is step 2's go and not
|
||||
step 1's. C2-13's precheck then checks the result, as it already does (rows brief a0ce2ffb8:729). Also
|
||||
re-cite :950: the user spec now rests on Q183, not on the stopped #421 plan.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. :1018 says the precheck "refuses if it has sudo or any group". A system user still has its own
|
||||
primary group. Say "no supplementary groups", and check `id -G` has one entry, or the precheck refuses
|
||||
every real install. The rows brief (:729) uses the same words, so fix both.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Webby review: Case 2 design r7, sections 9 and 10 and C2-13 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T20:02Z (from `date -u`). Author: Gosalyn. Subject: `git diff 1508fd175 8645cb456` on
|
||||
agents/gosalyn/work/case2/DESIGN.md (blob aeaad378a344) and `git diff a0ce2ffb8 8645cb456` on
|
||||
docs/plans/2026-10-20_case2-rows.md (the rows file arrived in a0ce2ffb8, between r6 and r7). Static
|
||||
review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
r6 B1 is fixed, and so is N1.
|
||||
- Section 10 step 2 (DESIGN.md:1054-1064) now splits 2a (root: useradd, /srv/mosaic 0700), 2b (the install
|
||||
as `<user>`) and 2c (root: install and enable the unit). It names ops-01 as owner, the localadmin
|
||||
path from #423 step 1, and Jason's go. Step 1 (:1045-1053) is right as written. Section 9 (:973-975)
|
||||
rests the layout on Q183, and the rows brief (:724-725) says the script doesn't create `<user>` or
|
||||
install the unit.
|
||||
- N1. "No supplementary groups" appears in all four places, and the precheck checks `sudo -n true` fails
|
||||
and `id -G` prints one entry (rows :730-731).
|
||||
- Mos's item 3. The residual split at :985-992 reads correctly against MELANIE-HOST-RECON.md:31, :35-36.
|
||||
|
||||
My one blocker is in step 2c and C2-13, not in step 1. I have no objection to the mos-1050 hold on step 1
|
||||
lifting now. Mos decides that.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Root installs a unit file that the unprivileged user wrote. C2-13's script runs as `mosaic` and
|
||||
writes the `mosaic-stack.service` file (rows :724-725, DESIGN.md :940). 2c then has root install and
|
||||
enable "the unit file that C2-13 writes" (:1063-1064). By 2c, 2b has already run `bootstrap.sh`, npm
|
||||
install and the release activate as `mosaic`, so third-party package code has run under that uid. Any
|
||||
code running as `mosaic` can change the file before 2c, for example to `User=root` or another
|
||||
`ExecStart`. Root then installs a root service. The same holds on every later re-install. So 2c turns
|
||||
the no-sudo user into a path to root, which is the boundary section 9 relies on.
|
||||
|
||||
Fix: 2c must not trust the content. Either root renders the unit from the reviewed template in the
|
||||
release, with `<user>` and the paths as inputs, or root copies the file to
|
||||
/etc/systemd/system/ as root:root 0644 and refuses unless `User=mosaic`, `Group=mosaic`, the
|
||||
`ExecStart` path, and the hardening lines (`ProtectHome` and the rest) match fixed expected values.
|
||||
Never `systemctl link` it, because a link keeps the file writable by `mosaic`. Put the check in C2-13's
|
||||
tests: a unit file edited to `User=root` makes the 2c step refuse.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. 2b runs the install "as `<user>`, by him or his delegate", but `mosaic` has no keys and no login.
|
||||
The only way in is `sudo -u mosaic` from `localadmin`. Say so, so that nobody adds a key to `mosaic`
|
||||
to make 2b work.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Webby review: Case 2 design r8, step 2c and C2-13 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T20:04Z (from `date -u`). Author: Gosalyn. Subject: `git diff 8645cb456 eab6e1a3a` on
|
||||
agents/gosalyn/work/case2/DESIGN.md (blob 5dd7a9c9b4c0) and docs/plans/2026-10-20_case2-rows.md. Static
|
||||
review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
r7 B1 is fixed, and so is N1.
|
||||
- `mosaic` writes no unit. In 2c (DESIGN.md:1084-1091, rows :728-736), root renders the unit from a
|
||||
root-owned checkout of the commit Jason's go names, outside /srv/mosaic. It checks `User=`, `Group=`,
|
||||
`ExecStart=` and the hardening lines, writes /etc/systemd/system as root:root 0644, never uses
|
||||
`systemctl link`, refuses a symlink, refuses a drifted installed unit, and reads nothing under
|
||||
`<user>`'s home. The tests (rows :753-759) cover an edit to `User=root`, an edited `ExecStart=`, a
|
||||
removed hardening line, a symlink, and the 0644 write.
|
||||
- Gosalyn's change from my fix is right, and my suggestion was wrong. The release is under /srv/mosaic and
|
||||
`mosaic` can write it, so a template there is the same path to root. The root-owned checkout closes it.
|
||||
- N1. 2b runs as `sudo -u mosaic -H` from localadmin, and `mosaic` gets no key (:1080-1081, rows
|
||||
:724-726). `sudo -u` with a command doesn't need a login shell.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The check covers named lines, not the whole file. A template line such as `ExecStartPre=+...`,
|
||||
`ExecStartPost=!...` or `PermissionsStartOnly=` runs as root even with `User=mosaic`. If it pointed
|
||||
into /srv/mosaic, that is the same path to root through a line the check doesn't read. The template is
|
||||
reviewed, so this is defence in depth. Either compare the whole render to one fixed expected file, or
|
||||
refuse any `Exec*=` value starting with `+` or `!` and any `PermissionsStartOnly`. Add one test case
|
||||
for it. The existing packages/cli/systemd/mosaic-bus.service.in has a single `ExecStart=` (:30), so
|
||||
the new template can follow it.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Webby review: Case 2 design r9 (#416)
|
||||
|
||||
Webby, 2026-10-20T20:48Z (from `date -u`). Author: Gosalyn (gosalyn-0035). Commit 29e85196b on
|
||||
gosalyn/case2-design, DESIGN blob 60a2d6cb751e. Round 8: case2-r8/REVIEW.md, approve on eab6e1a3a.
|
||||
|
||||
Scope: `git diff eab6e1a3a 29e85196b` on DESIGN.md and docs/plans/2026-10-20_case2-rows.md, including
|
||||
13a98bfd0 and 29404f996 as asked. Row A's and row B's briefs (f325657c7) are not part of this round. I
|
||||
read their citations only where the design shares them. This was a static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Model path (section 9). The citations hold on origin/refactor 088087ac9:
|
||||
- `ENV_ALLOW` (packages/seat/src/session.mjs:83-88) has no `ZAI_API_KEY`, so today the key is dropped.
|
||||
- pi-ai reads `zai` from `ZAI_API_KEY` (env-api-keys.js:89), and `glm-5.3` is a zai model id (see N1 for
|
||||
the file path).
|
||||
- pi's harness sets `countsAccount` to false (pi/index.mjs:42), so the account check at launcher.mjs:296-308
|
||||
never runs for pi. `preflight()` (:26) checks only the binary. `credentialStatus` (launcher.mjs:343) is
|
||||
metadata passed into the launch object (:385), not a refusal. So "a pi launch has no credential check"
|
||||
is true, and the precheck key-file line is the right place to catch a missing key.
|
||||
- `PI_PROVIDER` comes from `session.provider` (pi/index.mjs:57).
|
||||
|
||||
2d. The key file is the path the bus unit already loads (mosaic-bus.service.in:24), and the `%h` in that
|
||||
line resolves to /srv/mosaic because `<user>` is mosaic with that home (DESIGN 1010-1012). The design
|
||||
says plainly that melanie's sudo and her Hermes processes can read it until #423 step 5 (:1042). On
|
||||
Gosalyn's naming question, I'd keep `claude-session.env`. A new name means a second `EnvironmentFile=`
|
||||
line, which adds a line to case2-unit.sh's fixed set and departs from the bus unit. The cost of keeping
|
||||
it is a misleading name. A README line saying the file holds every session provider key, not only
|
||||
Claude's, fixes that, and it can ride row A's README edit.
|
||||
|
||||
C2-13 and the rows doc:
|
||||
- The marker prompt check now runs only for a claude-code creative instance, and a test shows a missing
|
||||
claude passes when there is none. That's right, since Bean and Chet run pi.
|
||||
- The key-file precheck has refusal cases for missing, 0644, symlink and empty, plus a search of the test
|
||||
output for the fixture value.
|
||||
- `EnvironmentFile=` joins the fixed lines, with removed and pointing-elsewhere cases.
|
||||
- The Exec prefix rule covers the whole leading run, with `+` and `-+` cases, as webby-0027 asked.
|
||||
- 13a98bfd0, `--profile factory|library`. `factory` requires the tester and refuses a library project
|
||||
(`profile-library-business`), and `library` refuses the tester. Each direction has a test, so a wrong
|
||||
flag can't reach a tester-on Case 2 install.
|
||||
- 13a98bfd0, node >= 24 and bwrap prechecks. Both have fake-binary refusal cases, and a bwrap refusal
|
||||
prints the userns sysctl and PATH state.
|
||||
- 29404f996, the copy id. The rule matches `copy_root` (scripts/stack-copy.sh:20): 40 lowercase hex and
|
||||
`scripts/mosaic` present. The tests cover short, uppercase, `/`, `..` and missing.
|
||||
|
||||
C2-20. The Hermes name count prints one count per persona file and never the line, number or word, and
|
||||
the test checks the output holds none of the matched words. The list comes from code at df5700ebe, not
|
||||
her profiles, so the script carries no persona text.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The model data file is `dist/providers/data/zai.json`, not `data/zai.json` (DESIGN:1023, and row A's
|
||||
brief). The id `glm-5.3` is there. Fix the path when the file is next touched.
|
||||
|
||||
N2. `copy_root` reads `COPIES` as `${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies`
|
||||
(stack-copy.sh:6), while case2-unit.sh fixes `<home>/.local/share/mosaic-stack/copies`. If mosaic's
|
||||
login env ever sets `XDG_DATA_HOME`, copies land elsewhere and the unit refuses. That fails closed, so it
|
||||
doesn't block. The install could check `XDG_DATA_HOME` is unset, or the rows doc could say why it's safe.
|
||||
|
||||
N3. "Sets a nonempty `ZAI_API_KEY`" should parse the file the way systemd does: the last assignment wins,
|
||||
and quotes are stripped. A file with `ZAI_API_KEY=x` then `ZAI_API_KEY=` passes a first-match grep but
|
||||
gives the session an empty key. Add that case to the refusal list.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Webby review: row A brief, ZAI_API_KEY in the session env allowlist
|
||||
|
||||
Webby, 2026-10-20T20:55Z (from `date -u`). Requested by Sage (sage-1477). Brief text: the "Brief: row A"
|
||||
section of docs/plans/2026-10-20_case2-rows.md at Gosalyn's a2d94b2c2 (file blob 81140d017441), which is the
|
||||
version Sage is re-pinning to. Local refactor ecbfd6deb still carries the 29e85196b text and isn't on origin.
|
||||
I read the cited code on origin/refactor. Static review.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Citations hold on origin/refactor:
|
||||
- `ENV_ALLOW` is session.mjs:83-88, with the provider line at :86 (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`).
|
||||
- `SESSION_CREDENTIALS` and `hasSessionCredential` are at :94-95, and `sessionEnv` copies only allowlist
|
||||
names at :97-99.
|
||||
- launcher.mjs:299 runs the account check only when `countsAccount` is true. pi returns false
|
||||
(pi/index.mjs:42).
|
||||
- cli README:435 is the "A Pi session gets the same variables" sentence, and :439-444 says a key that isn't
|
||||
an `ENV_ALLOW` variable launches and then fails at the first turn.
|
||||
- session.test.mjs:51-59 is the existing allowlist test. Its `deepEqual` input holds no `ZAI_API_KEY`, so it
|
||||
still passes after the change. :76 checks that every `SESSION_CREDENTIALS` name is in `ENV_ALLOW`, which
|
||||
keeps holding.
|
||||
- No test under packages/*/tests or scripts/test-*.sh pins a hash of session.mjs.
|
||||
|
||||
The scope is right:
|
||||
- Leaving `SESSION_CREDENTIALS` alone means a Claude Code launch still refuses without a Claude login, even
|
||||
when a zai key is set.
|
||||
- The mutant has a real target, because the new test asserts both membership and passthrough.
|
||||
- The README sentence states the cost plainly: every session the host starts gets the key, and its shell can
|
||||
read it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The cli README's "Unit" section says the file "holds one line, for example
|
||||
`CLAUDE_CODE_OAUTH_TOKEN=<token>`" (packages/cli/README.md:711). The brief adds a second sentence there. If
|
||||
the subagent only adds, the README will say both "one line" and "every session provider key". The brief
|
||||
should say to replace "holds one line" with something like "holds one line per variable, for example". This
|
||||
is a doc-only fix. Fold it into the re-pin if that's cheap, since a Haiku subagent follows the brief
|
||||
literally.
|
||||
|
||||
N2. The same paragraph says "nothing in the repository creates or reads it" (:713). That stays true for row
|
||||
A. It becomes false when C2-13's precheck parses the file, so C2-13 should own that line.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Webby review: Case 2 v1 briefs C2-09b, C2-10c and C2-15 (mos-1233), round d1
|
||||
|
||||
Webby, 2026-10-21T22:29Z (from `date -u`). Author: Gosalyn (gosalyn-0122). For mos-1233, Vikunja 464.
|
||||
Candidate: docs/plans/2026-10-20_case2-rows.md, the section "v1 rows past the slice", `git diff cca206cdc
|
||||
72ceda935` on gosalyn/case2-v1-briefs (one commit, +227). These are briefs, so there's no queue record.
|
||||
|
||||
What I checked, without runs: each brief against design 1.4 and 6 at gosalyn/case2-design 7a7c00464 and
|
||||
against the code it cites at cca206cdc:
|
||||
- broker.mjs: `thread.send` (:3511-3536), `#named` (:1480-1488), `TESTER_VERBS` (:117-119),
|
||||
`THREAD_PAGE` (:260), `#route` (:3678-3681) and `#delivery` (:3787-3797);
|
||||
- webui chat-http.mjs's STATUS table and safeMessage (:12-26);
|
||||
- the webui channel plugin's VERBS (:4-12);
|
||||
- library mapping.mjs's hybrid branch and mapping.test.mjs.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1. C2-15's `exclude` check drops the design's second form, and C2-01's own test uses that form.
|
||||
- Design 1.4 allows `{"exclude": "<source path or source/type>"}`.
|
||||
- mapping.test.mjs:57 pins the source/type form: `exclude: 'second/Characters'`, where `second` is a
|
||||
universe id with books `['Second World']` and `Characters` is a type folder. mapping.mjs:98-100 accepts
|
||||
it today.
|
||||
- The brief says the validator refuses "an `exclude` that doesn't lie inside a book folder of one of the
|
||||
hybrid's sources". `second/Characters` lies inside no book folder, so the row's first change would
|
||||
refuse a form the design allows and break C2-01's test.
|
||||
- The brief has to keep both forms and say how to tell them apart. My suggestion: if the first segment is
|
||||
one of the hybrid's sources and the rest is one key of `types`, it's the type form, and it excludes that
|
||||
type folder in every one of that source's books. Otherwise it must lie inside a source's book folder. If
|
||||
both readings fit, it refuses. The R6 excluded-link case, the guard case and the lookups' drop then
|
||||
have to cover both forms, with one test each. Also name how C2-05a (row 216) reads `exclude`, because
|
||||
C2-15's reader and guard have to agree with that index.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Your question, R8's second case. I agree with the reading in principle. A hybrid can't edit a
|
||||
source's file, so "a hybrid file changes it" (design 1.4) can only mean that a hybrid file states another
|
||||
value for the pinned field. Linking to the entity is a fair test of "about that entity". But as written
|
||||
it gives false positives. A place file in the hybrid with `age: 300` and a `relationships: [[Mara]]` link
|
||||
would report R8 against a pin on Mara's `age`. Narrow it to hybrid files whose type (from the type folder)
|
||||
equals the entity's type. Then think about the other gap: a hybrid copy of Mara with the same front matter
|
||||
`name` and no link back. I'd count that too, since it's the likeliest way she'd write a hybrid version of
|
||||
a character. Either way it's report-only, so it doesn't block. Say which front matter keys count as the
|
||||
link (all of them, or only `appears_in` and `relationships`), and test the place-file case as a non-R8.
|
||||
|
||||
N2. C2-15's validator should also refuse a `pin.entity` or a path `exclude` that lies under one of the
|
||||
source's playground prefixes. The hybrid reads only its sources' canon (REQ-LIB-3), so a pin there
|
||||
checks a file the hybrid never sees.
|
||||
|
||||
N3. C2-09b's `room.read` groups by shared `at`, body and sender. Every insert in one send uses one
|
||||
`#now()`, but two separate sends of the same body in the same millisecond (a double click) would merge
|
||||
into one entry with each instance listed twice. Since one send's rows go in one transaction, their
|
||||
`messages.seq` values are consecutive. Group only a consecutive seq run with the same `at`, body and
|
||||
sender, and dedupe `to`.
|
||||
|
||||
N4. C2-09b resolves a tag through `#named` or as an instance key. A tag can match one instance's display
|
||||
name and another instance's key. The brief should rule which wins, or refuse it as ambiguous, with a test.
|
||||
|
||||
N5. C2-10c's line "maps `unknown-addressee` and `room-thread-name-taken` to 409 and `room-not-open` to 409"
|
||||
reads as if two statuses were meant. If all three are 409, say "maps all three to 409".
|
||||
|
||||
N6. Pinning cost, as you said. Every briefed Case 2 row pins this file's blob, so 72ceda935 flags them all
|
||||
until Sage re-pins. Landing both of Sage's edits under one re-pin makes sense.
|
||||
|
||||
## Checked and fine
|
||||
|
||||
- C2-09b reuses what `thread.send` writes (:3511-3536): `#checkMessaging`, one message row, `#route`
|
||||
and one `human.input`. `#route` and `#delivery` only insert rows (:3678-3681, :3787-3797), so one
|
||||
dispatch transaction does undo a partial send. Leaving the room verbs out of `TESTER_VERBS` makes a
|
||||
tester refuse as for any other verb, and the gate's grep names tester-writes.test.mjs.
|
||||
- `room.open`'s refusal on a same-name thread elsewhere fits the per-project unique name
|
||||
(schema.sql:249), and "no schema change" holds.
|
||||
- C2-10c keeps the broker as the authority for addressing. The parity test reuses C2-09b's table. The
|
||||
plugin test checks that other verbs still refuse, which matters because VERBS (:4-12) is the webui
|
||||
channel's allowlist.
|
||||
- C2-15 names Opus and Filbert, and lists its after rows and the M9 refusal test. The out-of-scope line
|
||||
that sends draft sources to C2-01c matches the playground ruling.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Webby review: Case 2 v1 briefs C2-09b, C2-10c and C2-15 (mos-1233), round d2
|
||||
|
||||
Webby, 2026-10-21T22:34Z (from `date -u`). Author: Gosalyn (gosalyn-0125). Candidate: `git diff 72ceda935
|
||||
99b1e0859` on gosalyn/case2-v1-briefs, one commit. The rows file blob is c45816509, as Gosalyn said. I
|
||||
reviewed it against my d1 (REVIEW-d1.md, 35130c26b). These are briefs, so there's no queue record.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- B1. C2-15 keeps both exclude forms of design 1.4.
|
||||
- The type form is a first segment that is one of the hybrid's sources plus exactly one `types` key.
|
||||
`second/Characters` fits it: `second` is a universe id and `Characters` is a `types` key in
|
||||
mapping.test.mjs.
|
||||
- Anything else is the path form, inside or equal to a source book folder.
|
||||
- When both readings fit, it refuses.
|
||||
- One `excluded(mapping, hybrid, path)` helper in scope.mjs serves C2-05a's index, R6 and the guard.
|
||||
If C2-05a lands first, this row replaces C2-05a's own read, so all three share one reading.
|
||||
- Each form gets a case on the lookups, R6 and the guard, and the test at :57 still checks.
|
||||
- N1. R8 now counts a hybrid version of the entity: the same type from `classify`, and the same front
|
||||
matter `name` (or the same file name when either file has none). A link alone doesn't count. That's
|
||||
better than my narrowing: the hybrid Tom who links Mara and has his own `age` would have passed a
|
||||
type-only rule. The tests cover the no-link version as R8, and the place file and the other character
|
||||
as non-R8.
|
||||
- N2. A pin entity or path exclude under a source playground prefix refuses.
|
||||
- N3. `room.read` groups only a run of consecutive seqs from the human with the same `at` and body. The
|
||||
double-click case has a test.
|
||||
- N4. A tag that is one member's name and another member's key refuses `addressee-ambiguous` and
|
||||
inserts nothing. A tag that names one member both ways resolves to that member. The new code is on the
|
||||
line for Sage.
|
||||
- N5. C2-10c maps all four room codes to 409.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. R8's version rule says "a hybrid file" without a scope. A hybrid can have playground prefixes, and
|
||||
a brainstorm file there with the same name and another value would report R8. It's report-only, so the
|
||||
cost is noise in `library.status`, not a wrong lookup. If Melanie should be free to try other values
|
||||
in the playground, the builder can limit the version check to hybrid canon files and add a test for a
|
||||
playground version as non-R8. That's the builder's and Filbert's call, not a change to this brief.
|
||||
@@ -0,0 +1,360 @@
|
||||
# Webby static look: row 191 Console layout fix (sage-2364, mos-1248 condition 3)
|
||||
|
||||
Asked by Sage at 2026-10-22T02:50:06Z. Git reads only, no runs. Base: Rocko's 0f8235277
|
||||
(origin/rocko/row191-stoprun-ddfb-20261022).
|
||||
|
||||
## The failure, read from the base
|
||||
|
||||
- shell.test.mjs:380 asserts `scrollHeight === innerHeight` on the Board at 1440x1000. Rocko's gate
|
||||
reports 1062 against 1000.
|
||||
- index.html loads processing.css after shell.css (:4 and :6), and both use `.frame` (0,1,0). So
|
||||
processing.css:14 `min-height:0` wins over shell.css:135 `min-height:calc(100vh - var(--bar-h) -
|
||||
var(--foot-h))` at 760 px and wider. The frame has no floor of its own any more.
|
||||
- processing.css:12 makes body a flex column with `min-height:100vh`. Its in-flow children are the
|
||||
cmdbar (53 px, `--bar-h`), #processing-controls, the hidden banner, .frame, and the sticky footer (33 px,
|
||||
`--foot-h`). The tree is `position:fixed` (processing.css:16), so it takes no height.
|
||||
- Nothing caps the body at 100vh, so the page is cmdbar + bar + frame content + footer whenever that sum
|
||||
is over 1000. 1062 - 1000 = 62, and 62 is the bar's height from Sage's report. 1000 - 53 - 33 = 914 is
|
||||
the frame height shell.css:135 used to set. So the Board's content is at most 914 px and over 852 px.
|
||||
Before 191 it fit, and the bar now takes its share.
|
||||
- That means no rule on the frame alone can fix it while the window scrolls. The content needs 914 px,
|
||||
and the bar leaves 852. A fix has to do one of three things:
|
||||
1. take the bar out of flow, for example sticky or fixed over the frame's top, and pad the content by
|
||||
the bar's height only where the tree already sits;
|
||||
2. scroll the frame inside a body capped at 100vh, which changes :382-385 because the window would
|
||||
no longer scroll at 420 px;
|
||||
3. make the Board shorter.
|
||||
I'll judge Rocko's diff against these.
|
||||
|
||||
## Checks for Rocko's diff
|
||||
|
||||
1. **Layout.** At 1440x1000 the bar's box lies inside 0..1000, and scrollHeight equals innerHeight
|
||||
because the page fits, not because the window can't scroll. The same holds at 760-959 px (nav 200 px,
|
||||
shell.css:142-151) and at 1900 px and up (nav 280/320 px).
|
||||
2. **Tall page.** At 1440x420, scrolled to the end, :382-385 still see `scrollHeight > innerHeight`,
|
||||
with the tree between the cmdbar bottom and the footer top. The bar must not cover the tree or
|
||||
the first Board row.
|
||||
3. **:380 unchanged.** `git diff 0f8235277 <fix> -- packages/webui/tests/shell.test.mjs` adds lines and
|
||||
changes none at :373-385.
|
||||
4. **New assertion.** It must fail when the fix is reverted. For that it needs the bar's own box,
|
||||
`#processing-controls` getBoundingClientRect, against innerHeight and the cmdbar, at the size that
|
||||
failed. A check on scrollHeight alone at a size where the Board fits anyway would pass with the bug.
|
||||
5. **Mutant.** It must revert the CSS fix and nothing else, and it must name the killing test.
|
||||
|
||||
## Verdict on 5cd32f4c1 (rocko 02:57:02Z): CHANGES, B1
|
||||
|
||||
Read at 2026-10-22T02:59Z (from `date -u`). It's one commit on 0f8235277: processing.css (+12/-2), shell.test.mjs (+11/-0)
|
||||
and BUILD.md. Nothing has run, mine included.
|
||||
|
||||
**B1. As written, the CSS delta doesn't change the height of the page that failed.**
|
||||
(processing.css:12-24 at 5cd32f4c1)
|
||||
- Old body: a flex column with `min-height:100vh`, and the frame `flex:1; min-height:0`. The body's
|
||||
height is auto, so the frame's 0% basis resolves to its content, and the body comes to
|
||||
max(100vh, cmdbar + bar + C + footer), with C the frame's in-flow content height.
|
||||
- New body: a grid with `min-height:100vh` and the frame on row 4 as `minmax(min-content,1fr)`.
|
||||
min-content of the frame is the same C. The track can't go below it, and 1fr only spends space that is
|
||||
left over. So the body comes to max(100vh, cmdbar + bar + C + footer) again.
|
||||
- The two give the same number. For 1062 to come out of the old one, C has to be over 852
|
||||
(1000 - 53 - 62 - 33). In that case the new one gives 1062 too, and :380 fails exactly as before.
|
||||
1062 is exactly 1000 plus the bar, so C is likely 914 = `100vh - var(--bar-h) - var(--foot-h)`. That
|
||||
points at a box in the frame's flow that is sized to the viewport, and the delta doesn't touch it.
|
||||
I couldn't find which box it is from the stylesheets. processing.css:22 `min-height:0` does beat
|
||||
shell.css:135 on load order, and `.frame > .tree` is fixed. So it needs a measurement, not more CSS.
|
||||
- **Before the gate.** On a slot, run one probe at 1440x1000 on the Board (the shell.test fixture) and
|
||||
report:
|
||||
- the `.frame` rect height and `#main` scrollHeight;
|
||||
- the rect of every in-flow child of body;
|
||||
- the computed `min-height` and `position` of `.frame` and `.tree`.
|
||||
|
||||
Run it on 0f8235277 and on 5cd32f4c1. If the two match at 1062, this delta isn't the fix.
|
||||
Then there are three ways out:
|
||||
1. remove the box sized to 100vh - 86;
|
||||
2. take the bar out of the frame's height budget, for example by subtracting a `--processing-h`
|
||||
from the shell's calc() values, or by placing the bar in a row the shell already counts;
|
||||
3. if the Board really needs 914 px, it isn't a short page with the bar on it. Changing :374's
|
||||
viewport would be a test change for Sage and Mos to rule on, not for Rocko or me.
|
||||
- Rocko's BUILD.md says a second red stops the window and goes to Mos. That's one more reason not to
|
||||
open the gate on an unmeasured fix.
|
||||
|
||||
**What holds.**
|
||||
- **:380 is unchanged.** The diff removes no line in shell.test.mjs, and :373-380 and :392-397 (the old
|
||||
:381-386) are byte-identical.
|
||||
- **The new assertions (:381-391) help.** The visible and positive-height check catches a fix that hides
|
||||
the bar, which :380 alone would pass. The check that the bar sits under the cmdbar and inside the
|
||||
viewport is the bar's own box, as I asked. :391 repeats :380, which runs first, so it can never be
|
||||
the one that fails. N1: drop it, or leave it as documentation.
|
||||
- **KS59** reverts processing.css to 0f8235277's bytes. :380 would kill it, not the new assertions. That's
|
||||
fine, since a revert is the failing state. I'd add a second mutant that sets `#processing-controls`
|
||||
to `display:none` with the fix in place, so :388 has a kill of its own.
|
||||
- **The grid body's auto-placed children.** a.skip is `position:absolute` (app.css:24), #announce is
|
||||
.sr-only absolute, #toast is fixed, the dialog is closed, and a hidden banner leaves row 3 at 0. None
|
||||
of them takes a row, so the explicit rows hold. Sticky still works for the cmdbar and the footer as
|
||||
grid items. At 1440x420 the min-content row lets a long page grow, so :394-396 keep their premise.
|
||||
At 760-959 px the same rows apply with the 200 px nav. I see no regression there, but none of
|
||||
this matters until B1 is settled.
|
||||
|
||||
## Trace for sage-2368, and a correction to B1
|
||||
|
||||
Read at 2026-10-22T03:00Z (from `date -u`), git only.
|
||||
|
||||
**Correction.** Above, I treated 62 as the bar's height and guessed at a box in the frame sized to
|
||||
100vh - 86. Both were inferences, and the second is wrong. 62 is only the overflow:
|
||||
53 + barH + C + 33 - 1000. Neither the gate log nor the diff gives barH.
|
||||
|
||||
**What set the frame's height on the 1440x1000 Board under 0f8235277.** The frame's only in-flow child
|
||||
is #main, the Board's own content.
|
||||
- body children (index.html): a.skip, header.cmdbar, #processing-controls, #processing-banner (hidden),
|
||||
.frame, footer.foot, #announce, #toast, dialog#cmdk-dialog.
|
||||
- .frame children: nav.tree, main#main.content, aside#inspector (hidden).
|
||||
- **nav.tree.** processing.css:16 `.frame > .tree { position:fixed }` is (0,2,0). It beats `.tree`
|
||||
sticky at console.css:14 and shell.css:144, and static at console.css:39. So the tree is out of flow.
|
||||
- **aside#inspector.** shell.test opens it at :246 and closes it with Escape at :248, and :374 opens no
|
||||
session. app.js:88 sets `hidden = !selected`, and close() (app.js:402) clears `selected`. No
|
||||
.inspector rule sets `display`, so [hidden] holds and the inspector takes no space.
|
||||
- **min-height.** processing.css:14 `min-height:0` beats shell.css:135 and console.css:12 on load order
|
||||
(index.html:4 then :6), as Sage says. Rocko's cause, shell.css:135, isn't the cause.
|
||||
- That leaves #main's content C. The old page was max(1000, 86 + barH + C). Before 191 there was no bar,
|
||||
and shell.css:135 lifted the frame to 914, so the page fit whenever C <= 914. It went to 1062 because
|
||||
86 + barH + C > 1000: the Board's content and the bar together don't fit in 1000 px. shell.test:373
|
||||
already says the Board grew at row 95 ("the short page is taller").
|
||||
|
||||
**Does 5cd32f4c1 remove it?** No. Row 4 is `minmax(min-content,1fr)`, and min-content is the same C, so the
|
||||
page is again max(1000, 86 + barH + C) = 1062. B1 stands, with this cause.
|
||||
- The probe I asked for still settles it. It now only needs the `#main` height and the
|
||||
`#processing-controls` height at 1440x1000, which should sum with 86 to 1062.
|
||||
- No grid or flex rule can fix it while the window scrolls and the bar takes vertical space. The ways out
|
||||
change design or test:
|
||||
1. put the bar where it costs no page height, such as inside the fixed nav column or the cmdbar row;
|
||||
2. make the Board shorter;
|
||||
3. rule that the Board at 1000 px is no longer a short page, which means changing :374 or :380.
|
||||
|
||||
These are calls for Sage and Mos, not something to fix in CSS.
|
||||
|
||||
**Small checks.**
|
||||
- **No implicit row 6.** a.skip is `position:absolute` (app.css:24), and #announce is .sr-only absolute
|
||||
(app.css:23, shell.css:139). #toast is fixed (shell.css:104). The dialog is display:none while closed,
|
||||
and in the top layer while open. Absolutely positioned and top-layer boxes aren't grid items, so none
|
||||
of them adds a row. The hidden banner leaves row 3 at 0.
|
||||
- **760-959 px behaves as before.** console.css:37-38 drops the frame to one column at <=959. shell.css:143
|
||||
(760-959) restores `200px minmax(0,1fr)` and loads later with the same specificity, so
|
||||
`.frame > .content { grid-column:2 }` still has a column 2. The tree stays fixed at
|
||||
`--processing-nav:200px` (processing.css:28). 5cd32f4c1 changes none of that, only the body rows.
|
||||
|
||||
## Structure ruling for sage-2382 and Rocko 03:04:20Z
|
||||
|
||||
Read at 2026-10-22T03:06Z (from `date -u`), git only, on 5cd32f4c1. No runs.
|
||||
|
||||
**Ruling: (i), the labeled region as the first child of nav.tree, with conditions R1-R6. Drop sticky.**
|
||||
|
||||
Why (i) and not (ii).
|
||||
- **:380 edges.** In (i) nav.tree stays the same element, class and fixed box (processing.css:26,
|
||||
shell.css:136). A child can't move the nav's rect, so :396 and the 420-tall check hold as before.
|
||||
In (ii) the nav keeps its box too, but the controls sit over it.
|
||||
- **Fragility.** The controls have no fixed height. #processing-receipt is aria-live text that
|
||||
changes, and #processing-unavailable wraps to several lines at 200 or 240 px. (ii) has to reserve
|
||||
that space without a measurement. A fixed reservation goes wrong as soon as the receipt changes or the
|
||||
text zooms, and then the overlay covers nav links. Subgrid plus a wrapper is a larger change than the
|
||||
fix needs. (i) gets the real auto height from normal flow.
|
||||
- **Landmark.** ARIA allows a region inside navigation, and axe's top-level rules cover only
|
||||
banner, main, contentinfo and complementary. A landmark list then shows "Instance processing" under
|
||||
"Console". That's a cost, since Stop isn't navigation, but the name and role still say what it is.
|
||||
Tab order is unchanged: cmdbar, then Stop, then the nav links, the same as the body-child layout
|
||||
today. I accept the nesting for this row.
|
||||
- **Height.** With the controls out of body flow, the page is max(innerHeight, 53 + C + 33), C being
|
||||
#main's content. That's the pre-191 arithmetic, and it fits whenever C <= 914 at 1000 tall. If the
|
||||
Board's C is over 914 on its own, no layout fixes :380. The debug run settles that.
|
||||
|
||||
Why no sticky.
|
||||
- nav.tree scrolls itself (console.css:14 `overflow:auto`). A sticky region at its top covers a
|
||||
focused nav link whenever the nav is scrolled and the user tabs back up. That's WCAG 2.4.11 (focus
|
||||
not obscured), and nothing sets scroll-padding to stop it.
|
||||
- At 1440x420 the nav is 334 px tall (420 - 53 - 33). A sticky region with the note shown can take
|
||||
over half of it.
|
||||
- Without sticky, Stop is the nav's first content. It's in view at the nav's scroll origin, and
|
||||
focus scrolls it into view.
|
||||
|
||||
Conditions.
|
||||
- **R1. Markup.** Move the existing div as is: id, class, role=region and aria-label "Instance
|
||||
processing". It goes before `<h2 class="tree-title">Console</h2>` in index.html:22.
|
||||
#processing-state keeps role=status, and the receipt keeps aria-live. #processing-banner stays
|
||||
where it is.
|
||||
- **R2. Descendant rules.** As a child of .tree, the controls match rules written for nav content:
|
||||
- console.css:21 `.tree .btn{margin-top:auto}` hits Stop and Resume;
|
||||
- console.css:40 `.tree .btn{margin:0;order:1}` at <=959 px. The region is `display:flex`
|
||||
(processing.css:1), so order:1 moves Stop and Resume after the state text, the note and the
|
||||
receipt. The visual order then differs from the DOM order at 400 px;
|
||||
- live.css:5 `.tree .small{margin-top:auto}`, live.css:10 `.tree .small{flex-basis:100%;margin:0}`
|
||||
and shell.css:147 `.tree .small{flex-basis:auto;margin-top:auto}` hit the note and the receipt.
|
||||
|
||||
Add rules that put them back, with `.tree .processing-controls .btn` and `.small` at (0,3,0). Don't
|
||||
edit the shell rules. The new test asserts that Stop's computed `order` is 0 at 400 px, and a mutant
|
||||
that removes the reset must fail it.
|
||||
- **R3. Below 760 px.** .tree is a wrapping flex row (console.css:39). The region needs
|
||||
`flex-basis:100%`, or it shares a line with the "Console" title (`flex:1`, console.css:40). Then
|
||||
Stop sits above the title and the Projects list. Test at 400 px that Stop's rect top is above
|
||||
#projects' rect top, and that Stop is visible.
|
||||
- **R4. Remove dead layout.** Drop the body grid and the `.processing-controls` grid-row and
|
||||
margin-left rules from 5cd32f4c1. They apply to nothing once the controls are in the nav. Keep
|
||||
`.frame > .tree { position:fixed }` and the `--processing-nav` widths. The `.frame`
|
||||
`min-height:0` that beats shell.css:135 then goes back to what it did on 0f8235277. That's fine,
|
||||
because the frame's height is C either way.
|
||||
- **R5. Tests.**
|
||||
- shell.test :373-380 and :392-397 stay byte-identical.
|
||||
- :381-390 still hold. The bar's top sits under the cmdbar and its bottom inside the viewport at
|
||||
1440x1000. Add a check that the bar's rect lies inside the tree's rect.
|
||||
- :67 becomes the mos-1250 text (sage-2384): not inside #main or aside.inspector, and before #main
|
||||
in source. The bar is in nav.tree, which comes before #main, so it passes. The move-into-#main
|
||||
mutant must fail it.
|
||||
- Every shipped-strings assertion is unchanged.
|
||||
- My N2 still applies: a `display:none` mutant has to have :388 as its own kill.
|
||||
- **R6. Debug run (Sage's slot).** Report the height of #processing-controls and nav.tree at
|
||||
1440x1000, 1440x420 and 800x420, and #main's height at 1440x1000. If the region is over half the
|
||||
nav at 1440x420, tell me before the window. It isn't a blocker, but it's a usability call for
|
||||
Sage.
|
||||
|
||||
Not a condition. When the halt banner is shown it's still a body row, so a halted Board can scroll at
|
||||
1000 tall. :380 runs with it hidden. I'd file it as a follow-up and not widen this row.
|
||||
|
||||
## Sticky versus sage-2372 (Rocko 03:06:49Z)
|
||||
|
||||
Read at 2026-10-22T03:08Z (from `date -u`), git only. No runs.
|
||||
|
||||
**Ruling: the separate scroller. The controls don't scroll and the links scroll under them inside the
|
||||
same nav.tree box.** It meets both sage-2372 (the controls never scroll out) and 2.4.11 (nothing overlays
|
||||
a focused link). The controls aren't positioned over the scroller. They take their own flex row above it.
|
||||
|
||||
Conditions S1-S5, on top of R1-R6 (R1's "before the Console h2" still holds):
|
||||
- **S1. Markup.** nav.tree's children become the controls region, then one plain div with no role or
|
||||
label, for example `.tree-scroll`. That div holds the existing children in their existing order: the
|
||||
Console h2, #sections, #board-tree and the rest. Ids and classes don't change. No test or script
|
||||
reads nav children by position. Only ids are used: shell.test :272/:280/:282 #sections,
|
||||
board-held.test :110 #board-tree.
|
||||
- **S2. 760 px and up.** nav.tree keeps its fixed box (top --bar-h, height 100vh - bar - foot) and
|
||||
goes to `overflow:hidden`. The controls are `flex:none`. `.tree-scroll` is
|
||||
`flex:1 1 auto; min-height:0; overflow:auto`, and it repeats console.css:14's column flex and
|
||||
`gap:10px`. Then `.tree .btn{margin-top:auto}` (console.css:21) and `.tree .small{margin-top:auto}`
|
||||
(live.css:5, shell.css:147) still push the nav's own button and note to the bottom. The same applies
|
||||
at 760-959 under shell.css:144.
|
||||
- **S3. Below 760 px.** `.tree-scroll{display:contents}`, so its children stay flex items of the
|
||||
wrapping row (console.css:39-40, live.css:10). The `order:1`/`order:2` layout and shell :280 "one
|
||||
row" keep working without edits. The region gets flex-basis:100% (R3), and nav.tree stays static
|
||||
with no scroll.
|
||||
- **S4. Tests.** Everything in R5 still applies. Add a check at 1440x420: scroll `.tree-scroll` to its
|
||||
end, and Stop's rect is still inside the viewport, under the cmdbar. A mutant that sets
|
||||
`.tree-scroll{overflow:visible}` and nav.tree back to `overflow:auto` must fail it.
|
||||
- **S5. R6 still applies.** If the region is over half of nav.tree at 1440x420, the scroller gets
|
||||
small. That's Sage's usability call before the window.
|
||||
|
||||
## webby-0107 withdrawn (sage-2386)
|
||||
|
||||
Read at 2026-10-22T03:09Z (from `date -u`). Sage's sage-2386 (03:07:50Z) crossed with webby-0107. It accepts
|
||||
non-sticky, withdraws the sage-2372 long-nav visibility requirement and rules out a separate links
|
||||
scroller. The scroller existed only to meet that requirement, so S1-S5 are withdrawn. R1-R6 stand as
|
||||
written, with ordinary nav.tree overflow:auto. Under R6, I still want the region and nav heights at
|
||||
1440x420 on the debug run.
|
||||
|
||||
## Static look at da4334806 (rocko 03:10:31Z): no blockers
|
||||
|
||||
Read at 2026-10-22T03:12Z (from `date -u`), git only. One commit on 5cd32f4c1 touching index.html,
|
||||
processing.css, three test files and BUILD.md. Nothing has run. Manifest digest 245dceb1 (88 paths), as
|
||||
Rocko reported. I haven't recomputed it, so that's for the manifest round.
|
||||
|
||||
**R1 markup.** The region div moves byte-for-byte to nav.tree's first child, before the Console h2.
|
||||
Its id, class, role=region, aria-label, role=status and aria-live are all unchanged. The banner keeps its
|
||||
place in body, before .frame. processing.js is unchanged.
|
||||
|
||||
**R2 descendant rules.** `.tree .processing-controls .btn {margin:0; order:0}` and `.small {margin:0;
|
||||
flex-basis:100%}` are (0,3,0) and sit outside any media query, so they beat console.css:21/:40, live.css:5/:10 and shell.css:147 (all (0,2,0))
|
||||
at every width. The base `.btn` (shared/app.css:60) has no margin, so `margin:0` changes nothing on its
|
||||
own. `.processing-controls p {margin:0}` and the note/receipt `flex-basis:100%` already held, so the
|
||||
`.small` reset only cancels the tree rules. console.css:21 `justify-content:center` still reaches the
|
||||
buttons. That's harmless. No other `.tree` or `nav` descendant rule matches the controls.
|
||||
|
||||
**R3 below 760.** `.tree > .processing-controls {flex-basis:100%}` at <=759 puts the region on its own
|
||||
line ahead of the Console title. The new browser test checks Stop's top against #projects' top at 400 px.
|
||||
|
||||
**R4.** The body grid, the grid-row rules, the controls' margin-left and `.frame {min-height:0}` are all
|
||||
gone. With no body rule left in processing.css, shell.css:135 lifts the frame to 100vh - 86 again.
|
||||
`.frame > .tree {position:fixed}`, the content column and the nav widths stay.
|
||||
|
||||
**Height.** The body is block flow again: cmdbar 53, then the hidden banner at 0, then
|
||||
.frame = max(914, C), then the footer 33. So the page is max(1000, 86 + C), with no bar term. #main's
|
||||
content doesn't change in 191, so C is the pre-191 value that passed :380. The debug run confirms it.
|
||||
|
||||
**R5 tests.**
|
||||
- shell.test is additions only, at :389-399. :373-388 and every later old line are byte-identical, :380
|
||||
included. The new check puts the bar inside `.tree`'s rect, and the diagnostic logs heights.
|
||||
- processing-ui :67 has the mos-1250 text, quoted old and new in BUILD.md. `<main id="main"` and
|
||||
`<aside class="inspector"` match the literal markup, and there's exactly one `</aside>`. KS61 (into
|
||||
#main) moves controlsAt past mainAt, so it fails. If the aside markup is ever renamed, inspectorAt is
|
||||
-1, and `indexOf('</aside>', -1)` searches from 0. The check then fails closed, not open.
|
||||
- processing-browser: the new test covers 400x1000, 1440x1000, 1440x420 and 800x420. Order is
|
||||
checked at 800 and 400, inside the <=959 band where console.css:40 applies order:1, so KS62 is killed
|
||||
there. Visibility and the cmdbar/viewport bounds are checked on load.
|
||||
- Mutants: KS59 is a full revert of HTML and CSS, KS60 hides the controls (N2 met), KS61 moves them into
|
||||
#main, KS62 drops the order reset. The total is 77.
|
||||
|
||||
**sage-2378 checks.**
|
||||
1. Ids, role and label are kept.
|
||||
2. Nested in the nav landmark, which I accepted in webby-0106.
|
||||
3. Stop is visible at 400 and 1440x1000, and above Projects at 400.
|
||||
4. Every existing assertion is unchanged, other than :67 under mos-1250, and the shipped strings are
|
||||
untouched.
|
||||
5. The height arithmetic is above.
|
||||
|
||||
**Notes, none blocking.**
|
||||
- N1 (carried): :402 (the old :391) still repeats :380.
|
||||
- N3: `focus()` then activeElement only proves Stop is focusable and enabled when that line runs. It
|
||||
doesn't prove Tab order. Stop is `disabled` in the markup until processing.js reads the state. If the
|
||||
fixture leaves it disabled, this line fails on the run, so it's a fixture question, not a test hole.
|
||||
DOM order already gives cmdbar, Stop, nav links.
|
||||
- N4: R6 reporting is in place (ROW191_LAYOUT lines). Sage's half-nav call at 1440x420 waits on those
|
||||
numbers.
|
||||
|
||||
### Addendum for sage-2392: full 0f8235277..da4334806 range, Sage's two items, KS59-KS62
|
||||
|
||||
Read at 2026-10-22T03:13Z (from `date -u`), git only.
|
||||
- **Range.** `git diff --stat 0f8235277 da4334806 -- packages/` has five files: index.html 8, processing.css
|
||||
14, processing-browser +27, processing-ui +5/-1, and shell.test +22/-0 (0 removed lines). The body flex,
|
||||
the controls' margin-left and `.frame{flex:1;min-height:0}` from 0f8235277 are all gone, as is
|
||||
5cd32f4c1's grid. The verdict above holds for the whole range.
|
||||
- **Item 1, flex containers below 760.** .tree is `display:flex` at every width (console.css:14), and
|
||||
it's a wrapping row at <=959 (console.css:39). shell.css:158-171 (<=759) doesn't touch .tree's display.
|
||||
The region is `display:flex; flex-wrap:wrap` at every width (processing.css:1), and nothing overrides
|
||||
that. So `.tree > .processing-controls{flex-basis:100%}` works in the tree's row, and the `.small`
|
||||
flex-basis works in the region's row.
|
||||
- **Item 2, order.** `.tree .processing-controls .btn` is (0,3,0) against console.css:40's `.tree .btn`
|
||||
at (0,2,0). A media query adds no specificity, so the reset wins whatever the load order.
|
||||
- **KS59-KS62**, read from all-mutants.json in Rocko's packet (77 entries):
|
||||
- **KS59.** The HTML edit plus an extra_edits entry on processing.css restore 0f8235277's exact markup
|
||||
and CSS. That's the state the gate failed at 1062, so :380 kills it.
|
||||
- **KS60.** Appends `#processing-controls{display:none}` at (1,0,0), which beats the class rule. :380
|
||||
can't see it any more, so the visibility check at shell :388 is its kill. N2 is met.
|
||||
- **KS61.** Moves the region to the head of #main, so processing-ui :67's controlsAt < mainAt fails.
|
||||
- **KS62.** Deletes the `.btn` reset line, and the order assertion at 800 and 400 kills it.
|
||||
- All four are unrun.
|
||||
|
||||
N3 closed (Rocko 03:13:09Z, checked at 2026-10-22T03:14Z). processing.js:41-52 fetches /api/config with the
|
||||
state, sets `canWrite` (:46) and then calls draw(). The status text reads "Processing is enabled." only
|
||||
when that draw runs with a snapshot, and :29 sets `halt.disabled` in the same call. The test's setup
|
||||
waits for "enabled" (processing-browser:39) with config `send:true` (:27). So Stop is enabled before
|
||||
focus() runs.
|
||||
|
||||
## Debug run on da4334806 (Rocko, 03:14:37-03:14:53Z)
|
||||
|
||||
Read at 2026-10-22T03:16Z (from `date -u`), from Rocko's debug-da4334806 logs. I ran nothing.
|
||||
- **Counts.** shell.test reports `# tests 5 / pass 5 / fail 0 / skipped 0`, and processing-browser
|
||||
reports 9/9/0/0. The total is 14/0/0, as Rocko said.
|
||||
- **ROW191_LAYOUT matches the report.**
|
||||
- Writable controls are 127 px, and the tree is 914 at 1000 tall and 334 at 420.
|
||||
- Read-only controls are 212.5 at 1440x1000.
|
||||
- At 400x1000, Stop's top is 181.5, the cmdbar bottom is 129 and #projects starts at 352.6, with
|
||||
order 0.
|
||||
- **R6.** The writable controls are 127 of 334 at 1440x420 (38%), under half. The read-only ones were
|
||||
measured only at 1000 tall, but their height depends on width, not viewport height (wrapping at
|
||||
240 px). So at 1440x420 they would be 212.5 of 334, about 64%. That's over half.
|
||||
- **My read for Sage's call.** It's acceptable. The region isn't sticky and scrolls away with the nav, so
|
||||
the 64% applies only at the nav's scroll origin, and every nav link is still one scroll away.
|
||||
#projects already began below the fold at 1440x420 before 191: 636.6 - 127 = 509.6 is past 420. A
|
||||
420-tall desktop window isn't the normal case. If Sage wants it smaller, the fix is a shorter
|
||||
read-only note, not layout.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Webby review: row 192 r1, Q175 phase 1 SSO (#1690)
|
||||
|
||||
Webby, 2026-10-20T19:49Z (from `date -u`). Author: Launchpad. Candidate 83b32d8d8 on launchpad/row192, base
|
||||
53bb59462. Manifest `candidate-manifest.sha256` digest 3ad2a73d...badab, verified: all 18 path hashes match
|
||||
the candidate, the path list matches `git diff --stat 53bb59462 83b32d8d8`, and none of the 18 paths changed
|
||||
from the base to origin/refactor (aa1bd209e). Design: `agents/sage/work/q175-sso/DESIGN.md`. Static review
|
||||
plus one node probe of `returnPath` (below). I ran no suite and no mutant. I did not read
|
||||
/mnt/storage/scratch/launchpad-r192.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
The build follows the design closely. I checked these against the design and found them right:
|
||||
|
||||
- Callback steps 2-7 (sso.mjs:232-285):
|
||||
- The login record is taken and deleted before any check, and the provider comes from the record.
|
||||
- `validateApplicationLevelSignature` runs on the same Response, and its throw ends the callback.
|
||||
- The header alg is checked against `algs`, `iss` is compared raw, and `sub` is looked up in the subjects Map.
|
||||
- The `subject-unknown` line carries only the provider, iss and sub, JSON-quoted and capped at 255.
|
||||
- Sessions:
|
||||
- The store is keyed by sha256 and the cookie strings are exact.
|
||||
- The caps are 64 sessions and 32 logins.
|
||||
- Idle and absolute expiry are server-side.
|
||||
- `touch` happens only on a POST or a GET outside `/api/` (serve.mjs, the `sso.session` call).
|
||||
- Host (F4). Under `--sso`, anything but the public Host is 403, except GET `/healthz`. `/auth/*` routes go
|
||||
to `sso.handle` before the session check, and a request without a session gets 401 on `/api/` and a 303
|
||||
on a page.
|
||||
- Logout needs the exact Origin and `Sec-Fetch-Site: same-origin`, and a refused logout keeps the session.
|
||||
- Discovery (Q-B, sso.mjs:92-121, :151-182):
|
||||
- Unreachable and Refused are split as the design says.
|
||||
- A Refused provider stops the start. An unreachable one retries with doubling, capped at 300 s.
|
||||
- A Refused result on a retry stops the retries.
|
||||
- `readSlot` (sso.mjs:33-61):
|
||||
- lstat refuses a symlink, then the open uses O_NOFOLLOW, then fstat checks the file type, the uid and the
|
||||
mode against 0o077.
|
||||
- The realpath checks cover the repo, the checkout and the copies root.
|
||||
- The default copies root matches `stack-copy.sh:6`. I read the owner branch. It is right, and as the
|
||||
design says, no test covers it.
|
||||
- Tester listener (section 6, F8):
|
||||
- `startServer` throws on `tester` with `sso`.
|
||||
- Under `--sso` the tester listener refuses Authorization, either sign-in cookie and any `x-authentik-*`
|
||||
header, and `/auth/login` is 404 there.
|
||||
- The tester writer is unchanged.
|
||||
- The loader (C1) checks shape only, and test 25's FIFO slot proves no case opens a slot file.
|
||||
`mosaic-config.mjs` going async is safe because every reader runs it as a subprocess
|
||||
(cli/src/config.mjs:22, business/src/cli.mjs:32).
|
||||
- `webui-service.sh` refuses `--sso` with the preview set or the proof pair, and its status grep reads the new
|
||||
flag order.
|
||||
- The page: both app.js fetch paths, plus the two bus.js reads (F1), sign out on 401 `sign-in required` only.
|
||||
The link goes to `/auth/login?return=<path and query>`.
|
||||
- Every design mutant is in `mutants.py`, plus 10 more. A pattern that matches no test exits 0 and counts as
|
||||
SURVIVED, so a typo in a pattern fails safe.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Open redirect after sign-in. `returnPath` (sso.mjs:200-206) accepts any URL whose origin is
|
||||
`publicOrigin` and returns `u.pathname + u.search`. The callback puts that string in `Location` (:284). If
|
||||
the pathname starts with `//`, the browser reads the Location as scheme-relative and leaves the Console. I
|
||||
ran the function as written in node, with `publicOrigin = https://mosaic.example.com`:
|
||||
|
||||
| `return=` | `returnPath` | Browser goes to |
|
||||
| --- | --- | --- |
|
||||
| `/.//evil.example` | `//evil.example` | `https://evil.example/` |
|
||||
| `/x/..//evil.example` | `//evil.example` | `https://evil.example/` |
|
||||
| `https://mosaic.example.com//evil.example` | `//evil.example` | `https://evil.example/` |
|
||||
| the four test 14 cases | `/` | the Console |
|
||||
|
||||
Here is how an attacker uses it. They send Jason `https://<public host>/auth/login?return=/.//evil.example`.
|
||||
He signs in at the real provider, and the Console then sends him to the attacker's page. The `.`/`..` segments
|
||||
are why. Test 14's `//evil.example` parses with evil.example as the host, so the origin check catches it.
|
||||
`/.//evil.example` parses as a path on the Console, so the origin check passes, and dot-segment removal
|
||||
leaves the pathname as `//evil.example`. Design B2 and test 14 exist to stop exactly this.
|
||||
|
||||
Fix: return an absolute URL on the Console, `publicOrigin + u.pathname + u.search`. Then `//evil.example`
|
||||
becomes `https://<host>//evil.example`, which stays on the Console. Or refuse a pathname that starts with
|
||||
`//`. Add the three rows above to test 14, expecting `/` (or the absolute form, if you choose it), and add a
|
||||
mutant that restores the current return so test 14 kills it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The brief and BUILD.md:4 cite design blob eba861f5d2bb (d55d86bb0). Refactor and the candidate have
|
||||
9e3eadb15c80 (b416d2ede), which adds Mos's straggler check. The two differ only in the phase 2 window, so
|
||||
the build isn't affected. Cite the current blob in the next round.
|
||||
|
||||
N2. Design section 6 says the tester listener also refuses "the old auth and proof header names". Under
|
||||
`--sso`, cli.mjs:108 passes `testerProofHeader: proof?.header ?? null`, and that is always null because
|
||||
`--sso` refuses `--proof-*`. So the old proof header name isn't refused on 7333. The `x-authentik-*` prefix
|
||||
covers the old auth header. Nothing on 7333 reads the proof header, so this is harmless. Either add the
|
||||
name the live unit uses to the F8 refusal, or list it under Deviations.
|
||||
|
||||
N3. serve.mjs:64-66: the new `SIGN_IN_COOKIE` line went between the row 177 comment and
|
||||
`TESTER_CHAT_WRITE`, the line that comment describes. Move it above the comment, with its own comment.
|
||||
|
||||
N4. For the phase 2 window and row 200, not this row. Before 192, `mosaic-config.mjs` refuses any unknown
|
||||
top-level key (53bb59462 scripts/mosaic-config.mjs:112-116). Once `console` is in the live config.json, every
|
||||
loader from a pre-192 tree refuses the whole config, and every tool on that tree fails closed. That covers
|
||||
older pinned copies, any checkout behind 192, and a rollback target older than 192. A 192+ tree without
|
||||
`packages/webui/src/sso-config.mjs` refuses too (the import fail in `validateConsole`). The
|
||||
stack-release.test.mjs:1793 fixture copies `scripts/mosaic-config.mjs` without it, which is fine while the
|
||||
fixture config has no `console`. So the window has to re-pin to 192+ before it writes the key, and a rollback
|
||||
past 192 has to remove the key first. Put both in the window plan.
|
||||
|
||||
N5. A HEAD request for a page with no session gets 401 JSON, not the 303, because serve.mjs checks
|
||||
`req.method !== 'GET'`. Browsers don't send that, so no change is needed.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 192 r2, Q175 phase 1 SSO (#1690)
|
||||
|
||||
Webby, 2026-10-20T19:58Z (from `date -u`). Author: Launchpad. Candidate 37582e820 on launchpad/row192 (one commit on
|
||||
83b32d8d8), base 53bb59462, queue rev 2312. Manifest `candidate-manifest.sha256` digest 6f8cee14...7787,
|
||||
verified. All 18 path hashes match the candidate. The paths are `git diff --name-only 53bb59462 37582e820`
|
||||
less the four packet files under agents/launchpad/work/queue-192/. None of the 18 paths changed from the base
|
||||
to origin/refactor (ae3c36f24). Static review plus one node probe of `returnURL` (below). I ran no suite and
|
||||
no mutant. I did not read /mnt/storage/scratch/launchpad-r192, so the 28/28 and mutant results are
|
||||
Launchpad's report, not my run.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- r1 B1 is fixed. `returnURL` (sso.mjs:199-211) parses the return against publicOrigin, refuses another
|
||||
origin, and builds `publicOrigin + pathname + search` as an absolute URL. The callback's Location (:289)
|
||||
is now always absolute. Only the callback uses it; the export at :341 follows the rename. Test 14 has my
|
||||
three rows, `/board/./x`, and an origin assert on every case. The positive test expects the absolute
|
||||
form. Mutant 22 restores the r1 return under test 14.
|
||||
- Probe. I ran the function as written in node with publicOrigin `https://console.example.test` over 18
|
||||
inputs: my three rows, `/%2e%2e//evil`, `/.\\/evil`, `///evil`, `//evil`, `/\\evil`,
|
||||
`/@evil.example`, `/:evil`, CR/LF in the path (raw and encoded), a full-width dot, `:443`, upper-case
|
||||
scheme and host, and a fragment. Every result stayed on the Console's origin. Raw CR/LF are stripped by
|
||||
the URL parser, so no header split.
|
||||
- Mutant 23 (drop the second origin check). I agree it is equivalent. sso-config.mjs:43-44 holds
|
||||
publicOrigin to a bare https origin, and a parsed special-scheme pathname always starts with `/`, so the
|
||||
concatenation can't parse to another host. In the probe the mutant returned the same string as the build
|
||||
for all 18 inputs. Keeping the check as an unreachable guard is fine with me. Whether to keep it is
|
||||
Sage's call, as BUILD.md says.
|
||||
- N1 (design blob 9e3eadb15c80), N2 (under Deviations), N3 (serve.mjs:64-66, comment order) are done. N4
|
||||
moves to row 200 (#1698) and the window plan through Sage (mos-1058). N5 needs no change.
|
||||
|
||||
No new findings.
|
||||
@@ -0,0 +1,179 @@
|
||||
# Webby review: row 194 (#1692, agent lifecycle), build b4 as cut on b5, static
|
||||
|
||||
Webby, 2026-10-21 (times from `date -u` in the session line). Author: Honker (honker-0073, honker-0075).
|
||||
Request by sage-2230 and honker-0075: review b5, not f482890dc.
|
||||
|
||||
- **Candidate.** honker/row194-b5 at dfabbbe8a, six commits on origin/refactor da9555ca2.
|
||||
- **Manifest.** Honker gives 51 paths. `git diff --name-only da9555ca2 dfabbbe8a` gives 52. The extra one is
|
||||
agents/honker/work/queue-194/PRD-1.8-DRAFT.md, which stays out (sage-1626).
|
||||
- **The row.** It is in-progress, so I record nothing until Sage posts the request.
|
||||
|
||||
What I did, static only:
|
||||
- **b5 against b4.** On the 52 paths, `git diff f482890dc dfabbbe8a` touches only docs/plans/DEFERRED.md.
|
||||
So I read b4's code once, at f482890dc, and every line cite below holds at dfabbbe8a.
|
||||
- **Drift.** da9555ca2 is an ancestor of origin/refactor, and `da9555ca2..origin/refactor` touches none of
|
||||
the 52 paths.
|
||||
- **DEFERRED.md.** The diff keeps refactor's entries and adds the creative-delete entry after them.
|
||||
- **The read.** Four read-only subagents split the diff: bus, business, cli, and webui with harness. I
|
||||
re-read the code behind every finding below myself. Findings I couldn't confirm aren't listed.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
### B1. After a skipped reload, veto, hire and fire answer `restart: false`
|
||||
|
||||
- **Where.** packages/bus/src/runtime.mjs:280-282:
|
||||
|
||||
```
|
||||
const live=reload(id);
|
||||
...
|
||||
return verb==='instances.set'?{...result,restart:!live}:result;
|
||||
```
|
||||
|
||||
The writer's `commit()` always answers `restart: false` (instance-settings.mjs:386). So for
|
||||
`veto.set`, `instance.hire` and `instance.fire`, a reload that `sameFixed` refused (runtime.mjs:225-227)
|
||||
still answers `restart: false`.
|
||||
- **How a business gets there.** Brief item 7 lets the human add a credentialed definition. That add
|
||||
correctly answers `restart: true`. Its new `credentials` entry differs from `boot.roles` (`fixedRole(null)`),
|
||||
so every later reload of that business is refused until the bus restarts. A hand edit of a fixed key does
|
||||
the same.
|
||||
- **Failing scenario.**
|
||||
1. The human adds a credentialed agent and gets `restart: true`.
|
||||
2. Before restarting, the human runs `veto keep X`. The file gets X in `launch.keep`, the reload is
|
||||
refused, and the answer says `restart: false`.
|
||||
3. The broker's copy has no keep entry for X, so the chief's checks pass:
|
||||
- the keep check in `instance.fire` (broker.mjs:2570);
|
||||
- the `role.revoke` reason branch;
|
||||
- the B7 model check (broker.mjs:2478-2479).
|
||||
4. A chief fire of X runs step A: it revokes the claims, writes `lifecycle.end` with `kill: true`, and
|
||||
writes `fire.stopped` (broker.mjs:2595-2614).
|
||||
5. Step B's writer reads the file, so it refuses `instance-kept` (instance-settings.mjs:332), and
|
||||
`fire.done` is never written.
|
||||
6. X is left `instance-firing`. That refuses X's launch at the verb (:534) and at the take (:552). The
|
||||
veto the human set is what keeps the fire from finishing.
|
||||
- The same skipped reload leaves a hired instance unknown to the broker, and the hire still answers
|
||||
`restart: false`.
|
||||
- **What it breaks.**
|
||||
- DESIGN.md:428-430 (D3 item 3): "Any change that would need one of them still answers `restart: true` and doesn't
|
||||
reload."
|
||||
- DESIGN.md:408 (D2.4): "The broker enforces from its live copy of the business, which D3 keeps current."
|
||||
- The answer tells the human the veto is live when it isn't.
|
||||
- **Tests.** reload.test.mjs pins `restart: true` only for `instances.set` (:90-98, :157-163). No case
|
||||
covers a veto, hire or fire after a refused reload.
|
||||
- **Fix.**
|
||||
- Answer `restart: !live` for every committed verb, and print the restart line for veto, hire and fire in
|
||||
the CLI and the Console.
|
||||
- Add a reload test: a refused reload, then `veto.set`, which must answer `restart: true`.
|
||||
- Add a mutant that restores the `instances.set`-only condition.
|
||||
- Optional: have broker fire re-check keep against the file before step A. The writer already reads the
|
||||
file, so the check costs one read.
|
||||
|
||||
### B2. Fire ships without the SIGKILL escalation, and nothing records that
|
||||
|
||||
- **What happens.** Fire step A writes `lifecycle.end` with `kill: true` (broker.mjs:2611), and the
|
||||
launcher never reads it:
|
||||
- launcher.mjs:704 and :708 send every `lifecycle.end` to `terminate()`;
|
||||
- `terminate()` (:729-739) sends SIGTERM only.
|
||||
- So a fired runner that ignores SIGTERM keeps running with the model token.
|
||||
- No `lifecycle.end.killed` or `lifecycle.end.alive` is ever written, and the Console's "still running
|
||||
after fire" flag can't appear (BUILD.md:466-467).
|
||||
- T9a, T9b, T15 and M49-M55 are missing, and the halted checks (D1.1 item 2, D6) are missing too.
|
||||
- **What the design says.**
|
||||
- DESIGN.md D2.2: "After the grace, SIGKILL to any runner still alive ... and `lifecycle.end.killed`".
|
||||
- DESIGN.md D2.2: "Only fire escalates, because only a fired agent must lose the token at once. If row
|
||||
191 hasn't landed when this row builds, the build chains on its candidate (Q177)."
|
||||
- **What the build says.** BUILD.md:29 marks S6 "waits on row 191". Row 191 is in-progress in queue.json.
|
||||
b5 is offered to land in the 10-24 window without S6, and without a chain on 191.
|
||||
- **What I couldn't find.** A ruling that lets 194 land without S6. The DEFERRED.md diff adds only the
|
||||
creative-delete entry.
|
||||
- **Fix, either of:**
|
||||
- Build S6 on 191's candidate as the design says.
|
||||
- Get a Sage ruling that 194 lands without it, plus a DEFERRED entry. The entry names the gap (a fired
|
||||
runner can outlive SIGTERM with the token), the missing T9a, T9b, T15, M49-M55 and halted checks, the
|
||||
Console flag, and the row that binds them.
|
||||
- Since b4 lands before 191 either way, the ruling route is the cheaper one. It's Sage's call. I can't
|
||||
approve a fire that the approved design says must kill and that doesn't, with no record of the gap.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. A fire of a declared, delisted credentialed instance leaves its credentials live.
|
||||
- **Where.** instance-settings.mjs:335 returns `delisted: false` before `beforeWrite`, so `revoke` never
|
||||
runs.
|
||||
- **Scenario.**
|
||||
1. The human adds a credentialed coder-2, restarts, and issues its tokens.
|
||||
2. The human delists it with `launch: false`.
|
||||
3. The chief fires it. Fire answers done, and the tokens stay valid.
|
||||
4. The human can still launch an unlisted instance (V2).
|
||||
- **Design.** DESIGN.md D2.2 ("B finds I already delisted and counts as done") is about repeating a fire
|
||||
that partly failed. The brief's item 8 binds "before they change the business file", and this fire
|
||||
changes nothing, so this isn't a blocker as written.
|
||||
- **Fix.** Call `revoke` on that path when the entry has credentials. A revoke of an already revoked
|
||||
service should be a no-op. Add one test.
|
||||
|
||||
N2. A human add of a definition that declares the `host` service fails with the wrong message.
|
||||
- **Where.** `createdCredentials` (instance-settings.mjs:100, :127) maps every declared service, `host`
|
||||
included. `DATE_KEY` (credentials.mjs:12) has no `host`, so the entry gets a literal `"undefined"` key.
|
||||
Then `checkRoles` (business.mjs:140), which drops HOST_SERVICE, refuses with "must reference exactly the
|
||||
services its role definition needs".
|
||||
- **Effect.** It fails closed, and no shipped role declares `host`. The fix is to filter HOST_SERVICE
|
||||
first, or refuse the add by name.
|
||||
|
||||
N3. The B7 check (broker.mjs:2478-2479) looks only at `args.instances`. A chief `organization.set` that
|
||||
re-points the `models` entry a kept instance's `vars.model` names changes that instance's model at its
|
||||
next launch. DESIGN.md D2.4 says "that instance's model key". Re-pointing a shared key also changes every
|
||||
other instance on it, so I read the design as covering the instance's own key only. Say so in BUILD.md,
|
||||
or extend the check the way row 213 did for creative instances.
|
||||
|
||||
N4. A fire whose step B keeps refusing has no human escape. `#firing` (broker.mjs:537-543) clears only on a
|
||||
later `fire.done`, and only a chief fire writes one. Today only B1 makes this reachable. A human verb that
|
||||
clears it, or a take that checks keep against the file, would close it. Record it as a follow-up if B1's
|
||||
fix lands without one.
|
||||
|
||||
N5. Webui: `busStatus` (webui bus.mjs) maps none of the lifecycle refusal codes. A few examples are
|
||||
`instance-running`, `instance-kept`, `tester-off`, `tester-refused` and `unknown-request`, and all of them
|
||||
fall through to 502. The body is right. Separately, agents.js keeps polling an `unknown-request` for up to
|
||||
90 reads. Neither is harmful.
|
||||
|
||||
N6. CLI terminate (launcher.mjs:731-733) signals every registry run of a non-thread instance. That includes a
|
||||
run the chief launched after the terminate verb, whose claim was never revoked. DESIGN.md:118-120 (V3) names the
|
||||
verb's runs plus spawned-but-unbound ones. If this is meant, say so in the design's terms. The CLI text
|
||||
already hints at it.
|
||||
|
||||
N7. A delete that commits and then closes `outcome-unknown` (launcher.mjs:745-749) can't be retried. The
|
||||
broker allows a second delete of a retired name only after `workspace-rename-failed`, while DESIGN.md:173
|
||||
says a second delete "retries step 3 only" with no code limit. The window is narrow.
|
||||
|
||||
N8. DESIGN.md:625 still lists "SIGKILL escalation" under "Not in the row". That's r2 text that D2.2 r4
|
||||
replaced. It contradicts B2's design text. Don't edit the approved design. Note it in BUILD.md.
|
||||
|
||||
## Checked and fine
|
||||
|
||||
- **My design r4 notes.**
|
||||
- N1 is in: `#firing` (broker.mjs:537) refuses `instance-firing` at the launch verb (:534), at the take
|
||||
(:552) and in `role.launch` authority (:1770).
|
||||
- N2 is in: hire refuses unless roleVersion is 2 (broker.mjs:2647), and `checkHireable` refuses a version
|
||||
1 name (role.mjs:94).
|
||||
- **Creative hire.**
|
||||
- The writer refuses a CREATIVE_DEFINITIONS hire before `definitionFor` (instance-settings.mjs:304-308).
|
||||
- `checkHireable` refuses a creative name in a hireable list.
|
||||
- **endProtected.** The listing's `endProtected` and `deleteRefusal` call the same
|
||||
`endProtected` (instance-settings.mjs:32, :36, :170). agents.js:44 hides Delete on it.
|
||||
- **Reload.** The `libraries` key fixes each library entry whole, so a mapping change gets
|
||||
`restart: true` for `instances.set`.
|
||||
- **C1.** `testerNoSignIn` is gone from serve.mjs, webui cli.mjs and sso.test.mjs, with no reference left.
|
||||
- **Order and revision.** Fire runs A, B, C, and the consume happens only in C. `commit()` checks the
|
||||
revision before the revoke.
|
||||
- **Credential failures.** A failed revoke writes nothing, and the CLI text says the file is unchanged.
|
||||
- **Human tracker add.** It is required for vikunja, refused otherwise, and checked by `checkBot`, with
|
||||
bot uniqueness against the other instances and the sync bot.
|
||||
- **Human-only routes.** On the human listener, the webui routes need the signed-in Origin and Fetch Metadata
|
||||
checks. The tester listener refuses regenerate, veto and delete at the route, and the broker refuses
|
||||
them again.
|
||||
- **Escaping.** agents.js escapes every name and attribute it renders.
|
||||
|
||||
## Before the manifest round
|
||||
|
||||
The fixes for B1, and B2 or its ruling, are code and DEFERRED changes, and the gate reruns on them. I'll
|
||||
review the delta and then the manifest record. When I get a CLEAR, I'll run:
|
||||
- reload.test, lifecycle-verbs.test and chief-of-staff.test as whole files;
|
||||
- the B1 mutant;
|
||||
- a spot set of the D-table mutants, M2, M3, M22 and M27, on a scratch worktree.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Webby review: row 194 (#1692, agent lifecycle), build b6, static
|
||||
|
||||
Webby, 2026-10-21T23:52Z (from `date -u`). Author: Honker (honker-0083). Rulings in force: sage-2241,
|
||||
sage-2243, sage-2245.
|
||||
|
||||
- **Candidate.** honker/row194-b6 at af8d58c7d, one commit on b5 dfabbbe8a. 17 files, +280/-29.
|
||||
- **What I read.** The whole `git diff dfabbbe8a af8d58c7d`: source, tests, DEFERRED.md and BUILD.md "b6".
|
||||
I also read the code each fix leans on: the broker's organization gate and fire path, the runtime
|
||||
revoke, `Credentials.revoke` and `setRoutes`.
|
||||
- **What I didn't do.** No test or mutant has run. They wait for Sage's CLEAR. The row has no posted
|
||||
request for this round, so I record nothing.
|
||||
|
||||
## Verdict: CHANGES, B2 only (carried from b5)
|
||||
|
||||
B1 and N1-N3 are fixed in the code and have tests. N4 is deferred with a named test. B2 stays open
|
||||
until S6 is built on row 191's `stopRun`, per sage-2241 and sage-2245. Nothing new blocks.
|
||||
|
||||
### B1. Closed statically, runs pending
|
||||
|
||||
- **Restart.** runtime.mjs:33 and :290 answer `restart: !live` for `instances.set`, `veto.set`,
|
||||
`instance.hire` and `instance.fire`. Limits, routes and plugins writes keep their old answers, and
|
||||
reload.test pins that limits.set has no `restart` key.
|
||||
- **The fire check.** broker.mjs:2596 calls `write('instance.fireCheck')` before step A.
|
||||
- runtime.mjs:272 sends it to `checkFire` (instance-settings.mjs:351), which reads the file, runs
|
||||
`fireChecks` (:357) and returns before any write or reload.
|
||||
- `fire` runs the same `fireChecks` at step B.
|
||||
- `instance.fireCheck` appears only at broker.mjs:2596. A client that sends it gets no route to the
|
||||
writer.
|
||||
- **Printing.** The CLI veto prints RESTART_NEEDED or NO_RESTART. agents.js prints the matching
|
||||
receipt text. serve.mjs `/api/veto` needs a boolean `restart`, or it answers `outcome-unknown`.
|
||||
- **Tests.**
|
||||
- reload.test.mjs:150 runs my b5 sequence with the real writer. A credentialed add refuses the reload.
|
||||
Then hire, keep, unkeep and fire each answer `restart: true`. A fire of the name kept since refuses
|
||||
`instance-kept` with the file unchanged, the claim held and no `instance-firing`.
|
||||
- chief-of-staff T10 (new) has the writer refuse at fireCheck. The organization log holds only
|
||||
`instance.fireCheck`, the claim stays and `instance.launch` goes through.
|
||||
- The cli test and the Console tests cover the veto restart line and the shape check.
|
||||
- **What the CLEAR run must show.** Both of BUILD.md's mutants killed:
|
||||
- (a) the `instances.set`-only restart line. reload.test :70 and :150 should kill it.
|
||||
- (b) no `instance.fireCheck` call. reload.test :150 and chief-of-staff T10 should kill it.
|
||||
|
||||
### B2. Open
|
||||
|
||||
S6 isn't in b6. BUILD.md "b6" gives the `stopRun` contract (agents/rocko/work/queue-191/STOP-RUN-CONTRACT.md)
|
||||
and says 194 lands with or after row 191 in the 10-24 window. That matches sage-2241. I'll review S6
|
||||
when it's wired onto Rocko's commit. T9a, T9b, T15, M49-M55 and the halted checks come with it.
|
||||
|
||||
## The b5 notes
|
||||
|
||||
- **N1. Closed.** A fire of a delisted name now revokes with no write (instance-settings.mjs:334-339).
|
||||
- A repeat is safe. `Credentials.revoke` takes ENOENT (credentials.mjs:333-337), and the runtime
|
||||
revoke skips `credential-unknown` (runtime.mjs:151-155).
|
||||
- That path is also the repeat after a failed step C, which now revokes a second time and still goes
|
||||
on to `fire.done`.
|
||||
- The lifecycle-revoke.test case checks the receipt, the unchanged bytes, the empty store and the
|
||||
status.
|
||||
- **N2. Closed.** instance-settings.mjs:101 drops HOST_SERVICE. The new instance-settings test checks the
|
||||
references.
|
||||
- **N3. Closed for the chief.** `keptHumanOnly` runs in the limits writer on a re-point of a key a kept
|
||||
instance names and on its own `instances` key. The broker passes `instance-kept` for every verb. The
|
||||
"B7 kept instance" test (launch-settings.test.mjs:148) covers both paths for the chief, a key no kept
|
||||
instance names, and the human. The comparison `actor !== document.human` is the same one
|
||||
`creativeHumanOnly` already relies on.
|
||||
- **N4. Deferred.** The DEFERRED.md entry names the gap, a human-only verb and its test.
|
||||
- **N5, N7.** Follow-ups, as BUILD.md says.
|
||||
- **N6.** With Sage, tied to S6.
|
||||
- **N8.** Noted in BUILD.md, and the design isn't edited.
|
||||
|
||||
## New notes, non-blocking
|
||||
|
||||
N1. The tester half of N3 doesn't match what the tester can reach.
|
||||
- `limits.set` isn't in TESTER_ORGANIZATION_VERBS (broker.mjs:130). So the tester rows in the "B7 kept
|
||||
instance" test exercise the writer, and the bus never gets there.
|
||||
- `routes.set` is a tester verb. `setRoutes` (launch-settings.mjs:291-293) runs `creativeHumanOnly`
|
||||
and not `keptHumanOnly`. So the tester can change a kept instance's model through a route.
|
||||
- I didn't check `instances.set` change with `vars.model` for the tester.
|
||||
- D2.4 binds the chief, so this isn't a blocker. Either keep binds the tester too, and `setRoutes` gets
|
||||
the same check and a test, or it doesn't, and BUILD.md drops "the tester included". Sage's call.
|
||||
|
||||
N2. fireCheck reads the file without the writer's lock, so a keep written between the check and step B
|
||||
still strands the instance. The new DEFERRED entry names that case. I'm noting it only so the CLEAR
|
||||
run doesn't treat it as new.
|
||||
|
||||
## On CLEAR
|
||||
|
||||
On a scratch worktree at af8d58c7d, with TMPDIR under /mnt/storage/scratch/tmp:
|
||||
- whole files: reload, lifecycle-verbs, lifecycle-revoke and chief-of-staff (bus); instance-settings
|
||||
and launch-settings (business); cli instances; webui agents and agents-browser;
|
||||
- mutants: B1 (a) and (b) above, and M2, M3, M22 and M27 from the D table;
|
||||
- a blob check after each restore.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Webby review: row 194 (#1692, agent lifecycle), build b7, static
|
||||
|
||||
Webby, 2026-10-22T00:01Z (from `date -u`). Author: Honker (honker-0085). Rulings in force: sage-2241,
|
||||
sage-2245, sage-2246, sage-2248, sage-2249.
|
||||
|
||||
- **Candidate.** honker/row194-b7 at 0f0e2c7ef, nine commits on dcd8d4dda. dcd8d4dda is origin/refactor
|
||||
after row 254 landed.
|
||||
- **What I didn't do.** Nothing has run yet. Runs wait for Sage's CLEAR, after Rocko's row 191 slot. No
|
||||
request is posted for this round, so I record nothing.
|
||||
|
||||
## Verdict: CHANGES, B2 only (carried)
|
||||
|
||||
The rebase is clean, and the routes.set fix closes my b6 N1. B2 (S6) is still open, as ruled in sage-2241.
|
||||
|
||||
### The rebase
|
||||
|
||||
- **Commits.** `git range-diff da9555ca2..af8d58c7d dcd8d4dda..b0aed7527` gives `=` for six of the seven
|
||||
commits. The seventh, commit 2 (30f05f3d2), differs only in the channel-proof pin for webui/src/bus.mjs
|
||||
and in the STATUS context lines next to row 254's entries.
|
||||
- **channel-proof.** At 0f0e2c7ef, all four baseline pins match the sha256 of their files: human.mjs,
|
||||
human-cli.mjs, cli transport.mjs and webui bus.mjs (15e8123f). The diff against the base swaps row 254's
|
||||
pin for the merged file's and adds row 194's comment. Row 254 had no comment line of its own to lose.
|
||||
- **webui/src/bus.mjs.** Against dcd8d4dda, the diff is the two row 194 tracker entries. STATUS has no
|
||||
duplicate key.
|
||||
- **DEFERRED.md.** Against dcd8d4dda, the diff adds row 194's two entries, creative delete and the stuck
|
||||
fire (N4). It removes nothing.
|
||||
- **Drift.** da9555ca2..dcd8d4dda reaches the manifest on only those three paths: DEFERRED.md,
|
||||
channel-proof.test.mjs and webui bus.mjs. The b7 manifest is the same 53 paths as b6, with
|
||||
PRD-1.8-DRAFT left out.
|
||||
|
||||
### b6 N1, closed statically
|
||||
|
||||
6a742d371 calls `keptHumanOnly` in `setRoutes` right after `creativeHumanOnly` (launch-settings.mjs:294-295).
|
||||
The test "B7 kept instance: its route is the human's" covers three cases:
|
||||
- the chief is refused with `instance-kept` and the bytes unchanged;
|
||||
- the tester gets the same, which is now reachable on the bus because routes.set is a tester verb;
|
||||
- the human re-routes the instance.
|
||||
|
||||
M-next, which drops the call, has to be killed on the CLEAR run.
|
||||
|
||||
### The behaviour change (sage-2246)
|
||||
|
||||
An agent can no longer re-point a `launch.models` key that a kept instance names, and a shared key
|
||||
counts. I agree. Re-pointing a shared key changes the kept instance's model, which is what D2.4 B7
|
||||
protects. The chief still adds keys and changes the ones no kept instance names, and b6's test covers
|
||||
that.
|
||||
|
||||
### The mutant (a) correction
|
||||
|
||||
Right. The CLI test and the Console tests stub the bus, so only reload.test can kill (a).
|
||||
|
||||
## On CLEAR
|
||||
|
||||
Honker's runb7.sh list covers the whole files and mutants (a), (b) and M-next. I'll run on my own scratch
|
||||
worktree at 0f0e2c7ef, with TMPDIR under /mnt/storage/scratch/tmp:
|
||||
- **Row 194's files, each alone:** reload, chief-of-staff, lifecycle-verbs, lifecycle-revoke and
|
||||
channel-proof (bus); instance-settings and launch-settings (business); cli instances; webui agents and
|
||||
agents-browser.
|
||||
- **Rebase files:** webui library-status.test and bus library-sync.test. Row 254's tests ran against
|
||||
its own bus.mjs, and this merge changes that file.
|
||||
- **Mutants:** (a), (b), M-next, and M2, M3, M22 and M27, with a blob check after each restore.
|
||||
|
||||
B2 stays open until the S6 commit, with N6 narrowed as sage-2246 rules. I'll review it when it arrives.
|
||||
@@ -0,0 +1,307 @@
|
||||
# Webby review: row 194 (#1692, agent lifecycle), build b8 r2, static
|
||||
|
||||
Webby, 2026-10-22T00:41Z (from `date -u`). Author: Honker (honker-0092, honker-0094). Rulings in force: sage-2241,
|
||||
sage-2246, sage-2260, sage-2261, sage-2289.
|
||||
|
||||
- **Candidate.** origin/honker/row194-b8 at 899e30a80, on row 191's tip eb3abba12
|
||||
(origin/rocko/row191-stoprun-dcd8-20261022, four commits on dcd8d4dda). b8 r2 is b8 (0583569b7) plus
|
||||
one commit, and 0583569b7 is its parent.
|
||||
- **Manifest.** `git diff --name-only eb3abba12 899e30a80` gives 54 paths. Leaving out
|
||||
agents/honker/work/queue-194/PRD-1.8-DRAFT.md gives 53. None is under packages/processing-control, and
|
||||
none is a row 191-only path.
|
||||
- **What I read.**
|
||||
- The b7 to b8 range-diff, `git range-diff dcd8d4dda..0f0e2c7ef eb3abba12..0583569b7`, and every
|
||||
interdiff of the three `!` commits.
|
||||
- The whole S6 commit b3341c1cb and the r2 commit 899e30a80.
|
||||
- BUILD.md "b8 notes" and "b8 r2: sage-2289".
|
||||
- Row 191's `stopRun`, `ownStop` and `sweepRuntimes` (processing-control/src/runtimes.mjs at
|
||||
eb3abba12).
|
||||
- DESIGN.md V3 (:112-124), D2.2 (:276-312), D6 (:485-496), D7 (:498-503) and the T9a, T9b, T15 and
|
||||
M49-M55 rows.
|
||||
- **What I didn't do.** No test or mutant has run. They wait for Sage's CLEAR. The row has no posted
|
||||
request for this round, so I record nothing.
|
||||
|
||||
## Verdict: no blockers. B2 is closed statically, and APPROVE waits on the runs
|
||||
|
||||
B2 (S6) is built on row 191's `stopRun` as D2.2 asks. Terminate stays SIGTERM-only, and the halted
|
||||
checks match D6. The unknown outcome follows sage-2289. I found nothing new that blocks. The round's
|
||||
APPROVE depends on my CLEAR run below plus the full gate with Docker and P3 (sage-2261).
|
||||
|
||||
## The rebase onto 191
|
||||
|
||||
- Six commits are `=`. Commits 1, b4 and b6 are `!`, and in each one I checked, both sides are kept with
|
||||
no change to 194's behaviour. BUILD.md lists these file by file, and the interdiffs match:
|
||||
- the broker constructor ends `processing = null, testerOn = null, roleFile = null`;
|
||||
- runtime.mjs keeps 191's `broker = new Broker(...)`, `broker.reconcileProcessing(); processing.start();`
|
||||
and its imports, with `INSTANCE_VERBS` after them;
|
||||
- launcher `start()` takes `human = false, admissionGeneration`, and 191's `admission(...)` calls stay;
|
||||
- webui cli and serve pass both `agentWrite` and `processing`, and C1's `testerNoSignIn` removal sits
|
||||
beside 191's tester `processing` read;
|
||||
- the browser.test script pin is `['/editor.js','/processing.js','/brand.js','/app.js',
|
||||
'/markdown-preview.js','/chat.js','/agents.js','/bus.js']`.
|
||||
- 191 also changed 10 of the 194 paths. That's why these resolutions get a full run and not a carried
|
||||
verdict.
|
||||
|
||||
## B2 (S6) against the design
|
||||
|
||||
- **Fire goes through 191's stop.** `act` (launcher.mjs:763-770) sends a `kill: true` end to `fireStop`
|
||||
(:819). That calls `stopRun` with reason `fire` and owner `{ decision: <request> }` (:825), which is
|
||||
the shape `stopRun` accepts. `stopRun` checks identity before each signal, sends SIGKILL after the
|
||||
grace and observes. D2.2 items 3a-3c are 191's code, not a second copy.
|
||||
- **Terminate is SIGTERM only (V3).** `terminate` (:804) sends SIGTERM and nothing else. The
|
||||
T9a terminate test (lifecycle.test.mjs:670) pins one SIGTERM and no fire-stop record. M54 targets it.
|
||||
- **Outcomes.** `lifecycleDone` (broker.mjs:2322) accepts `stops` only on a `kill: true` end (:2343). It
|
||||
writes `lifecycle.end.killed` or `.alive` against the instance with run and pid. The pid check (`null`
|
||||
or a safe integer above 1) matches 191's identity check (`pid < 2` fails). The test at
|
||||
chief-of-staff.test.mjs:899 covers the refusals.
|
||||
- **Unknown, r2.** `fireStop` (launcher.mjs:835) maps `unknown` to alive with reason `unknown`, and the
|
||||
broker accepts that reason only on an alive stop (broker.mjs:2335). That closes more than the
|
||||
no-record case. `ownStop` returns a stored result before it signals (runtimes.mjs:267). So a run whose
|
||||
earlier halt sweep ended `unknown`, if it survived to a later fire, gets no signal from the fire, and
|
||||
r2 now flags it. Before r2 it would have vanished from the fire's report. The same holds for "another
|
||||
host owns delivery" and for a `stopRun` that throws (launcher.mjs:828). The new T9a test (:597) and
|
||||
M56 cover it.
|
||||
- **Console (D7).** `#aliveAfterFire` (broker.mjs:2351) lists alive runs with no `session.ended`, and the
|
||||
instances read adds them as `alive` (:2633). agents.js escapes each run with `txt`. The
|
||||
agents-browser test puts a hostile run name through it.
|
||||
- **Halted (D6).**
|
||||
- `#launchable` (broker.mjs:617) is used only by `instance.launch` and `instance.regenerate`. So
|
||||
`instance.end` keeps no halted check, and terminate works while halted (V3 item 5).
|
||||
- The take re-checks for every action except end and delete (:636-647).
|
||||
- Hire and fire refuse through 191's work gate, since neither is on its read list.
|
||||
- The tick (launcher.mjs:583) skips the thread and land passes while halted and still runs the
|
||||
lifecycle pass.
|
||||
- T15 covers the bus (lifecycle-verbs.test.mjs:150) and the cli (lifecycle.test.mjs:718).
|
||||
- **Halt joining a fire (T9b).** The sweep's `stopRun` gets the fire's in-flight promise from 191's
|
||||
`stopping` map. So it sends no second SIGTERM and doesn't restart the deadline, and the receipt keeps
|
||||
reason `fire` and the decision. lifecycle.test.mjs:686 checks one SIGTERM, the deadline, the single
|
||||
receipt and the counted run. M55 and M55-deadline target it.
|
||||
- **N6 (sage-2246).** `ending` (launcher.mjs:795) takes the verb's runs plus non-thread registry runs
|
||||
spawned at or before the end's `at`, using `spawnedAt` (:500) or the registry `startedAt`. That
|
||||
matches DESIGN.md:118-120. lifecycle.test.mjs:751 checks that a later run survives and the next end
|
||||
takes it. Fire uses the same selection. The only spawn that can follow a fire's verb is a human
|
||||
launch already taken, and D2.2 (:300-302) leaves that one to the human's terminate. So the narrowing
|
||||
doesn't cut into F4.
|
||||
- **Regenerate** now signals every registry run of the instance (launcher.mjs:772), with no N6 cut.
|
||||
It waits for an empty registry before starting, and a thread instance refuses regenerate at the verb.
|
||||
That's b7's behaviour, and it's right.
|
||||
- **Tests and mutants present.** T9a has four cli tests (:562, :597, :619, :670) and one bus test. T9b,
|
||||
T15 (bus and cli), N6 and the agents-browser flag are all there. BUILD.md lists M49-M56,
|
||||
M55-deadline, N6-widen, D7 and S6-done with named kill tests. The DESIGN.md test table names
|
||||
`launcher.test.mjs`, and the tests are in `lifecycle.test.mjs`. The b8 notes record that deviation,
|
||||
and I agree with it.
|
||||
|
||||
## Notes, non-blocking
|
||||
|
||||
N1. The clock caveat is narrower than BUILD.md says. The bus's `#now` (broker.mjs:524-526) is monotonic:
|
||||
`max(Date.now(), clock + 1)`. So after a clock step back, the end's `at` stays later than the launcher's
|
||||
spawn times, and the pass signals more runs, which was b7's breadth. It doesn't signal fewer. A run can
|
||||
be missed only when the clock steps back between its spawn and the verb, with no bus event in between to
|
||||
carry the clock forward. That's fine as it stands. BUILD.md could say which direction the error goes.
|
||||
|
||||
N2. `stops` is capped at 64 (broker.mjs:2330). A fire of an instance with more runs than that gets
|
||||
`invalid-request` from `lifecycleDone`. The request stays taken, the CLI reads outcome-unknown, and no
|
||||
killed or alive event is written, so the Console flags nothing. No business is near 64 runs of one
|
||||
instance today. Either raise the cap to the registry's real limit, or add one line to DEFERRED.
|
||||
|
||||
N3. The CLI `instances` output (instances.mjs:201) prints `running` and not `alive`. D7 binds only the
|
||||
Console, so a human on the CLI never sees "still running after fire". A follow-up row, not this one.
|
||||
|
||||
N4. An alive flag clears only on the run's `session.ended`. A run this host never spawned (the
|
||||
no-record case) has no exit handler here, so its flag stays until something writes `session.ended`. A
|
||||
false flag is the safe side per sage-2289. I'm noting it so nobody reads a lingering flag as a bug in
|
||||
the round.
|
||||
|
||||
N5. With r2, the Console says "still running after fire" for a stop that couldn't be confirmed, and the
|
||||
run may not be running. If a short change is cheap, "not confirmed stopped after fire" for
|
||||
`reason: unknown` would be more accurate. Cosmetic.
|
||||
|
||||
## On CLEAR
|
||||
|
||||
On my own scratch worktree at 899e30a80, with TMPDIR from `mktemp -d /mnt/storage/scratch/tmp/...`:
|
||||
- **Whole files, one at a time.**
|
||||
- bus: reload, chief-of-staff, lifecycle-verbs, lifecycle-revoke and channel-proof.
|
||||
- business: instance-settings and launch-settings.
|
||||
- cli: instances, lifecycle, launcher, launch-limit-checks, launch-limits, launch-routes,
|
||||
launch-account and thread-launch.
|
||||
- webui: agents, agents-browser and browser.
|
||||
- library: status and sync.
|
||||
- **Skips.** lifecycle.test skips when `sandboxProblem()` reports one (:33). I'll grep `^skip` and
|
||||
`# SKIP` and name every skip, because a skipped T9a isn't a pass.
|
||||
- **Mutants, with a blob check after each restore.**
|
||||
- B1 (a) and (b), and M-next.
|
||||
- From the D table: M2, M3, M16, M22 and M27.
|
||||
- S6: M49-M56 and M55-deadline (M52, M55 and M55-deadline edit 191's runtimes.mjs), plus N6-widen,
|
||||
D7 and S6-done.
|
||||
- The anchors are mine. I don't use Honker's runb8.sh.
|
||||
|
||||
## Final candidate 062375507 (honker-0098, sage-2290, sage-2291)
|
||||
|
||||
Checked at 2026-10-22T00:46Z (from `date -u`).
|
||||
- origin/honker/row194-b8 is 062375507, two commits on 899e30a80: 95c0e1220 (the N1-N5 notes) and 062375507
|
||||
(the N4 correction). Both are doc-only.
|
||||
- `git diff 899e30a80 062375507 -- packages` is empty. The changed paths are BUILD.md and
|
||||
docs/plans/DEFERRED.md. With the PRD draft left out, the manifest is still 53 paths.
|
||||
- **The DEFERRED line names what sage-2291 asked for.** The cap is 64 stops in `lifecycleDone`, and
|
||||
the refusal is `invalid-request`. The follow-up test is a done over the cap that refuses, while the
|
||||
runs it left alive still get the flag. Its consequence is right too. fireStop signals before the done,
|
||||
and `fire.done` is written in the verb, so only the events and the flag are lost.
|
||||
- **N1 as corrected in BUILD.md is still too strong.** "A run spawned before the verb can't fall
|
||||
outside it" holds only if some bus event moved `#clock` past the spawn. Nothing on the bus is written
|
||||
between a spawn and its bind, so a clock step back in that gap can still put the spawn after `at`.
|
||||
That's the narrow case from my N1. It's doc-only, so it doesn't block (decision 137), and it needs no
|
||||
new commit.
|
||||
- The static review carries to 062375507. All runs use 062375507.
|
||||
|
||||
## Rebased candidate 6411a31c0, origin/honker/row194-b9 (sage-2295, sage-2296, honker-0100)
|
||||
|
||||
Checked at 2026-10-22T01:08Z (from `date -u`).
|
||||
- 6411a31c0 is 062375507's 14 commits on Rocko's e6800380d, which is one test-only commit on eb3abba12
|
||||
(packages/discord/tests binding.test.mjs and recover.test.mjs, +2 each).
|
||||
- `git diff 062375507 6411a31c0 -- packages` is byte-for-byte `git diff eb3abba12 e6800380d`. Nothing
|
||||
outside packages changed.
|
||||
- `git range-diff eb3abba12..062375507 e6800380d..6411a31c0`: all 14 commits are `=`.
|
||||
- Against e6800380d the manifest is 53 paths without the PRD draft. None is under processing-control or
|
||||
packages/discord.
|
||||
- The static review carries to 6411a31c0. All runs use 6411a31c0.
|
||||
|
||||
## Rebased candidate b52b17514, origin/honker/row194-b10 (sage-2318, honker-0103)
|
||||
|
||||
Checked at 2026-10-22T01:59Z (from `date -u`).
|
||||
- b52b17514 is 6411a31c0's 14 commits on Rocko's 8e1f55c1d. 8e1f55c1d's parent is e6800380d, and it is a
|
||||
product fix, not a test-only one: webui processing.css and processing.js, plus processing-browser.test.mjs
|
||||
and processing-ui.test.mjs.
|
||||
- `git diff 6411a31c0 b52b17514 -- packages` is byte-for-byte `git diff e6800380d 8e1f55c1d`. Nothing
|
||||
outside packages changed.
|
||||
- `git range-diff e6800380d..6411a31c0 8e1f55c1d..b52b17514`: all 14 commits are `=`.
|
||||
- Against 8e1f55c1d the manifest is 53 paths without the PRD draft. None is under processing-control, and
|
||||
194 touches neither processing.js nor processing.css nor Rocko's two tests.
|
||||
- **Where the two meet.** 194 changes webui src/public/index.html (it adds `/agents.js` to the script
|
||||
line), and Rocko's processing-ui.test.mjs:66 reads index.html for the processing controls. Its asserts
|
||||
are on the processing nodes and the processing.js and processing.css tags, which 194 leaves alone, so I
|
||||
expect a pass. shell.test.mjs:40 also reads index.html with app.js and bus.js, and it wasn't on my list.
|
||||
- **Run list change.** I add webui processing-ui, processing-browser and shell to the whole files, which
|
||||
makes 23. The rest of "On CLEAR" stands.
|
||||
- The static review carries to b52b17514. All runs use b52b17514.
|
||||
|
||||
## Merge-scoped carry to b11 469f441e5, origin/honker/row194-b11 (honker-0105, sage-2330, mos-1160)
|
||||
|
||||
Checked at 2026-10-22T02:08Z (from `date -u`).
|
||||
- **Base.** eafcbd3e7 is Rocko's six 191 commits restacked on row 202's landing ddfb29af7. It is not a
|
||||
descendant of 8e1f55c1d. `git range-diff dcd8d4dda..8e1f55c1d ddfb29af7..eafcbd3e7` gives five `=` and one
|
||||
`!`. The `!` is commit 1 (9a7251403), and its only change is the scripts/mosaic dispatch line, which now
|
||||
carries 202's `access`. `git diff 8e1f55c1d eafcbd3e7 -- packages/processing-control` is empty, so the
|
||||
`stopRun` that S6 relies on hasn't changed.
|
||||
- **The 14 commits.** `git range-diff 8e1f55c1d..b52b17514 eafcbd3e7..469f441e5`: all 14 are `=`.
|
||||
- **b10 to b11.** `git diff b52b17514 469f441e5` touches the same 15 files as `git diff 8e1f55c1d eafcbd3e7`.
|
||||
The added and removed lines match file by file, and the only differences are hunk offsets.
|
||||
- **Tree.** `git merge-tree --write-tree b52b17514 ddfb29af7` (merge base dcd8d4dda) gives 510b79875. That
|
||||
tree differs from b11 only in scripts/mosaic. It has one conflict there, and b11 resolves it to
|
||||
`halt|resume|processing|...|thread|access`, which keeps both sides. 194's `instances|instance|veto` line
|
||||
(:32) is unchanged.
|
||||
- **Overlap with 202.** Only two paths are in both 202 and the 194 manifest: packages/cli/src/cli.mjs and
|
||||
scripts/mosaic. In b11's cli.mjs, 202's import is at :23 and its `if (verb === "access")` block is at :619.
|
||||
- **Manifest.** Against eafcbd3e7 it's still 53 paths without the PRD draft. None is under
|
||||
processing-control or access.
|
||||
- **Run list change.** 202's only test is packages/cli/tests/access.test.mjs. It reads cli.mjs and
|
||||
runtime.mjs, and 194 changes both, so I'm adding it. That makes 24 whole files. The mutant list stands.
|
||||
- The static review carries to 469f441e5. All runs use 469f441e5.
|
||||
|
||||
## Rebased candidate c13134eb1, origin/honker/row194-b12 (honker-0107)
|
||||
|
||||
Checked at 2026-10-22T02:33Z (from `date -u`).
|
||||
- 0f8235277 is two commits on eafcbd3e7: 4db8b5778, a one-line change in
|
||||
packages/webui/tests/processing-browser.test.mjs, and Rocko's packet BUILD.md.
|
||||
- `git diff 469f441e5 c13134eb1` is byte-for-byte `git diff eafcbd3e7 0f8235277`.
|
||||
- `git range-diff eafcbd3e7..469f441e5 0f8235277..c13134eb1`: all 14 commits are `=`.
|
||||
- Against 0f8235277 the manifest is 53 paths without the PRD draft. Neither of Rocko's two paths is in it.
|
||||
- The static review carries to c13134eb1. The run list (24 whole files) and the mutants stay as they are,
|
||||
and all runs use c13134eb1.
|
||||
|
||||
## b13 7609210a3 carry check (honker-0110, sage-2458)
|
||||
|
||||
Git reads only. No tests have run yet.
|
||||
|
||||
- The 14 commits sit on refactor 0fa7bf421, the landed 191. The base of b12 c13134eb1 was 0f8235277.
|
||||
- range-diff 0f8235277..c13134eb1 against 0fa7bf421..7609210a3 shows 14 of 14 `=`.
|
||||
- The series patch-id --stable is 086085a8dfe41a47cdaa0b12effa8d4542783637 on both sides.
|
||||
- The path list is identical, 54 paths.
|
||||
- The drift 0f8235277..0fa7bf421 covers 14 files. The only one 194 also touches is packages/webui/src/public/index.html. 191 changes lines 14 and 20-22, and 194 changes line 7 (adds agents.js to the script line), so the hunks are separate.
|
||||
- Tests that read index.html: agents.test (in 194, row 194 N5 at :214), processing-ui.test:66 and shell.test:40. None of them pins the script list. processing-ui checks the controls' placement and attributes, and shell checks the joined source. stack-release.sh drift is read only by the cli stack-* tests, and 194 touches none of those.
|
||||
- My run list stays at 24 files. processing-ui, processing-browser and shell are already in it since b10. Runs go on 7609210a3 at Sage's CLEAR, after Honker's runb8.
|
||||
|
||||
## b14 b565e589b carry check (honker-0113, sage-2493, sage-2498)
|
||||
|
||||
b14 is a carry of b13 with no blockers. These are git reads only. I did no runs, because the slot is Honker's.
|
||||
- 14 commits on refactor 23c3d6218.
|
||||
- range-diff 0fa7bf421..7609210a3 against 23c3d6218..b565e589b shows 14 of 14 `=`.
|
||||
- The series patch-id is 086085a8dfe41a47cdaa0b12effa8d4542783637 on both sides.
|
||||
- The path list is identical.
|
||||
- The drift 0fa7bf421..23c3d6218 (205 and 268, 24 files) meets 194 at two paths, broker.mjs and business.test.mjs. For both files, the +/- lines of 194's own diff are identical on the old and new bases. So 194's hunks are unchanged, and git applied them without touching 205's or 268's lines.
|
||||
- Whether the merged files run together is a matter for Honker's runs on b14, which Sage is gating.
|
||||
|
||||
## b15 ce7fd1b88 delta review (honker-0119, sage-2501/2503/2507)
|
||||
|
||||
Verdict: APPROVE, no blockers. These are git reads only, and nothing has run on b15.
|
||||
- b15 is b14 b565e589b plus two commits.
|
||||
- range-diff: commits 1-14 show `=`, plus new commits 15 (b73452287) and 16 (ce7fd1b88).
|
||||
- The series patch-id is 64303253f8e6. The path list is the same as b14's.
|
||||
- Neither new commit has an attribution line.
|
||||
|
||||
(a) b73452287, broker.mjs +4/-3:
|
||||
- The three function-argument checks (harnessOn, testerOn, roleFile) move to just after `invalid-business`, ahead of `this.#store` and every store read. Each check's text is unchanged. One new comment line goes above them, and the descriptive comments stay with their assignments.
|
||||
- Behaviour changes only when a bad function argument and a store failure occur together. The argument error now wins, and that is what T8b expects (chief-of-staff.test:868, `store: null`).
|
||||
- The only other pin on these messages is routes-verb.test:89, which uses a real store and is unaffected.
|
||||
|
||||
(b') ce7fd1b88, test-only:
|
||||
- T15 removes the exited reviewer stand-in's registry entry after `x.exited === 143`.
|
||||
- The refusal it avoids comes from business/src/launch.mjs:123, which refuses when a running session has no exact provider/model. The stand-in's entry is a fixture artifact, because the launcher's exit handler removes only the runs it started.
|
||||
- This is the same convention as N6 at lifecycle.test:763-764. standIn is unchanged.
|
||||
- I accept Sage's ruling of (b') over (b). Giving the stand-in entry a model would only move the refusal to the `{ sonnet: 1 }` limit.
|
||||
- The second remove in standIn's t.after is a no-op.
|
||||
|
||||
N1, non-blocking: no test pins the `testerOn must be a function` message. This is old, not new in b15.
|
||||
|
||||
Manifest digest 4aaa3316 (honker-0119, sage-2509): its 53 paths equal git's path list 23c3d6218..ce7fd1b88 minus the PRD draft, and every sha256 in it matches the blob at ce7fd1b88.
|
||||
|
||||
## b16 9cdc47d04 carry check (honker-0122, sage-2521)
|
||||
|
||||
b16 is a carry of b15 with no blockers. These are git reads only, and nothing has run.
|
||||
- 16 commits on ceadad993, which is origin sage/row279-carry-343b.
|
||||
- range-diff 23c3d6218..ce7fd1b88 against ceadad993..9cdc47d04 shows 16 of 16 `=`.
|
||||
- The series patch-id is 64303253f8e6, the same as b15. The path list is identical.
|
||||
- No commit has an attribution line.
|
||||
- The drift 23c3d6218..ceadad993 is 26 files, and none of them is a 194 path.
|
||||
|
||||
The drift does reach code that 194 runs. 279 changes processing-control/src/runtimes.mjs:333 in ownStop, from `while (now() <= deadline)` to `for (;;)`. Now the loop's only exit at the deadline is the final status check, which returns alive, killed, stopped or identity-changed. Before, the loop could fall through to `finish('unknown')`.
|
||||
- 194's S6 fire stop calls stopRun (lifecycle T9a/T9b, and the launcher's fireStop maps `unknown`). So the outcomes 194 sees at the deadline can change.
|
||||
- The whole runb8 rerun on b16 settles that. It is required, and a carry from b15's runs would not cover it.
|
||||
- M52, M55 and M55-deadline mutate this file. Before the mutants run, each anchor must still match runtimes.mjs at ceadad993 exactly once. The broker.mjs anchors were checked the same way at b15. 279 changed only line 333, so the anchors are probably intact, but the runner has to confirm it, not assume it.
|
||||
|
||||
Anchors (honker-0124): I spot-checked them on 9cdc47d04, whose runtimes.mjs is the same as ceadad993's. M52's line occurs once, M55/M55-deadline's ` let problem = null;` once, and the unknown return at :294 is there. Before the mutants run, runb8 asserts count==1 for each anchor. My b16 condition is met.
|
||||
|
||||
## b17 3f451772d carry (honker-0130), APPROVE
|
||||
|
||||
b16 9cdc47d04 was rebased onto b0fd28b6f (row 280 landed). I checked this from the canonical checkout.
|
||||
|
||||
- 3f451772d is honker/row194-b17. b0fd28b6f is its ancestor, with 16 commits on top.
|
||||
- `git range-diff ceadad993..9cdc47d04 b0fd28b6f..3f451772d` shows all 16 commits `=`.
|
||||
- The series patch-id is 64303253f8e6 on both bases.
|
||||
- The changed-path list is the same on both bases. The packages diff from b16 to b17 is only 280's three bus test files, and none is in 194's manifest. No 194 test or script mentions them.
|
||||
- DEFERRED.md is the only path where 280 and 194 overlap. 194's +/- lines there are byte-identical on both bases (same md5 over the diff lines).
|
||||
- The manifest file manifest-b17-3f451772d.txt has 53 paths and sha256 29f131ab...694afc. Its paths match `git diff --name-only b0fd28b6f 3f451772d`, minus PRD-1.8-DRAFT. Every blob matches `git show 3f451772d:path`. Of the manifest paths, only DEFERRED.md (f878b3de) differs from b16.
|
||||
- No attribution lines.
|
||||
|
||||
Verdict: APPROVE carries from b16 to 3f451772d. Once the review request naming 3f451772d is posted, I record it on the queue.
|
||||
|
||||
## b18 fd6dd327c carry (honker-0132, sage-2546), APPROVE
|
||||
|
||||
b17 3f451772d was rebased onto e122aad8e. The new base commits are 4c4b8559c (BUILD-LOG, SESSIONS) and e122aad8e (QUEUE.md, queue.json). Neither touches a 194 manifest path; I checked that before the rebase.
|
||||
|
||||
- fd6dd327c is honker/row194-b18. e122aad8e is its ancestor, with 16 commits on top.
|
||||
- `git range-diff b0fd28b6f..3f451772d e122aad8e..fd6dd327c` shows all 16 commits `=`. The series patch-id is 64303253f8e6.
|
||||
- The changed-path list is the same on both bases. `git diff 3f451772d fd6dd327c` lists only BUILD-LOG.md, docs/SESSIONS.md, QUEUE.md and queue.json, all from the base move.
|
||||
- The manifest file manifest-b18-fd6dd327c.txt has sha256 29f131ab..., the same digest as b17. Its 53 paths match the diff minus PRD-1.8-DRAFT, and every blob matches `git show fd6dd327c:path`.
|
||||
- No attribution lines.
|
||||
|
||||
Verdict: APPROVE carries to fd6dd327c. Once the request naming fd6dd327c is posted, I record it on fd6dd327c.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Webby review: row 194 design r2 (#1692, agent lifecycle and per-agent veto)
|
||||
|
||||
Webby, 2026-10-20T19:31Z (from `date -u`). Independent reviewer. Author: Honker.
|
||||
Subjects at c0a35f47a on honker/row194, both sha256 verified: `agents/honker/work/queue-194/DESIGN.md`
|
||||
(313c206e...fc25) and `PRD-1.7-DRAFT.md` (98f4f49e...9708). I read the whole of both and the diff
|
||||
from 14a6f5f9d. Code read at 53bb59462. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
One blocker. B3-B8, Q2 and N1-N8 from webby-0003 are fixed as the closing table says. I checked
|
||||
each against the text. B1 is fixed: hard waits, no grant, and a PRD 1.7 draft for the routed form.
|
||||
B2's floor is the gap.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. The hire floor is a deny list, and it misses actions a hireable definition can hold today.
|
||||
D2.3 refuses seven actions. role.mjs:40-43 already refuses the hard waits in both lists, so what is
|
||||
left is the routed set (vocabulary.mjs ROUTED). The floor doesn't name these:
|
||||
- `git.merge.protected`. `roles/merge-gate.json` holds it in crossRole and declares no credentials,
|
||||
so it passes D2.2 item 3 and the floor. Listed in `hireable`, the chief could hire its own merge
|
||||
gate. That is the same author-is-reviewer problem `review.verdict` is on the list for.
|
||||
- `decision.resolve.technical`. A hired holder resolves technical decisions, so the chief would pick
|
||||
who answers its own requests. `cto.json` is credentialed today, but a credential-free role
|
||||
holding it would pass.
|
||||
- `policy.change` and `prd.approve`. Both are routed. A credential-free role holding either passes.
|
||||
- `instance.name`. It was in my r1 B2 list, which Mos accepted (mos-1018), and it is missing here.
|
||||
Every action added to the vocabulary later is also hireable by default, which is the wrong
|
||||
direction for a fail-closed floor (AGENTS.md invariant 6).
|
||||
Fix: turn `HIRE_FLOOR` into an allowlist, `HIRE_ALLOWED`. A definition is hireable only when every
|
||||
action in withinRole and crossRole is on it. From the role files at 53bb59462, the list a working
|
||||
hire needs is `message.send`, `record.write`, `review.request`, `task.update.assigned`,
|
||||
`task.scope.change` and `git.push.working`. The last two only matter after row 199 (credentials).
|
||||
T8b and T8c then loop over `ACTIONS` minus the allowlist, one case each, so a new action is covered
|
||||
without a test edit. M30-M43 become "one action added to the allowlist in the broker" and "in
|
||||
role.mjs". Update the PRD draft's bound to match: "The chief hires only role definitions whose
|
||||
actions are all on a fixed list of working actions. Adding to that list is a PRD change."
|
||||
|
||||
## Q4 (for Sage, my view)
|
||||
|
||||
Start `hireable` empty. Under the hard-wait form Jason resolves each hire anyway. With the B1
|
||||
allowlist, today's credential-free definitions that pass are assistant and the five chief-officer
|
||||
roles (message.send and record.write only). assistant is END_PROTECTED. A first kind is a policy
|
||||
call and belongs in its own reviewed commit.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. D2.1 step 3 compares the call's arguments to "the decision body". Name the field-by-field
|
||||
comparison (business from the cap, name, definition, canonical vars). A body with extra keys should
|
||||
refuse, not be ignored.
|
||||
N2. The PRD draft's notes say "Replace" covers the model write. The REQ-COS-3 text itself says
|
||||
only "fire, replace or end". Put the model write in the requirement text, so a later reader of the
|
||||
PRD alone sees it.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Webby review: row 194 design r3 (#1692)
|
||||
|
||||
Webby, 2026-10-20T19:32Z (from `date -u`). Author: Honker. Subjects at d0afda024 on honker/row194:
|
||||
DESIGN.md sha256 885fce0d...65fc, PRD-1.7-DRAFT.md sha256 04c32917...d728 (verified). Read as the
|
||||
diff from c0a35f47a, which my r2 review (REVIEW-r2.md, webby-0010) covered. r3 crossed with webby-0010
|
||||
and doesn't answer it yet. Static review.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
r2 B1 stands in part. r3 adds `instance.name` to the floor. `git.merge.protected` (merge-gate.json,
|
||||
crossRole, no credentials), `decision.resolve.technical`, `policy.change` and `prd.approve` still
|
||||
pass it, and so will any action added later. The fix is unchanged: an allowlist, as in REVIEW-r2.md.
|
||||
M47 and M48 fold into the allowlist mutants.
|
||||
|
||||
## New blocker
|
||||
|
||||
B2. Fire can't be "in the same transaction" as the delist. The delist is a business file commit
|
||||
through the organization writer (organization.mjs write: lock, rename, receipt). The claim revoke
|
||||
and the `lifecycle.cancelled` and `lifecycle.end` events are store rows. No transaction spans the
|
||||
two. Fix: name the order and the failure between them. I'd write the store side first (revoke,
|
||||
cancel, end), then commit the delist. If the delist fails, the runs are ended and I is still
|
||||
listed, so the chief can fire again, and that second fire must work with no claims left. The other
|
||||
order leaves a delisted agent with a live process and the model token, which is the F4 case. Test
|
||||
a writer failure after the store commit, and add a mutant that swaps the order.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. REQ-LAND-2 says "Gitea itself then refuses a push to upstream or to another agent's fork". That
|
||||
holds only if the agent's Gitea user has no write access to those repos. Gitea allows a push from any
|
||||
collaborator. The bound should be the host's check that the remote's owner is the agent's own user,
|
||||
made at each push. Gitea's refusal is the second layer. Megavolt is checking this text against row
|
||||
199, so I'm sending it there as a note, not a blocker here.
|
||||
N2. F4's 10 s recheck reads a pid. Use the same start-time check as the signal (launcher.mjs:526),
|
||||
so a reused pid isn't reported as the fired run.
|
||||
|
||||
The rest of r3 reads right: F4's reason (ENV_ALLOW, session.mjs:83-94), T9a, M49-M51, R7 and the Q4
|
||||
ruling.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 194 design r4 (#1692)
|
||||
|
||||
Webby, 2026-10-20T19:40Z (from `date -u`). Author: Honker. Subjects at 7d0788236 on honker/row194:
|
||||
DESIGN.md sha256 824ef884...3f34, PRD-1.7-DRAFT.md sha256 379cc318...05bc (both verified). Read as the diff
|
||||
from d0afda024 (my r3 review), with the role files at origin/refactor. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 (webby-0010 and webby-0011) is fixed. D2.3 is now an allowlist, `HIRE_ALLOWED`, with six actions. T8b and
|
||||
T8c loop over `ACTIONS` minus the list and the hard waits. There is a mixed case and a pin test, and M47/M48
|
||||
are "some instead of every". The table of today's files checks out against the role files. chief-of-staff,
|
||||
cto, merge-gate, pm, reviewer and security-reviewer each hold an action off the list. Coder's six actions
|
||||
are all on it, but coder is credentialed (2). The C-suite files and assistant hold only `message.send` and
|
||||
`record.write`. The PRD bound now names the fixed list and says a change to it is a reviewed commit.
|
||||
|
||||
B2 (webby-0011) is fixed. Fire runs A (store), then B (delist), then C (consume). A and B are safe to repeat,
|
||||
and T9c covers both failure points. M56-M58 kill the order swap, the early consume and the `not-held`
|
||||
refusal.
|
||||
|
||||
The r2 N1 and N2 items and the r3 N2 item are in. The SIGKILL escalation through row 191's stop, with one
|
||||
stop receipt and the halt join, reads right (T9a, T9b, M51-M55). V3 terminate stays SIGTERM-only (M54).
|
||||
|
||||
## Non-blocking, for the build
|
||||
|
||||
N1. Between a failed B and the repeat, I is still listed, so a launch of I passes (`launch` takes listed
|
||||
instances). A repeat's A would stop that run, but nothing makes anyone repeat the fire. I'd have `launch`
|
||||
refuse `instance-firing` while I has a `fire.stopped` with no `fire.done`, and add that case to T9c.
|
||||
N2. "Every action is on the list" is true of a definition with no action lists at all. `roles/researcher.json`
|
||||
is `roleVersion` 1 with no withinRole or crossRole, and the design counts it as passing. The load check's
|
||||
version 2 rule refuses it, but the hire-time check in the broker should require version 2 too. A role file
|
||||
edited to version 1 after load would then refuse, not pass the allowlist with no actions. Add it to T8b.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Webby review: row 194 design r1 (#1692, agent lifecycle and per-agent veto)
|
||||
|
||||
Webby, 2026-10-20T19:14Z (from `date -u`). Independent reviewer. Author: Honker.
|
||||
Subject: `agents/honker/work/queue-194/DESIGN.md` at 14a6f5f9d on honker/row194, sha256
|
||||
eec9f011567d (honker-0003, which supersedes 8f80c278b from honker-0002; I read both and the diff).
|
||||
Brief blob a907109409ac. Static review only. I ran no tests and changed no product code.
|
||||
|
||||
Code base: `git archive 5d019d876`. `git diff --stat 5d019d876 53bb59462 -- packages adapters roles scripts`
|
||||
is empty, so every line below holds at the design's base. Paths are relative to `packages/` unless
|
||||
they start with `roles/`.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Eight blockers. The verb split, the launcher-only start, the D3 reload through the boot loader and the
|
||||
`retired` list are sound. The chief's side (O1, hire) and the request lifecycle need work.
|
||||
|
||||
## Citations checked
|
||||
|
||||
These hold: broker.mjs:145 (ORGANIZATION_KEYS), :208, :446, :770-774, :781-788 (decisions bind to the
|
||||
role instance), :1601-1628; launcher.mjs:232-327 (check reloads the file at :233), :239, :527,
|
||||
:624-668; broker.mjs:542-546; tools.mjs:116-119; vocabulary.mjs:135; runtime.mjs:75-81, :121-133; config.mjs:88-105;
|
||||
business.mjs:117-132; instance-settings.mjs:19-31; serve.mjs:282-289, :535-538; session.mjs:245-270.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. O1 can't load as written. Adding `role.hire` and `role.fire` to ACTIONS alone makes them hard
|
||||
waits (vocabulary.mjs:52-54, REQ-WAIT-3; broker.mjs:43-69 keeps its own ROUTED). role.mjs:42 then
|
||||
refuses `roles/chief-of-staff.json`, because withinRole lists a GATED_ONLY action. O1 needs both
|
||||
ROUTED lists, and vocabulary.mjs:52-54 says an action leaves the hard waits only when a PRD version
|
||||
says so. So O1 is a PRD change, which is Jason's, not only Sage's and Mos's. Fix: restate Q1 with
|
||||
that. O2 needs only the ACTIONS entries and fits the brief's "verbs, not authority". My
|
||||
recommendation is O2 for this row, and O1 after a PRD version routes the two actions.
|
||||
|
||||
B2. A hire can still create authority, and `hireable` needs a schema. The r2 `hireable` list is
|
||||
a reviewed role-file entry, which helps. But the structural rules (D2.2) still refuse only
|
||||
credentialed, END_PROTECTED and version 1 definitions, and `roles/chief-of-staff.json` declares no
|
||||
credentials. A `hireable` list that names chief-of-staff, by mistake or later, lets the chief hire a
|
||||
second chief with role.launch, role.end, organization.change, instance.name, and hire and fire. Under
|
||||
O2 there's no list at all. Sage's example, `security-reviewer`, holds review.verdict
|
||||
(`roles/security-reviewer.json`), so Mos should rule knowing that hire would let the chief add
|
||||
verdict holders. Fix: hire refuses (`definition-not-hireable`) any definition whose withinRole holds
|
||||
role.launch, role.end, role.revoke, organization.change, instance.name, role.hire or role.fire,
|
||||
whatever `hireable` and `noHire` say. role.mjs validates `hireable` (a new role-file key: name it as a
|
||||
role schema change) and refuses such an entry at load. Test with chief-of-staff.
|
||||
|
||||
B3. Re-hire contradicts itself and carries decisions. The D2.1 table refuses a hire "when ... the name
|
||||
exists". The text below it (DESIGN.md:156-157) says a fired name comes back by a hire of the same
|
||||
name. If that re-hire can name another definition, approvals bound to the instance
|
||||
(broker.mjs:781-788) carry to a different role. That is the problem `retired` solves for delete.
|
||||
Fix: pick one. Either re-hire of a fired name keeps its definition and refuses another, or re-hire
|
||||
is refused. Test the chosen rule.
|
||||
|
||||
B4. The V2 request isn't exactly-once, and the human's brake lapses at start. The pass runs `check()`
|
||||
then `start()`. It skips authorizeLaunch (launcher.mjs:653), and `check()` never reads launch_state,
|
||||
so the only revoked check is at request time (V2 step 1). A `launch.revoke` inside the 60 s window
|
||||
doesn't stop the launch, against "applies to the human until they restore it". Separately, if
|
||||
`lifecycleDone` fails after `start()`, the next tick sees the request still open. `check()` refuses
|
||||
`instance-running` only while the run lives (launcher.mjs:239), so a short run can start twice.
|
||||
Fix: one host op takes the request (writes `lifecycle.taken`) and re-checks revoked, retired and,
|
||||
for a tester request, `bus.tester` (B8) in the same transaction, before `start()`. A taken request never starts again. Test a revoke between
|
||||
request and pass, and a `lifecycleDone` that throws once.
|
||||
|
||||
B5. Terminate and delete race a launch in flight. A run that is spawned but not yet bound holds no
|
||||
claim. V3 answers `not-held` for it, and an open V2 request for I still starts after a terminate.
|
||||
Delete's `instance-running` reads claims only. The workspace rename isn't serialized with
|
||||
`start()`'s mkdirSync (launcher.mjs:350), and the design doesn't say which process renames it or
|
||||
what happens when the file commit succeeds and the rename fails. Fix: terminate closes open
|
||||
requests for I as `request-cancelled`. Delete refuses `instance-starting` while a request is open or
|
||||
the launcher's registry holds I. The rename runs under the launcher's `serialize` as a host op, and
|
||||
the design states the order and the recovery after a failed rename. Test both races.
|
||||
|
||||
B6. The tester gets delete. D4.1 says the tester gets no delete, but TESTER_ORGANIZATION_VERBS
|
||||
holds `instances.set` (broker.mjs:103), and `/api/instances` lets a tester through without `send`
|
||||
(serve.mjs:537). Op `delete` rides `instances.set`, so the tester can delete. M18 covers `veto.set`
|
||||
only. Fix: the broker refuses op `delete` from a tester session with `tester-refused`. Add it to T12
|
||||
and a mutant.
|
||||
|
||||
B7. Keep doesn't cover the chief's model write. Through `organization.set` the chief changes an
|
||||
instance's model key (broker.mjs:1266-1269). On a kept instance that changes the agent the human
|
||||
chose to keep, at its next launch, which the chief controls. Fix: `organization.set` refuses
|
||||
`instance-kept` for a write that names a kept instance. Test it in T10.
|
||||
|
||||
B8. D4.1's live `bus.tester` check has nothing live to read. `bus.tester` is a business var
|
||||
(broker.mjs:544). No bus verb writes business vars: ORGANIZATION_KEYS (broker.mjs:145) and
|
||||
`instances.set` don't reach them. So it changes only by hand edit, and D3 item 6 says a hand edit
|
||||
needs a restart. The broker's live copy then sees Jason turn testing off only at the next restart,
|
||||
which is today's behaviour, and T12a's "a live write turns `bus.tester` off" has no write to run.
|
||||
Fix: each tester call reads `bus.tester` from the business file as it stands, as `harnessOn` does
|
||||
(runtime.mjs:136-142), and an unreadable file refuses `tester-off`. T12a turns it off with a file
|
||||
edit and no restart. M20 stays as written.
|
||||
|
||||
## Q2
|
||||
|
||||
Yes, keep should block the chief's `role.end` on a kept instance. End then launch is a replace
|
||||
(E6), and the limits (`#stops`) and the human's V3 still stop a runaway. Two things go with it.
|
||||
The block belongs in the reason branch only (broker.mjs:1607-1613). The decision branch
|
||||
(:1614-1615) cites a decision the human resolved, which is the human's own act, so it should still
|
||||
end a kept run. Say so and test it. B7 extends the same rule to the model write.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. D3 item 2 lists six verbs. `plugins.set` and `routes.set` commit too (runtime.mjs:128-130).
|
||||
Reload after every committed write in one place in the callback, so a later verb can't miss it.
|
||||
N2. `noHire` must name definitions present in `rolesDir` (D2.3). A reviewed commit that removes a
|
||||
role file would then stop every business that lists it from loading. A stale entry on a deny list is
|
||||
harmless, so accept unknown names, or check them at write only.
|
||||
N3. Hire takes `vars`. Agent-layer vars include `harness` and the fallback route (vars.mjs:142-147),
|
||||
which is more than the chief sets today through `organization.set`. Limit hire's vars to that set
|
||||
plus the narrowing `limits.*` keys.
|
||||
N4. D3 item 4 changes the existing `instances.set` op `change` on a running instance. Name the
|
||||
existing business and webui tests that change with it.
|
||||
N5. `agents.js` must join the static file list at serve.mjs:28 and `index.html`.
|
||||
N6. V4 waits for the old run's exit. A run that ignores SIGTERM lets the request expire at 60 s.
|
||||
State that, and whether anything escalates to SIGKILL.
|
||||
N7. Keep refuses END_PROTECTED instances, so the human can't keep the assistant from the chief's
|
||||
`role.end`, which Q169 allows. Say that this is intended.
|
||||
N8. With `human: true`, `check()` must still send `launch.by` through the `pm` branch
|
||||
(launcher.mjs:235). Add that case to T2.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Webby review: row 197 implementation packet, round 2 (gate lanes)
|
||||
|
||||
Webby, 2026-10-20T20:44Z (from `date -u`). Author: Herb (herb-0023). Worktree herb-row197, branch herb/row197,
|
||||
parent a4e3e857e, source uncommitted. Round 1: REVIEW-impl.md at f942bd7d4 (webby-0029, CHANGES B1 B2).
|
||||
|
||||
Packet hashes I checked: BUILD.patch 0b3871bd...f43e, SOURCE.json 91ba149f...596b, gate.sh 48e06e23...fed1. All
|
||||
25 SOURCE.json rows match the worktree on sha256 and mode. The patch paths are the same 25 paths. The patch
|
||||
applies cleanly to a4e3e857e in a scratch worktree of mine, and the result is byte-identical to Herb's worktree
|
||||
for every path. I removed that worktree afterwards. BUILD-LOG.md has 25 added lines and no removed ones, outside
|
||||
the packet.
|
||||
|
||||
This was a static review. I ran no test, no suite and no Docker command, and I did not read the frozen checkout
|
||||
or Herb's other scratch roots. The counts below come from Herb's packet logs.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B2 is fixed. `leaseContext` (gate-lane-fixtures.test.mjs:29-33) takes `MOSAIC_GATE_LANE` and `MOSAIC_GATE_STATE`
|
||||
when they're set, and every lease goes through the active lane's real lock dir. The other band is checked with
|
||||
arithmetic (:10), and no test reserves the other lane's ports. The exhaustion test (:36-48) listens only on a
|
||||
port it leased itself, and narrows `BANDS` only inside its own process. It restores `BANDS` in `finally`.
|
||||
review-t4-lane-a.log: 8/8, and lane a's lock namespace is the only one present.
|
||||
|
||||
B1 is fixed. Each manifest case now has a test, and BUILD.md's table maps them all:
|
||||
|
||||
- T1 (MANIFEST.md:119-130). gate-lane.test.mjs:66-70 tests canonical, builder and existing-output refusals.
|
||||
`validateTooling` has missing-file and changed-file tests (:71-81). The 92-byte socket example passes and
|
||||
108 bytes refuses (:82-87). Duplicate steps stay, and NODE_TEST_CONTEXT is cleared from `cleanEnv` and from
|
||||
both supervised steps (:88-99). Scoped port 9 addresses refuse (:100-103).
|
||||
- T2 (:147-151). Two real shell wrappers race (gate-lane-state.test.mjs:84-98). There is one start receipt,
|
||||
and the loser exits 2. The supervisor passes `verifyLocks`, the step child's /proc/self/fd has no lock
|
||||
target, and the inode is stable. `activeConflicts` is tested for same-checkout and for a legacy land gate on
|
||||
a free lane (:99-106). Recovery kills the proven child, a foreign child survives, a recovery killed during
|
||||
Docker cleanup stays quarantined, and a resumed recovery clears the lane (:107-124).
|
||||
- T3 (:171-174). Missing receipt, missing image and wrong release tag (gate-lane-image.test.mjs:46-50). A tag
|
||||
that changes between steps stops the next step, and no terminal rc 0 follows (:51-61). A rebuild with the
|
||||
same input digest gets a new ID, the old receipt refuses it, and cold and warm counters stay separate
|
||||
(:62-72). The tag is checked again after the last step (:73-80). runGate calls the same
|
||||
`executePinnedSteps` (gate-lane.mjs:184), so the test covers the real path.
|
||||
- T4 (:201-209). Two generated node test files take different leases in the same lane and state. The lock is
|
||||
held while the child runs and released after it exits, and the other lease stays held
|
||||
(gate-lane-fixtures.test.mjs:49-62). A SIGKILLed allocator frees its own lock and leaves a foreign one alone
|
||||
(:63-72). `fixtureCacheEnv` has a replacement-env test (:73-79).
|
||||
- T5 (:226). Another process appends under a fixture HOME while a supervised step is running, and attribution
|
||||
puts that change in `unrelated` (:80-93).
|
||||
|
||||
Counts from the packet logs: review-t1-resume 11/11, t2 14/14, t3 8/8, t4 8/8, all with 0 skipped and 0
|
||||
cancelled. I counted the test blocks in each file, and they match. The cleanup-order fix is in place:
|
||||
`ctx.cleanup` stops both children and waits for them before it removes `root`, and leaseContext's after hook runs
|
||||
callbacks before it removes the state root.
|
||||
|
||||
The round 1 notes are addressed. N1: `closed` is no longer bound (lane-fixtures.mjs:16). N2: TMPDIR is a `tmp`
|
||||
subdir (:32). N3: `%iface` is stripped before the port 9 match (lane-state.mjs:28), with a test. N4: unit roots
|
||||
use gt-, gf-, gs- and gi-, and README:84-85 says so. N5: README:84 documents the node_modules link. N6: BUILD.md
|
||||
records the overlap with row 200.
|
||||
|
||||
The existing-file edits are the same shape as round 1. They swap helpers and add env and the concurrency
|
||||
option. No assert or timeout line changed (`git diff -U0` grep). Hash pins: no test under packages/*/tests or
|
||||
scripts/test-*.sh pins a sha256 next to any of the 25 path names.
|
||||
|
||||
gate.sh matches the template shape. It skips pkg-queue, which script-queue covers. It runs every package and
|
||||
script with `--test-concurrency=2`, stops on the first failure, and runs `cleanupProject` on exit.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Outside a lane, `leaseContext` falls back to lane a with a private gf- state root
|
||||
(gate-lane-fixtures.test.mjs:30). That is still the B2 shape. Suppose someone runs this file singly while a
|
||||
real lane-a gate holds closed leases. The exhaustion test can then take and listen on one of lane a's ports,
|
||||
because the private lock dir excludes nothing. A lane claim refuses beside a running `node --test`, but a single
|
||||
started after the claim isn't blocked by anything. Default to the real state root
|
||||
`/mnt/storage/scratch/mosaic-gate-lanes` when no lane is set, or document that singles of this file wait for
|
||||
`gate-lane.sh status` to show both lanes CLEAR.
|
||||
|
||||
N2. The band check at :10 compares literal numbers, not the imported `BANDS`, so it would pass whatever
|
||||
lane-state.mjs:7 says. Compare against `BANDS` (it's imported at :26).
|
||||
|
||||
N3. The T5 step child (:88) and the appender poll with no deadline. If the append never happens, the test hangs,
|
||||
and the runner has no timeout. Give both a bound, for example 5 s, and exit nonzero.
|
||||
|
||||
None of these block. Herb can fold them into the commit or record them as a follow-up.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 197 implementation packet, round 3 (gate lanes, N1-N3 delta)
|
||||
|
||||
Webby, 2026-10-20T20:53Z (from `date -u`). Author: Herb (herb-0026). Worktree herb-row197, parent
|
||||
a4e3e857e. Round 2: REVIEW-impl-r2.md at 1f404c973 (webby-0031, approve, N1-N3).
|
||||
|
||||
Packet hashes I checked: SOURCE.json 35847636...2aab, BUILD.patch 55fe390d...0fde, gate.sh 48e06e23...fed1
|
||||
(unchanged). All 25 SOURCE.json rows match the worktree. BUILD.patch applies to a4e3e857e in a temporary
|
||||
index, and every one of the 25 blobs equals the worktree file. I couldn't diff against the r2 files because
|
||||
I kept no copy. Instead, file mtimes show only two of the 25 paths changed after the r2 request (20:42Z):
|
||||
packages/cli/tests/gate-lane-fixtures.test.mjs and scripts/testing/README.md. That matches Herb's list. I
|
||||
read both in full where they changed.
|
||||
|
||||
This was a static review. I ran no test. Counts come from Herb's logs: review-r3-fixtures 8/8 and
|
||||
review-r3-lane-a 8/8, with 0 failed, skipped or cancelled and no `not ok` lines.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
N1 is fixed. `leaseContext` (gate-lane-fixtures.test.mjs:30) now defaults to the shared
|
||||
`/mnt/storage/scratch/mosaic-gate-lanes`, so a single run takes its leases through the same lock files as a
|
||||
live lane-a gate. It creates the root at 0700 if missing, the same mode lane-fixtures.mjs:20 uses, and no
|
||||
longer removes it. README:95-96 says so.
|
||||
|
||||
N2 is fixed. The band check (:8-10) reads the imported `BANDS`. The 127 widths stay literal, and that's
|
||||
fine because they pin the band size, which is the contract.
|
||||
|
||||
N3 is fixed. The appender (:87) and the step child (:89) each have a 5 s timer that exits 8. A missed
|
||||
append makes the step's rc nonzero or `appended` nonzero, and the asserts at :91 fail. README:97 says so.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. No test drives the 5 s path itself. It only guards against a hang, so I'm not asking for one.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Webby review: row 197 implementation packet (gate lanes)
|
||||
|
||||
Webby, 2026-10-20T20:23Z (from `date -u`). Author: Herb (herb-0018). Worktree herb-row197, branch herb/row197,
|
||||
HEAD a4e3e857e, base aa1bd209e. The source is uncommitted. BUILD.patch sha256 6a1cd3e5...7792 and SOURCE.json
|
||||
d0f6333a...d6ed, both checked twice. All 25 SOURCE.json rows match the worktree on hash and mode, and the patch
|
||||
paths equal the SOURCE.json paths. Plan: MANIFEST.md r2, which I approved at ca6b32d66.
|
||||
|
||||
This is a static review plus two FD probes on node 26.8.1 in a private mktemp dir. I ran none of the new tests, no
|
||||
suite and no Docker command, and I did not read Herb's scratch roots.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Most of the code matches the manifest. The existing-file edits change helpers and env only, and no removed line
|
||||
holds an assert or a timeout. The Docker guard turns every `compose run` into `--no-build --pull never` and refuses
|
||||
everything else except `info` and an inspect of the pinned tag. Cleanup goes by exact project label and checks
|
||||
each ID's label again before removing it. `compose.yaml` declares no named volumes, so containers and networks
|
||||
are everything a run can leave. The env allowlist drops `NODE_TEST_CONTEXT`. Status and recovery work from
|
||||
receipts, not from flock alone.
|
||||
|
||||
I had one open question about the lock FDs, and the probes settled it. A bash `exec 3>> 5>>` plus flock, then
|
||||
`exec node`, leaves node holding FDs 3 and 5, and a second `flock -n -x` on the lane file fails while node runs. A
|
||||
child started with `spawn(..., {stdio: ['ignore', fd, fd]})` lists only 0-2 in /proc/self/fd. That matches
|
||||
MANIFEST.md:156 ("ordinary child stdio does not inherit them").
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. Several tests the manifest requires are missing. BUILD.md:25 says T1-T5 source is present. The manifest names
|
||||
these tests, and the packet doesn't have them:
|
||||
|
||||
- T1, MANIFEST.md:121, 127, 130. No refusal test for a canonical or builder checkout or an existing output. No
|
||||
92-byte socket test and no over-107-byte socket test (`socketInventory` has no test). No test that both candidates
|
||||
carry the approved tooling. No test that duplicates stay, and none that Node entries clear `NODE_TEST_CONTEXT`.
|
||||
- T2, MANIFEST.md:147-148, 151. No claim race with one winner, no same-checkout rejection, no inherited-FD test,
|
||||
no surviving-child test and no interrupted-recovery test. The legacy test (gate-lane-state.test.mjs:17) checks
|
||||
status only. Nothing shows that a claim refuses while a legacy gate runs and the flock is free.
|
||||
- T3, MANIFEST.md:171-174. No test for a missing receipt, a missing image, a tag changed mid-gate or a rebuild
|
||||
from a mutable tag. No cold/warm counter test beyond `builds === 1`.
|
||||
- T4, MANIFEST.md:201-203, 209. Leases are taken one after another, not from parallel test files. No test for an
|
||||
exhausted band, an occupied port, owner death or a foreign listener, and none holding a lease through child
|
||||
shutdown. No cache test for `fixtureCacheEnv`, although the harness tests now import it.
|
||||
- T5, MANIFEST.md:226. Attribution has a unit test with fixed entries. Nothing appends from an unrelated session
|
||||
while a run is in progress.
|
||||
|
||||
The inherited-FD test is cheap now. The probe above shows the shape: hold the lane lock through the wrapper, spawn
|
||||
a step child, and check that the child's /proc/self/fd has no lock file and that the lock stays held after the
|
||||
child exits. Either add the missing tests, or list each one in BUILD.md as dropped with a reason and get Sage's ruling.
|
||||
A test the plan requires is part of the change, the same rule Sage applied to row 110 N1.
|
||||
|
||||
B2. gate-lane-fixtures.test.mjs:5-11 leases from a private state root, in both bands, and then listens on the port
|
||||
it got. That test runs inside the pkg-cli step of every lane gate. The private root's lock files don't exclude the
|
||||
real leases under `/mnt/storage/scratch/mosaic-gate-lanes/ports/<lane>`, and both leasers go upward from the
|
||||
band's first port (lane-fixtures.mjs:22). Here is how it fails in a paired proof. Lane a holds a closed lease on
|
||||
61000, with no listener, which is how closed endpoints work (MANIFEST.md:206). Lane b's cli step runs this test.
|
||||
The test's probe of 61000 succeeds, and :10 listens on 127.0.0.1:61000. Lane a's closed endpoint now accepts
|
||||
connections, and that test fails. The same thing happens inside one lane, because two test files run at once.
|
||||
This is the cross-lane interference the bands exist to prevent. Fix: when `MOSAIC_GATE_LANE` is set, lease only
|
||||
through the run's own lane and state. Check the other band's range with plain arithmetic instead of a lease. Drop
|
||||
the listen at :10, since no assertion reads it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. `leasePort` ignores `closed` (lane-fixtures.mjs:16). Every call site passes it, and gate-lane-fixtures:5 is
|
||||
named after it. Use it, for example by recording the lease kind for evidence, or drop it.
|
||||
|
||||
N2. `fixtureCacheEnv(root)` points TMPDIR at the config dir itself (lane-fixtures.mjs:32). Temp files from the
|
||||
claude and pi children then land next to the fixture config they test. A `tmp` subdirectory keeps them apart. I
|
||||
didn't trace whether any assertion lists that directory.
|
||||
|
||||
N3. The port 9 check compares exact address strings (lane-state.mjs:28). `ss` prints scoped forms such as
|
||||
`127.0.0.53%lo`, and this host shows `%lo` and `%tailscale0`. A listener on `127.0.0.1%lo` or `0.0.0.0%lo`
|
||||
covers 127.0.0.1:9 but doesn't match. Strip `%iface` in `hostFacts` (:22) before comparing.
|
||||
|
||||
N4. The new tests make roots like `/mnt/storage/scratch/ga-XXXXXX` (gate-lane.test.mjs:12,
|
||||
gate-lane-state.test.mjs:46, 66). That is the run-root prefix gate-lane.mjs:201 uses, and inside a lane they sit
|
||||
outside the run's own temp root. If a test crashes, its leftover looks like a lane root, and README:41-42 tells
|
||||
operators not to remove another run's resources. Use a different prefix such as `gt-`.
|
||||
|
||||
N5. gate-lane.test.mjs:20 symlinks `/mnt/storage/src/mosaic-stack/node_modules` into the fixture, which ties the
|
||||
test to the canonical checkout's tree. That is fine on dragon-lin, but please write it down in "Not tested" or as
|
||||
an assumption.
|
||||
|
||||
N6. Coordination: row 200 (Launchpad, approved, round 2 open) also edits packages/cli/tests/stack-release.test.mjs.
|
||||
It adds T56 and fixtures, and this row replaces its `freePort` calls. Whichever row lands second has to rebase
|
||||
and rerun that file singly. Neither manifest says so.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Webby review: row 197 manifest r2 (#1695)
|
||||
|
||||
Webby, 2026-10-20T19:44Z (from `date -u`). Author: Herb. Subject: `agents/herb/work/queue-197/MANIFEST.md` at
|
||||
bdc9cd56a, sha256 2a1a3ad0...7cec (verified). Read as the diff from 84f21923c (my REVIEW-manifest.md), against
|
||||
aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. P2 keeps every literal Board `:9` URL, and `closedEndpoint` is gone from T4. The two leases left
|
||||
are real close-then-rebind cases. preview.test.mjs:208-209 and serve.test.mjs:99-100 each bind a probe on port
|
||||
0, close it and use the freed port as the dead board. Another lane's fixture could take that port in the gap,
|
||||
so a lease belongs there. T1 refuses a listener covering 127.0.0.1:9, a missing inventory or
|
||||
`ip_unprivileged_port_start` at 9 or below, and makes no connection to the endpoint.
|
||||
|
||||
N1 is in P3. Sage's order (sage-1384: 197 lands first, 198 rebases onto it before its manifest) makes the
|
||||
two P3 lines row 198's to replace when it removes the board URL from those tests. The BUILD-LOG change is one
|
||||
appended correction entry.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Webby review: row 197 manifest (#1695)
|
||||
|
||||
Webby, 2026-10-20T19:39Z (from `date -u`). Author: Herb. Subjects on herb/row197: `MANIFEST.md` at 84f21923c,
|
||||
sha256 16768433...b23b, and `TIMING.md` at fa820fed6, sha256 148a1416...52bd (both verified). TIMING's commit
|
||||
sits on top of the manifest's. I read both whole, against DESIGN.md at 6ad25d745 (my r2 APPROVE) and the base
|
||||
aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
T1-T5 cover the design. My r2 N1 is in T5: metadata-only listings, with assertions only on lane-attributed
|
||||
entries. TIMING.md copies Sage's table and says what it is not: a controlled solo baseline. The 25 values sum
|
||||
to 659 s, as stated.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Drop the dead Board `:9` replacements from P2. Port 9 is not a resource the two lanes share. On this host
|
||||
`net.ipv4.ip_unprivileged_port_start` is 1024, so no test, lane or seat can bind 127.0.0.1:9. A connection
|
||||
there is refused at once in both lanes. Leasing a closed endpoint for it adds isolation for nothing, and it
|
||||
touches 20 files at aa1bd209e (`git grep -l "127.0.0.1:9'"`: 17 under webui/tests plus stack-copy,
|
||||
launch-routes and bus tasks). I approved R9 in the design. That was a miss on my part, because R9 only allows
|
||||
the change "where compatible", and nothing here needs it.
|
||||
The row 198 rev 3 design also rewrites those same lines, replacing the board URL with a dead-board context.
|
||||
Whichever row lands second would redo the other's edit.
|
||||
Fix: remove the `:9` rows from P2 (the settings-form/proof/bus/views/tester-listener row, the browser and
|
||||
tester-fixture row, the dead-Board parts of the tester-cli/proof-cli and stack-copy rows). Keep the leases
|
||||
where a test closes a port and binds it again (control-board serve, stack-release freePort, service closedPort,
|
||||
serve.test :100 if it rebinds). Add a T1 preflight refusal for a listener on 127.0.0.1:9, so a host where
|
||||
something does listen there fails loudly instead of quietly turning those tests into connects to a live
|
||||
service.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Tell Sage that row 198 edits `stack-copy.test.mjs`, `pinned-copy.test.mjs` and the webui serve/preview tests
|
||||
too. Even with B1, those are the files where the two rows meet.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Webby review: row 197 (#1695), queue round 2, merge
|
||||
|
||||
Webby, 2026-10-21T07:18Z (from `date -u`). Author: Herb (herb-0034). Request comment 29961 (revs 2722-2723,
|
||||
posted as jarvis, sage-1815). This round only checks the merge (sage-1760, sage-1776). Round 1 was APPROVE
|
||||
on manifest 6318f0d0 over base aa1bd209e (comment 29665). Candidate: manifest digest
|
||||
8119a6082d3bb3150874dbe45410ca4830bcbb3887d71bce8d1edbe67df47ce3, 32 paths, on herb/row197-r4-234
|
||||
304c67753. Base c771a36f4 is its ancestor, and c771a36f4..304c67753 is that one commit.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 2), not from Herb's packet. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 304c67753:<path>` matches its line.
|
||||
- Against round 1's manifest, 22 paths have the same hash, 3 changed and 7 are new. The new ones are
|
||||
Herb's agents/herb/work/queue-197 packet files. `git diff --name-only c771a36f4 304c67753` names only
|
||||
manifest paths.
|
||||
- Base drift: of round 1's 25 paths, only stack-release.test.mjs, webui service.test.mjs and
|
||||
scripts/test-discord.sh have a different blob at c771a36f4 than at aa1bd209e. Those are the 3 changed paths.
|
||||
- For each of those three, the -U0 +/- lines of aa1bd209e to round 1 are identical to the -U0 +/- lines of
|
||||
c771a36f4 to 304c67753. Round 1's tree came from a commit whose files match all 12 tracked lines of
|
||||
round 1's manifest (2767b8854). Each file at 304c67753 is the landed blob plus Herb's own lines, so
|
||||
every landed hunk survives. The changed lines are the two `leasePort` imports, the one-line
|
||||
freePort and closedPort bodies, PI_CODING_AGENT_DIR in the discord sandbox, and the
|
||||
MOSAIC_TEST_CONCURRENCY branch in node_tests. No assertion or timeout changed.
|
||||
- T46 (stack-release.test.mjs:913) keeps row 234's `host_grants` and `hosts` entries in the untimed list.
|
||||
- File modes are the same as round 1's: gate-lane.sh and test-discord.sh 100755, the rest 100644.
|
||||
- Overlap check (sage-1815): `git diff --name-only c771a36f4 c515273db` names 13 paths and none is in the
|
||||
manifest. So row 240's landing doesn't touch this candidate.
|
||||
|
||||
Herb's slot run on 304c67753 was 559 pass, 0 fail, no skips (r4-granted/RESULTS.md). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The merge keeps both sides, with the same change lines as the approved round 1. Round 1's notes stand as
|
||||
recorded. `leasePort` still drops the `closed` option at lane-fixtures.mjs:16, the way round 1 accepted it.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 197 (#1695), queue round 3 (r5), merge
|
||||
|
||||
Webby, 2026-10-21T08:13Z (from `date -u`). Author: Herb (herb-0036). Request comment 29981
|
||||
(sage-197-request-5, revs 2750-2751, posted as jarvis, sage-1846). This round only checks the merge past
|
||||
the 228 and 238 landings. Round 2 was APPROVE on manifest 8119a608 at 304c67753 over c771a36f4
|
||||
(rev 2724, comment 29961). Candidate: manifest digest
|
||||
69fad4e7ed8f9e67268378b356893cbee81e106942ffc894a9652320218c8e5e, 32 paths, on herb/row197-r5
|
||||
9aa6d52d9. That commit's one parent is e7078e6a8.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 3), not from Herb's packet. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 9aa6d52d9:<path>` matches its line.
|
||||
- It has the same 32 paths as round 2's manifest. The only line that changed is
|
||||
packages/cli/tests/stack-release.test.mjs.
|
||||
- That file is blob 7398247ce983, which is what `git merge-file` of 304c67753 and 238's candidate
|
||||
4c90099ea over c771a36f4 gives with no conflict. I ran that preview before Herb built r5 (webby-0063).
|
||||
At e7078e6a8 the file is 238's candidate blob, and the -U0 +/- lines of e7078e6a8 to 9aa6d52d9 are
|
||||
identical to round 2's c771a36f4 to 304c67753: the two `leasePort` imports and the one-line freePort.
|
||||
238's hunk at :1686 in T57 and row 234's T46 `host_grants` and `hosts` entries are kept.
|
||||
- The other 31 paths have the same blob and mode in 9aa6d52d9 as in 304c67753. e7078e6a8..9aa6d52d9
|
||||
touches no path outside the manifest.
|
||||
- Overlap: the refactor tip is 4ced5e63d. e7078e6a8..4ced5e63d is one sessions commit that changes one
|
||||
path, docs/SESSIONS.md, which isn't in the manifest.
|
||||
|
||||
Herb's slot run of stack-release.test.mjs on 9aa6d52d9 was 196/0 in 501 s (sage-1846). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
r5 is round 2's approved code on e7078e6a8, and the drifted path is only stack-release.test.mjs.
|
||||
Round 1's notes stand as recorded. The two-lane proof driver is a separate review (sage-1846) and isn't
|
||||
part of this verdict.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Webby review: row 197 (#1695), queue round 4 (r6)
|
||||
|
||||
Webby, 2026-10-21T09:29Z (from `date -u`). Author: Herb. Request comment 30004 (sage-197-review-4b,
|
||||
sage-1896, mos-1162). The last approved state is round 3, r5 9aa6d52d9, manifest 69fad4e7 (rev 2755,
|
||||
comment 29981). Candidate: manifest digest
|
||||
1556cd296be0527697793b626ce7ae7f94de437a4fe4dee8a5714d7db434fc82, 33 paths, on herb/row197-r6
|
||||
df0c59608. That commit's one parent is fefd54e13.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 4), not from Herb's packet. It hashes to the
|
||||
digest above, and each path's sha256 from `git show df0c59608:<path>` matches its line.
|
||||
- It is round 3's 32 paths plus agents/herb/work/queue-197/r6-OVERLAP.json. Four paths differ in blob
|
||||
or mode from 9aa6d52d9: gate-lane.mjs, gate-lane.test.mjs, Herb's BUILD.md and the new
|
||||
r6-OVERLAP.json. The other 29 have the same blob and mode.
|
||||
- No manifest path changed between e7078e6a8 (r5's base) and fefd54e13. fefd54e13..df0c59608 touches
|
||||
no path outside the manifest.
|
||||
- packages/bus/tests/process.test.mjs is identical at fefd54e13 and df0c59608, as intended (row 243
|
||||
owns the chmod).
|
||||
- Overlap: the refactor tip is 38b83ea5c. fefd54e13..38b83ea5c is one sessions commit, and none of its
|
||||
paths is in the manifest.
|
||||
|
||||
Herb's focused run on the granted slot: gate-lane.test 13/0, process.test 8/0 (sage-1892). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The one code change is scripts/testing/gate-lane.mjs:46. Each step now spawns
|
||||
`/bin/bash -c 'umask 022; exec "$@"' gate-step <argv...>`. On Sage's three questions:
|
||||
|
||||
1. Argv quoting is kept. `gate-step` fills `$0`, so `"$@"` is exactly step.argv, with no word
|
||||
splitting or globbing. Arguments after the `-c` string are positional, so bash doesn't parse a
|
||||
leading `-` in them as an option. `node` and `bash` resolve through the step env's PATH, as
|
||||
spawn did before.
|
||||
2. Exit status and signals are kept. `exec` replaces bash in the same pid, so `close` reports the
|
||||
test's own code or signal. The process is still the detached group leader, so `process.kill(-pid)`
|
||||
in abort (:49) and cleanProcesses still reaches the whole group. identity() reads pid and
|
||||
/proc starttime (lane-state.mjs:33), and exec doesn't change either, so the recorded `child` owner
|
||||
still matches under sameProcess. A missing command now exits 127 from bash. That's the same rc as
|
||||
the old spawn `error` path (:51), and bash's message lands in the step log.
|
||||
3. The supervisor keeps 077. gate-lane.sh sets `umask 077` at :3 and execs node at :26. The new umask
|
||||
is set only inside the bash child. The step log is opened by the supervisor with mode 0600 before
|
||||
the spawn (:44). The supervisor's other writes give explicit modes: the out and bin dirs 0700,
|
||||
docker.json 0600, the guard shim 0700 (:177-181), the state root 0700 (:89). mkdtemp roots are 0700.
|
||||
The new tests check the child's mask (022), a 0644 child file, the parent mask after the run (077),
|
||||
the 0600 log, and gate-lane.sh's 0700 state dir and 0600 locks.
|
||||
|
||||
The step env still comes from cleanEnv (:109-115). It keeps no BASH_ENV, ENV, SHELLOPTS or BASHOPTS,
|
||||
so the non-interactive bash sources nothing.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N8. bash adds PWD, SHLVL and `_` to the env the test process sees. That's closer to the legacy
|
||||
shell-run environment, not further from it, and nothing in cleanEnv depends on their absence.
|
||||
|
||||
N9. Files a step makes with 022 are 0644. Under the 0700 temp root and out dir they stay private. In
|
||||
the scratch checkout they get the same modes a legacy shell run would give.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Webby review: row 197 (#1695), queue round 5 (r7)
|
||||
|
||||
Webby, 2026-10-21T11:29Z (from `date -u`). Author: Herb. Request comment 30036 (rev 2819, sage-1958,
|
||||
posted by Sage as jarvis). Conditions are from sage-1955. Round 4 was APPROVE on r6 df0c59608
|
||||
(rev 2773, webby-0068). Its proof then stopped at solo-a script-release, because Compose 5.5.1's
|
||||
`run` has no `--no-build` (#1695 c30031). I missed that in round 4, since an argv-only test can't
|
||||
show that a flag exists. Candidate: manifest digest
|
||||
4e54024aacfb01dda7631b562db754d92a8bd1ffe4106e9262908883a72b45c3, 35 paths, on herb/row197-r7
|
||||
9c3166915. That's one commit on b1fece6f0, which is on origin/refactor.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 5). It hashes to the digest above, and each
|
||||
path's sha256 from `git show 9c3166915:<path>` matches its line.
|
||||
- b1fece6f0..9c3166915 touches only manifest paths.
|
||||
- Against r6 df0c59608, five paths differ in blob or mode: lane-docker.mjs, gate-lane-image.test.mjs,
|
||||
Herb's BUILD.md, and the new r7-no-build.mutant.patch and r7-OVERLAP.json. The other 30 have the
|
||||
same blob and mode, as Herb says.
|
||||
- Base drift: fefd54e13..b1fece6f0 changes none of the 35 paths. On those 30, the -U0 +/- lines over
|
||||
b1fece6f0 equal r6's over fefd54e13.
|
||||
- Overlap: the refactor tip is 335bb3e30. b1fece6f0..335bb3e30 changes none of the 35 paths.
|
||||
- I read the lane-docker.mjs and test diffs, the mutant patch, Herb's r7 BUILD.md section, the
|
||||
docker.json writer (gate-lane.mjs:172, :178) and the proof driver's temp-root cleanup
|
||||
(herb/row197-proof run-proof.mjs).
|
||||
|
||||
Herb's focused run on grant sage-1956: red 9/2 on the r6 guard (`unknown flag: --no-build`, plus the
|
||||
missing helper), green 11/0, mutant 0/1 killed with the same error, then restored 11/0 (BUILD.md). I
|
||||
ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
On Sage's four conditions:
|
||||
|
||||
1. The guard owns the files. For `compose run`, guardDocker returns `compose -f <checkout>/compose.yaml
|
||||
-f <tempRoot>/compose-no-build.yaml run --pull never --no-deps ...` (lane-docker.mjs:36-37).
|
||||
composeFiles checks the imageId is `sha256:` plus 64 hex characters and the checkout is absolute
|
||||
(:6-10). A caller's option before the service must be one of `--rm -T --no-deps --interactive -i`
|
||||
(:35). So `-f`, `--file`, `--file=`, `--project-directory`, `--build`, `--pull` and `--no-build`
|
||||
there refuse. A top-level `compose -f ...` or `compose --project-directory ...` refuses, because
|
||||
argv[1] isn't `run` (:33, :39). The test at gate-lane-image.test.mjs:120-126 checks all of them.
|
||||
The record the guard reads has checkout, tempRoot and imageId from the receipt (gate-lane.mjs:172).
|
||||
That closes the r6 failure class for the record fields.
|
||||
2. The override goes at `shortRoot(run.tempRoot)/compose-no-build.yaml` (:9). shortRoot needs an
|
||||
owned, private scratch dir of 30 bytes or less (lane-state.mjs:10-16). The file is written `wx`
|
||||
at 0600 to a nonce name, then hard-linked into place. EEXIST keeps the existing file (:15-19). It
|
||||
is reopened O_NOFOLLOW, and anything other than a regular file owned by us, at mode exactly 0600,
|
||||
with the exact bytes, refuses (:20-27). The test checks 0600, a reuse with the same content, a
|
||||
changed file and a symlink. The symlink case leaves the target untouched. Cleanup: the proof driver
|
||||
removes a passed gate's temp root with `rmSync(..., {recursive:true})` after its ownership checks,
|
||||
and keeps a failed one. The override sits inside that root.
|
||||
3. The config check runs the installed binary: `docker compose -f ... -f ... config --format json`
|
||||
on a copy of the candidate compose.yaml, with made-up env and auth and an empty DOCKER_CONFIG
|
||||
(:103-119). It asserts that `services.mosaic-agent` has no `build` key and that `image` equals
|
||||
run.imageId. It also checks `--no-deps` and `--pull never`. The fixture imageId stands in for the
|
||||
receipt's. In a real run, the guard takes it from docker.json, which gate-lane.mjs:172 fills from
|
||||
receipt.imageId.
|
||||
4. The parser test (:93-101) puts `--help` after `run` and before the rewritten options and SERVICE,
|
||||
so the installed Compose parses every flag and starts nothing. The mutant patch puts `--no-build`
|
||||
back at :37 only. Herb's red run and the mutant run both show `unknown flag: --no-build` from that
|
||||
test, which is the fault the r6 proof hit.
|
||||
|
||||
compose.yaml has one service, mosaic-agent, with `build:` and `image: ${MOSAIC_IMAGE_TAG...}`, and the
|
||||
repo has no compose.override.yaml. With `build: !reset null` and `--pull never`, `run` can't build or
|
||||
pull. A missing image fails.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N10. No test starts a container. The parser and config tests show that Compose accepts the argv and
|
||||
merges to an image-only service. They don't show that `run` with `--pull never` starts from an
|
||||
`image:` that is a bare `sha256:` ID. Docker accepts full IDs, so I expect it to work, but that's
|
||||
the step r6 failed at. Sage's fresh D5 proof is the evidence for it, and it counts only if it gets
|
||||
past solo-a script-release.
|
||||
|
||||
N11. `-f` sets the project directory to the candidate checkout, whatever the caller's cwd, and turns
|
||||
off compose.override.yaml auto-loading. Every current caller runs from the checkout:
|
||||
mosaic-task.mjs:397-399 (`cwd: PROJECT_ROOT`), hello.sh:24, verify.sh:45 and agent.sh:208. So nothing
|
||||
changes today. A future caller run from a copy would get the candidate's compose.yaml, not the copy's.
|
||||
|
||||
N12. A failed gate's kept temp root still holds the override. It contains only the image ID, so
|
||||
nothing secret is left behind.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Webby review: row 197 proof driver, a5b636750 (proof 4 watcher change)
|
||||
|
||||
Webby, 2026-10-21T15:19Z (from `date -u`). Author: Herb (herb-0090). Requested per sage-2029, sage-2034
|
||||
and sage-2035, after mos-1174. This is a review of the proof driver only. It isn't a queue round, and
|
||||
197's candidate stays r8 c45ddeb6b (manifest 80dc1468, my round 6 approval, rev 2867). Candidate:
|
||||
herb/row197-proof a5b636750, one commit on b8461266c.
|
||||
|
||||
What I checked, without runs:
|
||||
- b8461266c..a5b636750 changes exactly 3 paths under agents/herb/work/queue-197/proof/:
|
||||
run-proof.mjs (sha256 998f41948355ea86169c1711324a3c9e6cf4bdbe5157dcae9e78e414c1c16342, as Herb
|
||||
says), events.test.mjs (new, 7a40bb46ed4e) and BUILD.md (5ede409c4480). All three are 100644.
|
||||
- None of 197's 37 manifest paths is in that range. The branch sits on an older base (9aa6d52d9), so
|
||||
its tree holds older copies of the lane scripts. That doesn't matter, because the driver imports
|
||||
every lane module from `config.launcherRoot` (run-proof.mjs:85-89). It also refuses a launcher
|
||||
whose HEAD isn't `config.launcherCandidate` or whose checkout is dirty (:81-82).
|
||||
- I read the whole diff and r8's cleanupProject (lane-docker.mjs:41-52 at c45ddeb6b).
|
||||
|
||||
Herb's grant sage-2035 results (by BUILD.md and herb-0090): events.test.mjs 8/0, node --check passes,
|
||||
and one real-daemon probe with r8's cleanupProject saw all four events within 309 ms. Both objects
|
||||
were removed, and the post-probe listings and /proc scan were empty. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- The defect. Proof 3's watcher filtered both event types by the Compose project label in one
|
||||
stream. Docker 29's network events don't carry that label, so the watcher never saw a network
|
||||
create, and the evidence check reported missing creation evidence.
|
||||
- Network events (:12-14). The network stream filters only on type and event (:26-27), with no label
|
||||
filter. dockerEvent keeps a network only if its name starts with `<project>_`, so `<project>x_...`,
|
||||
`<project>-...` and the bare project don't match (the test at events.test:15-21). A network that
|
||||
does carry a foreign project label throws, which fails closed. The project is
|
||||
`mg197-[ab]-<12 hex>` (:7), checked before any watcher starts (:21). A foreign network would have
|
||||
to use our random id to match.
|
||||
- Container events (:15). These still need the exact project label, which the stream also filters
|
||||
(:27). A labelled container whose name falls outside the project throws "name outside" (:16),
|
||||
the same rule as the older recorded-name guard in logObject (:109).
|
||||
- Shared code. The proof's ensureWatchers now calls the same watchDockerProject (:116). The final
|
||||
cleanup awaits each watcher's bounded stop (:232), which SIGKILLs after 1 s and records it
|
||||
(:33-37). Wrapping the body in runProof and running it only on direct execution (:264) lets
|
||||
the test import the module without starting a proof.
|
||||
- The preflight (:39-64, :209-211). It runs after the image check and before wave 1, and a failure
|
||||
stops every wave. It creates one labelled network and one container from the pinned imageId with
|
||||
`--pull never`, `--network none` and no start. The test asserts no `start` or `build` argument
|
||||
(events.test:46). Cleanup always runs in `finally`, through r8's label-checked cleanupProject,
|
||||
which accepts the bounded invoke function (lane-docker.mjs:43). Both objects carry the project
|
||||
label, so cleanupProject finds and removes them. All four events must arrive within the 5 s
|
||||
deadline, and a late event counts as missing (:59, and the test at events.test:57-60). The result
|
||||
goes into summary.json and docker-summary.json.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. If the probe's own cleanup fails, a refusal or a timeout, the proof stops through `fail()`, but
|
||||
the `finally` cleanup loop (:231) covers only knownRuns, not the probe project. Leftover probe objects
|
||||
would stay until someone removes them by hand. The failure is loud and watcher-probe-plan.json
|
||||
(:209) names both objects, so nothing goes unseen. Adding the probe project to that loop would make
|
||||
the cleanup retry once.
|
||||
|
||||
N2. Herb asks Sage to update proof-phase.sh's digest pin to 998f4194...6342. That file isn't in this
|
||||
commit, so I haven't checked the pin. Sage should check it against the run-proof.mjs hash before
|
||||
proof 4.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Webby review: row 197 (#1695) two-lane proof driver, round 2
|
||||
|
||||
Webby, 2026-10-21T08:31Z (from `date -u`). Author: Herb (herb-0053). Requested by Sage (sage-1858,
|
||||
sage-1864); not a queue candidate. Scope: B1 and N3 only, from `git diff 8ae9d8acd b8461266c`. Subject:
|
||||
commit b8461266c on herb/row197-proof, a fast-forward from 8ae9d8acd. run-proof.mjs sha256 is
|
||||
ace376a9a333..., which I checked. The diff touches run-proof.mjs (+39 -14) and adds proof/BUILD.md. I
|
||||
didn't review BUILD.md beyond noting it now exists. Static review, I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. The `finally` takes each temp root from the run's start record in knownRuns. It never
|
||||
scans scratch (run-proof.mjs:174-193). Before it removes anything:
|
||||
- The path must match `^/mnt/storage/scratch/g[ab]-[A-Za-z0-9]{6}$` with the run's own lane.
|
||||
- lstat must show an owned real directory, not a symlink, and its realpath must equal the recorded
|
||||
path.
|
||||
- The gate's owner process must be gone, by `sameProcess` with the start time and boot id. No live
|
||||
process may be in one of the run's recorded `child` groups or have the temp root or checkout in its
|
||||
argv. If the process inventory fails, the root is kept and the run fails.
|
||||
- The last terminal record must say clean. Otherwise the root is kept and counted as a cleanup error.
|
||||
|
||||
A passed gate's root is then removed (:190). A failed gate's root is kept for diagnosis. Every outcome
|
||||
(`removed`, `kept` with a reason, or `error`) goes to `tempRoots` in docker-summary.json (:194), and any
|
||||
error sets exit 1. The launcher helpers it calls exist at 9aa6d52d9 with the fields it reads:
|
||||
processInventory gives `group`, `state` and `argv` (lane-state.mjs:48-50), and gates record
|
||||
`{kind:'child',group}` (gate-lane.mjs:46).
|
||||
|
||||
N3 is fixed. The handlers now go in at :9, before the config read, the launcher imports and any
|
||||
write. `abortOnSignal` is a no-op until :97 makes it abortActive. Preparation moved inside the `try`
|
||||
(:142). Each clone first yields with `setImmediate` and checks `interrupted`, so a pending signal
|
||||
ends preparation through `catch` and `finally`.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N5. Kept roots are left by design: failed gates, and runs whose cleanup wasn't verified. After reading
|
||||
the evidence, Sage removes each `kept` path listed in docker-summary.json `tempRoots`.
|
||||
|
||||
N6. One small orphan path remains in the launcher, not the driver. gate-lane.mjs makes its temp root
|
||||
at :162 but writes the start record at :172 and installs its signal handlers at :174. A SIGTERM from
|
||||
the driver's abort in that window kills the gate before any record exists, and the driver can't know
|
||||
the root. It's an empty or near-empty `g<lane>-` dir in scratch. That only happens if one lane fails
|
||||
while its partner is still validating at wave start.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Webby review: row 197 (#1695) two-lane proof driver
|
||||
|
||||
Webby, 2026-10-21T08:22Z (from `date -u`). Author: Herb. Requested by Sage (sage-1850); not a queue
|
||||
candidate. Subject: commit 8ae9d8acd on herb/row197-proof, parent 9aa6d52d9, adding
|
||||
agents/herb/work/queue-197/proof/run-proof.mjs (176 lines, sha256 b8bb3df6..., which I checked) and
|
||||
config.example.json. I read the driver whole and the launcher code it calls at 9aa6d52d9: gate-lane.sh,
|
||||
gate-lane.mjs runGate and cleanEnv, lane-docker.mjs guardDocker and cleanupProject, lane-state.mjs
|
||||
shortRoot, and lane-evidence.mjs summarizePairs. Static review, I ran nothing.
|
||||
|
||||
Sage named proof/BUILD.md for the procedure. No such file exists at 8ae9d8acd, and
|
||||
agents/herb/work/queue-197/BUILD.md has no proof procedure, so I reviewed against the code and Sage's
|
||||
four questions.
|
||||
|
||||
## Verdict: CHANGES (one blocker)
|
||||
|
||||
### Sage's four questions
|
||||
|
||||
1. Stops at the first failure: yes. A 100 ms monitor reads new bytes in each gate's out/*.log and stops
|
||||
at the first failure line (run-proof.mjs:107, :116). A gate that exits nonzero sets `firstFailure`
|
||||
(:151). Either way the driver sends SIGTERM to every active gate, then SIGKILL after 30 s (:98-101).
|
||||
The wave check (:154) throws, so no later wave starts. Each wave also requires both lanes CLEAR before
|
||||
it starts (:149) and after it ends (:157), and every gate must end with rc 0, clean, no image build and
|
||||
no required skip (:131-132). A SIGINT, SIGTERM or SIGHUP sets `interrupted` and aborts the same way
|
||||
(:140-141).
|
||||
2. Cleanup: containers and networks yes, temp dirs no (B1). The gate cleans its own project
|
||||
(gate-lane.mjs:190, :192). The driver's `finally` then inventories each project it saw, runs
|
||||
cleanupProject again, and fails if anything is left (:165). It also needs create evidence for a
|
||||
container and a network in each project (:172). That covers the failure and signal paths, because the
|
||||
gate's SIGTERM handler aborts its detached step group and runs the same cleanup. compose.yaml uses
|
||||
bind mounts only, and lane-docker.mjs allows only `compose run`, so no named volume can be created.
|
||||
3. Writes and reads: the driver's own writes are under runRoot (plan, clones, state, out, logs,
|
||||
docker-objects.jsonl, summaries), with one exception, B1. It reads the config, the image receipt, the
|
||||
launcher modules and the canonical repo through `git clone --local`. Its Docker inspect formats read
|
||||
only the name and the compose project label (:77), and it saves no raw events. It reads no secret and
|
||||
no seat scratch. Gate steps get cleanEnv's allowlist (gate-lane.mjs:108-112), not the driver's
|
||||
environment.
|
||||
4. Concurrency and isolation evidence: enough. summarizePairs refuses unless each pair has two lanes
|
||||
and two candidates, pair 2 swaps lanes, and the later start is before the earlier end
|
||||
(lane-evidence.mjs). summary.json keeps each gate's terminal receipt with startMs and endMs. Each run
|
||||
has its own clone, its own mkdtemp temp root, its own `mg197-<lane>-<nonce>` project, the lane's port
|
||||
band, home-before/after attribution and a socket inventory. docker-objects.jsonl records each Docker
|
||||
object against its project.
|
||||
|
||||
### Blocker
|
||||
|
||||
B1. Every gate leaves its temp root in /mnt/storage/scratch, outside runRoot, and nothing removes it.
|
||||
The driver passes no `--temp-root` (run-proof.mjs:122), so runGate makes
|
||||
`mkdtempSync('/mnt/storage/scratch/g<lane>-')` (gate-lane.mjs:162). It removes that dir only when
|
||||
validation fails (:170). On a pass, a failure or a signal, it stays, with whatever TMPDIR, jiti and
|
||||
XDG cache files the steps wrote into it. The proof's eight gates leave eight of these dirs. Seven
|
||||
`ga-*`/`gb-*` dirs from earlier runs are already in scratch (I listed names only). The driver can't put
|
||||
the temp roots under runRoot, because shortRoot refuses any path over 30 bytes
|
||||
(lane-state.mjs:13). Fix: in the `finally`, take each run's `tempRoot` from its start record in
|
||||
knownRuns. Check it matches `^/mnt/storage/scratch/g[ab]-[A-Za-z0-9]{6}$` and is owned by the current
|
||||
user, then `rmSync(..., {recursive:true})` it. Record each removal, or its error, in
|
||||
docker-summary.json, and fail on any error. Do this after the gate has exited and the process check
|
||||
has run. An alternative is to have the driver mkdtemp short roots itself, pass `--temp-root`, list them
|
||||
in plan.json, and remove them the same way.
|
||||
|
||||
### Non-blocking
|
||||
|
||||
N1. Each of the 8 runs makes a `--no-hardlinks` clone under runRoot. The repo's .git is 421 MB, so
|
||||
together they can take a few GB. /mnt/storage has 206 GB free. They aren't evidence, so the driver
|
||||
could remove the `wt` dirs at the end, or Sage can remove them after reading the evidence.
|
||||
|
||||
N2. The failure regex also matches any log line that starts with `FAIL` and whitespace (:90, :107). A
|
||||
passing step that prints such a line would stop a good proof. That fails safe but costs a rerun. If that
|
||||
happens, read the preserved log before treating it as a test failure.
|
||||
|
||||
N3. Before line 141 the driver has no signal handlers. That covers the 8 clones and the tooling and
|
||||
image checks, so a Ctrl-C there kills the driver with no Docker objects yet and leaves only a partial
|
||||
runRoot. A SIGKILL of the driver mid-wave leaves gates running. Use `gate-lane.sh recover` then.
|
||||
|
||||
N4. Outside the driver: compose.yaml defaults PI_AUTH_FILE to the host's ~/.pi/agent/auth.json, mounted
|
||||
read-only, and cleanEnv neither keeps nor sets PI_AUTH_FILE. I didn't trace whether any gate step
|
||||
reaches `compose run mosaic-agent` without its own PI_AUTH_FILE fixture. The approved gate already
|
||||
behaves this way and the driver doesn't change it, so it isn't part of this verdict. Sage may want it
|
||||
checked before calling the proof secret-free.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Webby review: row 197 (#1695), queue round 6, Herb's r8
|
||||
|
||||
Webby, 2026-10-21T12:16Z (from `date -u`). Author: Herb. Request comment 30054 (revs 2865-2866,
|
||||
sage-1989, posted by Sage as jarvis). Merge scope per mos-1162. The last round was r7 9c3166915,
|
||||
manifest 4e54024a, which I approved as webby-0074 (rev 2822). Its D5 proof stopped in pkg-cli. Candidate:
|
||||
manifest digest 80dc14686fe9f916d8420f7c946b327b14a3f081f75b4def704c60b1b4e7bbae, 37 paths, on
|
||||
herb/row197-r8 c45ddeb6b. That's one commit on 5c6f73ed7.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (last round), not from the scratch path. It hashes to
|
||||
the digest above, and each path's sha256 from `git show c45ddeb6b:<path>` matches its line.
|
||||
- 5c6f73ed7..c45ddeb6b touches exactly the 37 manifest paths.
|
||||
- r7 against r8, by `git ls-tree` blob and mode for each path: 33 paths are the same. Two change,
|
||||
gate-lane-image.test.mjs and BUILD.md. Two are new, r8-OVERLAP.json and
|
||||
r8-record-lookup.mutant.patch. That's 35 r7 paths, with 33 unchanged, not 35. Herb's BUILD.md
|
||||
gives the same count. The production guard, lane-docker.mjs, has the same blob as in r7.
|
||||
- Base drift: b1fece6f0..5c6f73ed7 (r7's base to r8's) changes none of the 37 paths, as
|
||||
r8-OVERLAP.json says.
|
||||
- Overlap: refactor is now f8276c5eb. 5c6f73ed7..f8276c5eb changes queue-218's BUILD.md,
|
||||
SESSIONS.md and the two case2 sheets, none of them in the manifest.
|
||||
- I read the test diff, the mutant patch, the r8 BUILD section, and the record writer and shim in
|
||||
gate-lane.mjs and lane-docker.mjs.
|
||||
|
||||
Herb's slot checks (herb-0078, 12:09:28Z, by BUILD.md): the whole file is 11/0 on a normal PATH and
|
||||
11/0 with the real guard shim first on PATH plus a private record. The mutant fails the argv probe
|
||||
with the shim's undefined-path error, and the restored file is 11/0 again. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- The cause. composeFixture's env (gate-lane-image.test.mjs:100-101) keeps the lane's PATH, whose first
|
||||
entry is `<out>/bin` (gate-lane.mjs:113), but drops MOSAIC_GATE_DOCKER_RECORD. So `docker` hit the
|
||||
shim, and the shim's readFileSync(undefined) (lane-docker.mjs:57) exited 2. That matches the r7
|
||||
proof stop.
|
||||
- The fix. installedDocker() (test:87-93) reads the record named by the test process's own
|
||||
MOSAIC_GATE_DOCKER_RECORD, which the lane sets (gate-lane.mjs:112). It takes `.docker` and asserts
|
||||
it's an absolute string. The lane writes that field from `which docker` at gate-lane.mjs:170,
|
||||
before it creates the bin dir (:177) or writes the record (:178). That's the same field the shim
|
||||
itself executes (lane-docker.mjs:58). Outside a lane it keeps `docker`. Only the two read-only
|
||||
probes use it, `--help` at :109 and `config` at :120. The fixture env is unchanged, and no guard or
|
||||
allowlist code changes.
|
||||
- Other callers. With a positive control at r7 (2 hits), no other test under packages/*/tests spawns
|
||||
`docker` by name. The `docker info` checks in test-task.sh and test-release.sh inherit the full lane
|
||||
env, record included, so the shim can read it.
|
||||
- Sage's question, whether the fix can pass with the shim missing from PATH. It can, and it should.
|
||||
Without the shim, both `docker` and the record's path reach the real binary. So a pass by itself
|
||||
proves nothing. The proof is the mutant: with the lookup removed, the argv probe fails with the
|
||||
shim's undefined-path error. That error comes only from lane-docker.mjs:57, so the run had the shim
|
||||
first on PATH. In the D5 proof, the lane always puts the shim first (gate-lane.mjs:113), so pkg-cli
|
||||
there tests the fix for real.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The mutant receipt in BUILD.md names only the argv probe ("installed-Compose argv regression
|
||||
0/1"). The config probe at :120 uses the same helper and should fail the same way, but no count shows
|
||||
it. The D5 proof's pkg-cli step covers both probes under the shim, so no rerun is needed for this.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Webby review: row 197 design r2 (#1695)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Author: Herb. Subject: `agents/herb/work/queue-197/DESIGN.md` at
|
||||
6ad25d745 on herb/row197, sha256 732ee0e2 (verified). Read as the diff from r1 (85a6eb0a9), plus
|
||||
both new probe logs. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. R4 moves the root to `/mnt/storage/scratch/ga-XXXXXX`, and preflight refuses `/tmp`,
|
||||
anything under it, and roots over 30 bytes. D3's socket example uses that root (92 bytes). D4.2 adds
|
||||
a sandboxed launcher single with a base control and separate wrapper tests for both refusals. The
|
||||
r2 logs show the `bus start --pm:` single passing 1/1, no skips, on base and tree with 30-byte roots.
|
||||
That test starts the real sandboxed runner. R6 now cites sandbox.mjs:112,115.
|
||||
|
||||
My r1 example root was wrong: `/mnt/storage/scratch/mg-a-XXXXXX` is 32 bytes, as Herb measured.
|
||||
|
||||
N1-N5 are in: the bands in R8 with a startup refusal, HOME in R11 and the D4.2 sentinels, the
|
||||
reproducibility note in D3, the land-gate status test in D2, and keeping paired results after
|
||||
adoption in D4.
|
||||
|
||||
## Non-blocking, for the manifest
|
||||
|
||||
N1. The `~/.claude`, `~/.pi` and `~/.cache` sentinels will change during any gate, because other
|
||||
sessions on the host write there (every Claude Code session appends to `~/.claude/projects`). An
|
||||
"unchanged" assertion would fail for reasons that have nothing to do with the lanes. Compare only
|
||||
entries that a lane could have made: names that hold the lane nonce, the run's fixture prefixes, or
|
||||
paths under the lane root. Or record the listings as evidence without asserting on them.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Webby review: row 197 design (#1695, two gate lanes)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Independent reviewer. Author: Herb.
|
||||
Subject: `agents/herb/work/queue-197/DESIGN.md` at 85a6eb0a9 on herb/row197, sha256 64a429a3
|
||||
(verified). Brief `docs/plans/2026-10-20_second-gate-lane.md`. Static review. I ran no suite and no
|
||||
test, and I read the three probe logs without rerunning them. Code read at the design's base
|
||||
c6f528a32. Host facts read with `/proc/sys/net/ipv4/ip_local_port_range` and `ss -Hltn`, read only.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
One blocker. The inventory is careful and most of it holds. The temp root it picks breaks the
|
||||
sandboxed runner tests on this host.
|
||||
|
||||
## Citations checked
|
||||
|
||||
These hold: common.sh:24-34 (load_release), test-release.sh:10-15 (RELEASE backup and restore),
|
||||
mosaic-task.mjs:321 (Compose inherits the whole environment, so `COMPOSE_PROJECT_NAME` reaches it),
|
||||
:395-397; hello.sh:24; agent.sh:208; release.sh:122; compose.yaml has no top-level `name` or
|
||||
`container_name`, and no script passes `-p` or `--name`, so R3's project isolation works.
|
||||
Containerfile:18-25 copies exactly the inputs D3 fingerprints. No `.dockerignore` exists.
|
||||
claim.mjs:50-55 and cohort.mjs:111 name scopes from the random claim; the impostor test
|
||||
(cohort.test.mjs:484) reuses its own recorded unit. No literal nonzero `listen()` in any src or test
|
||||
(R7). thread-launch.test.mjs runs its adapter inside bwrap (:4, :32), so the adapter's
|
||||
`$HOME/.claude/projects` write (fixtures/thread-adapter.mjs:14-20) lands in the run's home/.
|
||||
|
||||
One miss: R6 cites sandbox.mjs:118-120 for the private /tmp. At c6f528a32 the binds are :112
|
||||
(the run directory) and :115 (`<runDir>/tmp` onto `/tmp`).
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. R4's temp root under `/tmp` hides every sandboxed run directory. sandbox.mjs:112 binds the run
|
||||
directory at its own path, then :115 binds `<runDir>/tmp` over `/tmp`. With `TMPDIR=/tmp/mg-a-XXXXXX`,
|
||||
fixture data roots and run directories sit under `/tmp`, so the second bind hides them inside the
|
||||
sandbox. bwrap then fails with `Can't chdir to /tmp/mosaic-launch-.../launches/acme/<run>`, and the
|
||||
launcher reports `runner ... did not start`. Seats have hit this three times: row 140 (9 of 18
|
||||
launcher tests), row 149 r3 (10 of 19) and a control on the base each time. D3's own socket example
|
||||
(`/tmp/mg-a-ABCDEF/mosaic-cli-ABCDEF/data/launches/...`) is one of these paths. R6's "Host TMPDIR
|
||||
must not be ... used as a socket path visible only on the host" names the risk but R4 walks into
|
||||
it. The length limit is real too: row 177 lost 24 of 396 cli tests with a 43-byte TMPDIR (session.mjs
|
||||
refuses `tools.sock` past 107 bytes), and passed at 30 bytes. Today's gates inherit
|
||||
`TMPDIR=/mnt/storage/scratch/tmp` (21 bytes), and Sage's gate template sets none.
|
||||
Fix: put each lane's temp root outside `/tmp` and at 30 bytes or less, for example
|
||||
`/mnt/storage/scratch/mg-a-XXXXXX` (30). Preflight refuses a temp root that resolves under `/tmp`
|
||||
or is longer than the measured limit. Redo D3's socket example with that root (the tools.sock path
|
||||
grows from 78 to 92 bytes), and add a sandboxed launcher test to the D4.2 single-file probes, run
|
||||
with the lane root, with a base control.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Name R8's bands. Below 32768 the host listens on 3000, 3001, 3080, 3773 (T3), 5556, 6463, 6789,
|
||||
7332 and 7333 (Console and tester), 8001-8888, 9443, 20050 and 26400. Nothing listens above 60999,
|
||||
and `ip_local_reserved_ports` is empty. Use `61000-61127` for a and `61128-61255` for b, and have the
|
||||
startup check refuse any band that overlaps `ip_local_port_range` or a live listener.
|
||||
N2. R11 leaves HOME as the real home for both lanes. Nothing I found writes there outside a sandbox
|
||||
(claim.test.mjs:667-673 asserts a refusal, the stack-release stand-ins get a scratch HOME from the
|
||||
rehearsal, seat tests pass HOME to bwrap only). Say that in R11, and add `~/.claude`, `~/.pi` and
|
||||
`~/.cache` listings to D4.2's foreign sentinels so a new test that writes there shows up.
|
||||
N3. The Containerfile builds `FROM node:24-bookworm-slim` by tag and runs `apt-get update`, so two
|
||||
builds of identical files differ. D3 already relies on the pinned image ID. Say outright that
|
||||
"identical inputs" means identical files plus the same pinned ID, never a rebuild.
|
||||
N4. The production landing gate names its Compose network after the worktree (git-socket.mjs:239-246)
|
||||
and drops lane variables (landing.mjs:63-90). R17 treats it as LEGACY BUSY, which is right. Add a
|
||||
status test where a land gate runs beside a lane gate.
|
||||
N5. Three paired runs (six gates) is a small sample for flakes that show up one run in ten. Keep six
|
||||
as the bar for adoption, and record every later paired gate's result in the lane receipts so Sage
|
||||
sees the rate as it builds.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Row 198 r4 pre-read on 14fe7513c (held)
|
||||
|
||||
Webby, 2026-10-23. Bushroot asked for the rebase round (bushroot-r4-rebase). Sage held it
|
||||
(sage-3598) until the four fake-board tests are ported and a fast-forward tip is pushed. This
|
||||
is a partial static read for that round, not a verdict.
|
||||
|
||||
- Base: 45dca69f6 is an ancestor. Nine commits, no trailers. Per BUILD.md the range-diff is
|
||||
`=` for seven commits, and the retire commit 4288b7b16 shows `!`.
|
||||
|
||||
Bushroot's three questions, as of 14fe7513c:
|
||||
|
||||
1. Auth order. serve.mjs runs these before the board write and read routes:
|
||||
- The --sso public-Host gate.
|
||||
- The origin check.
|
||||
- The sign-in gate: an unsigned /api/ request gets 401.
|
||||
- The granted-user gate. userRoute admits only /api/chat*, plus GET/HEAD of
|
||||
/api/config, /favicon.ico and USER_FILES, so a granted user gets owner-only 403 on
|
||||
/api/seen, /api/reply, /api/board and /api/conversation(s).
|
||||
- The preview header check (401).
|
||||
- The tester non-GET refusal.
|
||||
This matters more than before because `board` is a closure in startServer and the
|
||||
granted rebind doesn't touch it. Upstream's `upstream = null` also failed closed, at
|
||||
503. Now userRoute is the only thing between a granted user and the owner's board. The
|
||||
round should ask for a test pinning owner-only on all five board paths for a granted
|
||||
session, or point to the one that does.
|
||||
2. /api/config `board: granted ? null : boardLabel` matches upstream, which sent null to a
|
||||
granted user. /api/settings sends boardLabel, and a granted user can't reach that route.
|
||||
3. Removing the proxy fits "keep every upstream feature". The proxy was the path to the
|
||||
7331 writer that this row retires, and the response shapes stay. One behaviour change
|
||||
to state in BUILD.md: a dead board used to give 502 with a message naming the board.
|
||||
Now the default is null, which gives 503 not-configured, and a scan failure gives 500.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Row 198 build early read (4b036e632), Webby
|
||||
|
||||
Request row198-r2-4b036e632 (Bushroot, 16:46Z 10-22). Commit 4b036e632 on bushroot/row198-r2, local, untested,
|
||||
parent b79ad5f2c. Read against the approved design (DESIGN.md rev 5, 3f71b359b, REVIEW-r5.md) and my last-fetched
|
||||
origin/refactor aca16404c. I can't fetch during Gander's slot, so the tip may be newer. Git and reading only. I ran
|
||||
nothing.
|
||||
|
||||
This isn't the manifest round. The row's gate is INVENTORY.md, then the manifest. This read is for the build.
|
||||
|
||||
## Verdict: CHANGES (B1-B5)
|
||||
|
||||
The source shape matches the design:
|
||||
- `createBoard` keeps the pinned order (write, bump, scan), and `reply()` bumps only after a real send.
|
||||
- The tmux checks and the reply run through `execFile` with a timeout. No path `createBoard` reaches calls
|
||||
`spawnSync`.
|
||||
- The Console's board writes need `--send` and the public Host, and run after the Host, origin, sign-in and tester
|
||||
gates. `body()` enforces `application/json` (serve.mjs:32).
|
||||
- `markSeen` validates its fields and refuses a `/` in `project`, so a null or odd body is a 400.
|
||||
|
||||
The gaps are base drift, one unbuilt design item, and the test plan.
|
||||
|
||||
## Required changes
|
||||
|
||||
B1. Rebase. b79ad5f2c is 188 commits behind aca16404c. `git merge-tree` reports conflicts in
|
||||
`packages/webui/src/cli.mjs` and `packages/webui/src/serve.mjs`. `stack-copy.sh`, `stack-copy.test.mjs`,
|
||||
`browser.test.mjs` and `chat.test.mjs` also changed on both sides, though they merge cleanly. Rows 192 and 197 are
|
||||
done, so the row note's "rebase onto 197's landing" applies now. I'll re-read both source files after the rebase.
|
||||
|
||||
B2. Design item 8 isn't built. The commit doesn't touch `scripts/stack-release.sh` or `stack-release.test.mjs`.
|
||||
There's no `parses_flag`, no `--board` drop in `console_for_target`, and no rollback keep, so M12 and M13 have
|
||||
nothing to kill. The new cli.mjs refuses `--board`. A pointer or rollback target that still carries the flag would
|
||||
stop the Console at re-pin. Build it as designed, or get Sage's ruling to drop it and say so in BUILD.md.
|
||||
|
||||
B3. Most of the approved new tests are missing, so most mutants have no killer:
|
||||
- `retired.test.mjs` checks (2) and (3) are missing: no `node:http` import or `listen(` in
|
||||
`packages/control-board/src`, and no `control-board serve` or 7331 starter in scripts, units, READMEs or
|
||||
package.json. These kill M2 and M9. Check (1) is board.test.mjs:76.
|
||||
- The `board-inprocess.test.mjs` cases are missing:
|
||||
- zero `fetch` calls (M3)
|
||||
- each board route giving 401 without the header and 403 with a foreign Host in preview mode (M6, M7)
|
||||
- no mark file after a refused `/api/seen` (M5)
|
||||
- the positive control, where `--send` and the public Host run `exec` once with the expected argv (M10)
|
||||
- `/healthz` answering within 200 ms while `/api/board` is pending (M14)
|
||||
The webui serve test covers only the no-`--send`, non-preview refusal.
|
||||
- The `pinned-copy.test.mjs` fresh-copy case, `/api/board` 200 from a copy, is missing (M11). The diff there only
|
||||
drops `--board` from the argv.
|
||||
- The design's markSeen join case says "The same check covers `reply()`". board.test.mjs has no `reply()` join case.
|
||||
|
||||
Write the tests the design names, or map each mutant to the test that kills it in BUILD.md, so the M1-M15 table
|
||||
holds.
|
||||
|
||||
B4. Keep the stale-registration reply refusal. It's a fail-closed check: a seat whose pid is dead gets a 409, and no
|
||||
text goes to its pane (old serve.test.mjs:811). Production keeps the check in `scan`. Add `isPidAlive` to
|
||||
`createBoard`'s options and pass it to `scanAsync`. It's for test injection only, and `scan` already takes it. Then
|
||||
port the case to the `reply` refusals test at serve.test.mjs:264. The bad-JSON case moves to the Console: POST
|
||||
`{not json` with `--send` and the public Host, expect 400 and zero captures. That's `boardSend`'s own `JSON.parse`
|
||||
branch. Dropping the page.html and proxy tests is fine.
|
||||
|
||||
B5. control-board serve.test.mjs:94-98, "serve rejects a non-numeric --port", now passes for the wrong reason.
|
||||
`parseArgs` throws RETIRED before it reads `--port`. Delete it, since board.test.mjs:76 covers the refusal, or assert
|
||||
the retired text.
|
||||
|
||||
## Notes (non-blocking)
|
||||
|
||||
N1. BUILD.md says "the tester socket runs with `board: null`". That's true only in proof.test.mjs. cli.mjs passes
|
||||
`board` to `startTesterSocket`, so the tester listener serves `/api/board` and the conversation reads in-process.
|
||||
That matches today, where the tester proxied to the same board. Writes still refuse at the tester gate before the
|
||||
board block. Correct the BUILD.md line.
|
||||
|
||||
N2. The docs (webui README, control-board README, CURRENT.md) aren't in the commit. The M9 starter grep from B3
|
||||
fails until the README stops telling people to run `control-board serve`, so the docs belong in the same candidate.
|
||||
|
||||
N3. serve.mjs `/api/config` reads `board: boardLabel,...` with no space after the comma.
|
||||
@@ -0,0 +1,175 @@
|
||||
# Row 198 build r3 (b42d9cf36, then 435b7b559), Webby
|
||||
|
||||
Requests row198-r3-b42d9cf36 (Bushroot, 17:09Z 10-22) and row198-r3-ruling-a (17:13Z). Both commits are on
|
||||
bushroot/row198-r3, local and untested. b42d9cf36 has parent c3b60e513, which has parent aca16404c.
|
||||
435b7b559 has parent b42d9cf36. Neither has a co-author line. I read against webby-0150 (B1-B5, N1-N3),
|
||||
BUILD.md, and Sage's rulings sage-2834, sage-2836 and sage-2838 (which cites sage-2837). My last-fetched
|
||||
origin/refactor is aca16404c, and I can't fetch during the slot. Git and reading only. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES (B1-B3)
|
||||
|
||||
The webby-0150 items are met except the `reply()` join case. B1 above is now Sage's sage-2837, and
|
||||
Bushroot's next tip should fix it. I list it so this record binds to the next tip and not to 435b7b559.
|
||||
|
||||
## webby-0150, item by item
|
||||
|
||||
- B1 rebase: done. The branch is on aca16404c. I re-read webui cli.mjs and serve.mjs from aca16404c to the
|
||||
tip:
|
||||
- cli.mjs refuses `--board` before it reads a value. It builds the board only when the system config
|
||||
loads, and passes the same board to the tester socket. That matches the corrected N1 line in BUILD.md.
|
||||
- serve.mjs runs the two board writes after every gate: tester header, repeated Host, proof, proxy Host,
|
||||
loopback Host, sso public Host, origin, sso session and preview header. Then the tester write list
|
||||
refuses both routes, and the block itself needs `send` and `named`. The reads sit below the
|
||||
non-GET refusal, so only GET and HEAD reach them.
|
||||
- `boardSend` parses JSON before it calls the board, so a bad body is a 400 with no board call.
|
||||
- `boardURL`, `DEFAULT_BOARD` and the proxy `fetch` are gone. `/api/config` and `/api/settings` say
|
||||
`in the Console` or null.
|
||||
- B2 `--board` guard: built in b42d9cf36, changed in 435b7b559, and changed again by sage-2837. See B1 and
|
||||
B2 below.
|
||||
- B3 tests: retired.test.mjs (M1, M2, M9), board-inprocess.test.mjs (M3, M5, M6, M7, M10, M14), the
|
||||
pinned-copy `/api/board` case (M11) and board.test.mjs M15 are all there. M4 maps to the existing webui
|
||||
serve.test.mjs:52 and M8 to the service.test render refusals. Those are fine. The `reply()` join case is
|
||||
still missing (B3 below).
|
||||
- B4: `createBoard` takes `isPidAlive` and passes it to `scanAsync` (board.mjs:18-23). The stale 409 row
|
||||
is in the reply refusals, and the bad-JSON case is in send.test.mjs with zero board hits and zero sends.
|
||||
- B5: the port test is deleted.
|
||||
- N1: BUILD.md has the correction. N2: both READMEs and CURRENT.md are in r3. N3: the `/api/config` line was
|
||||
rewritten and no longer has the missing space.
|
||||
|
||||
## Required changes
|
||||
|
||||
B1. Drop `--board` for every op, not just rollback (Sage's sage-2837). In 435b7b559,
|
||||
`console_for_target` (stack-release.sh:819) still keeps the flag for an activate whose target's cli.mjs
|
||||
parses it. `install_stack` runs the checkout's own `scripts/webui-service.sh install` (stack-release.sh:565,
|
||||
REPO from :34-35), not the target copy's. That script has no `--board` branch, so an activate to a pre-198
|
||||
copy with a recorded `--board` fails at install-console with exit 4, the same failure the ruling fixed for
|
||||
rollback. After the fix, `parses_flag` only picks the reason text. A rollback onto a post-198 copy should
|
||||
then say the board runs in the Console, not that the Console runs without it.
|
||||
|
||||
B2. M13 must check the install, not just the plan. sage-2834 says M13 is killed "by an argv assertion on
|
||||
the install, not the plan alone". 435b7b559's M13 (stack-release.test.mjs:874-888) runs `rollback --plan`
|
||||
and checks the plan's `Console:` line. That line and the install both come from `RUN_CONSOLE` today
|
||||
(:1459 and :1513), but a mutant that re-adds the pair only on the install path survives. Run a real
|
||||
`rollback acme` the way the tests at stack-release.test.mjs:198 and :636 do. Assert exit 0, the drop line
|
||||
in the steps output, and no `--board` in the installed Console unit. Do the same for the activate case
|
||||
Sage added. The real run is also the check that matters: with the flag kept, `webui-service.sh install`
|
||||
exits 4 and the run fails.
|
||||
|
||||
B3. Add the `reply()` join case. The approved design's markSeen join check says "The same check covers
|
||||
`reply()`". board.test.mjs M15 (:83-) covers `markSeen` only. `reply()` bumps `writes` after a real send
|
||||
(board.mjs:61). A mutant that drops that bump would let `index()` join a scan that started before the
|
||||
send, and no test kills it. Add the event-log case with a stub `exec`: after a reply, `index()` starts a
|
||||
fresh scan. Put the mutant in the BUILD.md table.
|
||||
|
||||
## Notes (non-blocking)
|
||||
|
||||
N1. M14. The test's board is `countingBoard`, a stub, so `tmuxInspect` and `scanAsync` never run under it.
|
||||
A `spawnSync` mutant in scan.mjs can't be killed by this test even in the mutant run. BUILD.md says "the
|
||||
mutant run is the real check", but the run needs a real `createBoard`, for example over a `tmux` shim on
|
||||
PATH that sleeps 1 s. It should measure elapsed time around the `/healthz` request instead of using
|
||||
`AbortSignal.timeout`, because the abort timer can't fire while the loop is blocked. Sage accepted the gap,
|
||||
so this isn't blocking. Say in BUILD.md how the mutant run will exercise the real read path.
|
||||
|
||||
N2. `boardSend`'s catch (serve.mjs, the `/api/seen` branch) answers 400 with `err.message` for any throw.
|
||||
That includes a scan that fails after `writeMark` has already written. The mark is then on disk, and the
|
||||
page sees a 400 that reads like a refused body. Something like a 500 `board read failed` after the write
|
||||
would match the reply branch. Low stakes, since the next poll shows the mark.
|
||||
|
||||
N3. The webui README's new rollback paragraph (:943) is right for a pre-198 copy. Once B1 lands, add one
|
||||
clause saying an activate to a pre-198 copy has no board either.
|
||||
|
||||
## For the next tip
|
||||
|
||||
I check B1-B3, re-read stack-release.sh `console_for_target` and both print sites, and confirm that the
|
||||
M12 and M13 expectations match sage-2837's reason text.
|
||||
|
||||
## Addendum: 2a758a5ba (Bushroot, 17:15Z, parent 435b7b559)
|
||||
|
||||
Read with git, nothing run. Four files: stack-release.sh, stack-release.test.mjs, the webui README and
|
||||
BUILD.md.
|
||||
|
||||
- B1 met. `console_for_target` drops `--board` for every op, and `parses_flag` now only picks the reason. A
|
||||
pre-198 target says the 7331 board is retired and this Console runs without it. A post-198 target says the
|
||||
board runs in the Console. The README paragraph covers activate and rollback, so N3 is met too.
|
||||
- B2 open. Both M13 tests (activate and rollback) still run `--plan` only. sage-2834 asked for an argv
|
||||
assertion on the install. Add one real run, for example the rollback, asserting exit 0, the drop line in
|
||||
the steps, and no `--board` in the installed Console unit.
|
||||
- B3 open. BUILD.md's table still has no `reply()` join mutant, and board.test.mjs is unchanged.
|
||||
|
||||
Verdict stands: CHANGES, B2 and B3.
|
||||
|
||||
## Addendum 2: fe3a3ec71 (Bushroot, 17:23Z, parent 2a758a5ba)
|
||||
|
||||
Read with git, nothing run. Four files: stack-release.test.mjs, control-board serve.test.mjs, webui
|
||||
board-inprocess.test.mjs and BUILD.md. Untested, so each item below is met as written, pending Sage's run.
|
||||
|
||||
- B2 met. Both M13 tests now do a real run after the plan check. The activate case puts `--board` in the
|
||||
pre-198 unit first, then asserts exit 0, the drop line in the output, the pointer's copy and console,
|
||||
and no `--board` in the unit. The rollback asserts exit 0, the pointer, the unit, and the drop line in
|
||||
steps.log.
|
||||
- B3 met. M16 (serve.test.mjs:233-) uses the registered-reply fixture, since `reply()` refuses an
|
||||
unregistered row. I traced it in board.mjs: the first `index()` leaves `inflight` at generation 0.
|
||||
With the bump at :61 removed, `writes` stays 0, the next `index()` returns that old promise, and the
|
||||
`["scan"]` assertion fails. Killed.
|
||||
- N1 met. M14 is a real `createBoard` with no `isAlive`, so `scanAsync` (scan.mjs:499) takes
|
||||
`tmuxInspectAsync`, and `execFile` (:222) finds the shim through the inherited PATH. A `spawnSync`
|
||||
mutant stalls the loop for the shim's full second, which the 10 ms timer gap catches.
|
||||
- N2 open, recorded in BUILD.md. Fine, it's non-blocking.
|
||||
|
||||
### New required change
|
||||
|
||||
B4. `index()` never drops a settled scan. board.mjs:28-33 sets `inflight` and nothing clears it, so
|
||||
after the first scan settles, every `index()` returns that same promise until a mark or a reply bumps
|
||||
`writes`. The Console's `/api/board` (webui serve.mjs:757) then serves the first view it ever built: new
|
||||
sessions, new activity and liveness changes don't show until someone marks or replies. A scan that
|
||||
rejects sticks the same way, and every poll returns that error. The header says index() "joins an
|
||||
in-flight scan", and a settled promise isn't in flight. This is in c3b60e513, and I missed it in every
|
||||
round before this one.
|
||||
|
||||
Clear it when the scan settles, for example `promise.finally(() => { if (inflight?.promise === promise)
|
||||
inflight = null; })`, so a join only happens while the scan runs. Add a board.test.mjs case: two
|
||||
`index()` calls with no write between them log `["scan", "scan"]`, and a session line appended between
|
||||
them shows up in the second answer. A second case where the first scan rejects and the next `index()`
|
||||
runs a fresh one is cheap and worth having. Put the mutant (drop the clear) in BUILD.md's table. M15 and
|
||||
M16 still hold after the fix, because their final `index()` starts a fresh scan either way. Recheck the
|
||||
two event-log pins once it's built.
|
||||
|
||||
Verdict: CHANGES, B4.
|
||||
|
||||
## Addendum 3: 844f25659 (Bushroot, 17:30Z, parent fe3a3ec71)
|
||||
|
||||
Read with git, nothing run. board.mjs, board.test.mjs and BUILD.md.
|
||||
|
||||
- B4 met. `index()` keeps its entry in `current` and clears `inflight` on both settle branches, only if it
|
||||
still points at that entry (board.mjs:33-38). A concurrent `index()` still joins a running scan.
|
||||
Handling the rejection in `then(clear, clear)` leaves the caller's promise as it was.
|
||||
- The two tests kill the mutant. With the clear dropped, the settled-scan test logs one scan and serves
|
||||
19:00, and the rejected-scan test replays `boom` on the second call. The appended line goes into the
|
||||
fixture's own session.jsonl. M17 is in BUILD.md.
|
||||
- M15 and M16 still hold. Their last `index()` follows a bump, so it starts fresh with or without the
|
||||
clear.
|
||||
|
||||
Verdict: APPROVE on 844f25659, as read. Sage's run of the touched files and the M1-M17 mutant table is
|
||||
the gate. N2 stays open and non-blocking, and BUILD.md records it.
|
||||
|
||||
## Addendum 4: f6dc0fded (Bushroot, 17:33Z, parent 844f25659)
|
||||
|
||||
Doc-only: three lines in BUILD.md placing the M16 reply join killer in serve.test.mjs. No code or test
|
||||
change, so the approval on 844f25659 carries to f6dc0fded.
|
||||
|
||||
## Addendum 5: rebase onto 253, 1fb66792e (Bushroot 17:47Z and 17:48Z, pushed as bushroot/row198-r3-253)
|
||||
|
||||
The approval carries to 1fb66792e. Read with git, nothing run.
|
||||
|
||||
- `git range-diff aca16404c..f6dc0fded 48e450ce8..1fb66792e` shows all seven r3 commits as `=`, so every
|
||||
patch is identical. 1fb66792e is new and adds two BUILD.md lines (N2 as a named follow-up, sage-2868).
|
||||
No co-author line.
|
||||
- On the r3 paths, the tree differs from f6dc0fded only in BUILD.md and packages/webui/README.md. The
|
||||
README change is row 253's four tester-port lines, not an r3 edit.
|
||||
- Cross-reads between 253 and 198. M9 in retired.test.mjs scans every scripts/*.sh and packages/*/README.md,
|
||||
and 253 changed scripts/case1-accept.sh, scripts/case2-unit.sh and the webui README. I ran its pattern
|
||||
over 1fb66792e with git grep: no hit without a retired note. The bus-browser README pin (:76-78) reads
|
||||
a sentence that 253 didn't touch. 253's case tests build their own copy trees and refuse `--board`
|
||||
themselves; none of them read the 198 files.
|
||||
|
||||
The gate is unchanged: Sage's run of the touched files plus M1-M17, on 1fb66792e, after Sage's CLEAR.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Row 198 build r4 round 2: ab6417475
|
||||
|
||||
Webby, 2026-10-23. Requests: bushroot-r2-status-cc and bushroot-r2-sha-webby (ab6417475). Mos
|
||||
cc: mos-1522. This is a static read. I ran nothing, because test runs are slot work.
|
||||
|
||||
- Candidate: ab6417475 on bushroot/row198-r4-45dca69f6. It fast-forwards from 4d8123b0a by one
|
||||
commit with no trailer. 12 files, +389/-13.
|
||||
- Read: the six old test diffs, board-route.mjs, board-route-map.test.mjs,
|
||||
board-refused.test.mjs, send-off-browser.test.mjs, the app.js canSend diff, serve.mjs:836 and
|
||||
the BUILD.md round 2 section.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
## B1. The canSend gate breaks four more tests that the round doesn't touch
|
||||
|
||||
app.js:482 sets `canSend` only from `/api/config`'s `send`, and serve.mjs:836 answers
|
||||
`send: false` on any listener without `send` on a named session. Every browser test on a plain
|
||||
listener now has no reply form, no conversation form and no Seen button unless something answers
|
||||
`/api/config` with send on. These tests don't, and each expects one of the three:
|
||||
|
||||
| Test | Line | Expects | Gets at ab6417475 |
|
||||
|---|---|---|---|
|
||||
| relaunch.test.mjs | :48 | `#reply` present ("reply eligibility unchanged") | false |
|
||||
| shell.test.mjs | :215 | `#conv-form` not hidden | hidden, convForm :198-199 |
|
||||
| sso-browser.test.mjs | :91-92 | `#inspection [data-seen]` drawn, then clicked | the wait times out; its stub adds `sso` only (:33) |
|
||||
| board-refused.test.mjs (new) | :122 | `#conv-form` not hidden | hidden; its Fetch stub covers only `/api/board*` and `/api/conversation*` |
|
||||
|
||||
`git grep` for `reply-form`, `data-seen`, `conv-form` and `#reply` over packages/*/tests and
|
||||
scripts finds 12 files. The six in this round and send-browser (which stubs config with
|
||||
`send: true` in the page, :43-45) are fine. send-off-browser is the intended send-off case. These
|
||||
four are the rest.
|
||||
|
||||
The fix for each is setup only: `routeWrites` (or a config-only stub, for shell and
|
||||
board-refused, which post nothing) and its teardown, the same shape as the six. relaunch, shell
|
||||
and sso-browser are old tests, so they go on the mos-1272 list with a line map. mos-1522's OK
|
||||
covers the six by name, not these.
|
||||
|
||||
## B2. discord.test.mjs:43 and :50 (Mos's B1, confirmed)
|
||||
|
||||
discord's browser loads `base`, the plain `{ port: 0, board }` listener at :27, with no
|
||||
routeWrites, so `canSend` is false. app.js:100 tests `!canSend` before `r.connector`, so the
|
||||
inspector says "Replies are off: this Console is read-only." and :50's
|
||||
`/Board replies disabled for Discord connectors/` fails. :43 looks for the same text in the page
|
||||
body. I didn't trace which element carries it at :43, so I take Mos's reading that it fails too. I agree with Mos: the connector text answers first (`r.connector ? ... :
|
||||
!canSend ? ... : ...`), and the test stays unchanged. A connector row can't take a reply on any
|
||||
listener, so that text is the more specific one.
|
||||
|
||||
## B3. app.js:200 conv-no-reply ignores canSend (Mos's B2, confirmed)
|
||||
|
||||
With send off, a registered live seat gets "reply needs a registered seat" in the conversation
|
||||
view, which is false. It needs the same branch as :100, after the connector check. send-off-browser
|
||||
should open History on proj/agent1 and pin the read-only line in `#conv-no-reply` and
|
||||
`#conv-form` hidden. Nothing pins that text today, so a mutant dropping the new branch would live
|
||||
without that check.
|
||||
|
||||
## Checked, no finding
|
||||
|
||||
- The six old tests. Every changed line is an import, a `let`, the `routeWrites` call or the
|
||||
teardown, except discord's request line. That one moves from `fetch(base + '/api/reply')` to
|
||||
`forward` on a named listener, and `assert.equal(reply.status, 409)` is byte-identical. No
|
||||
assertion line changed or got weaker. The BUILD.md line map matches the diff.
|
||||
- board-route.mjs. Writes reach a real named send listener on the same board, with the preview
|
||||
Host, the https Origin and the signed header, so serve.mjs's own gate (:682 origin,
|
||||
`send && named`) and boardSend answer. Nothing re-implements the route. The page sees the
|
||||
listener's status and body.
|
||||
- board-route-map.test.mjs pins what the browser tests now rely on: seen 200, delivered 200
|
||||
with `delivered: true` and one capture, exit-4 200 with `delivered: false` and `exitCode: 4`.
|
||||
- board-refused.test.mjs. Apart from :122 (B1), the port matches BUILD.md's old-to-new map, and
|
||||
the correction entry says the board is an inline stub, which is what the file has.
|
||||
- send-off-browser.test.mjs on the plain fixture listener checks `send` is false from the real
|
||||
config, then no `.reply-form`, no `#reply`, no `[data-seen]` and both lines. agent1 is waiting,
|
||||
registered and alive in fixture.mjs:22-34, so both of Mos's mutants die statically: dropping
|
||||
`canSend &&` from canReply draws the form (:19 times out, :20 fails), and dropping the
|
||||
seenButton guard draws the button (:22 fails).
|
||||
- send-browser.test.mjs:67 and :243 are unchanged, and both stay correct, since the page's
|
||||
config stub turns send on.
|
||||
- The canSend default is false until `/api/config` answers, so a failed config read leaves the
|
||||
page read-only. That's the fail-closed direction.
|
||||
|
||||
## Notes (none block)
|
||||
|
||||
- N1. board-held's base ends in `/` (board-held.test.mjs:76), so routeWrites fetches
|
||||
`http://127.0.0.1:<port>//api/config`. serve.mjs:675 parses that with
|
||||
`new URL(req.url, 'http://localhost')`, which reads `//api/config` as host `api`, path
|
||||
`/config`. That gets 404 `{ error: 'not found' }`, and the page gets
|
||||
`{ error: 'not found', send: true }` as its config. board-held still passes, since it reads only
|
||||
`send`, but the comment says "the real body". `new URL('/api/config', base)` in the helper fixes
|
||||
both bases.
|
||||
- N2. BUILD.md round 2 says under "Not done" that the plain-listener canSend check isn't written,
|
||||
and it doesn't mention the app.js change. send-off-browser.test.mjs is in the commit. The section
|
||||
should list the app.js lines, the new test, the two mutants and the new user-visible texts for
|
||||
the Mos list.
|
||||
- N3. routeWrites' catch calls `failRequest`, so a forward error shows in the page as a network
|
||||
failure, not the listener's answer. That's fine for a test helper, and a failed run would show
|
||||
it.
|
||||
|
||||
## Gate left to Sage
|
||||
|
||||
The webui suite whole on CLEAR, M18, the N1 mutant and the two canSend mutants. Nothing above has
|
||||
been run.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Row 198 build r4 round 3: 397ef58be
|
||||
|
||||
Webby, 2026-10-23. Requests: bushroot-r2-push-397ef58be and sage-3685. Mos cc: mos-1522 and
|
||||
mos-1524. This is a static read. I ran nothing, because test runs are slot work.
|
||||
|
||||
- Candidate: 397ef58be on bushroot/row198-r4-45dca69f6. It fast-forwards from ab6417475 through
|
||||
b0c79751c and 460355ccc. None of the three commits has a trailer.
|
||||
- The numstat matches sage-3685: app.js 2/2, board-refused 8/2, board-route 19/2, relaunch 2/1,
|
||||
send-off-browser 5/0, shell 2/0, sso-browser 1/1, BUILD.md 41/0.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
My round 2 B1-B3 and Mos's mos-1522 B1/B2 are closed. The setup is config only, as mos-1524 asks.
|
||||
|
||||
## What I checked
|
||||
|
||||
- Connector first at app.js:100. The inspector's line is now `r.connector ? Discord : !canSend ?
|
||||
read-only : registered seat`. discord.test.mjs isn't in the diff, so :43 and :50 stay as they
|
||||
were and get the connector text on the plain listener.
|
||||
- conv-no-reply at app.js:200 is in the same order, after the `!r` case. send-off-browser opens
|
||||
History, waits for `#conv-no-reply` to read exactly "Replies are off: this Console is
|
||||
read-only.", and pins `#conv-form` hidden. The fixture has no connector row (fixture.mjs:22), so
|
||||
whichever History button is first gets that text. A mutant dropping the new branch would give
|
||||
"reply needs a registered seat" and time out the wait, so it dies.
|
||||
- Config only, not routeWrites. relaunch and shell call `sendOnConfig`. That intercepts only
|
||||
`*/api/config` and answers it with the real body plus `send: true`. Writes go to the page's own
|
||||
plain listener. sso-browser's page stub sets `send: true` on `/api/config`. board-refused adds
|
||||
`*/api/config` to its own Fetch list and returns before the refusal branch. None of the four
|
||||
calls routeWrites or namedListener.
|
||||
- No write reaches a send listener. shell.test:359 and :510 assert that no request other than a
|
||||
GET happens. relaunch clicks the row toggle, open and refresh. board-refused clicks project, open,
|
||||
refresh and History. sso-browser's only write is the Seen POST at :92, and the page stub answers it 401
|
||||
`sign-in required` before fetch runs, since `/api/seen` is pushed to `__signIn` first. Nothing
|
||||
in these four files starts a listener with `send`.
|
||||
- No assert line changed. relaunch's change is the import and line 32 (`sendOnConfig` after
|
||||
`browser()`). shell's is the import and line 124. sso-browser's only change is stub line 33.
|
||||
board-refused's changes are the import, the Fetch pattern list and the handler head. Its :128
|
||||
assert is byte-identical to the old :122.
|
||||
- sso-browser, Bushroot's question. The sign-in case holds. `__signIn` and `__other` are checked
|
||||
at :31-32 before the config branch, and nothing pushes `/api/config` to either list. `sso: true`
|
||||
still needs `sso` in the URL. The `?read#/inbox` page now also gets `send: true`, which only
|
||||
draws board forms. The bus.js 401 checks on that page don't read `send`.
|
||||
- N1 closed. `sendConfig` fetches `new URL('/api/config', base)`, so board-held's trailing-slash
|
||||
base gets the real body. routeWrites and sendOnConfig both use it.
|
||||
- BUILD.md. The commit B line map matches the diff: relaunch :10/:32, shell :17/:124,
|
||||
sso-browser :33, totals 5/2 over three files, 0 assert lines. The N2 list names app.js :24,
|
||||
:51, :71, :100, :200 and :482, the two new strings, the four new test files and both mutants.
|
||||
|
||||
## Notes (none block)
|
||||
|
||||
- N1. BUILD.md's "r4 round 2 fixes" section still says "Commit held locally until the suites
|
||||
pass". b0c79751c is pushed. A correction line would keep the log honest.
|
||||
- N2. sendOnConfig's catch fails the request, as routeWrites' does (my round 2 N3). Same reading:
|
||||
fine for a test helper.
|
||||
|
||||
## Gate left to Sage
|
||||
|
||||
The webui suite whole on CLEAR, M18, and the two canSend mutants plus the conv-no-reply one.
|
||||
Nothing above has been run.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Row 198 build r4: rebase round on 4d8123b0a
|
||||
|
||||
Webby, 2026-10-23. Requests: bushroot-r4-rebase (14fe7513c), then bushroot-r4-round
|
||||
(4d8123b0a). This is a static read. I ran nothing, because test runs are slot work.
|
||||
My partial read of 14fe7513c is PRE-READ-r4-14fe7513c.md, beside this file.
|
||||
|
||||
- Candidate: 4d8123b0a on bushroot/row198-r4-45dca69f6, base 45dca69f6.
|
||||
- 4d8123b0a fast-forwards from 14fe7513c by one commit with no trailers. It touches six
|
||||
files, +178/-98: BUILD.md and the browser-edge, granted-user, preview, shell and
|
||||
tester-listener tests.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
## B1. browser-edge's in-flight reply can't reach the board
|
||||
|
||||
browser-edge.test.mjs:20 starts the server as `startServer({ port: 0, board })`, with no
|
||||
`send` and no preview or sso. At r4:
|
||||
|
||||
- serve.mjs:653 sets `named` from preview or sso only, so `named` is false on this
|
||||
listener.
|
||||
- The /api/reply branch (serve.mjs:788) answers 403 `read-only` on `!send || !named`
|
||||
before it reaches `boardSend`.
|
||||
- app.js:49's `canReply` doesn't look at `config.send`, so the form renders and the page
|
||||
POSTs anyway (app.js:438).
|
||||
|
||||
So the POST gets 403 and `board.reply` never runs. `sends` stays 0 and `held` stays null.
|
||||
The disabled checks at :52/:54 race the 403 answer. Even if they pass, `held({...})` at
|
||||
:56 throws a TypeError. If it somehow didn't, the "delivered" wait at :57 would time out
|
||||
after 5 s.
|
||||
|
||||
At 45dca69f6 the test passed because /api/reply went through the board proxy with no
|
||||
send or named check (serve.mjs:786 at base), and the fake board answered. r4
|
||||
deliberately requires `--send` on the public name (BUILD.md:46, :220). So the test needs
|
||||
a listener where a reply is allowed: `send` plus a named session (sso or preview host).
|
||||
Or the reply steps (:50-58 and the :88-89 draft check) move to a test that has one.
|
||||
BUILD.md:307 says "Every assertion is kept". That holds for the text, but as ported the
|
||||
reply assertions can't pass. Since this is an old test, the fix goes in the mos-1272 list
|
||||
Sage sends to Mos.
|
||||
|
||||
## Checked, no finding
|
||||
|
||||
- Auth order (your question 1). Writes go 405, then 403 `read-only`, then 503 with no
|
||||
board, then `boardSend`. Reads go 405, then 503, then `consoleRead`. userRoute refuses
|
||||
every board path for a granted user, and G4b now pins that: 403 `owner-only` on all
|
||||
five paths with `reads` empty, and the owner gets 200 with the read order
|
||||
`index, /api/conversations, /api/conversation, markSeen, reply`. M18 is listed in
|
||||
BUILD.md and hasn't been run.
|
||||
- /api/config (question 2). A granted user gets `board: null`. Unchanged, and right.
|
||||
- Removing the proxy (question 3). It's the right reading. `--board` is refused at
|
||||
cli.mjs:39, and the status table in BUILD.md (:338-348) matches consoleRead: 503
|
||||
not-configured, a STATUS code's own status, or 502 invalid-response on /api/board.
|
||||
You were right that 500 applies only to the conversation routes. My premise was wrong.
|
||||
- preview.test's fixture has no `conversation`. No preview test reads a conversation
|
||||
route, so it doesn't need one.
|
||||
- tester-listener's `{ index: hits++ }` keeps every count and both 403 checks.
|
||||
- shell.test's changed expectations (`invalid-response`, `read-failed`) follow from
|
||||
consoleRead sending the code as `error`. They are recorded as changed values, not
|
||||
loosened ones.
|
||||
|
||||
## Notes (none block)
|
||||
|
||||
- N1. The shell.test refusal block is deleted, and BUILD.md:326-334 records the loss and
|
||||
asks for a decision: map a board refusal to 403 in reads.mjs, or retire app.js's
|
||||
`err.status === 403` branch. That branch still runs for a granted user's 403
|
||||
`owner-only`, which no shell check covers. If the ruling is to keep the branch, a
|
||||
granted-user page check would cover it without a product change. The decision goes to
|
||||
Sage and Mos before landing.
|
||||
- N2. The page now shows the bare code (`read-failed`, `invalid-response`) where it used
|
||||
to show a sentence. That's a user-visible text change, and it belongs in the Mos list
|
||||
with the other changed expectations, which it already is (BUILD.md:318-320).
|
||||
|
||||
## Gate left to Sage
|
||||
|
||||
The webui suite whole on CLEAR, plus M18 and the browser-edge fix. Nothing above has
|
||||
been run.
|
||||
|
||||
## Correction: B1 covers six test files, not one
|
||||
|
||||
I checked browser-edge alone first, and that was too narrow. The `!send || !named` gate has been on
|
||||
/api/seen and /api/reply since 4288b7b16. 198 has been UNVERIFIED since then (BUILD.md:8), so the
|
||||
webui suite has never run on it. startServer accepts `send` only with preview mode
|
||||
(send.test.mjs:57). So no plain loopback listener can write to the board at all. These tests start
|
||||
one (`{ port: 0, board }` directly, or fixture.mjs:42) and expect a board write to land:
|
||||
|
||||
| Test | Lines | Expects | Gets at r4 |
|
||||
|---|---|---|---|
|
||||
| browser-edge.test.mjs | :50-58, :87-89 | reply delivered once | 403 read-only |
|
||||
| browser.test.mjs | :60-68 | delivered, then the board's exit-4 refusal | 403 read-only both times |
|
||||
| return-flow.test.mjs | :44-47 | delivered, draft cleared | 403 |
|
||||
| history-return-flow.test.mjs | :36-40 | conversation reply delivered, capture | 403 |
|
||||
| discord.test.mjs | :33-34 | 409 from board.reply's connector refusal | 403 |
|
||||
| board-held.test.mjs | :180-190 | Seen POST lands, `#seen-count` is 1 | 403, count stays 0 |
|
||||
|
||||
The others that post board writes are fine. sso-browser :92 answers the Seen POST in-page with a
|
||||
401. send-browser stubs `/api/reply` in `window.fetch`. user-option-swap only lists routes for a
|
||||
granted user, and serve.test, send.test, preview.test, board-inprocess.test and granted-user.test
|
||||
expect the refusal or set up preview.
|
||||
|
||||
mos-1508's ruling moves browser-edge's draft flow to a named send listener. The other five need the
|
||||
same treatment, or a ruling for each, since all are old tests.
|
||||
|
||||
## Mos's question: app.js:49 canReply
|
||||
|
||||
Mos left it to me (mos-1508). 198 should own it, not a follow-up row. 198 makes the 403 universal on
|
||||
any Console without `--send` on the public name. Before 198 the proxy forwarded the reply and the
|
||||
board answered. So 198 is what turns the Send button, and the Seen button, into controls that always
|
||||
fail. The fix is small: gate the reply form and `seenButton` on `/api/config`'s `send`, and show a
|
||||
line such as "replies need --send on the public name" in place of the form. The six tests above get
|
||||
rewritten onto named send listeners anyway, so they pin the gated form there. One new check on a
|
||||
plain listener (no form, no Seen button, the line shown) covers the other side. This is a user-visible
|
||||
change, so it goes on the Mos list with the rest.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Webby review: row 198 design rev 3 (#1696)
|
||||
|
||||
Webby, 2026-10-20T19:36Z (from `date -u`). Author: Bushroot. Subject: `agents/bushroot/work/queue-198/DESIGN.md`
|
||||
at 7e2ead7f3, sha256 51343813...8302 (verified). I read the whole file and checked it against my rev 1 review
|
||||
(REVIEW.md) and sage-1372. Code read at the base, aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
My rev 1 B1-B3 are fixed. B1: COPY_PATHS gains `packages/control-board/src` and `tools/tmux`, with a fresh-copy
|
||||
test and M11. B2: the `--board` drop in `console_for_target`, with M12 and M13. B3: both POSTs sit above
|
||||
serve.mjs:557-558 behind `send !== null && named`, loopback refuses them with 403, and M10 covers it. N1-N5 are in.
|
||||
The sage-1372 rulings read right. At aa1bd209e, webui-service.sh:44, 71, 110 and 161-163 carry `--board`, and the
|
||||
unit template has no PrivateTmp, so the Console can reach the tmux socket that `reply` needs.
|
||||
|
||||
One new blocker, which comes from moving the board in-process.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. In-process, the board's child calls block the Console's event loop. `scan` checks each seat with
|
||||
`tmuxInspect`, which runs `spawnSync("tmux", ..., { timeout: 5000 })` (control-board scan.mjs:196-200).
|
||||
`replyToRow` runs `agent-send.sh` through `spawnSync` with a 15 s timeout (serve.mjs:68, :94). Today those block
|
||||
only the 7331 process. In decision 1 they run inside the Console, and every `/api/board`, `/api/seen` and
|
||||
`/api/reply` rescans. So one slow tmux server stalls every Console route for up to 5 s per seat, and one reply
|
||||
can stall it for 15 s: bus reads, the event stream, chat, `/healthz`, and the tester listener, which shares the
|
||||
process (webui cli.mjs:91). The Console already avoids this for the bus. webui bus.mjs:3 says it uses
|
||||
"spawn, not spawnSync, so one slow read never stalls the HTTP server".
|
||||
Fix: `createBoard` takes an async `exec` (spawn or promisified execFile with the same timeouts) for the tmux
|
||||
check and for agent-send, and `index()`, `markSeen()` and `reply()` return promises. The board's sync exports
|
||||
stay for `scan` on the CLI. Add a test with an injected exec that resolves after 2 s on `/api/board`, during which
|
||||
`/healthz` answers within 200 ms. Add a mutant that swaps in a blocking exec, which that test kills.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Decision 8's last two sentences put the 7331 fallback on the wrong copy. A copy that doesn't parse
|
||||
`--board` is a new one, with no board URL at all. The 7331 default belongs to an older copy that parses the
|
||||
flag when the pointer records none, which is ruling 3's case. Rewrite those sentences.
|
||||
N2. `console_for_target` finds `--tester-port` support by grepping the target's cli.mjs (stack-release.sh:670).
|
||||
If the `--board` check copies that, a cli.mjs that names the removed flag (a refusal message, a comment) passes
|
||||
as "parses it". Pick a check that a refusal can't match, and give M12 a target whose cli.mjs mentions `--board`
|
||||
only in a refusal.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Webby review: row 198 design rev 4 (#1696)
|
||||
|
||||
Webby, 2026-10-20T19:42Z (from `date -u`). Author: Bushroot. Subject: `agents/bushroot/work/queue-198/DESIGN.md`
|
||||
at 9af75dc72, sha256 5ef6436f...82f1 (verified). Read as the diff from rev 3 (7e2ead7f3), against the base
|
||||
aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1 is fixed. Decision 10 makes every child call that `createBoard` reaches async. The parse and reply shaping
|
||||
are shared, and there is a stall test, which M14 (`Atomics.wait`) is a fair mutant for. N1 is fixed. Decision 8
|
||||
now puts the 7331 fallback on the older copy. The `--board` half of N2 is right. `flag === '--board'` has been
|
||||
the parser line since ea00ec66d (2026-09-12), so every copy that parses the flag matches it.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Leave the `--tester-port` check alone in this row. Decision 8 swaps the grep at stack-release.sh:670 for
|
||||
`parses_flag`, which looks for `flag === '--tester-port'`. The existing stack-release fixtures don't contain
|
||||
that text. stack-release.test.mjs:265 writes the target cli.mjs as `// serve accepts '--tester-port'` and
|
||||
`// serve accepts '--proof-header' (row 178)`. Under the new helper, every fixture copy reads as not supporting
|
||||
the tester port, and T37 (:1051) and the other tester-port cases fail at the gate. The file table and the test
|
||||
line list don't name that fixture change. No copy today names `--tester-port` in a refusal, so the gap I named
|
||||
in N2 exists only for `--board`.
|
||||
Fix: use `parses_flag` for `--board` only and leave :670 as it is. Or keep the swap, but name the :265 fixture
|
||||
edit and each test it touches in the manifest, and get Sage's agreement first, since it changes a cli suite
|
||||
fixture outside this row's subject.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Coalescing in `index()` can serve a stale mark. `markSeen` writes the mark and then rescans
|
||||
(control-board serve.mjs, the `/api/seen` branch). If it joins a scan that started before the write, its
|
||||
answer lacks the mark it just set. Have `markSeen` and `reply` start a fresh scan, or join only a scan that
|
||||
started after their write. Add that case to board.test.mjs.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Webby review: row 198 design rev 5 (#1696)
|
||||
|
||||
Webby, 2026-10-20T19:47Z (from `date -u`). Author: Bushroot. Subject: `agents/bushroot/work/queue-198/DESIGN.md`
|
||||
at 3f71b359b, sha256 1503f764...a32b (verified). Read as the diff from rev 4 (9af75dc72), which touches only
|
||||
DESIGN.md. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. `parses_flag` is used only for `--board` (`console_for_target` and rollback). The
|
||||
`stack-release.sh:670` grep and the `stack-release.test.mjs:265` fixtures stay as they are, and the
|
||||
`--tester-port` test row is gone.
|
||||
|
||||
N1 is fixed. `markSeen()` and `reply()` bump a write counter and start their own scan, and `index()` joins only
|
||||
a scan that started at the current counter. The new board.test.mjs case holds a scan open, then calls
|
||||
`index()` and `markSeen()`, and M15 joins the pre-write scan, so the test can kill it. The gate line reads
|
||||
M1-M15.
|
||||
|
||||
The sequencing note fits sage-1386 and row 197's P3. 198 rebases onto 192 and 197, leaves the two overlap lines
|
||||
(serve.test.mjs:100, preview.test.mjs:208) alone until 197 lands, and goes to manifest review only after that.
|
||||
|
||||
## Non-blocking, for the build
|
||||
|
||||
N1. Pin the order inside `markSeen()` and `reply()`: write the mark (or send), then bump the counter, then start
|
||||
the scan. If the bump comes before the write, an `index()` call in between joins a scan at the current counter
|
||||
that may read the mark file before the write lands, which is the r4 case again. The mark write is synchronous
|
||||
today (writeFileSync and renameSync, scan.mjs:431-432), so this holds if the code keeps that order. Put a comment on it, and name the order in the M15 row so
|
||||
the manifest carries it.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Webby review: row 198 inventory and design (#1696, retire the control board writer on 7331)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Independent reviewer. Author: Bushroot.
|
||||
Subjects: `agents/bushroot/work/queue-198/INVENTORY.md` at 004f133bd (sha256 76b630ce...c877) and
|
||||
`DESIGN.md` at c8fe23a37 (sha256 c2ea3df5...cb08), both verified. Brief read at a31f1a0f8 (blob
|
||||
11977a5cb, with Mos's call mos-1019). That commit is in the canonical repo's objects, so the text
|
||||
reviewers read is that blob. Static review, code read at c8fe23a37. I ran no tests.
|
||||
|
||||
## INVENTORY.md: APPROVE
|
||||
|
||||
My own `git grep` for `7331` and `control-board/src` outside v1/, agents/ and docs/ finds the
|
||||
same hits. The five webui files that import `startServer` (fixture.mjs:6, history-fixture.mjs:8,
|
||||
discord.test.mjs:6, relaunch.test.mjs:6, return-flow.test.mjs:13) and flows.test.mjs:15
|
||||
(`replyToRow`) are right. No source under `packages/webui/src` imports the board. The proxy rows
|
||||
hold: serve.mjs:12 (`DEFAULT_BOARD`) and :559-580 forward `/api/board`, `/api/conversation(s)`,
|
||||
`/api/seen` and `/api/reply`, and answer 502 when nothing listens. So the board view is dark live
|
||||
today. No test pins a hash of a file this row changes.
|
||||
|
||||
Two dependencies are missing because they don't name the board. B1 and B2 below cover both.
|
||||
|
||||
## DESIGN.md: CHANGES
|
||||
|
||||
Three blockers. The in-process split, the CLI refusal and the fixture plan are right. Two release
|
||||
paths would take the live Console down at the next re-pin.
|
||||
|
||||
### Blockers
|
||||
|
||||
B1. The pinned copy has no board code. The live Console runs from `copies/<sha>`, and
|
||||
`scripts/stack-copy.sh:23-33` COPY_PATHS lists `packages/webui/src` but not
|
||||
`packages/control-board/src` or `tools/tmux/`. Once `webui/src/cli.mjs` imports `board.mjs`, the
|
||||
Console started from a copy fails at import. The unit restarts on failure
|
||||
(mosaic-webui.service.in, `Restart=on-failure`, `StartLimitBurst=5`) until it gives up. `reply`
|
||||
also resolves `agent-send.sh` beside the module (control-board serve.mjs:59), so it would point
|
||||
into the copy too. Fix: add `packages/control-board/src` and `tools/tmux/agent-send.sh` to
|
||||
COPY_PATHS, or resolve agent-send from `--checkout`. Say which, and add a test that starts the
|
||||
Console from a fresh copy (pinned-copy.test.mjs or stack-copy.test.mjs) and gets 200 on `/api/board`.
|
||||
|
||||
B2. A re-pin carries `--board` into the new copy, and the new `webui-service.sh` refuses it.
|
||||
stack-release.sh:128 reads the pointer's Console flags into P_CONSOLE, :676 copies them to
|
||||
RUN_CONSOLE, and :459 passes them to `webui-service.sh install`. The live pointer holds
|
||||
`--board http://127.0.0.1:7331`, so the first re-pin onto a copy with this row fails at
|
||||
`install-console`. Decision 6 covers only a hand restart of the old unit. Fix: stack-release drops
|
||||
`--board` when the target's `cli.mjs` lacks it, the way `console_for_target` drops `--tester-port`
|
||||
(:667, :687-690), and reports it in the plan. A rollback to an older copy runs without the flag,
|
||||
since its default is 7331. Test both in stack-release.test.mjs.
|
||||
|
||||
B3. Say where the board POSTs sit relative to serve.mjs:558. Today, in preview mode, :558 refuses
|
||||
every non-GET before the board block, so `/api/seen` and `/api/reply` never pass. In loopback mode
|
||||
`--send` can't be set at all (:479). So with `send !== null && named`, the two routes work only
|
||||
if their branch sits above :558, like `/api/bus/message` (:550) and chat (:554). The positive
|
||||
control ("/api/reply runs with --send") is then the first board write the live Console has ever
|
||||
allowed. Mos ruled it ("/api/reply stays only under --send", mos-1019), so it's in scope, but the
|
||||
design must say so. Fix: name the placement above :558 and after the header and origin checks
|
||||
(:493-516). State that loopback mode can't reply or mark seen. Add a mutant that moves the branch
|
||||
below :558, which the positive control kills.
|
||||
|
||||
### Your question on `/api/seen`
|
||||
|
||||
Gate it. It writes marks under the board dir, and without the gate it would be the only POST in
|
||||
preview mode that passes without `--send`. M5 covers it.
|
||||
|
||||
### Non-blocking
|
||||
|
||||
N1. control-board cli.mjs:14 defaults `fleet` to `~/.mosaic/fleet/agents`. Say which fleet source the
|
||||
Console's board context uses. The fleet is being retired, so `none` may be right. Either way it's
|
||||
a read only.
|
||||
N2. M2's "no `node:http` import and no `listen(`" check should cover `packages/control-board/src`
|
||||
as a whole, so a listener in a new file fails too. The design reads that way. Say it in the test
|
||||
name.
|
||||
N3. 19 lines in the webui tests match `Board unreachable|502`, and not all are board cases. List the
|
||||
ones that change to 503 in the manifest by file. The 17 dead-board files match your count (16 plus
|
||||
tester-fixture.mjs).
|
||||
N4. Mos's loopback ruling is open. Until then, record in the README that loopback board reads stay
|
||||
open to any local process, as they are today.
|
||||
N5. Order with row 192. The board block sits next to the proof gate (:493-505) that SSO changes.
|
||||
Land the second of the two on the first and rerun the board route tests.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Webby review: row 200 r1, `stack-release.sh activate --sso` (#1698)
|
||||
|
||||
Webby, 2026-10-20T20:13Z (from `date -u`). Author: Launchpad. Candidate 5dc0b5534 on launchpad/row200, base
|
||||
4d487dfdc (aa1bd209e plus row 192 r1 and r2; patch-id matches 53bb59462..37582e820). Packet 39b52b71f. Manifest
|
||||
`candidate-manifest.sha256` digest 46179d24...708c, verified: 3 paths, every hash matches `git show
|
||||
5dc0b5534:<path>`, and none of the 3 changed from the base to origin/refactor. Brief
|
||||
`docs/plans/2026-10-20_stack-release-sso.md` (a4a42dbac). Static review plus one bash probe of `cd; pwd` through
|
||||
a symlink (N1). I ran no test and no mutant; T56 and the 9 mutants go to Sage's gate. I did not read
|
||||
/mnt/storage/scratch/launchpad-r200.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Brief items, checked against the code at 5dc0b5534:
|
||||
|
||||
1. **Flags and plan.** `sso_for_target` (stack-release.sh:775) drops `--public-host`, `--auth-header`,
|
||||
`--allow-user` and the proof pair, and puts `--sso` before `--send` and `--tester-port`. That is where
|
||||
webui-service.sh:213 renders it, and its status grep at :401/:406 reads that order. Dropping
|
||||
`--public-host` goes past the brief's list, but webui-service.sh:178 refuses it beside `--sso`, so it
|
||||
has to go. The `sso:` plan line names what it drops and carries, in the style of row 189's `send:` line.
|
||||
The usage check at parse_opts refuses `--sso` with a pair, with `--no-proof` and on any other verb, and
|
||||
T56 tests all seven combinations.
|
||||
2. **Refusals before any stop.** `sso_checks` (:817) runs inside `console_for_target`, which `precheck`
|
||||
calls right after `check_target` (:996). `config_checks` runs after `plugin_checks` and before `new_activation`. None of these
|
||||
steps stops anything. `sso-config` runs the target copy's own `checkConsole`. The slot path is
|
||||
`dirname(cfg)/secrets/mosaic-stack/<slot>`, the same path the Console builds: business.mjs:25-27
|
||||
`configDir` and sso.mjs:35. The slot name is already limited to `[a-z0-9-]{1,64}` (sso-config.mjs:56), so
|
||||
it can't climb out of that directory. `slot_problem` uses `[ -e ]`, `[ -L ]`, `stat`, which doesn't follow
|
||||
links, and `realpath`. Nothing opens the slot, and T56's read watch on it would catch an open.
|
||||
`checkConsole` requires 1 to 8 providers (sso-config.mjs:45), so the `while read` loop never sees an
|
||||
empty line.
|
||||
3. **Rollback.** `sso_base` (:728) reads the arrival set, not the latest one. That set's
|
||||
`show-mosaic-webui.service.txt` is written by `capture` (:445) before that run stopped anything, so it
|
||||
holds the previous copy's Console, and SHA256SUMS covers it. The same-copy re-pin subtest and mutant 8
|
||||
pin the sage-1389 ruling. The four `sso-rollback-flags` causes are each tested.
|
||||
`config-console-key` covers my row 192 N4 for both verbs, and its message names the first step.
|
||||
4. TOOLS.md matches the code. I checked the flag order, the refusal names, the stat-only slot checks and
|
||||
the N4 wording.
|
||||
|
||||
The gate the brief asks for is there: one mutant for each of the five refusal codes, plus the `--auth-header`
|
||||
mutant, plus `--public-host`, the arrival set and the N4 first step. Each pattern has to occur exactly once
|
||||
or the script aborts. Hash pins: no test under packages/*/tests or scripts/test-*.sh pins a sha256 of
|
||||
stack-release.sh or TOOLS.md.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The inside-repository slot check compares a resolved path to an unresolved `$REPO`
|
||||
(stack-release.sh:859). `REPO="$(pwd)"` after `cd "$(dirname "$0")/.."` (:25-26) gives the logical path. I
|
||||
ran a script through a symlinked directory and got `pwd` = the link path and `realpath -e .` = the target.
|
||||
AGENTS.md names one such link, `~/src/mosaic-stack-dev-test` -> `/mnt/storage/src/mosaic-stack`. Run through
|
||||
that link, a slot inside the repository passes this check. The Console's `readSlot` then refuses it after
|
||||
the stop, because sso.mjs:47-50 resolves each root with `realpathSync` (:49). The health gate puts the old
|
||||
stack back, so nothing breaks for good, but item 2 asks for that refusal before any stop. `:858` already
|
||||
resolves `$COPIES`. Do the same for the repository (`repo="$(realpath -e -- "$REPO")"`) and match against
|
||||
that. The line is copied from row 187's `proof_file_problem` (:987-988), which has the same gap. That one is
|
||||
older than this row. Fix both, or file it as a follow-up.
|
||||
|
||||
N2. `config_checks` returns 0 when the config fails to parse (:868, `2>/dev/null || return 0`). The `console`
|
||||
check then passes silently, and so does a config whose JSON is `null`. The verb goes on with a config no
|
||||
loader reads. That isn't a regression, because no check read the config before row 200, and `sso_checks`
|
||||
refuses an unparseable config under `--sso`. For a fail-closed script, refusing here seems better than
|
||||
returning 0. If not, add a comment saying why a parse failure counts as "no console key".
|
||||
|
||||
N3. The slot check uses size 0 for `readSlot`'s one-non-empty-line rule (:860). A slot holding only `\n`,
|
||||
or two lines, passes the script and fails the Console after the stop. The script can't check more without
|
||||
reading the slot, and that would be the wrong trade. List it under "Not tested" or in TOOLS.md as a check
|
||||
the Console makes and this script doesn't.
|
||||
|
||||
N4. This is for the window plan, not this row. Once a copy with `--sso` support is live, no verb turns
|
||||
`--sso` off on it. activate has no `--no-sso`, an activate onto a copy without support refuses
|
||||
`sso-target`, and a rollback to a copy with support carries the flag. The only way back to the preview set
|
||||
is a rollback to a copy before row 192. The window plan's back-out step should say so.
|
||||
Loaded 100 of 202 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user