# Row 194 build notes Design r4 approved: DESIGN.md sha256 824ef884 at 7d0788236 (Webby webby-0014, review at e1e64ff01 on webby/reviews; Mos mos-1042; Sage sage-1387 "build now"). Build base: refactor aa1bd209e merged into honker/row194 at fa9eaa4c3. ## Build notes from the approval (each gets a test and a mutant) - W-N1 (webby-0014 N1): launch refuses `instance-firing` while I has a `fire.stopped` with no `fire.done`. Test case in T9c. Mutant M59: the check is dropped. - W-N2 (webby-0014 N2): the broker's hire-time check requires roleVersion 2, so a definition with no action lists refuses. Test case in T8b. Mutant M60: the version check is dropped. - Grace (sage-1387): 5 s, one constant shared with row 191. ## Chain state (2026-10-20T19:45Z) Rows 190, 191 and 193 have no product commits yet (rocko/row191 and darkwing/row190 sit at da230ca48, the brief commit). Slices that don't need row 191 build first. S6 waits for row 191's candidate. ## Slices | Slice | Scope | Tests | State | |---|---|---|---| | S1 | business: vocabulary (role.hire, role.fire hard waits, HIRE_ALLOWED), role.mjs `hireable`, business.mjs keep/noHire/retired, instance-settings delete and veto ops, chief-of-staff.json `hireable: []` | T6 (business), T7, T8c, T17, D2.2 writer | done, see below | | S2 | bus: reloadBusiness and runtime callback, instance.launch/end/regenerate requests, instances.set delete, veto.set, instance.hire/fire, keep checks, D4 hook, tester checks | T8-T10, T9c, T12-T14, T18 | S2a (D3), S2b (veto, hook, keep, T13), S2c (requests, terminate, delete) and S2d (hire, fire, W-N1) and S2e (PRD 1.7 routing, sage-1471) done | | S3 | cli: launcher lifecyclePass, host ops, `business instance` verbs | T1-T6, T11 | S3a (launcher pass) and S3b (verbs, veto, end to end, T2a, T4, T6, T11) done | | S4 | harness: launch tool schema, hire and fire tools | T8 (D3 item 5) | done, see S4 notes | | S5 | webui: Agents section and routes | T16 | done, see S5 notes. The "still running after fire" flag waits for S6 | | S6 | row 191 chain: halted checks, fire SIGKILL through row 191's stop | T9a, T9b, T15 | waits on row 191 | | S7 | brief item 7 (rev 2547, sage-1586): a created agent's credential entry, the restart route | T19 (new) | waits on row 199 landing, see item 7 plan | | S8 | brief item 8 (rev 2556, sage-1588): fire and delete revoke the agent's credentials before the file changes | T20 (new) | waits on row 199 landing, see item 8 plan | ## Changed assertions (D3 item 4, N4) Listed here as the build changes them. - `packages/business/tests/instance-settings.test.mjs:53, :67, :72`: the row 165 read and add views gain `kept: false` (D1, the read gains `kept` per instance). No other field changed. - `packages/business/tests/instance-settings.test.mjs:70` (`added.restart` true to false) and `:91` (`redefined.restart` true to false): the writer no longer asks for a restart. The bus decides, after its reload (D3 items 2-3). The two test names say so. - `packages/cli/tests/instances.test.mjs`: `setup` boots the bus from the file through `bootConfig`, as the unit does, instead of a one-role stub. The add test (T1) now expects the "no restart" line and binds `sec-1` on the running bus. The `RESTART_NEEDED` text pins moved, unchanged, to a new test that boots the old stub (`setup(t, { stub: true })`). Its coder has no credential references and the file's has, so the bus doesn't reload, prints `RESTART_NEEDED` and doesn't bind `sec-1` (D3 item 3). - `packages/webui/tests/instances.test.mjs:25`: the fake writer's default answer is `restart: false`, the usual answer now. The real-broker test boots through `bootConfig` and expects `restart: false` (`:71`) and the log line `done restart=false` (`:74`). The answer-shape rows at `:140-145` keep `restart: true`. Each row tests another field, or `restart: 'yes'`, so the value is not what they check. - `packages/bus/tests/instances-verb.test.mjs` T12 (S2c): the human delete now answers `{request}` where S2b refused `invalid-request`. The stub writer answers `instances.check` with the revision, and the calls list gains that one check, actor `jason`. Nothing else in the test changed. - `packages/bus/tests/tester-writes.test.mjs:19` (row 175 pin): TESTER_VERBS gains `instance.launch`, `instance.end` and `instance.request` (D4.1). TESTER_ORGANIZATION_VERBS is unchanged. - `packages/bus/tests/instances-verb.test.mjs:78` keeps `restart: true`, now for a D3 item 3 reason: its boot copy is a stub without the file's credential references. A comment there says so. `reload.test.mjs` covers the reload from a file boot. - `packages/bus/tests/hard-waits.test.mjs:38` (S2d): the every-hard-wait loop gives `role.hire` and `role.fire` a target and `arguments`, which their raise now requires (D2.1). The class and route it checks are unchanged. - S2e (PRD 1.7 REQ-COS-3) takes back the S2d entry above. `role.hire` and `role.fire` left the hard waits, so `packages/bus/tests/hard-waits.test.mjs` and `packages/business/tests/hard-waits.test.mjs` drop them from WAITS, and the bus file's every-hard-wait loop is back to its S2c form. The business file gains a test that both are routed and not in GATED_ONLY. - `packages/bus/tests/reload.test.mjs` (S2e): its hard-wait example used `role.hire`, now routed. It uses `spend`. The check is unchanged. - `packages/business/tests/chief-of-staff.test.mjs` (S2e): the chief's pinned authority and the test name gain `role.hire` and `role.fire`. `packages/business/tests/role.test.mjs` gains the check that no other shipped role holds either. - `packages/harness/tests/tools.test.mjs` and `packages/harness/tests/bundle.test.mjs` (S2e): the chief's pinned tool lists gain `hire` and `fire`. Every held action needs a typed tool (`untypedActions`), so the tools come forward from S4. - `packages/bus/tests/chief-of-staff.test.mjs` (S2e): the S2d decision-form tests are rewritten to the reason form (below). The S2d raise tests with `arguments` are gone with the key. - `packages/bus/tests/lifecycle-verbs.test.mjs` V5 retry (S3a): `lifecycleDelete` on a retry now answers `{transaction, retry: true}` with the failed delete's transaction, where S2c answered `{transaction: null, retry: true}`. The launcher needs it to name `.retired/I-`. The test checks the value and that the retry still neither checks nor writes the file. - `packages/harness/tests/tools.test.mjs:48` (S4): the launch tool's `instance` was an enum of the start-time listing. It is now `{type: "string", description, pattern: ID_PATTERN.source}`, and the listing moves to the tool description, which the test matches. `:107` (the pm test's chief check) asserts there is no enum and the description names `chief.launch.instances`. - `packages/business/tests/chief-of-staff.test.mjs` (S4): the contract test gains a match on the new sentence that a hire is listed for `launch` at once but runs only after the human sets its route. - `packages/webui/tests/browser.test.mjs:21-22` (S5): the page's script list pins the scripts in order. It now has `/agents.js` between `/chat.js` and `/bus.js`, and the comment names row 194. ## S1 notes - Tests: `tests/lifecycle-settings.test.mjs` (new) holds T6's business part (V5 order, last listed, clean delete, retired refusals for add and hire), T7 (V6) and the writer's hire and fire (D2.2). T8c is in `role.test.mjs`, T17 in `business.test.mjs`. - `deleteRefusal`'s entry half (`entry.tracker`, `entry.credentials`) follows V5's wording "holds credentials or a tracker bot". In a file that validates it is the same as the definition check: a definition with no credential refuses an entry tracker or credentials at load. So no test reaches it alone, and no mutant is listed for it. - END_PROTECTED is `assistant` only. The chief's own instance refuses delete and fire as `organization-instance-referenced` (launch.by), not as protected. - The writer refuses an `assistant` hire `organization-definition-refused` through `definitionFor`, as row 165's add does. - PRD draft: 69544f342 (sha256 c1f52d98…), sage-1395: adding to the fixed list is a PRD change. ## S2a notes (D3 live reload) - `Broker.reloadBusiness(id, doc)` runs the boot check (`checkBusiness`, moved out of the constructor unchanged) on a clone, then refuses a changed `human` and swaps the document in. - The runtime reloads after every committed write, through the chain `bootConfig` uses (`loadBusiness`, `resolveInstance` with `organization.system`, `busBusiness`). Then `limits.refresh` runs. `instances.set` answers `restart: !live`. Any other verb name now throws `invalid-request` where it used to fall through to `plugins.set`. - D3 item 3 check: a role in the boot copy must keep its credential references and tracker bot. A role not in the boot copy must have neither. The tracker sync block must match. Otherwise no reload and `restart: true`. The first version compared one fingerprint over all roles, so every create failed it. T14's create test caught that. - Consumers checked against item 3 (`runtime.mjs` startBroker): - The Credentials store takes the role and sync references and the project roots it protects at start. The tasks adapter takes the bots at start. All of these are compared. - Limits read `settings.current` at each ask. The human proof reads `human` from the boot copy, and `reloadBusiness` refuses a changed human, so the two agree. - Web grants: the broker reads `roles..web` from its own document (`broker.mjs:1545`), so a reload carries the grant. A hireable definition has no `web` tool. - Usage loads its `usage-source` plug-ins from `b.plugins` at start. That was already so before this row (`plugins.set` never restarted them). A reload doesn't change it, and it isn't compared. - Instance-running: `instances.set` op change with a `definition` on a declared instance with a claim holder (or thread holders for a thread instance) refuses `instance-running` before the writer. A vars or launch change still passes. - Tests: `packages/bus/tests/reload.test.mjs` (T14, 6 tests). ## S2b notes (veto.set, the D4 hook, keep checks) - `LIFECYCLE_HUMAN_VERBS` (exported from `broker.mjs`) has one entry per verb and per `instances.set` op: `instance.launch`, `instance.end`, `instance.regenerate`, `veto.set`, `instances.set add`, `instances.set change`, `instances.set delete`. Choice: `#mayHuman(s, verb, target)` refuses `human-required` for an entry missing from the list. So the list and the calls can't drift apart, and T12 shows the hook got the exact entry: each listed human call passes. The tradeoff is that a new human verb has to join the list before it works, which is what row 193 needs anyway. - `bindHuman` already refuses any human but the owner, so the hook's owner check can't fail on any path today. It is the body row 193 replaces. - M46 (`veto.set` skips `#human`) is equivalent while the generic check runs first: no session without `s.human` gets past `human-required`, and only `bindHuman` sets `s.human`, always with `via: 'cli'` and `outsideAgent: true`. T18 shows the refusal. `#human` stays as the design asks. - `instances.set` now checks `op` in the broker (`add`, `change`, `delete`, else `invalid-request`). The tester's delete refuses `tester-refused` (B6). Until S2c records the delete request, a human delete refuses `invalid-request` after the hook. Without that, the writer's `set` op `delete` would commit with no running or starting check and no workspace rename. - Keep: the `role.revoke` reason branch refuses `instance-kept` for any caller, not only the chief. Only the chief holds `role.end` today, so this is the same set. A caller with `role.end` added later is held to the forbid too. The `organization.set` check refuses when any key of `args.instances` is kept, before the writer and before the grant is spent. The human's `limits.set` isn't checked: the human owns the forbid. - T13 needed no code. `organization.set` and `limits.set` take only `ORGANIZATION_KEYS`, and `limits` and `models` write only `launch.limits` and `launch.models`. - Tests: `chief-of-staff.test.mjs` T10 (reason end, decision end, model write), T13 and T18's veto.set and instances.set paths on a stub writer. T18's file-bytes check and its hire and fire cases come with S2d. `instances-verb.test.mjs` T12 (sessions, tester, hook). `reload.test.mjs` gains veto.set. ## S2c notes (lifecycle requests, terminate, delete) - A request is an `action.allowed` event, action `lifecycle.launch`, `.regenerate`, `.delete` or `.end`, body `{action, request, via, by, ...}`. The human's also gets `human.input {kind: 'admin'}`. The tester's gets none. Steps are `lifecycle.taken`, `lifecycle.done {request, run?, transaction?, refusal?}` and `lifecycle.cancelled {request, by}`, with no role or run in the session. No new table or event kind, so no migration. - Host ops for the launcher pass (S3), each in LAUNCH_OPS: `lifecycleRequests`, `lifecycleTake`, `lifecycleDelete`, `lifecycleDone`. Choice: `lifecycleDelete` commits through the bus's own organization callback, so the D3 reload runs after it as after any other write. The tradeoff is one more host op instead of the launcher calling the writer. - The take re-checks: request-expired (60 s, `LIFECYCLE_REQUEST_MS`), then launch-revoked and instance-retired for launch and regenerate only, then tester-off for a tester request. A refused take writes `lifecycle.done` with the refusal, so the request closes. `lifecycle.end` never expires: the runs it names still need the signal. - instance-starting counts any taken request of I, and any open one still under 60 s, delete included. An open request past 60 s can only close request-expired, so it doesn't count. The end request doesn't count. - Delete order at the verb (V5): keys and identifier, the 64-hex revision, the retired-name retry gate, then `instances.check` (the writer's plan and validation, no write). A named organization refusal fails at once. A BusinessError (the last-listed refusal) is held until instance-running and instance-starting have passed, then answers invalid-request. Then revision-conflict, then the request with `{revision}`. `lifecycleDelete` checks running and starting again after the take (other requests only), then calls `instances.set` op delete with actor = the request's `by`. - Retry: a retired name that isn't in roles deletes again only when its last delete closed `workspace-rename-failed`. The request carries `retry: true`, and `lifecycleDelete` answers `{transaction: null, retry: true}` with no check and no write. The launcher (S3) retries the rename. - `lifecycleDone` carries `transaction`, so the delete's done names the commit the rename used. - Terminate (V3): every claim holder of I, or every thread holder for a thread instance, is revoked with the reason (default "ended by the human" or "ended by the tester"). The answer names the held runs plus the bound runs of I not yet ended, cancels open requests of I and lists taken ones. With none of these it refuses not-held. The tester ends only its own threads and cancels only its own requests. - The tester's business scope needs no check: its cap is bound to one business (M19), so another business's instance answers unknown-role. `instance.request` needs no tester-file check: a tester reads only requests it made. - The tester switch is read through a `testerOn` option. The runtime reads `bus.tester` from the live file through `settings.current`, so a change applies with no restart. Anything but `true`, or a read error, is off. - Refactor: the organization error mapping moved into `#organizationFail(e, verb)`, shared by the organization verbs and `lifecycleDelete`. Same mapping as before. - Deferred: the halted checks (S6, row 191) and W-N1 (S2d with fire). - Tests: `packages/bus/tests/lifecycle-verbs.test.mjs` (12 tests: T12 lifecycle part, T12a, V2-V5, the take and the runtime delete end to end). Probe mutants on the expiry, starting (verb and take), the take's expiry and tester checks, the retry gate, the tester's own-request read and not-held all died. They are not the M-list run. ## S2d notes (the chief's hire and fire, W-N1) - Still hard waits (PRD 1.6). Under PRD 1.7 they move to ROUTED in S2e, with the reason form. - The raise: `decision.raise` takes `arguments`, only for `role.hire` (`{name, definition, vars?}`) or `role.fire` (`{name, reason}`), and requires it for those. `target` must equal `arguments.name`. The raise event records it, and the human's decision view shows it under `authorization.arguments`. The shape checks are the call's (`#hireFireArgs`): identifiers, not `human` or `tester`, vars only `model` and `limits.*`, reason 1-500 characters. - The call: `instance.hire {name, definition, vars?, decision}` and `instance.fire {name, reason, decision}`, from a chief session only. Order: tester-refused, read-only and agent-required (`#agent`), keys, `role-not-chief` (launch.by), not-configured, decision-mismatch for a decision of another business, `#checkAuthority`, the call's arguments against the raise's (canonical JSON, decision-mismatch), decision-consumed, instance-kept. The writer gets the chief as actor (`chief-of-staff.chief`) and the context `{instance, run, decision}`, which changes.jsonl records. - Hire: `#hireable` loads the definition and the chief's own role file through the new `roleFile` option (the runtime passes `loadRole(rolesDir, name)`). Version 1, any action off HIRE_ALLOWED (`unhireableAction`), not on the chief's `hireable` list, on `noHire` or no file all refuse definition-not-hireable before the writer. Then the writer's hire, then the consume with `{transaction}`. Without `roleFile` a hire refuses not-configured. - Fire, before step A: instance-kept, unknown-role, organization-definition-refused (END_PROTECTED), organization-instance-referenced (launch.by), and invalid-request for the last listed name. Step A, one transaction: revoke the claims with the decision and reason, cancel open requests, list taken ones, write `lifecycle.end {via: 'chief', by, runs, reason, kill: true}` when runs exist, and `fire.stopped {cites, request}`. Step B: the writer's fire (already delisted is done). Step C, one transaction: the consume with `{reason, transaction}` and `fire.done {cites, transaction}`. - The fire's own events cite the decision under `cites`, not `decision`, so `#spent` counts only the consume. A failure at B or C leaves the decision unspent, so the repeat runs. - Choice: a repeat after a failed B or C asks for the kill again for any run of I still bound. A revoked run stays bound until the launcher reports it ended, and a second signal to a dead process does nothing. The tradeoff is a duplicate `lifecycle.end` against a run that may outlive the first one. - W-N1 `#firing`: a `fire.stopped` with no `fire.done` for the same `cites`. It refuses instance-firing at the human and tester verbs (`#launchable`), at the take of a launch or regenerate (`#takeRefusal`), and at the chief's `role.launch` (`#authorityFor`). - `#organizationFail` passes the writer's own definition-not-hireable and instance-kept through by name for hire and fire. They come from the file as it stands, after the broker's checks passed. - `lifecycleView` shows `kill: true` when the request carries it. - Deferred: the kill escalation in the launcher (S3, S6). - Tests: `packages/bus/tests/chief-of-staff.test.mjs`, 16 new (T8 x5, T8a, T8b x2, T9 x2, T9c x2, T10, T18, the runtime end to end, W-N1 at the take). 39 in the file pass. Probe mutants on 21 lines (the arguments check, each W-N1 site, M60's version check, keep, the other-business check, last-listed, kill, the chief's list, noHire, role-not-chief, the pre-check consume, both consumes' events, the raise's argument rules, the revoke, the cancel, the pass-through, the view's kill) all died. They are not the M-list run. ## S2e notes (PRD 1.7 REQ-COS-3 routing, sage-1471, mos-1089) - Role-policy change, for Webby to review on its own: `roles/chief-of-staff.json` withinRole gains `role.hire` and `role.fire`. `packages/business/src/vocabulary.mjs` and `packages/bus/src/broker.mjs` move both from the hard waits to ROUTED. `roles/chief-of-staff.md` and `roles/pm.md` drop them from the hard-wait list, which `workflow-roles.test.mjs:113` pins to GATED_ONLY. No other shipped role gains either action, and `role.test.mjs` pins that. - The call takes the reason form, as `role.end` does: `instance.hire {name, definition, vars?, reason}` and `instance.fire {name, reason}`, with no decision. The reason is 1-500 characters for both. - Choice: the decision form is gone, not kept beside the reason form. A within-role raise closes at once, so a decision there adds an event and no check. The `decision.raise` `arguments` key from S2d goes with it. Tradeoff: if a later PRD gates hire again, the raise path comes back as new code. - Choice: hire takes a reason too. The PRD logs every hire and fire with its reason. - Order: tester-refused, read-only and agent-required, keys, `role-not-chief` (launch.by), not-configured, `#checkAuthority` with `{target: name}`, instance-kept. `role-not-chief` comes before the authority check, so a non-chief role refuses `role-not-chief` even when its file grants the actions. A chief whose file lacks the grant refuses `decision-required`. - The writer gets only its own keys. The reason goes in the change context `{instance, run, reason}`, which changes.jsonl records. The hire's consume detail is `{reason, transaction}`. - W-N1 `#firing` now goes by order, since the reason form has no decision id: a `fire.stopped` of the subject with no later `fire.done` (`seq` after it) for the same subject. `fire.stopped` carries `{action, request}` and `fire.done` `{action, transaction}`. - Harness: the typed `hire` and `fire` tools (verbs `instance.hire` and `instance.fire`) come forward from S4, because a held action with no typed tool fails `untypedActions`. Each tool follows its own grant. `roles/chief-of-staff.md` already names them. The rest of the S4 contract text and the runner end to end for hire and fire stay in S4. - Going live is a later re-pin step with Mos (sage-1471). Landing the change doesn't put it on the live stack. - Correction to S2d (found in S2e): S2d's broker imported `business/src/role.mjs`, which is not in `seat/src/sandbox.mjs` RUNNER_FILES. The runner imports broker.mjs, so a session died with ERR_MODULE_NOT_FOUND: cli `launcher.test.mjs` failed 15 of 29 and seat `sandbox.test.mjs` 4 of 10 at 089b92ca3. A bisect showed S2c passes 29 of 29. I had not run those two files for S2d. Fix: `unhireableAction` moves into vocabulary.mjs, which is mounted, and role.mjs re-exports it. RUNNER_FILES is unchanged, so the sandbox boundary does not widen. Rule from here (sage-1497): a change to broker.mjs imports, or those of any RUNNER_FILES file, runs these four singly: seat `sandbox.test.mjs`, cli `launcher.test.mjs`, cli `thread-launch.test.mjs` and cli `git-socket.test.mjs`. - Mutant (sage-1497): broker.mjs imports `unhireableAction` from role.mjs again. Killed: sandbox.test fails 4 of 10 (role.mjs outside RUNNER_FILES), launcher.test 15 of 29, thread-launch 7 of 11. git-socket passes 24 of 24 under it, so it does not catch this one and stays on the list for its own sandbox runs. - Tests: `packages/bus/tests/chief-of-staff.test.mjs` S2d block rewritten to the reason form (39 pass), with new tests for the reason bounds, `role-not-chief` with the grant, `decision-required` for a chief without each grant, W-N1 by order, and the runtime end to end with the reason in the receipt context. `packages/harness/tests/tools.test.mjs` gains the hire and fire tool shape test, and that no coder, reviewer or pm gets either tool. - Probe (not the M-list run): 16 mutants, all killed. ROUTED drops of each action in the broker and in vocabulary, the role file drops of each, `#firing` without the order and without the subject, the reason unchecked, the writer given the reason, the context without it, no `#checkAuthority`, no `role-not-chief`, the consume without the reason, the hire tool's reason optional, and the fire tool gated on the hire grant. The last one survived at first and a one-grant check in tools.test killed it. ## S3a notes (the launcher's lifecycle pass) - `packages/cli/src/launcher.mjs` `lifecyclePass` runs on the tick after the thread and land passes. It lists the open requests (host op `lifecycleRequests`) and carries each under `serialize`: the take, the act, then `lifecycleDone`. A take that answers no closes nothing more here. A refusal goes in the launch log as `refused` with the request id. A done that throws leaves the request taken with no done, which S3b's CLI reports as `outcome-unknown`. The log line names a run that did start. - Launch: `check()` and `start()` gain `human`. The human's request may name any declared instance, as V2 asks. The listing limits the chief. launch.by still takes the `pm` branch (N8). A tester request launches with `launchedBy: "tester"`. - End: SIGTERM to each run the verb named and to any other run of the instance in the registry, each after the start-time check, with a `lifecycle-end` entry. The exit handler writes the one `end` record. A thread instance's end signals only the runs the verb named, so the tester's end leaves the human's threads alone. No SIGKILL escalation here: that is row 191's stop (S6). - Regenerate: the end's signals, then a wait until the old run leaves the registry, then a launch. The wait runs under `serialize`, so no other start runs meanwhile. Past the request's mark (`at` + 60 s, `lifecycleRequestMs` for tests) it closes `regenerate-timeout` and starts nothing. A closing host closes it `host-closing`. Tradeoff: one stuck regenerate holds other starts for up to 60 s. The other choice, a second pass that comes back for it, needed state across ticks for the same result. - Delete (V5 steps 1-3): `instance-running` against the registry first, then `lifecycleDelete` (the writer's commit), then the rename of `workspaces//I/` to `workspaces//.retired/I-/`, both 0700. The file goes first, so a failed rename can't leave a launchable name on the old workspace. A failed rename closes `workspace-rename-failed` with the transaction. A later delete of the name is the retry, and the broker now answers the failed transaction, so the rename target is the same (changed assertion above). No workspace means nothing to rename. - `--no-launch` (row 163): the ticker now runs on a no-launch host, but only the lifecycle pass, and `act` closes each request `launches-disabled` before any signal, start or rename. The thread and land passes stay off, so `launcher.test.mjs`'s no-launch tests pass unchanged. - Tests: new `packages/cli/tests/lifecycle.test.mjs`, 9 tests on the in-process host of thread-launch.test.mjs, with ticks called by the test: T2 (unlisted and launch.by launches, a request past 60 s, a check refusal with its code in the log), T3 (`--no-launch`, `capacity-full` with the limit in the log, `instance-running`), T4 (end at the verb, the signal, one end record, the cancelled request never runs), T5 (a new run on the same workspace, `regenerate-timeout` with a SIGSTOPped runner, `regenerate-thread` at the verb), V5 (a failed rename then the retry, modes 0700, no workspace). - Singles at S3a: seat sandbox 10/10, cli launcher 29/29, thread-launch 11/11, git-socket 24/24, bus lifecycle-verbs 12/12, cli instances 6/6, cli lifecycle 9/9, bus process 8/8. broker.mjs imports did not change, and the four must-run files ran anyway. ## S3b notes (the CLI verbs) - `packages/cli/src/instances.mjs`: `business instance launch|regenerate BUSINESS I [--wait S] [--tester]`, `terminate BUSINESS I [--reason TEXT] [--tester]`, `delete BUSINESS I --revision REV [--wait S]` and `business veto BUSINESS keep|unkeep INSTANCE|nohire|hire DEFINITION --revision REV`. `--wait` takes 0 to 600 s and defaults to 30. Every usage error exits 4 before any bus call. add and set refuse `--wait` and `--reason`. - Launch, regenerate and delete print the request id on stderr, then poll `instance.request` every 200 ms. Closed with a run or a transaction prints it and exits 0. A refusal prints its reason, the request id and any transaction. A cancel prints who cancelled it. Still open at `--wait` says so and names the 60 s expiry. Taken with no done is `outcome-unknown` and points at the launch log. Terminate doesn't wait: it prints the revoked runs, the cancelled requests and any taken request past cancelling. - Exit codes: every lifecycle outcome that isn't done (refusal, cancel, still open, outcome unknown) exits 3, as the design's `--wait` timeout does. Tradeoff: errors.mjs has 1 for an unknown outcome elsewhere. One code lets a script treat "the request did not do it" as one case, and the message says which. A refusal at the verb keeps the transport's code (2), as add and set do. - The bus `instances` read gains `running` per instance (`#running`, from the claims), so the CLI and S5 can show it without a second verb. An answer with no instances map passes through as before. `bus instances-verb.test.mjs`'s stub writer answers `{ok: true}` and caught the first version, which threw. The CLI view marks `kept` and `running r1, r2` and adds `retired:` and `the chief may not hire:` lines. - `scripts/mosaic` sends `business veto` to packages/cli with instances and instance. - Tests: cli `instances.test.mjs` +8: the V6 veto ops and kept-delete refusal, the `running` read, launch and regenerate printing the run, a closed refusal with its id (exit 3), `--wait 0` still open and taken `outcome-unknown`, request-cancelled, terminate output and `not-held`, delete to its transaction and `workspace-rename-failed` naming it, usage errors (exit 4, no call), and the scripts/mosaic veto route. The existing route test gains the veto assertions. cli `lifecycle.test.mjs` +5: the CLI end to end through the pass (launch, regenerate, `capacity-full` with the request id, terminate, delete), T2a (`launch-revoked` at the take with no process, a done that fails once leaves the request taken and the next pass starts nothing), T4 (a spawned run the bus never bound is signalled, a mismatched start time is not), T6 (run evidence bytes unchanged by a delete, the launch log only grows, a create and a launch of the retired name refuse), T11 (two roots with business `acme`: a launch, a terminate and a delete in one leave the other's files byte-identical). bus `chief-of-staff.test.mjs` +1: T11's bus half, a chief on a bus with two businesses hires and fires only in its own. - Not covered here: T6's `instance-starting` and the V5 refusal order are bus tests from S2c. `check()` refusing a retired name is shown through the verb and the take, since `check()` reads a file that no longer lists the name. - Singles at S3b: cli instances 14/14, cli lifecycle 14/14, bus lifecycle-verbs 12/12, bus instances-verb 3/3 (after the fix), bus chief-of-staff 40/40, webui instances 5/5, seat sandbox 10/10, cli launcher 29/29, thread-launch 11/11, git-socket 24/24, usage 2/2, project 4/4, app 30/30, host 42/42, tester-threads 5/5, tester-reads 6/6, stack-copy 12/12, queue dispatch 4/4, runs task-cli 9/9, webui pinned-copy 10/10, service 31/31, views 3/3. The broker change adds no import. The four must-run files ran while the `instances` guard was being written, and the guard touches only the read. No test pins a hash of scripts/mosaic. ## S4 notes (the launch tool takes a hire, D3 item 5) - `packages/harness/src/tools.mjs`: the launch tool's `instance` is a string with the ID_PATTERN pattern, not an enum fixed when the bundle was built. The start-time listing stays in the description. The host checks the listing at each launch (`instance-not-listed`), so the chief launches its own hire in the same session. The hire and fire tools came forward in S2e. - The runner does not enforce a tool's schema pattern. A bad name reaches the host and refuses `instance-not-listed`, so the end-to-end test asserts that. M15 (the enum kept) is killed by the tools.test shape assertion, not by the end-to-end test. - `packages/cli/tests/lifecycle.test.mjs` gains the T8 end to end: the chief's session hires `sec-2` (definition chief-information-security-officer, the shipped definition `hireable` accepts: message.send and record.write, no credential) with `TOOL hire`, a launch refuses `no-route`, the human sets the route with `instances.set` op change, the same session launches it (launchedBy chief), `NOT A NAME` and `reviewer` refuse `instance-not-listed`, then `TOOL fire` ends its run, leaves it `launched: false` and a later launch refuses `instance-not-listed`. - Route gap, raised with Sage (honker-0027): harness, model and thinking are agent-layer vars (`business/src/vars.mjs:142-147`) and hire vars allow only model and limits.* (N3). So a hire runs only after the human sets harness and thinking. D3 item 5 says the same session launches it, and it does, after that human step. Letting hire vars carry the route would widen the chief's authority, a policy change that is not mine to make. As built the human step stays, and `roles/chief-of-staff.md` says so. Sage ruled (b) in sage-1521: the human route step stays, and the manifest notes say that launch refuses `no-route` until the human sets the route. - sage-1521 also keeps exit 3 for every lifecycle outcome but done (outcome-unknown included, the S3b tradeoff line above stands for Webby) and the S3a choices (a `--no-launch` host runs only the lifecycle pass, `lifecycleDelete` answers the transaction on a retry). `roles/chief-of-staff.md` is role policy: the manifest names it and the review request flags it as a policy change. - `roles/chief-of-staff.md` (role policy, named in the manifest): one paragraph on the hire's route step and on a fired instance staying in the file off the list. - Singles, each run alone: business chief-of-staff 10/10, spec-review-contracts 3/3, role 19/19, resolve 10/10, harness bundle 22/22, tools 23/23, runner 34/34, pi-session 8/8, seat sandbox 10/10, cli launcher 29/29, thread-launch 11/11, git-socket 24/24, lifecycle 15/15, webui reads 11/11. No test pins a hash of tools.mjs or roles/chief-of-staff.md. tools.mjs gains an import from vocabulary.mjs, already in RUNNER_FILES, and the four must-run files passed after it. ## Rebase onto row 192 (2026-10-20T22:10Z) Row 192 (4429a104f, Q175 phase 1 SSO) changed webui `serve.mjs`, `cli.mjs`, `app.js` and `index.html`, the files S5 changes. Before S5 the branch was rebased onto origin/refactor f41729178 as `honker/row194-b2`. `honker/row194` stays at f70b4da50 (no force-push). The only file both sides changed was `PRD-1.7-DRAFT.md`, whose blob was the same on both (9bfc6f76), so its five draft commits dropped as already upstream. The tree check: the rebased tip differs from f70b4da50 by exactly the base's changes. Every test file the branch changes, the four must-run files and webui sso, service and reads were run again singly on the new base: 23 files, all pass. ## S5 notes (the Console's Agents section, D7) - `packages/webui/src/public/agents.js` (new, `window.MosaicAgents.section`): Settings draws it after the role instances, from the same instances read. The table has instance, definition, launched, kept and running runs, and each row has Launch, a reason box and Terminate. A human also gets Regenerate, Keep or Unkeep, and Delete. The protected assistant has no Delete. Each definition has a No-hire or Allow-hiring button, and a Create form adds an instance of an accepted definition. Retired names show as one line. D7's "retired" column is that line, since a retired name has no instance row. - Regenerate and Delete take a second click. The confirm says what happens: for Delete, "Its evidence and its workspace under .retired stay." Cancel asks nothing. - Choice: every answer that names a request is followed at `/api/agent/request` every 2 s, up to 90 reads, until the request closes. D7 names launch and regenerate. Delete and terminate also answer a request (S2c), so the page follows them too, and the receipt shows the close or its refusal reason. A failed read is shown and the poll goes on. The request is never sent again. Past 90 reads the page says the request is still open and asks for a fresh read. A sign-out stops the poll. - With `send` false the section shows "Agent actions are unavailable here. Use mosaic business instance from the instance CLI." and every control is disabled. The tester sees Launch and Terminate only. - `serve.mjs`: `/api/instances` takes op `delete` with the revision and the instance alone and answers the request. `/api/agent/launch`, `/regenerate`, `/terminate` and `/api/veto` each have `instanceSend`'s boundary and one call to `agentWrite`, with only the route's keys. A reason is never logged. `GET /api/agent/request?id=` takes one request id and adds the reason for a closed refusal or a cancel. Every refusal code the S2/S3 verbs answer has a reason line (`AGENT_REASONS`), and a code missing there shows as itself. No route has a `--send` exception. - `cli.mjs`: `agentWrite` reaches only `instance.launch`, `instance.regenerate`, `instance.end`, `veto.set` and `instance.request` (`agentCaller` throws for any other). The tester listener gets launch, end and request only (D4.1). - D4.1 at the webui: the tester listener lets launch and terminate through and refuses regenerate and veto with no call. A tester delete on `/api/instances` is refused in `instanceSend` with no call (`tester-refused`). The bus refuses it too (broker tester-refused), and this keeps the webui from asking. The agents test covers it, and with the guard removed that test fails (control run). - Deferred to S6: the "still running after fire" flag needs the `lifecycle.end.alive` event, which comes with row 191's stop. - Tests: webui `agents.test.mjs` (new, 8: actions and logs, boundary refusals, read-only, refusal and unknown outcomes, the request read, delete, tester routes, agents.js served) and `agents-browser.test.mjs` (new, 1: read-only, launch followed to close, confirms, terminate reason, refusal, keep and unknown outcome). - Singles, each run alone, after the last code change: webui agents 8/8, agents-browser 1/1, browser 1/1, instances 5/5, instances-browser 1/1, serve 4/4, service 31/31, sso 28/28, sso-browser 2/2, routes 5/5, routes-browser 1/1, tester-listener 10/10, tester-browser 2/2, tester-cli 2/2, settings-write 4/4, send 13/13, send-browser 2/2, shell 5/5, cli launcher 29/29, thread-launch 11/11, git-socket 24/24, stack-copy 12/12, cli stack-release 174/174, seat sandbox 10/10. No test pins a hash of a file S5 changes. shell.test reads bus.js strings and its POST count, which S5 leaves alone because agents.js holds the section's writes. ## Item 7 plan (brief rev 2547, sage-1586, from row 199 N2, duckworth-0068) Choice: create states that the agent needs a restart before `issue`. It doesn't register the entry with the running bus. - Today create and hire refuse a definition that declares credentials (`organization-credentials-out-of- scope`, `instance-settings.mjs:75-77`). So no created agent has a credential entry, and item 7 has nothing to act on until row 199 lands. Row 199's D13 (origin/megavolt/row199-on-190 `agents/megavolt/work/queue-199/DESIGN.md:295-307`) has row 194 lift that refusal for the human's create only. The agent is created with `store` references and no files. Hire stays uncredentialed. - The runtime already treats credential references as fixed at start (`runtime.mjs:115-134`). A file whose references differ from the boot copy isn't reloaded, and `instances.set` answers `restart: true` (`runtime.mjs:182`). The CLI prints RESTART_NEEDED (`packages/cli/src/instances.mjs:59, 235`), and the Console's instances section prints RESTART_TEXT (`packages/webui/src/public/bus.js:691, 733`). So after the lift, a human create of a credentialed definition answers `restart: true` with no new runtime code. `credentials.issue` then refuses `credential-unknown`, because the broker's copy of the business doesn't hold the agent (`broker.mjs:1443` on row 199) and Credentials has no reference for it (`credentials.mjs:148-150, 157`). - Why not register: registering means a new path in row 199's Credentials that adds references to a running store, plus a change to the fixed-at-start rule that row 194's D3 review accepted. The restart route changes neither. It costs one bus restart per credentialed create, which the human already makes for any credential change. - Build, after row 199 lands and this branch moves onto it (new branch name, no force): 1. `instance-settings.mjs`: a human `add` of a credentialed definition writes `store` references with the date fields row 199 requires and no files. Hire keeps the refusal. The human's create is `instances.set` op `add` (agents.js posts it to /api/instances), so it already answers `restart`. The runtime comment at `runtime.mjs:115-116` ("Create and hire add neither") changes to say a human create can. 2. `agents.js` createAgent: the receipt carries the answer's restart text, as bus.js does. Today it says "Created X." and drops `restart`. 3. T19, bus test: a human create of a credentialed definition answers `restart: true`. Issue refuses `credential-unknown` before a restart. After a bus restart over the same directories, issue works and the status reads `valid`. Hire of the same definition still refuses. A browser assertion covers the restart text in the Agents receipt. 4. Mutants: M61 the create refusal stays for the human (T19 fails at create). M62 `fixedRole` ignores `credentials`, so the reload goes live and answers `restart: false` (T19 fails). M63 the Agents receipt drops the restart text. - D13's revoke on fire and delete: Sage put it in row 194 as brief item 8 (sage-1588). See the item 8 plan. ## Item 8 plan (brief rev 2556, 722136772, sage-1588, row 199 D13) `fire` and `delete` call `credentials.revoke(business, agent, {reason, actor})` before they change the business file. A failed revoke stops the verb with `credential-revoke-failed` and leaves the file unchanged. - Order: the verb's own checks, then the revoke, then the write. A verb its checks refuse revokes nothing. Fire's checks are `instance-settings.mjs:254-265` (unknown, kept, end-protected, launch.by, last name). Delete's are `planDelete` (`instance-settings.mjs:189-193`). A delete writes in the launcher's pass, through `set` op `delete` after the take (comment at `instance-settings.mjs:187-188`), so its revoke goes there, after `planDelete` and before the commit. The write runs under the same revision the checks read, so a revision race refuses as today, after the revoke. That leaves an agent revoked with its file unchanged, which is the safe side: Jason runs `issue` again if they still want it. - Where: the business package doesn't import the bus. The runtime passes the instance writer a revoke callback, and fire and delete call it between the checks and the write. With no Credentials store (no row 199 config) the callback is absent and nothing changes. - A fire of a name that isn't in `launch.instances` writes nothing (`instance-settings.mjs:262`) and calls no revoke. - A revoke for an agent the running bus has no entry for (sage-1592): `credential-unknown` counts as nothing to revoke only when the agent has no token file under the store, `secrets/agents// /`. If a file is there while the bus doesn't know the entry (a file issued before a restart that dropped the entry), the verb stops with `credential-revoke-failed`, so no agent is fired with its token still on disk. Every other refusal stops the verb too. - Row 199 as of 86963d281 (origin/megavolt/row199-on-190) doesn't cover that case. `revoke(instance, service)` calls `#known` first (`credentials.mjs:197-198`), and `#known` refuses `credential-unknown` from the boot-time entries alone (`credentials.mjs:148-150`). It never looks at the disk. The store path is `///` (`credentials.mjs:71-76`), with storeRoot `/secrets/agents` (`credentials.mjs:40`). So the extra check stays: on `credential-unknown`, look for any entry in the agent's store directory. Recheck at landing. If the landed revoke deletes or refuses in that case, cite the line and drop the check. - Also to recheck at landing: D13's call reads `credentials.revoke(business, agent, {reason, actor})`, but the class method is per service, `revoke(instance, service)`, and `remove(instance)` revokes every service and then clears the agent's directory (`credentials.mjs:211-218`). The broker verb `credentials.revoke` takes `agent, service, reason` (`broker.mjs:1437`). The build uses whatever the landed code offers for every service of one agent, and records which. - Sage's pick (sage-1593): fire calls `revoke(instance, service)` for each service in the agent's entry and keeps the directory, so the status still reads revoked and Jason deletes the Gitea or Vikunja token (199 A5). Delete calls `remove(instance)`. Both count as the revoke for the order, the refusal on failure, T20 and M64-M67. If the landed code differs from 86963d281, follow it and note the difference here. - What that does to the sage-1592 check, as of 86963d281: `remove` takes its services from the boot-time entries, so for an unknown agent it revokes nothing and doesn't refuse. It then lists the agent's store directory and unlinks every token and tmp file, refusing `credential-file` for any other name (`credentials.mjs:211-224`). So delete already leaves no token on disk, and the extra check applies to fire only (sage-1592: cite the line and drop it). T20's delete case with a file and no entry expects the file gone and the delete done. M67 runs against fire only. Recheck all of this at landing. - The reason: fire carries the chief's reason from the change context. Delete carries the human's request reason. The actor is the verb's actor. - T20, bus tests, each for fire and for delete: - With a credentialed agent: the revoke runs, the status reads revoked, and the file changes. - With a revoke that fails (a stub store that throws): the verb refuses `credential-revoke-failed`, the file's revision is unchanged, and the agent is still listed. - The revoke runs before the write. The stub records the file revision it saw, and that revision is the old one. - With no entry in the running bus but a token file in the agent's store directory: the verb refuses `credential-revoke-failed` and the file's revision is unchanged. With no entry and no file: the verb goes on and the file changes (sage-1592). - Mutants: M64 drop the revoke call (the success case fails, the status isn't revoked). M65 move the revoke after the write (the order check fails). M66 ignore the revoke's failure (the failing case writes the file). M67 treat any `credential-unknown` as nothing to revoke (the token-on-disk case writes the file). M64-M66 run against fire and delete, M67 against fire only (see sage-1593 above): M64a-b to M66a-b, M67. ## b3 notes (items 7 and 8 on row 199's landing cb5fdb064, sage-1601) Branch `honker/row194-b3`, a new name per sage-1601. Its first commit 589ae2c1d squashes honker/row194-b2 85edab7c7 onto cb5fdb064. A rebase of b2's commits stopped on the same five files many times, so I squashed. The five conflicts kept both sides: - broker imports, constructor, dispatch, and the credential and lifecycle verbs - runtime imports - the organization write, which takes detail and context - cli credentials and veto dispatch - the vocabulary comment, as row 199 wrote it Line numbers below are the b3 tip's. ### Deviations from brief 722136772 and plan 85edab7c7 1. Item 7 dates, sage-1605 pick (a). The human create writes the UTC create day as `rotateBy` and `expires` (`instance-settings.mjs:118-120`). `checkCredentialRef` skips both date checks only for a store reference with no file (ENOENT), and keeps the "not issued" warning (`business/src/credentials.mjs:73, 79-81, 110-113`). Every other reference keeps the checks. Tests: `business/tests/cli.test.mjs` "validate: a not-issued store agent dated today passes, and an issued one with a past expiry fails", and T19 in `instance-settings.test.mjs` (validate exits 0 with "not issued"). 2. fixedRole, sage-1605. A reference compares by kind and path only (`runtime.mjs:154-167`), because row 199's issue rewrites the date in the file outside the reload. Test: T19 in `bus/tests/reload.test.mjs` (add, restart, issue, then a vars change goes live with restart false). The T14 date-only test stays. 3. Delete takes all-store references, sage-1605. `deleteRefusal` refuses only when some reference isn't `store: true` (`instance-settings.mjs:43-49`). A file or env reference refuses `organization-credentials-out-of-scope` at the request, before any take (T20 delete case 2). 4. Fire on a file or env reference revokes in memory, sage-1605. The result's `credentials.outsideStore` names the services whose reference is outside the store (`runtime.mjs:122-124, 141-142`). T20 fire case 1 checks `outsideStore: ['vikunja']` for coder. 5. The `credential-revoke-failed` store-dir check stays, sage-1605 (`runtime.mjs:132-140`). It now runs after delete's `remove` too. `remove` already leaves no token, so on delete it only refuses a name `remove` would itself have refused. 6. Tracker on add, honker-0049 and sage-1610 (a). A human add takes an optional `tracker: {bot, botId}`. It is required for a definition with a vikunja credential (`organization-tracker-required`) and refused for any other (`invalid-request`). checkBot and the bot uniqueness rule (other instances and `tracker.sync`) run on the add path (`instance-settings.mjs:88-124`, `organization-tracker-bot-taken`). `change` with a tracker refuses (`instance-settings.mjs:214`). The broker passes `tracker` through (`broker.mjs:193-194`). CLI: `instance add ... [--bot BOT --bot-id N]` (`cli/src/instances.mjs:13, 73, 87-90, 107`, reasons at 22-27). The Console doesn't take a tracker: `INSTANCE_KEYS` in serve.mjs is unchanged, the Agents and Settings forms still list credentialed definitions as not accepted, and a vikunja add from the Console answers `organization-tracker-required` with a reason that names the CLI (`webui/src/serve.mjs:332-333`, `webui/tests/instances.test.mjs`). 7. Delete with a tracker bot, sage-1610 (b). The delete's result names the bot and says to remove the Vikunja bot user and any token by hand (199 A5) (`instance-settings.mjs:199-202`). `lifecycleDelete` returns `bot` (`broker.mjs:1386-1387`), the launcher carries it to `lifecycleDone` (`cli/src/launcher.mjs:752-767`), `lifecycleDone` takes `bot` checked against BOT_NAME (`broker.mjs:132, 1392-1393`), and `lifecycleView` shows it (`broker.mjs:147`). The CLI line (`cli/src/instances.mjs:177-180`) and the Agents receipt (`webui/src/public/agents.js:113-115`) name it. Tests: T20 bus delete case 1, T20 in `cli/tests/lifecycle.test.mjs`, and `agents-browser.test.mjs`. 8. No Vikunja account. The create makes none and the delete removes none. Q188 and the `account.create` hard wait aren't touched (sage-1610). 9. The tester's add keeps the credential refusal. Row 175 W2 (`bus/tests/tester-writes.test.mjs:91`) says the tester refuses credentials, and items 7 and sage-1610 name the human's add. The writer lifts the refusal only when the actor isn't `tester` (`instance-settings.mjs` add, the `definitionFor` call). The broker reserves that name: no human or role may be called `tester` (`broker.mjs:234, 237`). W2 has a new case, a tester add with a valid tracker, which still refuses `organization-credentials-out-of-scope`. 10. The commit hook is `beforeWrite` (`instance-settings.mjs:332-360`). A first draft named it `before` beside a `let before`, a SyntaxError, so I renamed it. 11. A failed delete revokes first. `Credentials.remove` revokes every service it knows before it reads the directory (row 199 `bus/src/credentials.mjs` remove). When the directory then holds a name `remove` refuses, the delete stops with the business file unchanged and the tokens already gone. That is the safe side and matches item 8's note on a revision race. T20 delete case 1 asserts it. 12. T20 uses real failures, not a stub store: a 0500 agent directory (fire) and a foreign file in the store (delete). The revoke-before-write order is checked by the failing cases: a revoke after the write would leave the file changed (M65). ### Hunks for Duckworth's scoped check (sage-1605) - `packages/business/src/credentials.mjs`: 12 (DATE_KEY exported), 73, 79-81 (notIssued), 110-113 (the no-file skip). - Credential verbs: `packages/bus/src/credential-verbs.mjs` and `bus/src/credentials.mjs` are unchanged from cb5fdb064. The broker's `#requestCredential` (`broker.mjs:1679-1709`) is row 199's text as merged in the 589ae2c1d squash. b3's own credential hunks: the runtime revoke callback (`runtime.mjs:109-143`) and the broker's `credential-revoke-failed` mapping (`broker.mjs:1816-1817`). - fixedRole: `packages/bus/src/runtime.mjs:154-167`. ### Mutants to run on Sage's slot (after M1-M60) - M61 the human create keeps the credential refusal (T19 business and bus fail at create). - M62 `fixedRole` ignores `credentials` (bus T19 fails: restart false). - M63 the Agents create receipt drops the restart text (agents-browser fails). - M64a/b drop the revoke call on fire and delete (T20 success cases fail). - M65a/b move the revoke after the write (T20 failing cases see the file changed). - M66a/b ignore the revoke's failure (T20 failing cases write the file). - M67 any `credential-unknown` counts as nothing to revoke, with the store-dir check dropped (T20 fire case 2 writes the file). - M68 fixedRole compares the whole reference, dates included (bus T19 fails: restart true after issue). - M69 `checkCredentialRef` drops the no-file condition (the issued-past-expiry validate test passes wrongly). - M70 drop the vikunja tracker requirement (T19 business and the CLI tracker-required case fail). - M71 `deleteRefusal` refuses a tracker bot again (T20 delete cases and CLI T20 fail). - M72 skip checkBot on the add path (T19 business bad name and bad botId fail). - M73 the tester's add binds credentials too (tester-writes W2 fails on both reviewer-2 cases). ### Pins and a missed failure - `bus/tests/channel-proof.test.mjs` pins `webui/src/bus.mjs`. b3 adds two STATUS entries, so it is repinned with a comment, as rows 165 and 171 did. No proof or transport change. - `bus/tests/project-runtime.test.mjs` pins the broker's vocabulary import. b2 imported `unhireableAction` and never updated the pin, so the test fails on b2 too. I have no log of it running on b2. b3 fixes the pin. - `bus/tests/tester-writes.test.mjs` W2 caught the tester binding credentials through a tracker add. The writer now lifts the credential refusal only when the actor is not `tester` (deviation 9), and W2 has a case with a valid bot that still refuses. ### b3 singles (2026-10-21T01:02Z) Every test file under `packages/{business,bus,harness,cli,webui}/tests`, run singly with `node --test`: 158 files, 1501 pass, 0 fail. The first pass failed channel-proof, project-runtime and tester-writes (fixed above). After the fixes and the tester change in `instance-settings.mjs`, 17 files reran singly: 167 pass, 0 fail. The total uses the rerun counts for those 17, summed by awk over the logs. Suites and mutants M1-M73 wait on Sage's slot. ### sage-1620 (2026-10-21T01:05Z) - W2 reading confirmed. Each refused W2 case now asserts the log length as well as the bytes and revision (`bus/tests/tester-writes.test.mjs`, inside the loop). M73 stays in the batch. - Deviation 11 accepted. The refusal text says a retry is safe: `credential-revoke-failed` has a reason in the CLI (`cli/src/instances.mjs` REASONS) and in the Console (`webui/src/serve.mjs` AGENT_REASONS, shown on a closed request's read). CLI T20 now fails a delete with a foreign file in rev-2's store, checks the text and the unchanged revision, then clears the file and deletes. Console agents.test checks the read's reason. - Mutants for the slot: M74 drops the CLI reason (CLI T20 fails); M75 drops the Console reason (agents.test fails). - Seat: every `packages/seat/tests` file singly (164 pass, 0 fail, 7 files) and the seat suite whole (`node --test tests/`, 164 pass, 0 fail), for the broker import change (row 210's gate finding). Touched files singly after this change: tester-writes 8, cli lifecycle 16, cli instances 14, webui agents 8, agents-browser 1, all pass. ### Follow-up for 194: N1, a revoke that succeeds before a failed write (sage-1630, 2026-10-21T01:15Z) Duckworth's N1 (#1692 comment 29818), noted here as a follow-up with no b3 change. When the revoke succeeds and the write then refuses `revision-conflict` or `outcome-unknown`, the business file is unchanged and the tokens are gone, but neither refusal says so. Only `credential-revoke-failed` has the retry-safe text. - Order: `business/src/instance-settings.mjs:361` runs `beforeWrite` (the revoke), then `:362` runs `writer.write`, which checks the revision under its lock (`business/src/organization.mjs:188`) and again before publication (`:215`). - Fire reads a fresh revision (`instance-settings.mjs:311`), so a conflict there needs a concurrent write. - Delete is not a race. It commits with the revision the request recorded at raise time (`bus/src/broker.mjs:1855`, checked against the file then), passed at `broker.mjs:1382` and `instance-settings.mjs:193-198`. Any organization write between the raise and the take makes that revision stale, and nothing compares it before the revoke. So a delete taken after such a write revokes the agent's tokens and then refuses `revision-conflict` with the instance still listed. - Fix options: compare the request's revision with the fresh read before `beforeWrite` (the writer still re-checks under its lock); or run `beforeWrite` inside `writer.write` after the `:188` check; and in either case give `revision-conflict` and `outcome-unknown` text that says tokens may already be revoked when a revoke ran. ### sage-1634: N1 fixed in b3 (2026-10-21T01:20Z) Sage ruled N1 a blocker for 194: it is the row's own code in files the row already changes. Option 1 is in. The section above stays as written; this entry supersedes its "no b3 change". - `business/src/instance-settings.mjs` `commit()`: before `beforeWrite`, compare the requested revision with `writer.read(business).revision` and refuse `revision-conflict` on a difference, without calling `beforeWrite`. It runs for every caller (add, set, veto, hire, fire, delete). Every caller already read the file with `writer.read` (it validates), so a file that went invalid in between refuses `BusinessError`, also before any revoke. - Fire reads its revision fresh (`:311`) in the same synchronous call, so it reaches a stale revision only through another process writing between that read and `commit()`. No test makes that happen without an injected writer; the check is the same line, and the delete test kills both mutants. - Test: bus `lifecycle-revoke.test.mjs` T21. A delete raised on revision R and taken; a credential issue for rev-3 writes the file; the delete refuses `revision-conflict`. The business file's bytes and rev-2's Credentials entry are unchanged, the vikunja token file is still there with its bytes, the store holds only it, and the status reads valid. - Refusal text: `revision-conflict` says the file changed since that revision (CLI `instances.mjs` REASONS, Console `serve.mjs` INSTANCE_REASONS). Neither says anything was revoked, so no change. The Console's AGENT_REASONS has no entry for it. - Mutants for the slot: M76 drops the early check (T21 fails: the token file is removed); M77 moves it after `beforeWrite` (T21 fails the same way, then refuses). - Runs, singly or whole as Sage asked: business whole 207 pass, bus whole 525 pass, seat whole 164 pass (sandbox.test included), cli instances 14, cli lifecycle 16, webui agents 8, all 0 fail. Follow-up (option 2, not in b3): run `beforeWrite` inside `writer.write`, under its lock and after the `organization.mjs:188` check. Until then a window remains: a write landing between the early read and the writer's lock still lets a fire or delete revoke and then refuse `revision-conflict`, and an `outcome-unknown` after a revoke still doesn't say the tokens are gone. ### b4: rebase onto c3d88840c (sage-2157, sage-2164, 2026-10-21T21:20Z) `honker/row194-b4` is b3's five commits replayed onto c3d88840c (rows 196, 210, 213, 234, 235, C2-03a/04 landed in between). b3 is unchanged. The first commit conflicted in 18 hunks across 11 files and the second in 4; every one kept both sides (list in honker-0067). The `webui/src/bus.mjs` channel-proof pin was recomputed on the merged file. What the rebase exposed, fixed on b4: - Creative hire (row 213, M10). `closed()` lets a creative definition through `definitionFor` for the human's add and change, and hire used `definitionFor`. Hire now refuses `definition-not-hireable` for any CREATIVE_DEFINITIONS entry, and `checkHireable` refuses a creative name in a role's hireable list at load. - Reload and library projects (C2-04). A library entry has no `root` and its mirror is ensured at start, but `fixedAtStart().roots` took every project's root, so a changed mapping reloaded live. `roots` now holds the non-library roots, the Credentials store's set, and `libraries` fixes each library entry whole (reload T14). - Creative delete (sage-2164). `deleteRefusal` refuses END_PROTECTED, creative definitions included, while the listing's `protected` is false for a creative instance, so the Agents page offered a Delete the server refuses. The listing adds `endProtected`, the same check `deleteRefusal` runs, and the page hides Delete on it. The refusal stays; the human delete of a creative instance is a DEFERRED entry. A human can't fire one either (fire refuses END_PROTECTED for every actor); `instances.set` with `launch: false` delists it. - Row 213's companion listing test (`instance-settings.test.mjs:274`) didn't have 194's `kept`; it now pins `kept` and `endProtected`. - DEFERRED C1 (Duckworth N2 on row 196): `testerNoSignIn` bound the next row touching `serve.mjs`. 194 touches it, so the dead parameter is removed from `serve.mjs`, `webui/src/cli.mjs` and `sso.test.mjs`. - Manifest notes for Webby (sage-2166): C1 is closed by 194 (the three `testerNoSignIn` lines), and `docs/plans/DEFERRED.md` is its own manifest path (the creative delete entry; C1 marked at landing). - Slot run 1 (sage-2217, 2026-10-21T22:36Z, 25 whole files on 646cd493a plus the tree): 312 pass, 2 fail. Both came from combining rows on the c3d88840c base, so committed 646cd493a fails both too: - `instance-settings.test.mjs:283` (row 213) expected the writer's `restart` true on a creative definition change. 194's D3 makes the writer always answer false, and the bus sets restart from its reload, so bean isn't running and a reload is the designed answer (reload T14 covers the running case). Repinned to false. - `agents.test.mjs` T16 started the tester listener on a TCP port. Row 196 (7247d80c7) serves it only on the private socket (`tester listener requires private socket`), so T16 now uses `startTesterSocket` under a temp data root and `testerRequest`, as `tester-socket.test.mjs` and `sso.test.mjs` do. - Drift: c3d88840c..eb017d77f overlaps the manifest only on `docs/plans/DEFERRED.md` (merge-tree clean); the requirements tests changed their own counts, `host-run.test.mjs` took the #453 T27 fix. ### b6: Webby's b5 review (webby-0094) and Sage's rulings (sage-2241, sage-2245), 2026-10-21T23:50Z `honker/row194-b6` is on b5 (dfabbbe8a). It covers B1, N1-N4, and notes for N5-N8. B2 (S6) waits for row 191's `stopRun`. No test has run on b6 yet. Every run waits for Sage's CLEAR. B1, restart on every committed lifecycle verb: - `runtime.mjs` adds `restart: !live` to the answer of every instance-writer verb: `instances.set`, `veto.set`, `instance.hire` and `instance.fire` (`INSTANCE_VERBS`). Before, only `instances.set` carried it. Limits, routes and plugins writes don't go through the instance writer, and they keep their own answers. - The CLI's veto output and the Console's veto receipt print the restart line. `/api/veto` treats an answer without a boolean `restart` as a bad shape (agents.test.mjs, calls 6 to 7). - The stuck-firing sequence: a keep the bus copy hasn't loaded (a refused reload) passed the broker's checks, step A stopped the runs, and step B refused `instance-kept`, leaving the instance firing. The broker now calls `instance.fireCheck` before step A. That runs the writer's `fireChecks` against the file with no write, so the fire refuses before it stops anything. `fire` runs the same `fireChecks` at step B. - Tests: in reload.test, after a refused reload, veto, hire and fire answer restart true. A fire of a name kept since then refuses `instance-kept` with the file unchanged, the run still held and no `instance-firing`. After an unkeep, it fires. chief-of-staff T10 covers a fireCheck refusal that stops nothing. The CLI stub-bus test covers a veto with RESTART_NEEDED. - Mutants for the gate: (a) put back the `instances.set`-only restart line, which reload.test's new test, the CLI veto test and the Console tests should kill; (b) drop the `instance.fireCheck` call, which the stuck-sequence test and T10 should kill. N1: a fire of a delisted name now revokes its services, with no write: `delisted: false`, `transaction: null` and the revoke receipt. A repeat revoke is safe. Test in lifecycle-revoke.test.mjs. N2: `createdCredentials` drops HOST_SERVICE, as `checkRoles` does (instance-settings.test.mjs). N3: I extended the check, as row 213 did for creative instances. `keptHumanOnly` (launch-settings.mjs) is called in the limits writer against the file. It refuses a re-point of a `launch.models` entry a kept instance's model key names, and a change of a kept instance's own key, for every actor but the human (the tester included, as with `creativeHumanOnly`). That makes a shared-key re-point also refused for agents whenever any kept instance uses that key; the chief can still add or change keys no kept instance names. The broker passes `instance-kept` from the writer for every verb. Test: "B7 kept instance" in launch-settings.test.mjs. N4: not small, so it's deferred. B1's file check removes the reachable case Webby named. A step B that keeps refusing (a revoke that fails every time, or a file changed between the check and the write) still strands the instance. The DEFERRED.md entry names a human-only verb and its test (sage-2241: never an agent verb). N5 (Console `busStatus` codes and the `unknown-request` polling): follow-up, not in b6. The body is right and the status is 502. Neither is harmful. N6 (CLI terminate signals every registry run of the instance): this is broader than V3's text (DESIGN.md:118-120), which names the verb's runs plus spawned-but-unbound ones. A run the chief launched after the verb is ended too, and its claim wasn't revoked at the verb. I haven't changed it in b6, because the launcher's signal pass is what S6 rewires onto `stopRun`. When S6 lands, either the pass narrows to the verb's runs plus unbound registry runs, or the design's next revision says terminate ends every run of the instance at the take. Sage's call. N7 (a delete that closes `outcome-unknown` after commit can't be retried; DESIGN.md:173 says a second delete retries step 3): follow-up. The window is narrow. N8: DESIGN.md:625 still lists "SIGKILL escalation" under "Not in the row". That's r2 text, and D2.2 r4 (the fire stop, T9a, M51-M55) replaced it. The approved design isn't edited. D2.2 governs. Line 128's "no SIGKILL escalation" is V3 terminate and still holds (TERM only). B2, S6 (sage-2241, sage-2245): 194 lands after or with row 191 in the 10-24 window. The per-run stop belongs to row 191. Rocko and I agreed its shape in `agents/rocko/work/queue-191/STOP-RUN-CONTRACT.md` (sha256 dd9bead59372cb3394fc15060e87a824c458ee8e0e45a2b519141ba94c965bf8): - Call: `stopRun({root, business, run, reason, owner})` from `processing-control/src/runtimes.mjs`. - Answer: `{run, outcome, pid, signals, receipt}`, where outcome is stopped, killed, alive, identity-changed or unknown. - Grace: fixed 5 s TERM and 2 s observe. Identity is checked again before each signal. - The first owner joins, with one immutable receipt. - It writes nothing to the bus and needs no claim. S6 maps killed and alive to `lifecycle.end.killed` and `lifecycle.end.alive`, and V3 terminate stays TERM only. It wires onto Rocko's implementation commit (`rocko/row191-stoprun-v1`) when that arrives. S6 doesn't use the root-wide halt sweep. ### b7: rebase onto row 254 (dcd8d4dda) and Webby's b6 review (webby-0095, sage-2246, sage-2248, sage-2251) `honker/row194-b7` replays all eight row 194 commits since da9555ca2 onto dcd8d4dda (row 254 landed as 331597709, with docs in eb9672c91), plus the routes.set fix. b6 isn't changed. - Conflict: one, in `channel-proof.test.mjs`, the `webui/src/bus.mjs` pin. I kept row 194's comment and pinned the merged file (15e8123f...). The other three pins (human.mjs, human-cli.mjs, transport.mjs) still match their files. - `webui/src/bus.mjs` merged clean. Row 194's two tracker STATUS entries and row 254's six library entries sit in separate lines, with no duplicate key. - `DEFERRED.md` merged clean and keeps both of row 194's entries. - Base drift that touches the manifest: those three paths only. Behaviour change for review (sage-2246, the b6 N3 fix): an agent can no longer re-point a `launch.models` key that a kept instance's `vars.model` names. Since the key is shared, that also changes the model for every other instance on that key. Only the human can. The chief may still add a key, or change one that no kept instance names. Before b6, the chief could re-point any key. webby-0095 N1 (sage-2248): routes.set was a second way to write a kept instance's model. setRoutes now calls `keptHumanOnly` after `creativeHumanOnly`, so the chief and the tester both get `instance-kept` and the human can still re-route. That makes b6's "the tester included" true on the bus: limits.set isn't a tester verb, but routes.set is. Test: "B7 kept instance: its route is the human's" in launch-settings.test.mjs. - **M-next**: drop that `keptHumanOnly` call from setRoutes. The new test has to kill it. Correction to the b6 notes: mutant (a), the instances.set-only restart line, can only be killed by reload.test. The CLI and Console tests stub the bus, so they never run runtime.mjs. The run list uses reload.test for (a). The run list for Sage's CLEAR is `scratch/honker-t/b7/runb7.sh `: - Each touched test file runs alone, on the committed candidate: reload, chief-of-staff, lifecycle-revoke, channel-proof, instance-settings, launch-settings, cli instances, webui agents and agents-browser. - Then mutants (a), (b) (no fireCheck before step A: reload and chief-of-staff) and M-next (launch-settings), each restored and checked clean. - The full gate with Docker reachable, and P3, wait for S6. N6 (sage-2246), done in the S6 commit, not before: when terminate's signal pass moves onto `stopRun`, it narrows to DESIGN.md:118-120. That means the runs the verb wrote into `lifecycle.end`, plus registry runs of the instance that have spawned but not bound. The test: a run that binds after the verb survives the pass, and the next terminate ends it. The mutant widens the pass back to every registry run of the instance. ## b8 notes (row 191's stopRun base, S6; sage-2264, sage-2267, sage-2273, sage-2276, sage-2284) Base: b7's nine commits (origin/honker/row194-b7, last 0f0e2c7ef, on dcd8d4dda) rebased onto row 191 at origin/rocko/row191-stoprun-dcd8-20261022. That tip moved three times while I built, and each move was a test-only fast-forward: 7aa18b0f6, then 81889b58d, then c78f48946 (webui processing.test), then eb3abba12 (lane-image inputs and initializeControl in the launch-*.test fixtures). b8 sits on eb3abba12. Only the first rebase, onto 7aa18b0f6, had conflicts. The three moves after it replayed clean, because none of those test files is a row 194 path. The 194 manifest is `git diff eb3abba12 `, and it holds no row 191 path. S6 changes no file under packages/processing-control. The full interdiff, b7 against b8, is `git range-diff dcd8d4dda..0f0e2c7ef eb3abba12..`. Six commits are `=`, which means the same patch. Three are `!`: commit 1, b4 and b6. Each resolution follows, file by file. I kept both sides every time, and no 194 behaviour changed. Commit 1 (row 194 on row 199's landing): - `bus/src/broker.mjs`, the constructor. 191 added `processing = null`, and 194 added `testerOn = null, roleFile = null`. The merged signature ends `processing = null, testerOn = null, roleFile = null`. 191's `#processing` field and `#processingState()` sit beside 194's fields. - `bus/src/runtime.mjs`, startBroker. 191 turned `const broker = new Broker({` into an assignment (`broker = new Broker({`) and added `broker.reconcileProcessing(); processing.start();` after it. 194's `roleFile` and `testerOn` arguments stay inside that call. - `cli/src/launcher.mjs`, start(). 191 added `admissionGeneration`, and 194 added `human = false`. The merged parameters end `thread = null, human = false, admissionGeneration`. 191's `admission(...)` calls and the tick's `try { admission(); } catch { return; }` are unchanged. - `webui/src/cli.mjs`, the main startServer call. 191 passes `processing` and 194 passes `agentWrite`, which gives `..., routesWrite, agentWrite, processing`. 191's `let processing = null;` is kept. - `webui/src/public/index.html`, the head. 191's `processing.css` link and `processing.js` script come first. 194's `agents.js` comes after `chat.js`, before `bus.js`. - `webui/src/serve.mjs`, the static list, which is now `[..., 'chat.js', 'processing.js', 'agents.js']`. The css list is 191's. In startServer's signature, `agentWrite = null` comes before 191's `processing = null`. 191's `/api/processing` route is ahead of the settings route, and 194's routes are unchanged. - `webui/tests/browser.test.mjs`, the script-list pin, which is now `['/editor.js', '/processing.js', '/brand.js', '/app.js', '/markdown-preview.js', '/chat.js', '/agents.js', '/bus.js']`. The comment names rows 129, 154, 191 and 194, and 191's single-processing.js assertion is kept. b4: - `webui/src/cli.mjs`, the tester socket. 194 drops `testerNoSignIn: ssoOn` (C1), and 191 adds `processing: { read: () => call('processing.status') }` on the next line. Both are kept. - `webui/src/serve.mjs`, startServer's signature. 194 drops `testerNoSignIn = false`, and 191's `processing = null` stays. b6: - `bus/src/runtime.mjs`, the imports. 194's `INSTANCE_VERBS` constant goes after 191's three imports (`createProcessing`, `discordProcessingHooks`, `createContainerStopper`). S6 (sage-2241, sage-2246, sage-2261; Rocko: not through sweepRuntimes). Uncommitted until this round's commit: - **Fire.** fire's lifecycle.end with `kill: true` now goes through 191's `stopRun`, using the fire's own options: reason `fire` and owner `{ decision: }`. That covers the SIGTERM, the grace, the identity re-check before each signal and the SIGKILL. The launcher's stopRun outcomes go back as `stops`. lifecycleDone writes `killed` as lifecycle.end.killed and `alive` as lifecycle.end.alive, each with [subject, request, run, pid]. The instances read sets `alive` from the alive events. The Console shows "still running after fire: " (`.agent-alive`, D7). - **Terminate (V3).** terminate keeps the SIGTERM-only pass and never sends SIGKILL. - **Halt.** While halted, the launcher's tick runs only lifecycle passes. A launch or regenerate closes `processing-halted`. The bus's `#launchable` refuses `processing-halted` at the verb. A halt during a fire's stop joins that stop through stopRun's `stopping` map. sweepRuntimes then reads the fire's receipt, and no second SIGTERM goes out (T9b). - **N6.** The pass takes the verb's `runs`, plus runs of the instance that the bus never bound, but only those spawned at or before the end's `at`. Spawn time comes from the launcher's `spawnedAt` map, or `startedAt` when the map has no entry. A run spawned after the verb survives, and the next end takes it. Clock caveat: `at` is the bus's clock, and spawn time is the launcher's. Both are Date.now() on one host, so the order holds unless the clock steps back. Consequence: if a launch the verb reported in `taken` spawns after the verb, it is the next end's run. I updated the V3 comment in broker.mjs to say so. - **Open question for Sage.** A registry run with no 191 runtime record makes stopRun answer `unknown`. The launcher logs it as fire-stop with that outcome and doesn't map it to alive, because nothing shows it running. Should it count as alive? - **Deviations.** The cli tests are in `cli/tests/lifecycle.test.mjs`, not `launcher.test.mjs` as DESIGN.md names them. That file already has the bus-and-launcher fixture, and startBroker initializes control there, so no new fixture calls initializeControl by hand (sage-2284). D7 reads alive through `instances..alive`. - **Tests.** - cli lifecycle T9a, three tests: bound, spare and stuck; identity before and between; and V3 with no SIGKILL. - cli lifecycle T9b, T15 and N6. - bus chief-of-staff T9a, the stops events. - bus lifecycle-verbs T15, the verb refusal. - agents-browser, the alive flag. - T4's hand-added registry entries now carry `startedAt`, which N6 needs. - **Mutants.** These are in `scratch/honker-t/b8/runb8.sh`. Each anchor is matched exactly once, and each has a named kill test. - M2, M3, M16, M22 and M27. - M49: no action.allowed for the end. - M50: kill takes only `r.runs`. - M51: fire goes through terminate. - M52: drop deliver's status re-check. - M53: keep only alive. - M54: terminate gets fire's path. - M55: a second SIGTERM on join. - M55-deadline: a fresh grace on join. - N6-widen and D7. - S6-done: lifecycleDone drops the stops. M52, M55 and M55-deadline mutate 191's `processing-control/src/runtimes.mjs`. That file isn't in the 194 manifest, but 194's tests are what kill those mutants. Run list on Sage's CLEAR: `runb8.sh `. - It runs 20 files alone on the committed candidate: Webby's 12 (webby-0098), then cli lifecycle, launcher, launch-limit-checks, launch-limits, launch-routes, launch-account and thread-launch, and webui browser. - If any file is red it stops. Otherwise it runs the mutants, restores each one and checks the blob, then sweeps for leftovers. - After that come the full gate with Docker reachable, and P3 (sage-2261). Skips are named from `^skip` and `# SKIP`. ### b8 r2: sage-2289, unknown reads as alive Sage's answer to the open question: an `unknown` stop counts as alive. A false "still alive" costs one look, but a false "killed" hides a live run. - **Launcher.** fireStop maps stopRun's `unknown` to `{ outcome: "alive", reason: "unknown" }`. The fire-stop log keeps the raw `unknown`. - **Bus.** lifecycleDone accepts an optional `reason` on a stop, and only `unknown` on an alive stop. The lifecycle.end.alive event carries it as `reason` in its body. The event has no detail field, so the reason goes in as a body key, and it adds no new event type. `#aliveAfterFire` and the Console's flag treat the run like any other alive run. - **Tests.** - cli lifecycle, the T9a no-runtime-record test: a managed bound run stops, and an unmanaged registry run gets no signal. The test checks lifecycle.end.alive with reason unknown, no killed event, `instances.coder.alive` = ['coder-ghost'], and fire-stop outcomes unknown and stopped. - bus chief-of-staff T9a (bus): a reason on a killed stop is refused, a reason other than unknown is refused, and the unknown reason lands in the event body. - **Mutants.** M56 maps unknown to killed, and the new cli test kills it. M53's anchor follows the new return. ### b8 r3 docs: Webby N1-N5 (webby-0100, sage-2290) Doc-only. `git diff 899e30a80 -- packages` is empty, so the runs carry over (webby-0101). - **N1, correction to the b8 N6 clock caveat.** The b8 note said the order holds unless the clock steps back. That's wrong about the direction. The bus's `#now` (`broker.mjs:524`) is `Math.max(Date.now(), clock + 1)`, so an end's `at` can only run ahead of the launcher's clock. A run spawned just after the verb can fall inside the pass, which signals one run too many. A run spawned before the verb can't fall outside it. - **N2.** A DEFERRED line covers the 64-stop cap (sage-2290: doc-only, no r3). Sage's ruling says a done over the cap "kills nothing". That isn't right: fireStop signals every run before the done, so the killed runs are dead. The refused done then writes no killed or alive events, the end request stays taken, and a run left alive gets no flag. The DEFERRED line says this, and names the follow-up's test. - **N3, follow-up.** `mosaic instances` in the CLI doesn't print `alive`. D7 asks only for the Console. - **N4, follow-up.** The flag on a no-record (unknown) run clears only on that run's session.ended. If the process exits without a bus binding, it never binds and never ends, so the flag stays until a human verb or a new end clears it. The follow-up row designs that clear. - **N5, no change.** The "still running after fire" wording for reason unknown stays. - **Correction to N4 above.** "Until a human verb or a new end clears it" is wrong. `#aliveAfterFire` checks only `session.ended`, and today nothing else clears the flag. It stays until the follow-up row adds a clear. ### b15 (a), product change: the constructor's argument checks move ahead of the store reads (sage-2501) - **Why.** The b14 runb8 (`run-20261022T052118Z`) went red on chief-of-staff T8b (`:864/868`). It builds `new Broker({ store: null, roleFile: 'roles' })` and expects `roleFile must be a function`. Row 191 (4bdc7e0e8) reads `dirname(store.directory)` in the constructor before the roleFile check, so a null store throws its own TypeError first. The order was already like this at b8, but the last 194 run before 191 landed was b7, and runb8 had never run on 191's base until today. - **Change.** The harnessOn (row 65), testerOn and roleFile type checks move up, unchanged, to just after the invalid-business check and before `this.#businesses`, `this.#store` and every store read. Each descriptive comment stays with its assignment, and one new comment line goes above the moved checks. Nothing else moves. 191's lines are byte-identical. invalid-business stays first because it was already ahead of every store read. Diff: broker.mjs +4/-3. - **Pins.** chief-of-staff.test `:868` (roleFile, null store) now gets its message. routes-verb.test `:89` (harnessOn 'yes', a real store) keeps its message. ### b15 (b'), test fixture: T15 drops the reviewer stand-in's registry entry (sage-2503, sage-2507) - **Why.** The other b14 red was cli lifecycle T15 (`:744`). The debug run printed the launch log tail: after the resume the coder launch closes `launch-failed` with why `running session has no exact provider/model; reconcile before launching` (business/src/launch.mjs usageCounts, row 122). The reviewer stand-in has exited, but its registry entry is still there, and that entry carries no provider/model. The launcher's exit handler only removes the runs it started itself. - **Not (b).** I didn't give standIn's entry provider/model (honker-0118). prepare()'s limits are `{ sonnet: 1 }`, and the reviewer and coder both run claude-sonnet-5-5, so the stale entry would count 1 against a limit of 1 and the same launch would refuse on the limit instead. Sage ruled (b') in place of (b) (sage-2507). standIn is unchanged. - **Change.** T15, after `assert.equal(await x.exited, 143)`, adds `s.launcher.registry.remove("acme", "reviewer-run")` with N6's comment. That's the convention at N6 `:763-764`, and it's test-only. standIn's `t.after` removal still runs, and a second remove of a missing run is the same no-op it already is in N6.