# Row 152 (#1646): a message to a role launch.by can't launch says so at once Builder: Dewey. Reviewer: Filbert. Gate owner: Sage. Brief: docs/plans/2026-10-15_landing-followups.md, the second `##` section. Assignment: sage-0479 (row briefed at rev 1494, claimed at rev 1495). Source: Filbert's row 146 note 2. Base: 56639c99b (base.txt, origin/refactor after rows 151 and 153). The candidate is build.patch on that base. candidate-manifest.sha256 lists the seven changed paths with their sha256. repro.sh applies the patch to a fresh detached worktree and checks the digests; it printed DIGESTS_OK. Owned files: the brief's six, plus `packages/cli/README.md`, the talk waiting-lines paragraph only. Sage added that file in sage-0481 without re-pinning the brief. The paragraph lists every line talk prints after `sent`, so without it the new line would be undocumented there. docs/TOOLS.md lists none of the lines. ## The change - **packages/bus/src/broker.mjs**, `#waiting` and `#notice` only. - `waiting` gains `launchable`. It's `true` when the target is one of `launch.instances`, `false` when it isn't, and `null` with no launch block, the same as `launcherRunning`. For a message to `launch.by` itself it's `false`: business.mjs refuses an `instances` list that includes the launcher. Nothing else changes in the result, and a reader that ignores the new key sees row 146's shape. - When the target isn't launchable, the message is still stored and routed as before, so a later claim delivers it. `launch.by` is still told (see the call below), but the notice's body says the launch would be refused: `Message from to waits: has no running session and isn't in the launch block's instances, so a launch of is refused. Tell why not with message.send in_reply_to .` A launchable target keeps row 146's text, byte for byte. - **packages/cli/src/cli.mjs**, `waitingLines` only. An explicit `launchable: false` prints ` isn't running and isn't in the launch block, so can't launch it`, in place of "has been told". No restart line follows, since starting `launch.by` wouldn't launch the role. - This check comes after the no-launch-block and the message-to-`launch.by` cases, so their lines are unchanged. - It comes before the tester line, so a tester's message to an unlaunchable role gets the more specific launch-block line. - An older bus sends no `launchable`. `undefined` isn't `false`, so talk against that bus prints what it prints today. - **Docs.** - packages/bus/README.md: the second notice body, why `launch.by` is still told, and the new key in the `waiting` shape. - packages/cli/README.md: the new line, and that no restart line follows it. - docs/TOOLS.md: one sentence in the talk paragraph pointing at the cli README's list. - **Tests.** - launch-notice.test.mjs: the five existing `waiting` deepEquals now include `launchable`: `true` twice, `false` for the message to chief (launch.by), `null` with no launch block, and `true` for chief's own message to pm. - A new bus test sets `instances: ['pm']`, so coder can't be launched. It asserts: - the whole `waiting` object, with `launchable: false` and `told: true`; - the message stored with `to_role` coder; - one notice, to chief, in_reply_to the message, with the exact new body; - pm, in the same business, still gets `launchable: true` and row 146's text; - coder's later claim receives the stored message. - A new cli test adds a `qa` role outside `instances: ['cto', 'coder']`. In it: - `talk qa` prints only the launch-block line, with pm down and no restart line; - `talk cto` in the same business still prints "pm has been told" and pm's restart line; - pm got both notices, the first with the new text and the second with row 146's. ## The call: launch.by is still told The brief leaves to me whether `launch.by` hears about a message to a role it can't launch. It is still told, for three reasons. 1. **The Console's only signal is the notice.** The Console never reads `waiting`: no file under packages/webui/src reads it. Its web-send receipt lists the replies to the human's message (bus.js, the row 81 comment: "a sent receipt lists the replies to that message"). The notice is `in_reply_to` that message, and the bus README says "The Console's replies under a human's sent message include the notice". With no notice, a human who sends from the Console sees "No reply yet" and nothing else. With the new notice text, they see at once that no launch can follow, which is this row's problem stated for the Console. 2. **The contract already covers the answer.** roles/chief-of-staff.md: "A message from `_bus` is the bus telling you that a message waits for a role with no running session. ... Launch that role within the launch block's limits, or tell the sender why not with a message `in_reply_to` the waiting message." It also says to launch "for the instances and within the limits the business file's launch block lists. A launch outside them is refused, not queued." The new text names the refusal up front and asks only for the second branch, so chief needs no contract change and spends no turn on a launch the bus would refuse. The contract file isn't an owned file, and this reading doesn't need it to change. 3. **The message still waits, and chief owns the structure.** The message is stored and reaches the role at a later claim. The contract makes chief keep "the organization's structure: which role instances exist, who holds them and what they may do". Telling chief keeps the role that owns the structure aware of a message no launch can serve. The contract doesn't say which change would add a role to `launch.instances`, so the notice doesn't name one. The cost is one chief turn per such message, the same as row 146 already spends for a launchable one. Mutant B4 (not told when unlaunchable) is killed, so this call is pinned. ### Other choices - **The name `launchable`**, a boolean or `null`. It answers the question the sender has ("can this launch at all?"), it matches `launcherRunning`'s nullability, and it's additive. - **`false` for launch.by itself.** It's true to the data: `instances` can't list the launcher. talk checks the launcher case first, so the line for it doesn't change. - **No restart line after the launch-block line.** The restart starts `launch.by`, which still couldn't launch the role. Mutant C2 pins this. - **Not covered by a test: talk against an older bus** (`launchable` absent). waitingLines isn't exported, and a broker in the tests always sends the key. The `=== false` check and its comment carry it. A mutant to `!w.launchable` would survive the suite today, because no test sends a `waiting` without the key. I note it here and don't add an export only for a test. ## Mutants mutants.py applies one fault at a time, runs the owning test file with the spec reporter, and restores the source after each one. Preflight: each target occurs once. Run 2026-10-16T00:41:24Z to 00:41:36Z on 56639c99b plus the candidate, in the sage-0483 slot: 8 of 8 killed, `restored True` (mutants-run.log, logs in mutants/). | Mutant | Fault | Runs | Result | Failing tests | |---|---|---|---|---| | B1-always-launchable | every target is launchable | bus launch-notice | KILLED | the new unlaunchable test; the launch.by/tester/human/thread test (chief's `launchable: false`) | | B2-no-block-false | no launch block gives `false`, not `null` | bus launch-notice | KILLED | the no-launch-block test only | | B3-launcher-launchable | launch.by counts as launchable | bus launch-notice | KILLED | the launch.by/tester/human/thread test only | | B4-not-told-unlaunchable | launch.by isn't told for an unlaunchable target | bus launch-notice | KILLED | the new unlaunchable test only | | B5-launch-text-always | the notice keeps row 146's text | bus launch-notice | KILLED | the new unlaunchable test only | | B6-field-dropped | `launchable` dropped from `waiting` | bus launch-notice | KILLED | four tests: no launch block, the new unlaunchable test, the stopped-role test, the launch.by/tester/human/thread test | | C1-no-launch-block-line | talk drops the launch-block line | cli verbs | KILLED | the new talk test only | | C2-restart-after-line | talk adds launch.by's restart line after it | cli verbs | KILLED | the new talk test only | ## Receipts Single files on the candidate, without the slot, TMPDIR in my scratch, 0 fail and 0 skipped, on 56639c99b: bus launch-notice 8/8, cli verbs 9/9. Slot granted by Sage in sage-0483 (busy.sh CLEAR at 00:41:02Z). - **Mutants**, 00:41:24Z to 00:41:36Z: 8 of 8 killed, source restored. See the kill table above. - **Gate**, gate.sh from this packet on ~/dewey-scratch/r152/wt (56639c99b plus the candidate), 2026-10-16T00:42:47Z to 00:46:38Z, exit 0 (gate-run.log, logs under gate/). It covers the bus, cli, business and webui suites (each with its package.json test command) and every scripts/test-*.sh, as the brief's gate names them: | Step | Result | |---|---| | bus | 276 pass, 0 fail, 0 skipped | | cli | 215 pass, 0 fail, 0 skipped | | business | 108 pass, 0 fail, 0 skipped | | webui | 113 pass, 0 fail, 0 skipped | | test-auth | 15 passed, 0 failed | | test-conductor | 17 passed, 0 failed | | test-config | 24 passed, 0 failed | | test-discord | 78 passed, 0 failed | | test-extension-package | 18 passed, 0 failed | | test-foundation | 44 passed, 0 failed | | test-land-work | 84 passed, 0 failed | | test-queue | 28 passed, 0 failed | | test-release | 14 passed, 0 failed | | test-task | 98 passed, 0 failed | No recurrence of flakes #258, #261, #262 or #263. gate.sh exports `COMPOSE_PROJECT_NAME=dewey-r152`, and its EXIT trap printed `removed network dewey-r152_default`. Afterwards no network named dewey* or wt* exists. - **Candidate:** candidate-manifest.sha256, sha256 c333f4beffec1d901ca23200365698e42003c8256a609ad662429d15e63747c1, 7 files.