Files

84 KiB

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-<transaction>. 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.<I>.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/<b>/I/ to workspaces/<b>/.retired/I-<tx>/, 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/<business>/ <agent>/. 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 <storeRoot>/<business>/<agent>/<service> (credentials.mjs:71-76), with storeRoot <configDir>/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 <sha>:

  • 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 <b8>, 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..<b8>. 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: <request id> }. 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.<name>.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 <b8 sha>.

  • 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 <this commit> -- 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.