Candidate on 56639c99b: waiting gains launchable, and a message to a role
outside launch.instances gets a notice saying the launch is refused and a
talk line saying launch.by can't launch it. Mutants 8/8 killed; gate green
(bus, cli, business, webui and every scripts/test-*.sh).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
9.9 KiB
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,
#waitingand#noticeonly.waitinggainslaunchable. It'struewhen the target is one oflaunch.instances,falsewhen it isn't, andnullwith no launch block, the same aslauncherRunning. For a message tolaunch.byitself it'sfalse: business.mjs refuses aninstanceslist 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.byis still told (see the call below), but the notice's body says the launch would be refused:Message <id> from <sender> to <role> waits: <role> has no running session and isn't in the launch block's instances, so a launch of <role> is refused. Tell <sender> why not with message.send in_reply_to <id>.A launchable target keeps row 146's text, byte for byte.
- packages/cli/src/cli.mjs,
waitingLinesonly. An explicitlaunchable: falseprints<role> isn't running and isn't in the launch block, so <launch.by> can't launch it, in place of "has been told". No restart line follows, since startinglaunch.bywouldn't launch the role.- This check comes after the no-launch-block and the message-to-
launch.bycases, 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.undefinedisn'tfalse, so talk against that bus prints what it prints today.
- This check comes after the no-launch-block and the message-to-
- Docs.
- packages/bus/README.md: the second notice body, why
launch.byis still told, and the new key in thewaitingshape. - 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.
- packages/bus/README.md: the second notice body, why
- Tests.
- launch-notice.test.mjs: the five existing
waitingdeepEquals now includelaunchable:truetwice,falsefor the message to chief (launch.by),nullwith no launch block, andtruefor chief's own message to pm. - A new bus test sets
instances: ['pm'], so coder can't be launched. It asserts:- the whole
waitingobject, withlaunchable: falseandtold: true; - the message stored with
to_rolecoder; - one notice, to chief, in_reply_to the message, with the exact new body;
- pm, in the same business, still gets
launchable: trueand row 146's text; - coder's later claim receives the stored message.
- the whole
- A new cli test adds a
qarole outsideinstances: ['cto', 'coder']. In it:talk qaprints only the launch-block line, with pm down and no restart line;talk ctoin 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.
- launch-notice.test.mjs: the five existing
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.
- 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 isin_reply_tothat 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. - The contract already covers the answer. roles/chief-of-staff.md: "A message from
_busis 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 messagein_reply_tothe 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. - 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 ornull. It answers the question the sender has ("can this launch at all?"), it matcheslauncherRunning's nullability, and it's additive. falsefor launch.by itself. It's true to the data:instancescan'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 (
launchableabsent). waitingLines isn't exported, and a broker in the tests always sends the key. The=== falsecheck and its comment carry it. A mutant to!w.launchablewould survive the suite today, because no test sends awaitingwithout 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 (
56639c99bplus 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 printedremoved network dewey-r152_default. Afterwards no network named dewey* or wt* exists. -
Candidate: candidate-manifest.sha256, sha256 c333f4beffec1d901ca23200365698e42003c8256a609ad662429d15e63747c1, 7 files.