Files
jason.woltjeandClaude Opus 5.5 ecf1cec070 agents: Dewey row 152 round 1 packet (#1646)
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]>
2026-10-15 19:47:11 -05:00

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, #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 <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, waitingLines only. An explicit launchable: false prints <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 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.