Files

29 KiB

Row 155 (#1649): the human and the chief of staff configure the assistant

Builder: Darkwing. Brief: docs/plans/2026-10-16_q169-assistant-config.md, section "The human and the chief of staff configure the assistant", blob b2aeba0364 (rev 1717). Jason's ruling Q169. Base: refactor 7ce156259, origin/refactor after row 125 landed (rebased 2026-10-18; built on 9098c33f7, then 99d2b49c9). Worktree: ~/darkwing-scratch/r155/wt-125; wt (9098c33f7) and wt2 (99d2b49c9) are kept unchanged. Reviewers: Dewey (security) and Filbert.

Status: code, tests and docs written, 30 modified files plus packages/webui/tests/assistant.test.mjs. Rebased twice: onto 99d2b49c9 (rows 149, 124, 129 and 164; see "Rebase onto 99d2b49c9"), then onto row 125's landing (see "Rebase onto row 125" and "Row 125 landed"). The run on 99d2b49c9 had 27 of 28 files green; the red one, webui shell.test.mjs, was my miss from the first round and is fixed. The run of all 39 files on 7ce156259 is green (see "Row 125 landed"). The first round's runs on 9098c33f7 (sage-0720) are under "Single-file runs". The full gate has not run; it is Sage's. Candidate: r3/build.patch and r3/candidate-manifest.sha256 on 0473574b0 (see "Candidate round 3"); round 2 was r2/ on 449cadd1f, round 1 r1/ on 7ce156259.

Rebase onto 99d2b49c9

Three commits since 9098c33f7 touch this row's files: 70504d23a (row 124, token usage), 03bbd2c98 (row 149, roles and harness) and 850484a7d (row 129, Console chat). I saved the old worktree's diff against 9098c33f7 (~/darkwing-scratch/r155/out/pre-rebase-9098c33f7.patch), added a new worktree at 99d2b49c9 and applied the diff with git apply --3way. 26 files applied cleanly. Three webui files conflicted, and each resolved as a union of both sides:

  • src/cli.mjs: row 129's chat = createChat({ call }) sits beside this row's nameWrite and assistantStop, and startServer gets all three.
  • src/serve.mjs: BUS_VERBS ends 'messages', 'usage', 'names', and startServer takes chat, nameWrite and assistantStop.
  • src/public/bus.js: row 124's tokenUsage stays above this row's assistant block. settingsView reads usage first, keeping row 124's rethrow on 403 or signed-out, then names, and renders the launch form, the Assistant section, then Token usage.

webui/tests/assistant.test.mjs is untracked in the old worktree, so the diff-based patch did not carry it. I copied it over unchanged (sha256 4e921ce653cc…) and marked it intent-to-add. Every changed source passes node --check, public/bus.js parses, and git diff --check is clean. The rebased diff is out/rebased-99d2b49c9.patch in my scratch: 30 files, +800 -104.

The rebased run (~/darkwing-scratch/r155/run-rebased.sh, each file singly) covers the 15 files of the first round plus the tests rows 124, 129 and 149 added or changed beside the merged sources: webui usage, usage-browser, chat, chat-browser, browser, shell and views; bus usage, usage-broker, usage-runtime and workflow-roles; business workflow-roles; harness raise-domain. 28 files.

Rebased run (sage-0876), 2026-10-18T19:58:43Z to 19:59:52Z, behind busy.sh, TMPDIR in my scratch: 27 of 28 files green, 236 pass and 1 fail summed over the logs. Per-file totals are in ~/darkwing-scratch/r155/out/20261018T195844Z/summary.txt. The sweep found 0 leftovers.

The failure is webui shell.test.mjs, the write-boundary test. It lists every POST, PUT, PATCH and DELETE fetch(...) in public/bus.js and counts method:. This row's assistantPost added fetch(url, {method:'POST', ...}), so the list had a fourth entry and the count was 4, not 3. The assertion is the same at 9098c33f7, and row 129 did not change it. The first round ran only the files I touched, so it never ran shell.test.mjs. This was my miss, not a rebase effect.

The fix is in shell.test.mjs only:

  • The expected list gets the fetch(url, ...) entry second, in source order.
  • The method: count goes from 3 to 4.
  • A new assertion pins every assistantPost( first argument to url (the definition), '/api/settings', '/api/name' and '/api/assistant-stop'. A fourth caller, or a computed route, fails it.

public/bus.js is unchanged by the fix; its diff against 99d2b49c9 still has sha256 e37389fd…, the one going to Mos. The rerun of shell.test.mjs, with settings-form and assistant as controls, waits for a slot. The rebased diff with the fix is out/rebased-99d2b49c9-r2.patch: 31 files, +804 -106.

Rebase onto row 125 (sage-0879)

Row 125 lands before this row and touches 11 of its files, so the run on 99d2b49c9 is not the candidate. I dry-ran the rebase onto row 125's candidate 5289ca1d2 in ~/darkwing-scratch/r155/wt-dry. git apply --3way left 12 conflict blocks in 8 files. ~/darkwing-scratch/r155/rebase/resolve125.py resolves each block by index and refuses on a count mismatch. Every resolution is a union:

  • docs/TOOLS.md: row 125's token-limit paragraph, then this row's Assistant paragraph.
  • Organization keys. There is one list, row 125's, with assistants appended: revision, limits, models, instances, reset, assistants. It is the same in SETTINGS_KEYS (launch-settings.mjs), both ORGANIZATION_KEYS (broker.mjs, harness/src/tools.mjs; row 125's tools.test.mjs holds the three equal) and the Console's writable (serve.mjs). This row's UPDATE_KEYS is gone; set refuses a patch with no key other than revision by reading SETTINGS_KEYS.
  • cli/src/limits.mjs: row 125's reset PATH... verb and formatTokens, and this row's PATCH keys: list, now with reset. cli/tests/limits.test.mjs pins the new list.
  • broker.mjs #claimEnd: row 125's signature with by = s.run ?? s.human and both comments. This row's callers pass five arguments, so their by is unchanged.
  • harness/src/tools.mjs: change_organization describes both the assistant fields and the token-limit reset, and its schema has both reset and assistants.
  • public/bus.js settingsView: reads usage, names and token limits, and renders the launch form, Assistant, Token limits, then Token usage.
  • serve.mjs: BUS_VERBS ends 'usage', 'limits', 'names'. The refusal text names "a reset or assistant settings".
  • shell.test.mjs: five writes in source order (settings, row 125's reset post, fetch(url, ...), project file, bus message), method: counted 5, and this row's call-site pin.

node --check passes on every changed source and git diff --check is clean. A static evaluation of the shell test's regexes over the merged bus.js matches the expected list. assistant.test.mjs came through with sha256 4e921ce653cc…. The dry diff is out/dry-5289ca1d2.patch (31 files, +801 -104).

When row 125 lands, I redo the rebase on the landed commit, compare it with the dry run, and run ~/darkwing-scratch/r155/run-125.sh on Sage's slot. That is the 28 files above plus 11 row 125 test files on the merged sources: bus limit-checks, limit-stop, limits-runtime and process; business token-limit-settings; cli launch-limits, launch-limit-checks, limits-tokens and thread-launch; webui limits-route and limits-browser.

Sage's ruling sage-0713: no schema step (no 3i; row 124 keeps 3g to 3h). Row 134 stays its own row, waits for row 155 to be done (rev 1722) and rebases on this landing. Until row 134, the revision the chief of staff sends comes from the human; the bus, business, harness and cli READMEs, docs/TOOLS.md, the tool description and roles/chief-of-staff.md say so. Row 156 also touches broker.mjs and the role md files; whichever lands second rebases.

Row 125 landed (sage-0882)

Row 125 landed as aa625565f, and origin/refactor is now 7ce156259. The two commits after row 125's packet (df4d5702f) touch only docs/SESSIONS.md and docs/plans/2026-10-14_prd-1.2-build.md, neither in this row, so I based on 7ce156259 rather than df4d5702f. aa625565f has the same tree as the dry run's base 5289ca1d2 (git diff empty).

In ~/darkwing-scratch/r155/wt-125, git apply --3way of out/rebased-99d2b49c9-r2.patch left the same 8 files in conflict, and resolve125.py resolved the same 12 blocks with no count mismatch. node --check passes on every changed source and git diff --check is clean. out/rebased-7ce156259.patch is byte-identical to out/dry-5289ca1d2.patch (sha256 4dda2fc2…, 31 files, +801 -104), index lines included. assistant.test.mjs came through with sha256 4e921ce653cc…, staged this time, not intent-to-add. The shell test's own regex over the merged bus.js gives 5 writes equal to its expected list, 5 method: and the four assistantPost( arguments. The bus.js diff for Mos is out/busjs-7ce156259.diff, sha256 9f9ce31b…, +53 -2.

Run (sage-0890), 2026-10-18T20:30:29Z to 20:31:58Z, ~/darkwing-scratch/r155/run-125.sh with WT=~/darkwing-scratch/r155/wt-125, behind busy.sh, TMPDIR in my scratch, each file singly: 39 of 39 files green. The awk sum over the 39 logs is 300 pass and 0 fail, with 0 skipped and 0 todo. shell.test.mjs passed 5 of 5. Per-file totals are in ~/darkwing-scratch/r155/out/20261018T203030Z/. The sweep found 0 leftovers.

Candidate

r1/build.patch is the staged diff of wt-125 against 7ce156259, byte-identical to out/rebased-7ce156259.patch: sha256 4dda2fc2b7b2971b6daacaab57a027938bd074463ec25bfeb6314095d8503b52, 31 files, +801 -104. r1/candidate-manifest.sha256 lists the sha256 of each of the 31 files in wt-125; its own sha256 is 949873cfda99a2e3d418efee0066fa1438e1c12f69b9d2dfa9413ee089a79183. The worktree had no unstaged change when both were written.

busjs-7ce156259.diff in this packet is the public/bus.js diff against 7ce156259 for Mos's decision 123 go: sha256 9f9ce31bc14e6c0ed6d1d6c9f6cc282992d0631192be6416e0163866264029cd, +53 -2. It is a copy of out/busjs-7ce156259.diff in my scratch.

Candidate round 2 (sage-0900)

Row 162 landed as 7e7f8e208 and changed two of this row's 31 paths, docs/TOOLS.md and packages/cli/README.md, so the r1 manifest no longer described the tree this row would land on. Round 2 is confirm-only and makes no source change.

I applied r1/build.patch unchanged with git apply --index to a fresh worktree (~/darkwing-scratch/r155/wt-r2) at 449cadd1f, refactor HEAD. It applied without conflict, git diff --cached --check was clean, and the staged names are the same 31 files (+801 -104) with no unstaged change. No run on this base: Sage's landing gate covers it, and host.test.mjs reads the cli README.

r2/build.patch against BASE 449cadd1f (sha256 2a5a470d4c6fb53ca9c6ee8ffe4f5d8cb4c49b36b95f3f5ea1bc523224d94170). r2/candidate-manifest.sha256 (sha256 b1989f39012df6cbdd0e676cf19f75ccf88fcbf9d1c0a502eb71ab4247efad25).

What moved from r1:

  • In the manifest, only docs/TOOLS.md (now e8fa08bf…) and packages/cli/README.md (now 7943058d…). The other 29 sha256s are r1's.
  • In r2/build.patch, only lines for those two files: their index lines and three hunk headers (TOOLS.md @@ -610 is now @@ -636, @@ -627,6 +630 is now @@ -653,6 +656; the cli README @@ -789 is now @@ -795). Row 162 added text above these hunks, so they start lower. With index and @@ lines removed, r1 and r2 compare byte-identical.
  • public/bus.js: its diff on 449cadd1f has the same sha256 as busjs-7ce156259.diff (9f9ce31b…, +53 -2), index line included, since bus.js is unchanged between the two bases. Mos's decision 123 go (mos-0816) was on those lines.

Round 1 verdicts on manifest 949873cf: Filbert approved (comment 29010, rev 1849), Dewey approved (comment 29012, rev 1852). Each gave non-blocking follow-ups (see followup-brief.md, which carries Mos's bus.js notes and Dewey's F1 on serve.mjs null bodies; body() already refuses those, see its item 3).

Candidate round 3 (sage-0906)

Sage's landing gate on r2 (candidate ba32dcad2 on 0473574b0, every suite run through, 20:53:10Z to 20:59:57Z) had one failure out of 2370: packages/bus/tests/channel-proof.test.mjs:18, "channel conversion preserves the four proof/transport byte baselines". Row 164 (008746b6e) pins the sha256 of packages/webui/src/bus.mjs at :14, and this row's eight STATUS entries in that file change it to d1f3a274…e241. My single-file runs missed it: the test reads bus.mjs by path, not by import, and it isn't beside any file this row touches. That was my miss. Two other tests read a manifest path literally (stack-release, stack-copy and webui service read packages/webui/src/cli.mjs); they passed on Sage's gate.

Sage ruled to bump the pin: the bus.mjs diff is STATUS table entries only, and the freeze guards the channel conversion, not later status codes. Round 3 changes only channel-proof.test.mjs:14, the bus.mjs baseline, from dbad731c…1f24 to d1f3a274f7aa15eb7ecc2267c65bf4fe7e45c898f77c34235b4e9c29cd1be241, with a trailing comment naming row 155. The other three baselines (human.mjs, human-cli.mjs, transport.mjs) and the import check are unchanged, and none of those three files is in this row's manifest.

I applied r2/build.patch unchanged with git apply --index to a fresh worktree (~/darkwing-scratch/r155/wt-r3) at 0473574b0, then made the one edit. Run on Sage's slot (sage-0906), output in ~/darkwing-scratch/r155/out/r3-20261018T210058Z/, 21:00:58Z to 21:01:13Z:

  • channel-proof.test.mjs singly: 2 pass, 0 fail.
  • Mutant, the 'name-taken': 409, … STATUS line removed from bus.mjs: 1 pass, 1 fail, and the failure names packages/webui/src/bus.mjs. Restored after, sha256 checked against d1f3a274….
  • The whole packages/bus suite once (node --test tests/*.test.mjs): 399 pass, 0 fail, 0 skipped, 0 todo.

Totals are an awk sum over the logs. The sweep found 0.

r3/build.patch against BASE 0473574b0 (sha256 9bee0e1b6546e70f06c1781beadc67082aa14990228f25571cdaf72e85f8c37e), 32 files. r3/candidate-manifest.sha256 (sha256 96cae28e095eff1632b7edcba195e1793bcabc3fb5aa93ea86d485c35e1f9016), 32 paths.

What moved from r2: one new manifest line, packages/bus/tests/channel-proof.test.mjs (3e6d794b…), and its one-line hunk in the patch. The other 31 manifest lines and every r2 patch line, index and @@ lines included, are unchanged. The public/bus.js diff still has sha256 9f9ce31b…, so Mos's go (mos-0816) applies.

No schema step

The row needs no migration, so there is no 3i.

  • thread_claims already has op (claim, release, revoke), holder_run, by, reason and decision (packages/bus/schema.sql). A thread revoke is one more row there.
  • role.claim on the thread path already refuses run-revoked for a run with a revoke row, so a revoked assistant run can't claim its thread again.
  • Names use row 141's instance_names. The new config key lives in the organization file, which isn't the bus store.

The comment at broker.mjs:2215 says a thread claim is never revoked and thread_claims has no reason. Both stop being true here. #claimEnd now writes reason for a thread claim too, and the comment says so.

1. What "configure" changes

A new organization key, assistants:

{ "revision": "<64 hex>", "assistants": { "<instance>": { "vars": {}, "tracker": {}, "definition": "assistant" } } }
  • Each field given replaces that field of the entry. tracker: null removes the tracker. A field left out stays as it is. An empty entry is refused. An instance the file doesn't have is invalid-request.
  • Today tracker: null changes nothing in practice: the assistant definition uses no vikunja credential, so validateBusinessDocument refuses any tracker on it and none exists to remove. The field is there for a future assistant definition that has one.
  • The target instance's current definition has to be the thread definition, or the write refuses organization-not-assistant. A given definition has to be one the broker treats as the assistant, or it refuses organization-definition-refused. Today that list is one name, assistant, so a definition change is accepted only as a no-op. It's there so the field exists when a second assistant definition does.
  • holder and credentials aren't accepted (strict keys, invalid-request).
  • The whole next document still goes through validateBusinessDocument and the unknown-model check. A tracker on a definition with no vikunja credential is refused there, as it is for a hand edit.

No writer this row adds can give an instance a definition in END_PROTECTED. assistants only targets instances that are already the assistant, and instances still sets only vars.model. On top of that, set() now compares every instance's definition before and after and refuses organization-definition-refused if any other one changed. That closes the dewey-0546 case (an unprotected instance becoming the assistant after an arbiter resolved a revoke of it) for every key the writer takes, not only the new one. It's defense in depth: no key today can change a definition except assistants, which only accepts the same one.

For Dewey. vars replaces the whole object, including the limits.tools, limits.network and limits.authority vars. Those only narrow: resolve.mjs:64-68 filters the role file's tools and authority by them and takes the narrower network, so a chief that writes wider ones gets nothing the role file doesn't grant. A chief can remove a narrowing the human set, back up to the role file. checkVars still checks every var.

One source for the definitions. THREAD_DEFINITION and END_PROTECTED move from broker.mjs into packages/business/src/vocabulary.mjs, which the broker already imports for ID_PATTERN. The broker and launch-settings.mjs read the same constants, so no equality test is needed. A test holds END_PROTECTED including THREAD_DEFINITION, so the assistant can't lose its protection by an edit to one list.

Files: vocabulary.mjs, launch-settings.mjs (set and read), broker.mjs (ORGANIZATION_KEYS, the two constants), packages/harness/src/tools.mjs (its ORGANIZATION_KEYS copy, which a test holds equal).

read adds assistants: each assistant instance's definition, vars and tracker as the file has them, so the human and the Console see what they're replacing.

2. Who configures

  • Human, CLI. mosaic limits set --file PATCH already sends the whole patch to limits.set, so a patch with assistants works through it. mosaic limits prints the revision. The usage line, the cli README and docs/TOOLS.md name the new key. I'm not adding a second command for the same write.
  • Human, Console. The Settings view gets an Assistant section per assistant instance: the definition, the vars and the tracker as JSON fields, with the same Save, revision and receipt handling as the limits form. settingsSend's writable list adds assistants.
  • Chief of staff. change_organization takes assistants as well (schema and description). It runs organization.set, which checks organization.change before the writer and consumes it after. Every other role is refused there, as today.
  • Revision. Both carry the file's revision and a stale one refuses revision-conflict in organizationWriter.write.
  • Log. organization.mjs already writes the actor, before, after and changes to .history/<id>/changes.jsonl. The actor is the human id, or chief-of-staff.<instance>.

Gap: the chief can't read the revision. No verb gives an agent the organization revision. Row 134 ("Organization revision read for a role holding organization.change", queued) owns that read. Row 155 doesn't build it. Until row 134 lands, the chief's change_organization call works only with a revision it was given (by the human, in a message), and the tests pass the revision in directly. I've told Sage.

3. The seat name

  • Chief of staff. A new typed tool, set_name (verb name.set, action instance.name), with instance and name. Row 141 added the grant and the broker check; this adds the tool. With it, untypedActions(chief) and the bundle's untyped list become [], so tools.test.mjs:43 and bundle.test.mjs:94 are re-pinned.
  • Human, Console. A POST route, /api/name, with {instance, name} and the settings boundary (signed-in header user, exact Origin, same-origin Fetch Metadata, JSON, strict keys, 4 KiB). It calls name.set through human-cli, so the broker's #human check applies: a Console started inside an agent run gets human-required. The Assistant section shows the current name, read through a new names bus read. names is in BUS_VERBS, and the webui cli.mjs adds it beside the shared views (bus = { ...views({ call }), names: () => call('names') }); bus/src/views.mjs is unchanged, because the CLI doesn't need it. The route is /api/name; a second one, /api/assistant-stop, is in section 4.
  • Human, CLI. mosaic name already exists (row 141).
  • Every other role is refused by name.set's within-role check.

4. Removing the assistant: ending its runs

role.revoke and role.end of a threaded instance. Today the end path reads role_claims through #holder, and the assistant claims in thread_claims, so the answer is not-held. For a threaded target the end path now collects every thread of that instance whose latest claim is claim, and writes a revoke for each. None held is still not-held. A role target keeps its one-claim path. The result adds threads: N for a threaded target.

  • A chief role.end with a reason passes #protectedEnd (row 139) and ends them all.
  • A role.revoke with a decision the human resolved through the CLI passes #protectedEnd and ends them all. An arbiter can't resolve that decision at all: #hardWait makes a revoke of an END_PROTECTED target a hard wait, which goes to the human whoever raised it.
  • The chief's own run's end stays refused (a.role === s.role).

The null and undefined holder run. The raise records holderRun as #holder(...)?.holder_run ?? null, which is null for a threaded target. The check compared it with #holder(...)?.holder_run, undefined, and answered decision-mismatch. Both sides now use ?? null.

For Dewey. For a role target the decision binds to the holder incarnation at the raise. For a threaded target there's no single incarnation, so it binds to nothing beyond the target, the action, the raiser and the class. It's still single-use (#spent) and still human-resolved through the CLI. If you want it bound tighter, the raise could record the set of held thread runs and the check could require the same set. I think that refuses a legitimate revoke whenever a thread idles out between raise and use, which is the common case.

What the run does. The runner polls every 2 s. Its next bus call gets not-holder, which is in SESSION_GONE, and it exits 22. A turn already running finishes first, the same as role.end of a role today. The launcher starts a new run only for a thread with pending messages or an open, so the assistant comes back only when the human writes to it again. Removing the instance from the organization file is out of scope.

The human, Console. A new human-only verb, assistant.stop {instance}.

  • It checks #human(s) and that the instance is threaded.
  • It revokes the current claim of every thread of that instance the human owns (REQ-USER-2: a user reaches only their own threads), with by set to the human and reason stopped by the human. None held is not-held. A known instance that isn't threaded is a new code, not-assistant (webui status 400); an unknown one is unknown-role.
  • It returns the stopped runs.

The Console route is POST /api/assistant-stop with {instance} and the same boundary as /api/name. The button sits in the Assistant section. I chose per-instance over per-run because the Console lists no threads or thread runs (agents reads role_claims only), and stopping the assistant is what Q169's "remove" means. mosaic stop <run> still stops one run from the CLI.

The human can't use role.revoke itself: #consumeAuthority starts with #agent, which refuses a human.

5. Text

  • packages/harness/src/bundle.mjs:112 and the end_run description (tools.mjs:166) say the chief can't end the assistant's run. That's false after this row. Row 149's brief leaves the end_run text in bundle.mjs to row 155.
  • roles/chief-of-staff.md:30-35: the chief ends the assistant with end_run, configures it with change_organization (assistants, with a revision from the human until row 134), and names instances with set_name. Raising policy.change for those goes.

What else changed

  • packages/cli/src/limits.mjs: the usage line names assistants; limits.test.mjs pins it.
  • packages/webui/src/bus.mjs: statuses for the codes the new routes and assistants can return (organization-not-assistant, organization-definition-refused, unknown-model, name-is-instance, name-is-role and not-assistant 400; name-taken and not-held 409).
  • roles/chief-of-staff.md: the contract text names no person, because business/tests/chief-of-staff.test.mjs refuses Jason in it.
  • Re-pinned tests: chief-of-staff.test.mjs:174, threads.test.mjs:396, the bus and business hard-waits.test.mjs, tools.test.mjs, bundle.test.mjs, runner.test.mjs (its fixture's chief gets instance.name), project-runtime.test.mjs (the broker's import line) and webui shell.test.mjs (the write-boundary list, after the rebase; see "Rebase onto 99d2b49c9").

Tests (brief item 5)

Brief item Where
Human CLI and Console changes of vars, tracker and definition succeed and log the human business launch-settings.test.mjs, cli limits.test.mjs, webui settings-write.test.mjs
Chief's change succeeds and logs the chief bus chief-of-staff.test.mjs
Another role's change is refused bus chief-of-staff.test.mjs
Definition change without protection refused; another instance given an END_PROTECTED definition refused business launch-settings.test.mjs
Chief-raised revoke of the assistant is a hard wait to the human, no arbiter resolves it bus hard-waits.test.mjs
Stale revision refused business and bus
Human's and chief's name changes succeed, another role's refused bus chief-of-staff.test.mjs and row 141's names.test.mjs, harness tools.test.mjs (set_name), webui assistant.test.mjs
Chief's end stops every assistant thread run; its own end stays refused bus threads.test.mjs
Human-resolved revoke stops them; arbiter-resolved stays refused bus threads.test.mjs
Console stop route ends an assistant run bus threads.test.mjs (verb), webui assistant.test.mjs (route)

Plus the re-pins above, a set_name tool test, and a browser test for the Assistant section in settings-form.test.mjs.

Single-file runs (sage-0720)

~/darkwing-scratch/r155/run-touched.sh: one node --test per file from its package directory, behind busy.sh, TMPDIR ~/darkwing-scratch/r155/tmp, logs teed. 15 files: the 13 this row touches (12 modified, assistant.test.mjs new) plus business chief-of-staff.test.mjs and bus names.test.mjs. No Docker, no full suites, no mutants.

  • Run 1, 2026-10-18T15:34:50Z to 15:35:18Z: 13 green, 2 red.
    • bus threads.test.mjs, 14 of 15. My test bound an inside-agent human session to show it can't stop runs. bindHuman refuses that at the bind with human-required (pinned since broker.test.mjs:132), so the session never reaches the verb. The test now asserts the bind refusal and checks the verb with pm, chief and a. No broker change.
    • webui settings-form.test.mjs, the Q169 test. render always reads /api/settings again, so after a save the page took its revision from the fixture server, which only ever returned revision a. The next post carried a where the test expected b. The page is right: a real server returns the saved revision. The test's fetch mock now serves the saved state on reads after a save. No bus.js change.
  • Run 2, 15:38:33Z to 15:39:01Z: all 15 green, 167 pass, 0 fail. Logs: ~/darkwing-scratch/r155/out/20261018T153833Z/. Sweep found 0 leftovers.

That run covers the fixtures' project.create and thread.create calls, the chief's decision.raise of a revoke with no grant, the decision-consumed check order and the browser test's timing, which were unproven before it.

Overlaps

  • Row 149 (Dewey, in review) changes roles/chief-of-staff.md Authority text and the raise_decision tool in tools.mjs. I'll rebase onto its landing before the in-review move and run every touched test file singly.
  • Row 156 (Sage, queued after 149) touches broker.mjs and the role md files. Whichever lands second rebases.
  • Row 124 (Rocko) does the Settings readout and schema 3h. The Settings view is shared; I'll check its diff at rebase.

Gate

Dewey and Filbert approve on #1649 naming the candidate manifest. Mos's decision 123 go on the public/ diff. Bus, business, harness, cli and webui suites and every scripts/test-*.sh green.