Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
24 KiB
Row 141 (#1635): messages addressed by agent name
Builder: Darkwing. Brief: docs/plans/2026-10-15_q167-rows-r3.md, section "Messages are
addressed by agent name", blob 6805904555. PRD 1.5 REQ-MSG-2. Base: 18aee8fa1. It was rebased from 4ebef2284 onto b0424e9cc, then onto
18aee8fa1 (see "Rebase and first run" and "Second run and second rebase" below).
Status: rebased onto 18aee8fa1 in ~/darkwing-scratch/r141/wt3, with the fixes from two stopped
runs. The third run stopped on a pin the candidate missed (see "Third run"). The fourth run passed
the round and the full gate (see "Fourth run"). The candidate is build.patch (26 files) and
candidate-manifest.sha256 here.
Scope and owned files
The brief's owned files, plus two additions Sage made:
packages/bus/schema.sql,packages/bus/src/migrations/3f-3g.sqlandpackages/bus/src/store.mjs: the 3f→3g step.packages/bus/src/broker.mjs: name resolution inmessage.send, the writername.setand the readnames.packages/cli/src/cli.mjs:mosaic nameandmosaic names.packages/harness/src/tools.mjs: thesend_messagetool'stodescription.- Tests and the bus, cli and harness READMEs. The harness tests and README change because the chief's new grant has no tool until row 155 (see "The harness").
roles/chief-of-staff.json, for the grant alone. Source: sage-0573. 139 landed first without the grant, so 141 carries it. Brief line 457 says whichever of the two lands second adds it.packages/business/src/vocabulary.mjsand the business role tests that pin the chief's grants. Source: sage-0589.
The action: instance.name, routed under REQ-MSG-2 (1.5)
A grant needs an action, and no existing action covers names. sage-0589 ruled out reusing
organization.change, which the chief already holds within-role and which would have left the role
file unchanged.
instance.name joins ACTIONS and ROUTED in packages/business/src/vocabulary.mjs and in broker.mjs's
own ACTIONS and ROUTED. REQ-WAIT-3 makes a new action a hard wait "until a PRD version says it
isn't", and the reviewed commit that adds it has to say which it is. The second half is met by PRD 1.5
REQ-MSG-2: "The chief of staff sets them once its role file grants that within-role." A hard wait
can't be granted within-role (REQ-WAIT-2), so that sentence is the PRD version naming the action
routed. The commit message will say: instance.name is routed under REQ-MSG-2 (1.5).
HARD_WAITS and GATED_ONLY don't change, so the hard-waits pins stay. The bus hard-waits test already
checks the broker's ACTIONS against the vocabulary's, and HARD_WAITS against GATED_ONLY. With
ACTIONS equal, that also makes the two ROUTED lists agree, so no new agreement test is needed. A new
assertion pins instance.name in ROUTED and out of GATED_ONLY, per sage-0589.
TASK_VERBS is the task.* actions, so the new action doesn't reach the task socket.
Schema 3f→3g
instance_names is an append-only log, like every other bus table. The latest row for an instance is
its current name.
CREATE TABLE instance_names (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
at TEXT NOT NULL,
business TEXT NOT NULL,
instance TEXT NOT NULL,
name TEXT NOT NULL,
by TEXT NOT NULL
) STRICT;
The brief names (business, instance, name). seq, at and by follow the other tables: at puts
the table in timedTables, and by records the human or the chief's run.
Triggers:
instance_names_no_update,_no_delete,_no_replace, the same three every table has.instance_names_unique: an insert refuses when another instance of the business currently holds the name, where "currently" means that instance's latest row. Renaming an instance frees its old name.
The broker checks the rest, because the store doesn't know the business file: the name matches
ID_PATTERN, and it equals no instance key, no role definition the business knows, and neither
human nor tester. _bus fails ID_PATTERN.
messages gains to_name TEXT. to_role stays the instance key, so delivery, claims and threads read
what they read today. In schema.sql the column goes in the form ALTER TABLE leaves in sqlite_master,
body TEXT NOT NULL\n, to_name TEXT) STRICT;, as decisions does at line 61.
migrations/3f-3g.sql is one CREATE TABLE, its triggers and one ALTER TABLE. store.mjs gets version
3g, the new SCHEMA_SHA256, a { from: '3f', to: '3g' } step and instance_names in timedTables.
Its chain check proves that 3b plus every step gives schema.sql's digest.
message.send
to takes a name or an instance key, as before, or human or tester. The broker resolves to
first, before every check that reads it. That covers the tester's role check, the authority target,
the tester-reply check, unknown-role, the messaging lists, the thread, the insert, the route and the
waiting notice. Each reads the instance key. The message records the instance in to_role and the
name in to_name. A send by instance key leaves to_name null, so sending to a role works as it
does today.
An instance key, human, tester and a role name win over a name (sage-0590). name.set refuses a
name equal to any of them, but the owner can later add an instance whose key, or a definition whose
name, some instance already holds as its name. The key then reaches its instance, and a send to the
definition refuses unknown-role, since to never addresses a definition. The old name stops
resolving until it is renamed. Name-first would have sent those messages to the named instance.
A name the business doesn't hold refuses unknown-role, as an unknown instance does.
The writer: name.set and names
name.set { instance, name }:
- The human through the CLI (
#human) always may. - An agent may only when its file grants
instance.namewithin-role, and the write consumes it (action.allowed, target the instance). Any other class refusesnot-authorizedbefore#checkAuthorityruns, so a cross-role grant or a gated decision can't set a name. That follows REQ-MSG-2 and sage-0573, which give names to the human and the chief alone. - A tester or reader refuses, as elsewhere.
- Refusals, in the order the broker checks them:
not-authorized(orread-only,tester-refused),unknown-rolefor an instance not in the business,invalid-requestfor a name that fails ID_PATTERN,name-is-instance,name-is-role, thenname-taken. Only then does an agent's write consume the grant, so a refused write logs noaction.allowed. The store's trigger backs upname-taken. byis the human's name or the chief's run id, asprojects.created_byrecords it.
name-is-role reads the definitions the loaded business knows: every definition one of its instances
uses, which is what loadBusiness puts in definitions. A shipped role file no instance uses, such as
cto in a business without one, is not checked; the broker doesn't read roles/. Sage accepted this
(sage-0590). The PRD reading: "role name" in REQ-MSG-2 means a definition in the loaded business.
#definitions(b) is the one list both name.set and the send-time resolver read.
names lists the current name of each named instance in the business. The human and agents may
call it. The tester and reader lists don't change; the Console route is row 155's.
The harness
The chief's typed tool for names is row 155's. Row 141 changes send_message's to description to
"Role instance, its name, or "human"", so an agent can send by name now.
Until row 155, the chief holds instance.name with no tool. untypedActions returns it, the bundle
manifest lists it in untyped, and the prompt carries the existing line "Actions you hold that have
no bus tool yet: instance.name. Say so instead of working around it." Two harness pins said the chief
had nothing untyped (tools.test.mjs:42, bundle.test.mjs:92); they now pin ["instance.name"]
and the prompt line. The harness README says the same. Row 132's 3d re-pin window checked that
manifest, so the landing window should expect untyped: ["instance.name"] for a chief bundle built
from this tree.
CLI
mosaic name <instance> <name> [--business <id>] calls name.set and prints named <instance> <name>. mosaic names [--business <id>] [--json] prints instance name lines. Both are human only and
refuse inside an agent, like every other human verb.
Tests
New packages/bus/tests/names.test.mjs, which reads the shipped roles/chief-of-staff.json:
- The chief's
name.setsucceeds, logs exactly oneaction.allowedforinstance.name(class within-role, target the instance) and recordsbyas the chief's run. - The human's write succeeds with
bythe human's name. - A role without the grant refuses
not-authorized, and so does one holding it cross-role; a decision argument isinvalid-request; the reader and tester refuse; nothing is written. name-takenfor the human and the chief;name-is-instancefor every instance key,humanandtester;name-is-rolefor a used definition, the chief's and a business-only one;invalid-requestfor six bad names;unknown-rolefor an unknown instance. A rename frees the old name. The store refuses a duplicate insert, an update and a delete on its own.- Send by name delivers to the instance and records
to_roleandto_name; a send by key leavesto_namenull; the human and the tester send by name; an unknown name and a renamed-away name areunknown-role. - A name equal to an instance key, written straight into the store to stand in for an instance added later, doesn't take that key's messages.
- Names equal to a used definition (
coder) and a business-only one (reviewer), written the same way, don't take sends to those words: each refusesunknown-roleand neither instance receives anything (sage-0590).
New packages/bus/tests/name-migration.test.mjs, modelled on spec-migration.test: a 3f store built
from tests/fixtures/schema-3f.sql and fixture rows, never the live store. It migrates to 3g, keeps
every old row and column, adds an empty instance_names and a null to_name, and leaves the
untouched copy unchanged.
Updated:
- comment-migration, thread-migration, spec-migration and chief-of-staff tests in the bus package,
where they pin
3f. comment-migration's fixture-only future step moves from 3f→3g to 3g→3h. thread-migration also compares whole message rows after its migration, and those now carry a nullto_name. packages/bus/tests/schema.test.mjscounts the tables and triggers inschema.sql: 15 and 67.packages/business/tests/chief-of-staff.test.mjsAPPROVED list, and role.test, which pins the shipped grants.packages/business/tests/hard-waits.test.mjs:instance.nameis in ROUTED and not in GATED_ONLY.packages/business/tests/chief-of-staff.test.mjs's classification test, which lists what the chief holds.role.test.mjsasserts the chief holdsinstance.nameand no other shipped role file holds it at either class.- New
packages/cli/tests/names.test.mjs:mosaic nameandmosaic names(text and--json), the verb and arguments sent,talkby name reaching the instance, and the usage errors. packages/harness/tests/tools.test.mjsandbundle.test.mjs: the chief's untyped list.
Kills
Each is a probe in round.sh: one sed edit, one test file, the named test must fail, and the file's
sha256 must match after the restore. Every edit was dry-run on copies; each applies once.
kill-1-grant: removeinstance.namefromroles/chief-of-staff.json. The chief's test in busnames.test.mjsmust fail (sage-0589).kill-2-resolution: the lookup returns null inmessage.send. The send test must fail, alone.kill-2b-key-first: drop the key-first check. The key-beats-name test must fail, alone.kill-2c-role-first: drop the send-time role check. The role-beats-name test must fail, alone.kill-3a-name-is-instanceandkill-3b-name-is-role: each check becomesif (false). The uniqueness test must fail, alone.kill-4-not-authorized: the refusal becomesif (false). The refusal test must fail.kill-5-routed: comment out ROUTED'sinstance.namein the vocabulary. The new routed pin in businesshard-waits.test.mjsmust fail.kill-6-by:bygoes back tos.human ?? s.run. The chief's test must fail, alone, onby. Added after the first run (see below).
The round
round.sh <worktree> <logdir> runs, in order:
- singly: bus
names,name-migration,spec-migration,comment-migration,thread-migration,schema,project-runtime,chief-of-staff,hard-waits,messagingandbroker; businesschief-of-staff,roleandhard-waits; clinames,app,verbs,followandtester; harnesstoolsandbundle; - the nine kills;
- the whole bus, business and harness suites, the cli suite without the two real-sandbox files, and those two singly under the inherited TMPDIR.
It is a builder round (decision 136). Every scripts/test-*.sh belongs to the landing gate.
Rebase and first run (sage-0603, sage-0608)
Row 150 landed as 0f7bca083 (pushed in 63c82f1c4..b0424e9cc). Three of the 24 manifest paths
changed between 4ebef2284 and b0424e9cc:
packages/bus/src/broker.mjs: a comment in themessagescase. The patch applied cleanly.packages/cli/src/cli.mjs:inbox --read. The patch applied cleanly; the USAGE has both new lines.packages/cli/README.md: one conflict. I kept row 150's sentence ("Console only reads history, so a reply to a Console send stays unread ininboxuntil it is marked read.") and mynamebullet after it.
I applied build.patch with git apply --3way to a new worktree at b0424e9cc. The rebased
candidate was patch 2bc50492, manifest bd8d2b30. Only those three hashes differed from the
pre-rebase manifest. Copies: pre-rebase/ holds the 4ebef2284 pair (f47f0eca, 1089500b), and
rebase-1/ holds the rebased pair. Between b0424e9cc and 435d1c616 no manifest path changed.
The first run, on Sage's go (sage-0603), went from 2026-10-16T20:50:15Z to 20:50:16Z. It stopped
on the first file, bus names.test.mjs: 4 pass, 3 fail (gate-run.log, gate/bus-names.log).
None of the failures came from the rebase: row 150 changed no logic in these files. The tests had
never run before.
- The key-first and role-first tests insert
instance_namesrows withat='x'. The store's#verifyTimesrefuses that (invalid-timestamp). The test was wrong. The rows now use2026-01-01T00:00:00.000Z. So does the uniqueness test's direct insert. That test passed because the trigger fires before the time check, but it shouldn't depend on that order. - The chief's test expected
by='chief-run'and got'0'. I checked whats.runholds:bindLaunchstoresrecord.run, so for the test's chief it ischief-run. A launched session hashuman: false, not null.s.human ?? s.runtherefore gavefalse, which SQLite stored as 0. The code was wrong and the test was right.byis nows.human || s.run, as#session'ss.human || nulldoes.kill-6-bypins it.
While in that code I found that my name.set case sat between row 121's two-line comment and
the project.create case it describes. I moved the comment back above project.create.
The fix changes packages/bus/src/broker.mjs and packages/bus/tests/names.test.mjs only.
round.sh gains kill-6-by; I dry-ran its edit on a copy. Fixed candidate: build.patch
9c04f224, candidate-manifest.sha256 e41d74dd, round.sh 1ccc4c0e.
Second run and second rebase (sage-0609, sage-0610, sage-0615)
The second run, on sage-0609's go, used build.patch 9c04f224, manifest e41d74dd and round.sh
1ccc4c0e. It went from 2026-10-16T20:55:49Z to 20:55:56Z and stopped on bus
thread-migration.test.mjs: 2 pass, 1 fail (gate-run.log, gate/). The four bus files before it
passed. rebase-1-run2/ keeps that run's patch, manifest and script.
The failing test compares every message row after its 3d→3e migration with the rows before it.
The store now migrates on to 3g, which adds to_name, so each migrated row has one more key. My
earlier edit to that test only changed the version strings. The test was wrong. It now compares
the migrated store with the old rows plus to_name: null, and the untouched copy with the old
rows as they were.
Before asking for the slot again I looked for other tests the 3g schema breaks without having run:
- Tests that compare whole message rows. The others pick fields, so a new column doesn't change
them. That includes row 137's new
land.test.mjs. schema.test.mjscounts the tables and triggers inschema.sql.instance_namesadds one table and four triggers, so the counts go from 14 and 63 to 15 and 67. The test was wrong, and it joins the candidate as its 25th file and the round's single bus files.- Every
INSERT INTO messagesnames its columns, row 137's new one included, so the new column breaks none of them.
Row 137 landed as f1ee9a989, pushed in b0424e9cc..18aee8fa1. It changed seven of the candidate's
paths. I kept the b0424e9cc candidate with both fixes in pre-rebase-2/ (patch a5493068) and
applied it with git apply --3way --index to a new worktree at 18aee8fa1.
packages/bus/src/broker.mjs: one conflict. Both rows add an import afterrequirements.mjs. I kept row 137'slanding.mjsimport and put mine,ID_PATTERNfrom the business vocabulary, after it. Every probe's edit text still appears once in the rebased file.#classificationand#consumeAuthority, whichname.setcalls, keep their signatures; row 137 split#checkAuthorityand added#authorityForwithout changing either.packages/bus/README.md,packages/cli/README.mdandpackages/harness/README.md: applied without conflict. The verb rows, the CLI lines and the harness paragraph sit where they did.packages/harness/src/tools.mjs,tests/tools.test.mjsandtests/bundle.test.mjs: applied without conflict. Row 137 addsland_workunder a newlandoption and tests for it. The untyped pins still mean what row 141 intends.untypedActionslists the actions a role holds that no tool covers. The chief's role file holds nogit.merge.protected, soland_workcan neither add to nor take from the chief's list, and the chief bundle test builds withoutland. Both pins stay["instance.name"].
Between 18aee8fa1 and 52f35e7a0, the next commit, no candidate path changed. The patch applies to
the canonical checkout at 52f35e7a0 (git apply --check). Rebased candidate: build.patch
0149754c, candidate-manifest.sha256 4d500898, round.sh 3f6500c8.
Third run (sage-0626)
On sage-0626's go, the /proc check and busy.sh were clear at 2026-10-16T21:57:39Z. The chain was
round.sh, then a full gate (~/darkwing-scratch/r141/gate.sh, adapted from row 137's: every
package suite except queue's, cli's two real-sandbox files apart, then every scripts/test-*.sh).
Two mistakes of mine:
round.shhad lost its execute bit, so the round never started ("Permission denied").- I chained the two with
| tee ... &&without pipefail, so the gate started anyway.
The gate stopped at 21:57:53Z on its first suite, bus: 324 pass, 1 fail
(gate-r3/pkg-bus.log; gate-run3.log holds the round's one line, gate-r3/gate-run.log the gate's). project-runtime.test.mjs pins the broker's
exact import list, "no new imports", because the broker runs in the sandbox from RUNNER_FILES.
My ID_PATTERN import from packages/business/src/vocabulary.mjs was the extra line. The round's
whole bus suite would have caught this on either earlier run had they got that far, and my
pre-run audit of tests the candidate breaks missed it, because I looked for row and schema pins,
not source-text pins.
Widened pin: project-runtime.test.mjs's import list gains one line. The reason is that the import
keeps the closure. vocabulary.mjs has been in RUNNER_FILES since 57948a81f and imports
nothing, so the sandbox loads no file it didn't load before. The update follows the one row 137
made for landing.mjs: my line, with a comment, and a new assertion that vocabulary.mjs
imports nothing, so a later import there fails this test instead of the sandbox (sage-0630). The test joins the candidate as
its 26th file and the round's single bus files. A narrower audit (tests that readFileSync a
touched source, and whole-list pins of ACTIONS, ROUTED or the schema hash) found no other
candidate. Every other hit is in the candidate already or inserts with named columns.
The sweep found nothing at 21:58:16Z, and no darkwing network existed. rebase-2-run3/ keeps the
stopped candidate (0149754c, 4d500898, 3f6500c8). Fixed candidate: build.patch bb036c6d,
candidate-manifest.sha256 d474b330, round.sh 570524c0. It applies to the canonical checkout at
862af3079. Everything after 18aee8fa1 is docs: nothing under packages/ or scripts/.
Fourth run (sage-0630)
chain.sh now runs under set -uo pipefail and gates each stage on its exit status: the /proc
check, busy.sh, bash round.sh (rc 0 and a ROUND_PASSED line), then gate.sh (GATE_PASSED).
The first start refused on busy.sh at 22:00:09Z. One node --test process, PID 3127134, had its
cwd in Dewey's r148r3 scratch, and it was gone when I looked again. I sampled busy.sh every 2 s
for 60 s and got 0 hits in 30 samples, then started again.
Slot start 2026-10-16T22:03:11Z, base 18aee8fa1. The /proc check found no other node --test.
The round (gate-run-r4.log, gate-r4/):
- Bus files singly: names 7, name-migration 1, spec-migration 1, comment-migration 5, thread-migration 3, schema 1, project-runtime 3, chief-of-staff 13, hard-waits 12, messaging 13, broker 16. Business: chief-of-staff 10, role 14, hard-waits 4. Cli: names 2, app 30, verbs 9, follow 6, tester 7. Harness: tools 19, bundle 22. All 0 fail.
- Kills: all nine killed. 1-grant (2 failing), 2-resolution, 2b-key-first, 2c-role-first, 3a-name-is-instance, 3b-name-is-role, 4-not-authorized, 6-by, 5-routed (2 failing).
- Whole suites: bus 325, business 113, harness 108, cli 203, cli launcher 19, cli thread-launch 8,
all 0 fail.
ROUND_PASSED22:05:14Z.
Full gate (gate-r4-full/; the whole chain is in chain-r4.log), 22:05:14Z to 22:11:12Z, every
line 0 fail:
| Suite | Pass |
|---|---|
| pkg-bus | 325 |
| pkg-business | 113 |
| pkg-cli | 203 |
| pkg-cli-launcher | 19 |
| pkg-cli-thread-launch | 8 |
| pkg-control-board | 124 |
| pkg-conversation | 182 |
| pkg-discord | 183 |
| pkg-harness | 108 |
| pkg-ledger | 86 |
| pkg-mosaic | 69 |
| pkg-plugins | 28 |
| pkg-project | 30 |
| pkg-runs | 43 |
| pkg-seat | 150 |
| pkg-tasks | 97 |
| pkg-webui | 113 |
| script-auth | 15 |
| script-conductor | 17 |
| script-config | 24 |
| script-discord | 78 |
| script-extension-package | 18 |
| script-foundation | 44 |
| script-land-work | 86 |
| script-queue | 156 node, 28 suite |
| script-release | 14 |
| script-task | 98 |
GATE_PASSED and CHAIN_PASSED 22:11:12Z. The gate's trap removed darkwing-r141_default. No
flake. The sweep at 22:11:22Z found 0 processes, no darkwing network and no *.orig. The candidate
still matches candidate-manifest.sha256 d474b330 (sha256sum -c), and the staged diff is still
build.patch bb036c6d. None of the 26 paths changed between 18aee8fa1 and 862af3079
(origin/refactor) or e1e8dda0b (local HEAD).
For row 155 and follow-ups
- Row 155's Problem list, for Sage to carry: row 155 must give the chief a tool for
instance.nameand bringuntypedActions(chief)and the chief bundle'suntypedback to[], re-pinningpackages/harness/tests/tools.test.mjs:43andbundle.test.mjs:94(sage-0590). Earlier copies of this line said 42 and 92, which were the base's lines, not the candidate's. - Bounded follow-up, for Sage to open: whoever adds instances or definitions to a business should refuse a new key or definition equal to a current name, or report the name it shadows. Today the key or role wins and the name stops resolving, which is the safe side. Row 155 excludes adding instances, so it doesn't carry this (sage-0590).
Commit message
The commit message will say: "instance.name is routed under REQ-MSG-2 (1.5)", as REQ-WAIT-3's second
sentence requires.
Open
Filbert's review on #1635. The landing gets its own window on Mos's go. The live stack is
52f35e7a0 at schema 3f, so the 3f→3g migration window plan names 52f35e7a0 as OLD (sage-0626).