# Row 156 build: the host starts the landing run (#1651) Brief `docs/plans/2026-10-18_landing-run-launch.md` (blob aeddbc43224f), design `DESIGN.md` in this packet (approved sage-0808). Built on de74dd473 in a scratch worktree. Reviewer: Filbert. Status at 2026-10-18T22:20Z: the code, tests and docs are written and rebased onto d88613bac. Run 1 of the singles was red, with 3 failures in this row's own new tests, which are now fixed. Run 2 was green on the tests, but mutant P02 survived. The fix is in process.mjs (Sage took it in sage-0933), with a land.test process test that names the op. Run 3 is green: 22 singles 248/248, and P01, P02 and RU01 killed. The candidate is `candidate-manifest.sha256` (afea1df3) and `build.patch` (see "Runs"). ## Files (14) | File | Change | |---|---| | packages/bus/src/broker.mjs | `landHolders(business)`, `landRelease(record)`, the `#held` generator shared with `landHeld`, and the decision-id turn in `#requestUsage` (sage-0808). | | packages/bus/src/process.mjs, runtime.mjs | Launch ops `landHolders` and `landRelease`. In process.mjs, `launchOp`'s last line is now an explicit `credentialStatus` branch and a `launch-op-refused` throw (run 2, mutant P02). | | packages/cli/src/launcher.mjs | `start()` reads `landHolders` once for a non-thread instance and takes `land_work` and session.json `landing` from that one answer. `landPass()` runs after `threadPass()` in `tick()`. | | packages/harness/src/runner.mjs | `landingRequest`, `landingOk`, and one turn per `landing` entry before the message loop, with a usage bracket on the decision id. | | packages/bus/tests/land.test.mjs | landHolders (2 tests), landRelease (1), usage for a landing turn (1), and the bus process answering `landHolders` and `landRelease` from their own `launchOp` lines (1, after run 2). | | packages/cli/tests/launcher.test.mjs | The :391 test now answers `landHolders`. Four landing-pass tests. | | packages/harness/tests/runner.test.mjs | The template, landing turns, empty landing, malformed landing. | | packages/cli/tests/thread-launch.test.mjs | **Not in the brief's owned list.** Its hand-built ops map gains `landHolders` and `landRelease` (two lines). Every non-thread `start()` now asks `landHolders`, so without them the test's non-thread launches refuse. | | packages/cli/README.md, docs/TOOLS.md | "Landing runs" section, the `bus start` tick sentence, the Git socket sentence. | | roles/merge-gate.md, cto.md, chief-of-staff.md | One sentence each: the host now starts the run, and row 149's steps are the fallback. workflow-roles.test.mjs:125-150 still matches all three. No other test reads them. | ## Choices - **Usage for a landing turn (sage-0808).** `usage.begin` accepts a decision id that `landHolders` lists for the run's instance. `usage.end` accepts a turn that has a begin record for the same run. Both apply only to a non-thread run, and usage.mjs is unchanged. These are additions to the brief's test list, made per sage-0808. - **Release with messages waiting (sage-0808).** `landRelease` writes only the claim row, with `by='land-relaunch'`. Messages delivered and not read keep the run (`{released:false, unread}`, retried next tick). Messages still waiting get no `deliveries` row and go to the next holder (land.test "still waiting", and launcher.test :463). - **Launcher tests drive the pass by hand.** `landingHost` sets `threadTickMs` to an hour, so only the test's own `tick()` runs a pass. The host answers `landHolders` from `state.holders`. `landRelease` answers from `state.release`, or goes to the real broker when that is null. The broker's side of both is pinned in land.test. - **The coder is the test holder.** It is in `launch.instances` and shares sonnet (limit 1) with the reviewer. A running coder therefore makes the reviewer's landing start refuse `capacity-full`, which is what the refused-start test (:569) uses. - **The refused-start record** is `{event:"refused", by: , instance, decision, code, why, limit?}`. It is logged once per code per decision, and a start clears it. The release refusal is logged once per code per run. ## Equivalent mutants (not run) - `!s.thread` in `#requestUsage`. A thread run's role is a thread instance, and `landHolders` leaves out thread instances' decisions, so begin can't match. End needs a begin with that turn, which therefore can't exist. The design's "a thread run refuses" case has no separate test for this reason. - `thread-not-allowed` in `landRelease`. A thread run's role is never listed, so `decision-not-held` refuses it anyway. - ~~`thread ? null : landHolders` in `start()`. A thread launch reads `landHeld` and never uses `holding`.~~ **Correction (round 2, Filbert's F2 in 29033):** not equivalent. `holding` also writes session.json's `landing` (launcher.mjs:347), so without the guard a `landHolders` entry naming the thread instance would reach the thread run's session file. thread-launch.test now asserts it doesn't, and LA14 is the mutant. ## Gaps and follow-ups 1. **Runner usage turn id.** No runner test can catch a wrong turn id. The runner fixture has no held merge decision, so both usage calls refuse `not-recipient` either way. runner.test asserts that both calls happen around each landing turn, and land.test pins what the broker accepts. 2. **"None on a thread run"** (closed in round 2: thread-launch.test asserts it, LA14). Was: not asserted at the launcher level. It follows from the third equivalent mutant above. A thread launch's `landing` would need a thread holder in `landHolders`, and the broker never lists one (land.test :392). 3. **Process wiring.** process.test.mjs isn't owned, so the two op lines are covered through launcher.test against the real host. P01 and P02 are listed below. (Correction, run 2: P02 survived, because a missing line answered as `credentialStatus`. Since the process.mjs fix in "Runs", a missing line refuses `launch-op-refused`, and a land.test test forks process.mjs and asserts each op's own answer, so P01 and P02 die there on a named assertion.) 4. **Docs outside the owned list:** packages/bus/README.md (launch ops list, :143) and packages/harness/README.md (:142-151, the turn order) don't mention landing turns. Follow-up. 5. **A superseded approval stays held and listed (sage-0928).** `#held` (broker.mjs:1024) yields a cross-role MERGE decision that is resolved with the approval choice, targets a task ref and is unspent. It doesn't apply `readLanding`'s superseded rule (packages/bus/src/landing.mjs:136-139: a later MERGE decision that supersedes it, or has the same task ref or work ref, refuses `superseded`). If a second approval is raised for the same task, the first one is never spent, so `landHeld` counts it as held and `landHolders` lists it indefinitely. The cost is one wasted turn per stale decision per host start: the host starts the holder with that `landing` entry, and the turn's `land_work` refuses `superseded`. Nothing lands wrongly. The land.test landHolders test pins this behavior: `first` refuses `superseded` and stays listed. Row 156 keeps the approved contract that `landHolders` lists exactly what `landHeld` counts. The fix goes in `landHeld`, or in how a later raise retires the earlier approval, and `landHeld` and `landHolders` should change together. Both options change base code outside 156's files. Sage files it on P19 after 156 lands. ## Follow-up draft: the chief-of-staff's end step ends the host's landing run (sage-0907) Sage ruled this a follow-up, not row 156. The step in roles/chief-of-staff.md is unchanged here apart from the brief's one sentence. Sage will fold this draft into docs/plans after 156 lands. **Finding.** Row 149's step: if `launch` for the instance is refused `instance-running` and the run belongs to that instance, end it with `end_run` (reason naming the decision), then launch once it has exited. The cto messages the chief on every approval (cto.md). With the host running, landPass (launcher.mjs:484-536) has already acted within one 2 s tick of the resolution: - holder not running: it starts the holder with the `landing` entry; - holder running from before the approval: it calls `landRelease` (which waits for delivered, unread messages) and SIGTERMs the run, and the next tick starts the holder with `landing`. The chief acts later, after the cto's message and its own turn. Its `launch` then refuses `instance-running` because of the host's landing run, and the step tells it to end that run. `role.revoke` with a reason (broker.mjs:1512-1531) has no turn check, and `END_PROTECTED` is `['assistant']`. The result is still one landing: if the landing turn's `land_work` ran, the relaunch gets no `landing` entry and a second `land_work` refuses `decision-consumed`. If it didn't, the decision stays held and the chief's relaunch gets the entry from `landHolders`. The cost is a wasted run and possibly a revoke in the middle of the landing turn. **Proposed change (Sage's pick over dropping the end step).** End the run only if it is a run of that instance that started before the approval. Dropping the step would leave the chief nothing to do if the host never starts the holder. **What the chief reads.** Both stamps come from the broker's `#now()` as ISO strings, so they compare as text. Both tools are given to every role (tools.mjs:227-229) and are scoped to the business only. 1. The approval time: `trail` with the decision id. The `decision_events` row with `op` `resolved` has `at` (3b.sql:62-69). It is the same stamp `landHolders` reports as `resolved_at`. 2. The run: `list_agents`. The instance's row is its current claim, with `holder_run` (the run id) and `at` (the claim time) (3b.sql:36-45, broker.mjs:2048-2054). 3. The run's start: `trail` with that run id. The `session.launched` event has subject = run (broker.mjs:437) and is written at bind, before the runner claims, so its `at` is the run's start. The claim `at` from step 2 is later than that and works as a bound when the trail is long. Rule: end the run only when the `session.launched` `at` (or, failing that, the claim `at`) is earlier than the `resolved` `at`. If `list_agents` has no claim row for the instance, the run is still starting: don't end anything, and try `launch` again on a later turn. **What stays.** A run that started before the approval is also the one landPass releases. If the host is running, the chief and the host may both act on that same run. The run ends either way, but the chief's revoke doesn't wait for unread messages the way `landRelease` does. The draft keeps this because the fallback exists for a host that isn't starting holders, and the chief can't tell that case from a host one tick behind. A test for the text: workflow-roles.test.mjs:125-150 matches the three role files and would need its chief-of-staff pattern updated. ## Overlap with row 155 and the base - My diff applies cleanly on 0473574b0 alone and on 0473574b0 plus 155 r3 (`git apply --check` and `--3way --check`). With both applied, every changed source and test file parses. - Shared paths with 155: docs/TOOLS.md, broker.mjs, packages/cli/README.md, runner.test.mjs, roles/chief-of-staff.md. The hunks are in different places. My only `#claimEnd` call passes `by`, which was already on the base (row 125). - Base drift: de74dd473..d88613bac touches the candidate's paths in 7e7f8e208 (row 162: docs/TOOLS.md, packages/cli/README.md) and b2f57ca70 (row 155's landing). - **Rebased onto d88613bac (sage-0910), 2026-10-18T21:24Z.** The de74dd473 diff (sha256 `3ae30c32…b908`, 14 paths) applies cleanly with `git apply --3way` on d88613bac. Every changed `.mjs` passes `node --check`, and the mutant spec checks clean there ("33 mutants, 0 problems"). Build manifest on the new base: sha256 `39c9164e…c958`, 14 paths. The singles and mutants run on this tree. - No row 164 freeze file is touched (human.mjs, human-cli.mjs, cli/src/transport.mjs, webui/src/bus.mjs), so channel-proof.test.mjs isn't in the singles. No test pins a sha256 of a changed path; the one 64-hex pin in a test that imports broker.mjs (thread-migration.test.mjs:46) pins migration DDL. - Singles: 22 files. These five were added because they read the source of, import or fork a changed file: bus project-runtime (broker.mjs import list), limit-checks (runtime, broker and process source), broker, tasks, credential-status. ## Mutants (33, `mutants.mjs` in this packet once run) Broker (land.test): B01 threaded instance listed · B03 task_ref is the decision id · B04 unread released · B05 decision not checked · B06 any run of the holder's role releases · B08 an ended run releases · B09 release `by` the run · B10 begin accepts any turn · B11 end without a begin · B12 end accepts another run's begin. Ops: P01 no landRelease op and P02 no landHolders op (land.test and launcher.test since run 3) · RU01 no runtime landHolders (launcher.test). Launcher (launcher.test): LA01 no landPass · LA02 unlisted holder started · LA03 started again · LA04 start not noted · LA05 own landing released · LA06 unreleased run stopped · LA07 no SIGTERM · LA08 no land-relaunch record · LA09 release refusal every tick · LA10 no landing written · LA11 land from a separate read · LA13 start refusal every tick. Runner (runner.test): RN01 unchecked · RN02 extra key · RN03 no length limit · RN04 loose task ref · RN05 no landing turns · RN06 no state block · RN07a no landing begin · RN07b no landing end. ## Runs **Run 1, red (slot sage-0927).** Singles on wt-b against build manifest `39c9164e…c958`, 21:45:03Z-21:46:11Z, busy check CLEAR at 21:45:03Z. 22 files, awk sum: tests 247, pass 244, fail 3. All three failures are in this row's own new tests. The source didn't change for the fixes. The mutants didn't run. - land.test landHolders: the test landed `first` after `second` was raised for the same task. `readLanding` refuses `superseded` for that. The fix: assert that `first` refuses `superseded`, land `second`, and assert that `first` stays listed (follow-up 5, sage-0928). - land.test landRelease: the human's message was taken to be the only one delivered. The resolve in `ready()` also sends merge-gate a DECISION message (broker.mjs:1736-1747), so the receive gets 2. The test now asserts unread 2, then unread 1 once the human's message is read, and reads the DECISION message before the release. - runner.test "a malformed landing" (the 160-character decision at the end): the SIGTERM went out right after the claim and killed the landing turn's adapter, so the stderr had no `ok` line. The test now sends a REQUEST and waits for its reply, which comes only after the landing turn, before the SIGTERM (no sleep). After the fixes, the build manifest is `887c47e7e7bfa55529efc92e1d38073dab4756a3a3a2110d1b808b6835c515ac`, 14 paths. Only land.test.mjs and runner.test.mjs differ from `39c9164e`. Both pass `node --check`, and the spec check reads "33 mutants, 0 problems". **Run 2 (slot sage-0928), logs in `run2/`.** Manifest `887c47e7…15ac` (`run2/build-manifest.sha256`). Busy check CLEAR at 21:52:49Z, run 21:52:49Z-22:05:38Z, awk sums: - the two touched files singly: tests 46, pass 46, fail 0; - the 22 singles: tests 247, pass 247, fail 0; - mutants: 3 controls ok, 32 killed, **1 survived (P02)**, 0 timeouts. P02 removes the `landHolders` line from `launchOp` in process.mjs. The op is still in `LAUNCH_OPS`, so it fell through to the last line, `return runtime.credentialStatus(m.business, m.role)`. With no role, that returns `[]`, which reads as "no holders". The landing tests in launcher.test stub `landHolders` at the host (:391, :431), so no test saw the difference, and gap 3's claim that P02 was covered through launcher.test was wrong. In production a missing line would mean landings never start, with no error. The fix is in process.mjs. The last line is now an explicit `credentialStatus` branch followed by `throw new BusError('launch-op-refused')`, which is the code the catch at :86 already uses. Every `LAUNCH_OPS` name has its own line, which was checked by a loop over the set, so no working op changes. Only a missing line reaches the throw. New build manifest: `2a35cd383456a935405772cb1e2ade11cc05d1f60629cb5b0eedb0415a6eb82d`; only process.mjs differs. **This changes behavior from base.** On d88613bac, any `LAUNCH_OPS` name without its own line in `launchOp` fell through to the last line and was answered as `credentialStatus`. Every name now has an explicit line, and the fall-through refuses `launch-op-refused` (fail closed). No name in the set reaches the throw today. An op that is not in `LAUNCH_OPS` is unchanged: it still gets `invalid-host-request` at process.mjs:98, and the bus closes (:113-116). **After run 2: a named test for P01 and P02 (sage-0933).** Sage asked for a test that names an op outside `LAUNCH_OPS` and asserts `launch-op-refused`. That can't be written as stated, because such an op never reaches `launchOp` (see the paragraph above). The throw is reachable only from a `LAUNCH_OPS` name with no line, which is what P01 and P02 make. The substitute in land.test: fork process.mjs, boot a demo business, and send the ops with ids. It asserts `landHolders` → `{ok:true, result:[], id:1}`, `landRelease` for an unknown run → `{ok:false, error:'unknown-run', id:2}`, `credentialStatus` → `{ok:true, result:[], id:3}`, and exit 0 on close. The messages read "landHolders, not launch-op-refused" and "landRelease, not launch-op-refused". `mutants.mjs` now runs P01 and P02 against `[land.test, launcher.test]`. The spec check reads "33 mutants, 0 problems". New build manifest: `afea1df3e042ccf41f5fa750b2841793605110bb45f55bc8018167d23c90fe1f`, 14 paths. Only land.test.mjs differs from `2a35cd38`. Nothing ran before run 3. **Run 3 (slot sage-0935), logs in `run3/`.** Manifest `afea1df3…fe1f` (`run3/build-manifest.sha256`), checked at the start and the end. Busy check CLEAR at 22:14:13Z, run 22:14:13Z-22:18:52Z, awk sums: - the 22 singles: tests 248, pass 248, fail 0, cancelled 0, skipped 0 (land.test 16/16; the one more than run 2 is the process test); - P01 killed: land.test fails "landRelease, not launch-op-refused" (land.test:470), and one launcher.test test fails (:463); - P02 killed: land.test fails "landHolders, not launch-op-refused" (land.test:469), plus 9 launcher.test failures; - RU01 killed: 9 launcher.test failures; - every control ok. The other 30 mutants were killed in run 2 on `887c47e7` and were not rerun. Since then only two files have changed. process.mjs has the explicit line and the throw; only a missing op line reaches that code, and none of the 30 mutate process.mjs. land.test gained one test and kept its others byte for byte, so the B mutants' killing assertions are still there. ## Round 2 (Filbert's CHANGES in 29033, rulings sage-0942) Built by 2026-10-18T22:42Z on 70c9131f0 (`base.txt`) in ~/dewey-scratch/r156/wt-c. The round 1 diff applied with `git apply --3way`. Only docs/TOOLS.md had drifted, and it merged cleanly. Nothing has run yet; the run is `run4.sh` on Sage's slot. - **F1, landRelease during a landing turn.** broker.mjs keeps `#received`, the runs that have asked `message.receive`. `landRelease` answers `{released:false, landing:true}` for any other run (broker.mjs:1016). The route and the rejected alternatives are in DESIGN.md, "Correction, round 2". README "Landing runs" and the TOOLS `bus start` sentence now hold as written. A run that never reaches its first receive (a hung landing turn, or a broker restart with a run that stops receiving) keeps the release closed. The decision then falls back to row 149's path: the chief ends the run and launches the holder (sage-0944). Tests: - land.test: the landRelease test asserts `landing: true` twice before the run receives. - runner.test: new test, "a run in a landing turn isn't released for a second held decision until its landing turns end and it has read its messages". It uses a real runner and broker. The adapter holds landing turn 1 open on a file while a second decision is raised and resolved. Two `landRelease` calls 300 ms apart answer `landing: true`, and the claim stays. Then the turn ends, the release goes through by `land-relaunch`, and the runner exits 22 (`not-holder`). - launcher.test :463: a `landing: true` answer at a tick stops nothing and records no `land-relaunch` (the release count moves 4 → 5). **The tick mapping.** Sage's wording names a test that ticks. No single test has a real launcher over a real broker holding a real merge decision. The acme fixture's roles have no merge-gate instance, and the launcher tests stub `landHolders`. The coverage is split: - runner.test has the real runner and broker, and calls `landRelease` the way the launcher's tick does; - launcher.test shows that a tick given that answer does nothing. B13 is killed by land.test and runner.test, and LA06 by launcher.test. - **F2.** thread-launch.test: "a thread run's session file has no landing, even when landHolders names its instance". The BUILD.md:56 equivalence claim is struck and corrected above. - **F3.** land.test: "a landing run started after the resolution passes checkLand for its held decision before any message.receive, and lands it". The broker is real, and the call is made directly as the landing run (merge-gate-land): `checkLand` gives a ticket naming the decision, the commit, the verdict and the choice, and `recordLand` records the merge with `actor_run` merge-gate-land. Afterwards `landHolders` is empty and no message was delivered to the run. - **Notes (doc-only).** The README skipped-turn sentence is in. The bus and harness README follow-ups (gap 4) stay follow-ups. Mutants: 36 (`mutants.mjs`; spec check "36 mutants, 0 problems"). New: - B13: drop the `#received` check (land.test, runner.test); - B14: drop the add on receive (land.test, runner.test); - LA14: a thread launch asks landHolders (thread-launch.test). All 33 earlier mutants rerun, because land.test's release test changed. Singles: 26 files. These four were added because round 2 touches the `message.receive` case: - bus messages.test - messaging.test - agents.test - tester.test Build manifest: `build-manifest-r2.sha256` (sha256 `9fafa2b0…1785`), 14 paths, the same paths as round 1. Patch: `build-r2.patch` (sha256 `2fd16539…d03c`). Every changed `.mjs` passes `node --check`. The test hash-pin grep finds only thread-migration.test.mjs, which pins migration DDL. ### Correction, round 2 run 4 (red, 2026-10-18T22:48:24Z to 22:49:27Z, sage-0945 slot) The busy check was CLEAR at 22:48:24Z, and wt-c matched `9fafa2b0`. run4.sh stopped at step 1. The awk sum over the 4 touched files: - tests 84, pass 83, fail 1, cancelled 0, skipped 0. - land.test: 17 tests, 1 fail. runner 32/0, launcher 24/0, thread-launch 11/0. - No singles or mutants ran. The F3 test's last assert was wrong, at land.test.mjs:473. "no message was delivered to the run" counted every `deliveries` row for merge-gate-land and found 2. Those are the `op='routed'` rows the run's `role.claim` writes when it routes the waiting messages to it as their holder. Nothing was read. Every earlier assert in the test passed. The fix filters on `op='delivered'`, the same filter the release test uses at :445. The F3 line above should read "no message was read by the run". New manifest: `build-manifest-r2.sha256`, sha256 `72a5c1b6…b700`; only land.test changed, `cca44e64` to `fb699ffb`. New patch: `build-r2.patch`, `a9fa9b75…8e48`. The red run's logs and the old manifest are in `run4-red/`. The rerun is the same run4.sh, from step 1, on its own slot. ### Round 2 run 4, rerun (green, 2026-10-18T22:53:04Z to 23:08:35Z, sage-0947 slot) Same run4.sh, from step 1, on wt-c against `build-manifest-r2.sha256` (`72a5c1b6…b700`). The busy check was CLEAR at 22:53:04Z. wt-c matched the manifest at the start and at the end. Awk sums over the logs in `run4/`: - touched (4 files, singly): tests 84, pass 84, fail 0, cancelled 0, skipped 0; - singles (26 files, singly): tests 290, pass 290, fail 0, cancelled 0, skipped 0; - mutant controls (4 files): tests 84, pass 84, fail 0; - mutants: spec check "36 mutants, 0 problems"; killed 36, survived 0, timeout 0; `mutants.sh rc=0`. The F3 line above stands as corrected in run 4's correction: no message was read by the run. After the run, two fake-adapter shells from the runner tests (started 22:56Z and 23:07Z, cwd a deleted dir under my TMPDIR) were still alive; I killed them by PID. Round 2 candidate: `build-manifest-r2.sha256` (`72a5c1b6…b700`, 14 paths), patch `build-r2.patch` (`a9fa9b75…8e48`), base 70c9131f0. ### Round 2 verdict (Filbert, comment 29039, rev 1894) Approve on 72a5c1b6. There are two notes, neither blocking, and both are follow-ups: - cli/README.md doesn't describe the hung-landing-turn and broker-restart cases yet (doc-only). - The broker's `#received` set is never pruned, the same as `#ended`.