Files
jason.woltje 158205c5cc lead: commit darkwing's untracked evidence (348 files: queue-137 queue-141 queue-150 queue-162 queue-165 queue-166 queue-167 queue-169 queue-179 queue-180 queue-181 queue-182 queue-185 queue-186 queue-187 queue-188 queue-190 queue-93)
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.
2026-11-02 15:05:14 -06:00

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.sql and packages/bus/src/store.mjs: the 3f→3g step.
  • packages/bus/src/broker.mjs: name resolution in message.send, the writer name.set and the read names.
  • packages/cli/src/cli.mjs: mosaic name and mosaic names.
  • packages/harness/src/tools.mjs: the send_message tool's to description.
  • 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.mjs and 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.name within-role, and the write consumes it (action.allowed, target the instance). Any other class refuses not-authorized before #checkAuthority runs, 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 (or read-only, tester-refused), unknown-role for an instance not in the business, invalid-request for a name that fails ID_PATTERN, name-is-instance, name-is-role, then name-taken. Only then does an agent's write consume the grant, so a refused write logs no action.allowed. The store's trigger backs up name-taken.
  • by is the human's name or the chief's run id, as projects.created_by records 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.set succeeds, logs exactly one action.allowed for instance.name (class within-role, target the instance) and records by as the chief's run.
  • The human's write succeeds with by the human's name.
  • A role without the grant refuses not-authorized, and so does one holding it cross-role; a decision argument is invalid-request; the reader and tester refuse; nothing is written.
  • name-taken for the human and the chief; name-is-instance for every instance key, human and tester; name-is-role for a used definition, the chief's and a business-only one; invalid-request for six bad names; unknown-role for 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_role and to_name; a send by key leaves to_name null; the human and the tester send by name; an unknown name and a renamed-away name are unknown-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 refuses unknown-role and 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 null to_name.
  • packages/bus/tests/schema.test.mjs counts the tables and triggers in schema.sql: 15 and 67.
  • packages/business/tests/chief-of-staff.test.mjs APPROVED list, and role.test, which pins the shipped grants.
  • packages/business/tests/hard-waits.test.mjs: instance.name is 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.mjs asserts the chief holds instance.name and no other shipped role file holds it at either class.
  • New packages/cli/tests/names.test.mjs: mosaic name and mosaic names (text and --json), the verb and arguments sent, talk by name reaching the instance, and the usage errors.
  • packages/harness/tests/tools.test.mjs and bundle.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.

  1. kill-1-grant: remove instance.name from roles/chief-of-staff.json. The chief's test in bus names.test.mjs must fail (sage-0589).
  2. kill-2-resolution: the lookup returns null in message.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.
  3. kill-3a-name-is-instance and kill-3b-name-is-role: each check becomes if (false). The uniqueness test must fail, alone.
  4. kill-4-not-authorized: the refusal becomes if (false). The refusal test must fail.
  5. kill-5-routed: comment out ROUTED's instance.name in the vocabulary. The new routed pin in business hard-waits.test.mjs must fail.
  6. kill-6-by: by goes back to s.human ?? s.run. The chief's test must fail, alone, on by. 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, messaging and broker; business chief-of-staff, role and hard-waits; cli names, app, verbs, follow and tester; harness tools and bundle;
  • 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 the messages case. 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 in inbox until it is marked read.") and my name bullet 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.

  1. The key-first and role-first tests insert instance_names rows with at='x'. The store's #verifyTimes refuses that (invalid-timestamp). The test was wrong. The rows now use 2026-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.
  2. The chief's test expected by='chief-run' and got '0'. I checked what s.run holds: bindLaunch stores record.run, so for the test's chief it is chief-run. A launched session has human: false, not null. s.human ?? s.run therefore gave false, which SQLite stored as 0. The code was wrong and the test was right. by is now s.human || s.run, as #session's s.human || null does. kill-6-by pins 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.mjs counts the tables and triggers in schema.sql. instance_names adds 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 messages names 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 after requirements.mjs. I kept row 137's landing.mjs import and put mine, ID_PATTERN from the business vocabulary, after it. Every probe's edit text still appears once in the rebased file. #classification and #consumeAuthority, which name.set calls, keep their signatures; row 137 split #checkAuthority and added #authorityFor without changing either.
  • packages/bus/README.md, packages/cli/README.md and packages/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.mjs and tests/bundle.test.mjs: applied without conflict. Row 137 adds land_work under a new land option and tests for it. The untyped pins still mean what row 141 intends. untypedActions lists the actions a role holds that no tool covers. The chief's role file holds no git.merge.protected, so land_work can neither add to nor take from the chief's list, and the chief bundle test builds without land. 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.sh had 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_PASSED 22: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.name and bring untypedActions(chief) and the chief bundle's untyped back to [], re-pinning packages/harness/tests/tools.test.mjs:43 and bundle.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).