Files
stack/agents/darkwing/work/queue-110/placement.md
T

6.2 KiB

Row 110 (#1604): where the tester switch lives, for Sage's ruling

Brief: docs/plans/2026-10-13_pm-tester-and-fresh-state.md, section "Bus tester identity for PM tests" (blob ecfa8ba49). The brief asks for this proposal before any build: the tester binding fails closed unless the business turns it on, and the packet says where that switch lives. Read at HEAD d0cc1ac90. No code is written yet. The build also waits for row 109 to land, since both rows touch broker.mjs and the bus README.

Recommendation

A business variable, bus.tester, boolean, settable at the business layer only, default false.

Jason turns it on by adding "bus.tester": true to vars in his business file, <configDir>/businesses/mosaic-stack.json. The broker binds a tester only when businesses[id].vars["bus.tester"] === true. A missing key or false means bindTester refuses with tester-off, and so does any business that isn't on the bus.

Why this place:

  • The business file is Jason's. The stack never writes it (packages/business/README.md, "Business file"), so an agent can't turn testing on through a reviewed commit. A project file can't either: checkVars refuses a key set at a layer it isn't declared for, and this one is declared for business only.
  • The var registry already exists for settings like this. packages/business/src/vars.mjs declares each key once with its type, layers and default. human.discordUserId is a business-only key of the same kind. An unknown key or a non-boolean value refuses when the file loads, so a typo fails closed at bus start instead of leaving testing on or silently off.
  • mosaic business resolve prints provenance, so business:mosaic-stack shows where the value came from.
  • It needs no new plumbing. bootConfig (packages/cli/src/config.mjs) passes busBusiness(b, resolved) to the broker, and that is a clone of the loaded business, vars included. The broker reads it once at boot, as it reads b.human and b.arbiters.

What it costs:

  • packages/business/src/vars.mjs is not in the brief's file list. The change there is one registry line, plus a packages/business test that the key refuses at the project and agent layers and refuses a non-boolean, and one sentence in packages/business/README.md "Variables". I ask Sage to add those three paths to row 110.
  • The switch is read at boot. Turning it on or off takes a bus restart, which is under Mos's go. There is no live off switch. I think that is acceptable for a test-only binding, but Sage should know it before ruling.

The other places, and why not

  • A new top-level key in the business file, such as "tester": true. That means a TOP_KEYS change and a validator in packages/business/src/business.mjs, a change to the example file and a check in the broker's business validation. It's more schema change for one boolean, and the var registry exists so that settings don't each grow their own key.
  • A host flag (mosaic bus start --tester, or a unit drop-in like the PM's pm.conf) or the system config. Neither is per business, and neither is the business's record of who may speak on its bus. The brief also names only the business config or a business var.
  • A date instead of a boolean (bus.testerUntil), so a forgotten switch turns itself off. It's a little more code and one more refusal to test. I'd take it if Sage wants testing to expire, but the tester binding is meant to replace the tmux path for good, so I don't recommend it.

Two points the build depends on, for the same ruling

Neither is the switch, but each changes the design. A ruling now saves a round later.

  1. How a tester connects. The human path proves itself through verifyHuman on each socket request (authenticateHuman in packages/bus/src/runtime.mjs, called from server.mjs when a request carries human). The tester needs no proof, so I propose a tester: { business } field on a socket request that the server passes to broker.bindTester. With the switch on, any process that can open the socket can bind as tester. The socket is <dataRoot>/bus/broker.sock, mode 600 in a 700 directory, so that means any process running as Jason's user, agents included. That is what the brief asks for ("works from inside an agent"). The switch, the recorded tester sender, no human.input and the [mos test] prefix are what keep it from passing as Jason.
  2. Where replies to a tester go. Roles address the human as to: 'human' today, and message.send accepts only human or a role name (broker.mjs, message.send). If the PM answers a tester message to human, the answer lands in Jason's Inbox. I propose tester as a third address: a tester's message.receive reads to_role 'tester', and a role may send to tester only with in_reply_to naming a tester message. The brief lets a tester use messages and replies, which read the human's history. I'll keep those as the brief says unless Sage narrows them.

One more scope note. The brief wants the PM's prompt to say a tester message comes from a tester and is never Jason's instruction. The PM sees an incoming message through turnRequest() in packages/harness/src/runner.mjs. It prints from ${m.from_role}, so a tester sender already shows as from tester. A sentence saying it is never Jason's instruction needs either one line in turnRequest() or one rule in roles/pm.md. Row 109 owns both files now, and row 110 lists neither. I propose one line in turnRequest() after row 109 lands, with its case in packages/harness/tests/runner.test.mjs. That adds those two paths to row 110 as well.

A tester message also writes no human.input, so the PM can't cite one as the request behind task_create. A test message therefore can't create a task. I'll add a test for that.

What I ask Sage to rule

  1. The switch is bus.tester in the business file's vars, business layer only, default false. Add packages/business/src/vars.mjs, its test and packages/business/README.md to row 110's files.
  2. A tester binds through a tester field on a socket request, with no proof.
  3. Replies to a tester go to a new tester address, sendable only in reply to a tester message.
  4. The "never Jason's instruction" line goes in turnRequest(), adding packages/harness/src/runner.mjs (turnRequest only) and packages/harness/tests/runner.test.mjs to row 110, after row 109 lands.