# Row 239 (#1745) build notes (append only) ## 2026-10-22 b1 (Honker) Design d1 5da5f28f6 was approved by Duckworth with C1 and C2 and no blockers (duckworth-0165, note agents/duckworth/work/row239/REVIEW-design-d1.md). The code is on honker/row239 from aca16404c. No test has run yet: every run waits for Sage's CLEAR. ### Duckworth's conditions and answers - C1 (option a): `outcome-unknown-revoked` is in the Console's `OPEN` list (webui bus.mjs), so `sendOutcome` answers it as `outcome-unknown` with `sent: 'unknown'` and never "Nothing changed". T6 covers it and M3f is its mutant. - C2: `write()` sets `revoked = true` after `beforeWrite()` returns, and a catch around the whole `withLock(...)` call marks the error. So a throw from the lock's release in its finally is marked as well. This is structural and has no test, as Duckworth said. - Q1: `beforeWrite` runs after both validations. Q2: two codes. Q3: M2 removes the `:188` check (now organization.mjs:195). ### Notes N1-N5 - N1: `beforeWrite` must not write the same business, because it would find its own lock and refuse `organization-busy`. This is said in the comments on `write()` and `commit()`. Today's revokes (the bus's credential revoke) write no business file. - N2: the lock is held across the revoke. A fire or a delete now keeps other writers of that business out for as long as the revoke takes. They refuse `organization-busy`, as they do for any held lock. - N3: a revoke that fails partway (some tokens revoked, then `credential-revoke-failed`) behaves as today. The error carries no `revoked` mark, because `beforeWrite` didn't return, and nothing is written. This row doesn't change that. - N4: a delete closed with `outcome-unknown-revoked` exits 3 in the CLI, as every closed refusal does. Exit 1 (transport EXIT) applies only to a direct verb answer. - N5: I grepped for "before the write" and similar comments. The ones left (lifecycle-revoke.test:2, instance-settings:56, cli instances.mjs:53, serve.mjs:365) are still true and are unchanged. T21 in bus lifecycle-revoke.test:173 is unchanged. ### Differences from the design - The business tests are in lifecycle-settings.test.mjs, not instance-settings.test.mjs. The fire tests and their fixture already live there. The fixture now returns `dir` and `rolesDir` too. - T3 covers T3b (the delete) in the same test, looping over fire and delete. It also checks that a revoke throwing `credential-revoke-failed` carries no mark and writes nothing. - T4b is the plain-code check. The design's "T4b, outcome-unknown-revoked for an fs-style error" is in T4, which checks EIO with and without the mark for both a fire and a delete. - The bus test's fake org gets `deleteError`, thrown from its `instances.set` branch, so T4 can drive a delete. - transport.test gets one assertion, `busExit("outcome-unknown-revoked") === 1`, for M3g. - A latent Console text: a direct (not closed-request) `revision-conflict-revoked` answer in the Console would still get " Nothing changed." appended, because only `OPEN` codes skip it. That sentence is true there: the file is unchanged. No direct human path gives this code today, since a delete reaches the Console as a closed request's refusal. - The Console's STATUS entry for `outcome-unknown-revoked` (503) can't be reached while the code is in `OPEN`. It stays for symmetry with the CLI and is harmless. ### What changed - packages/business/src/organization.mjs: `write()` takes `beforeWrite`, runs it under the lock after the revision check and both validations, and marks any later error `revoked`. The body is reindented inside the try. - packages/business/src/instance-settings.mjs: `instanceSettings` takes an optional `writer`. `commit()` passes `beforeWrite` to `writer.write` instead of calling it. The early check stays, and its comment is rewritten. - packages/bus/src/broker.mjs: `#organizationFail` maps a marked error to `revision-conflict-revoked` or `outcome-unknown-revoked` before the existing mappings. `#fireJob`'s notice adds "Its credential tokens may already be revoked." for a `-revoked` code. - packages/bus/src/runtime.mjs: comment only. - packages/cli/src/instances.mjs: two REASONS entries. packages/cli/src/transport.mjs: EXIT `outcome-unknown-revoked` 1. - packages/webui/src/serve.mjs: two AGENT_REASONS entries. packages/webui/src/bus.mjs: `OPEN` entry and two STATUS entries. - Tests: - business lifecycle-settings.test: "row 239 T1", "row 239 T2", "row 239 T3". - bus fire-revokes-grants.test: "row 239 T4", "row 239 T4b". - cli instances.test: "row 239 T5 CLI". cli transport.test: one assertion in "busExit and refuseInsideAgent". - webui agents.test: "row 239 T6". - bus channel-proof.test re-pins packages/cli/src/transport.mjs (662642a8...) and packages/webui/src/bus.mjs (1eba9774...), with a comment on each. Neither changes a proof or the transport. The design's file list missed this. The pin grep found it before the first run. ### Mutants for the gate (exact-once replacements) - M1 (instance-settings.mjs, beforeWrite before the lock): replace ` const receipt = writer.write(business, next, { revision, actor, context, beforeWrite });` with ` if (beforeWrite) beforeWrite();\n const receipt = writer.write(business, next, { revision, actor, context });`. The kill is lifecycle-settings "row 239 T1" (the revoke spy has a call). - M2 (organization.mjs, the fire refusal dropped): delete the line ` if (revision !== before) fail("revision-conflict", "organization revision changed; read again");`. The kill is "row 239 T1" (the write lands over the hand edit). - M3a (organization.mjs, the mark dropped): ` if (revoked && e instanceof Error) e.revoked = true;` becomes ` void revoked;`. The kills are "row 239 T2" and "row 239 T3" (business). T4 drives the mark through a fake org, so it doesn't see M3a. - M3b (broker.mjs): delete the line ` if (e.revoked) fail(e.code === "revision-conflict" ? "revision-conflict-revoked" : "outcome-unknown-revoked");`. The kill is fire-revokes-grants "row 239 T4". - M3c (cli instances.mjs): delete the five lines from ` "revision-conflict-revoked": "the business file` through ` "business file again with \`mosaic business instances\` before trying again",`. The kill is instances.test "row 239 T5 CLI". - M3d (webui serve.mjs): delete the two lines starting ` 'revision-conflict-revoked': 'the business file` and ` 'outcome-unknown-revoked': 'the agent\'s tokens`. The kill is agents.test "row 239 T6". - M3e (broker.mjs): ` const tokens = code.endsWith('-revoked') ? ' Its credential tokens may already be revoked.' : '';` becomes ` const tokens = '';`. The kill is "row 239 T4". - M3f (webui bus.mjs, C1): the `OPEN` line loses `, 'outcome-unknown-revoked'`. The kill is "row 239 T6" (the launch POST answers 503 outcome-unknown, sent unknown). - M3g (cli transport.mjs): delete the line ` "outcome-unknown-revoked": 1,`. The kill is transport.test "busExit and refuseInsideAgent". A kill counts only as a failed assertion in the named test, not a load error or a different test. ### Gate plan (mos-1275 timeouts) - My row 256 gate (2026-10-22 10:34Z, same machine) took 590s for the cli package, 57s for webui and 38s for test-foundation, the longest shell script. - Packages: `timeout -k 10 1800` per package (3x the cli run). - Each scripts/test-*.sh: `timeout -k 10 300` (more than 3x the 38s longest). - Single files and mutants: `timeout -k 10 600` per file run (cli instances.test whole is the longest single file). - Order on Sage's CLEAR: the touched test files whole (business lifecycle-settings, bus fire-revokes-grants and channel-proof, cli instances and transport, webui agents), then M1-M3g, then the full gate. - Before the in-review move: grep packages/*/tests for the changed comment and error text and for 64-hex pins on the changed files, and drift-check BASE..refactor against them. ## 2026-10-22 r2 rebase (Honker) - Duckworth approved b1 b0c28366f statically, with no blockers (duckworth-0168). On N1 (`endsWith('-revoked')` would also match `access-revoked`, which can't reach a fire job today), the check stays as it is, so the approved candidate doesn't change before the gate (honker-0163). - Rows 253 (48e450ce8) and 275 landed. b1 is rebased onto refactor 94c33a43c as honker/row239-r2. The push branch honker/row239 isn't rewritten. - Drift between aca16404c and 94c33a43c on the 15 paths: only packages/bus/src/broker.mjs, where row 275 changes the viewer verbs. Neither of row 239's two hunks (`#fireJob` and `#organizationFail`) moves in meaning. The rebase applied with no conflict. The two pinned files (cli transport.mjs, webui bus.mjs) and channel-proof.test have no upstream change, so the b1 pins stand. - Each M1-M3g replacement string still matches exactly once. - The touched test files run whole again on the new base, in the slot (the rebase rule). ## 2026-10-23 r3 rebase (Honker, sage-3730) - r2 (70c6e3772) is rebased onto refactor 93694c518 as honker/row239-r3. honker/row239-r2 isn't rewritten. - Between 94c33a43c and 93694c518, five of the 16 paths changed upstream: bus broker.mjs and runtime.mjs, channel-proof.test, and webui bus.mjs and serve.mjs. Every file merged on its own except channel-proof.test, where row 301 (59f0e44f4, mos-1449) re-pinned webui bus.mjs for the user transport fold. The merged pin keeps row 301's comment and row 239's, and takes sha256 e3f52172…1c2a89 of the merged bus.mjs. The transport.mjs pin (662642a8…6e55b) still matches the file. - Row 239's hunks in broker.mjs (`#fireJob`, `#organizationFail`), runtime.mjs (the revoke comment), bus.mjs (OPEN, STATUS) and serve.mjs are unchanged apart from their position. The range-diff shows only the channel-proof pin and the hunk context moved. - Each M1-M3g replacement string still matches exactly once. M3f's target is the whole `OPEN` line, since `, 'outcome-unknown-revoked'` also appears in the STATUS line. - No 64-hex pin in packages/*/tests or scripts names a changed file except channel-proof.test. - On the CLEAR (sage-3730): the touched files whole, then M1-M3g, then the counts here. Sage runs the full gate on the replay at landing. ## 2026-10-23 r4 replay and the slot run (Honker, sage-3773 CLEAR) - On the CLEAR, refactor was 546366f4a (row 320 landed at 094124b4e). None of the 16 manifest paths changed between 93694c518 and 546366f4a. Upstream did change library src/index.mjs, which bus runtime.mjs imports, and business src/index.mjs, which the cli imports, so the run was on a replay onto 546366f4a rather than on r3's base. That replay is honker/row239-r4 0cae2dad5. The range-diff of r3 against r4 is four `=` lines with equal patch-ids, so duckworth-0292's read of r3 applies to r4. r3 isn't rewritten. - The run was on 0cae2dad5 with a clean tree, TMPDIR under /mnt/storage/scratch/tmp, 22:01:27Z-22:01:52Z. - The touched files, run whole: business lifecycle-settings 12/12, bus fire-revokes-grants 24/24, bus channel-proof 2/2, cli instances 15/15, cli transport 3/3, webui agents 9/9. That's 65/65 with 0 skipped, and no first-run red. - Mutants: 9 of 9 killed. Each was applied exactly once, and each made its named test fail as an assertion: - M1 lifecycle: 3 fail (T1, T2, T3). - M2: T1. - M3a: T2 and T3. - M3b fire-revokes-grants: T4. - M3c instances: T5 CLI. - M3d agents: T6. - M3e: T4. - M3f agents: T6. - M3g transport: "busExit and refuseInsideAgent". T4b passed under M3b and M3e. Every restore matched the HEAD blob by sha256, and the tree was clean after the run. - No full gate (sage-3730): Sage runs it on the replay at landing.