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-firingwhile I has afire.stoppedwith nofire.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 gainkept: false(D1, the read gainskeptper instance). No other field changed. -
packages/business/tests/instance-settings.test.mjs:70(added.restarttrue to false) and:91(redefined.restarttrue 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:setupboots the bus from the file throughbootConfig, as the unit does, instead of a one-role stub. The add test (T1) now expects the "no restart" line and bindssec-1on the running bus. TheRESTART_NEEDEDtext 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, printsRESTART_NEEDEDand doesn't bindsec-1(D3 item 3). -
packages/webui/tests/instances.test.mjs:25: the fake writer's default answer isrestart: false, the usual answer now. The real-broker test boots throughbootConfigand expectsrestart: false(:71) and the log linedone restart=false(:74). The answer-shape rows at:140-145keeprestart: true. Each row tests another field, orrestart: 'yes', so the value is not what they check. -
packages/bus/tests/instances-verb.test.mjsT12 (S2c): the human delete now answers{request}where S2b refusedinvalid-request. The stub writer answersinstances.checkwith the revision, and the calls list gains that one check, actorjason. Nothing else in the test changed. -
packages/bus/tests/tester-writes.test.mjs:19(row 175 pin): TESTER_VERBS gainsinstance.launch,instance.endandinstance.request(D4.1). TESTER_ORGANIZATION_VERBS is unchanged. -
packages/bus/tests/instances-verb.test.mjs:78keepsrestart: 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.mjscovers the reload from a file boot. -
packages/bus/tests/hard-waits.test.mjs:38(S2d): the every-hard-wait loop givesrole.hireandrole.firea target andarguments, 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.hireandrole.fireleft the hard waits, sopackages/bus/tests/hard-waits.test.mjsandpackages/business/tests/hard-waits.test.mjsdrop 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 usedrole.hire, now routed. It usesspend. The check is unchanged. -
packages/business/tests/chief-of-staff.test.mjs(S2e): the chief's pinned authority and the test name gainrole.hireandrole.fire.packages/business/tests/role.test.mjsgains the check that no other shipped role holds either. -
packages/harness/tests/tools.test.mjsandpackages/harness/tests/bundle.test.mjs(S2e): the chief's pinned tool lists gainhireandfire. 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 withargumentsare gone with the key. -
packages/bus/tests/lifecycle-verbs.test.mjsV5 retry (S3a):lifecycleDeleteon 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'sinstancewas 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 nameschief.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 forlaunchat 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.jsbetween/chat.jsand/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 inrole.test.mjs, T17 inbusiness.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
assistantonly. The chief's own instance refuses delete and fire asorganization-instance-referenced(launch.by), not as protected. - The writer refuses an
assistanthireorganization-definition-refusedthroughdefinitionFor, 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 changedhumanand swaps the document in.- The runtime reloads after every committed write, through the chain
bootConfiguses (loadBusiness,resolveInstancewithorganization.system,busBusiness). Thenlimits.refreshruns.instances.setanswersrestart: !live. Any other verb name now throwsinvalid-requestwhere it used to fall through toplugins.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.mjsstartBroker):- 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.currentat each ask. The human proof readshumanfrom the boot copy, andreloadBusinessrefuses a changed human, so the two agree. - Web grants: the broker reads
roles.<I>.webfrom its own document (broker.mjs:1545), so a reload carries the grant. A hireable definition has nowebtool. - Usage loads its
usage-sourceplug-ins fromb.pluginsat start. That was already so before this row (plugins.setnever restarted them). A reload doesn't change it, and it isn't compared.
- Instance-running:
instances.setop change with adefinitionon a declared instance with a claim holder (or thread holders for a thread instance) refusesinstance-runningbefore 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 frombroker.mjs) has one entry per verb and perinstances.setop:instance.launch,instance.end,instance.regenerate,veto.set,instances.set add,instances.set change,instances.set delete. Choice:#mayHuman(s, verb, target)refuseshuman-requiredfor 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.bindHumanalready 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.setskips#human) is equivalent while the generic check runs first: no session withouts.humangets pasthuman-required, and onlybindHumansetss.human, always withvia: 'cli'andoutsideAgent: true. T18 shows the refusal.#humanstays as the design asks. instances.setnow checksopin the broker (add,change,delete, elseinvalid-request). The tester's delete refusestester-refused(B6). Until S2c records the delete request, a human delete refusesinvalid-requestafter the hook. Without that, the writer'ssetopdeletewould commit with no running or starting check and no workspace rename.- Keep: the
role.revokereason branch refusesinstance-keptfor any caller, not only the chief. Only the chief holdsrole.endtoday, so this is the same set. A caller withrole.endadded later is held to the forbid too. Theorganization.setcheck refuses when any key ofargs.instancesis kept, before the writer and before the grant is spent. The human'slimits.setisn't checked: the human owns the forbid. - T13 needed no code.
organization.setandlimits.settake onlyORGANIZATION_KEYS, andlimitsandmodelswrite onlylaunch.limitsandlaunch.models. - Tests:
chief-of-staff.test.mjsT10 (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.mjsT12 (sessions, tester, hook).reload.test.mjsgains veto.set.
S2c notes (lifecycle requests, terminate, delete)
- A request is an
action.allowedevent, actionlifecycle.launch,.regenerate,.deleteor.end, body{action, request, via, by, ...}. The human's also getshuman.input {kind: 'admin'}. The tester's gets none. Steps arelifecycle.taken,lifecycle.done {request, run?, transaction?, refusal?}andlifecycle.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:lifecycleDeletecommits 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 writeslifecycle.donewith the refusal, so the request closes.lifecycle.endnever 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}.lifecycleDeletechecks running and starting again after the take (other requests only), then callsinstances.setop delete with actor = the request'sby. - Retry: a retired name that isn't in roles deletes again only when its last delete closed
workspace-rename-failed. The request carriesretry: true, andlifecycleDeleteanswers{transaction: null, retry: true}with no check and no write. The launcher (S3) retries the rename. lifecycleDonecarriestransaction, 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.requestneeds no tester-file check: a tester reads only requests it made. - The tester switch is read through a
testerOnoption. The runtime readsbus.testerfrom the live file throughsettings.current, so a change applies with no restart. Anything buttrue, or a read error, is off. - Refactor: the organization error mapping moved into
#organizationFail(e, verb), shared by the organization verbs andlifecycleDelete. 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.raisetakesarguments, only forrole.hire({name, definition, vars?}) orrole.fire({name, reason}), and requires it for those.targetmust equalarguments.name. The raise event records it, and the human's decision view shows it underauthorization.arguments. The shape checks are the call's (#hireFireArgs): identifiers, nothumanortester, vars onlymodelandlimits.*, reason 1-500 characters. - The call:
instance.hire {name, definition, vars?, decision}andinstance.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:
#hireableloads the definition and the chief's own role file through the newroleFileoption (the runtime passesloadRole(rolesDir, name)). Version 1, any action off HIRE_ALLOWED (unhireableAction), not on the chief'shireablelist, onnoHireor no file all refuse definition-not-hireable before the writer. Then the writer's hire, then the consume with{transaction}. WithoutroleFilea 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, andfire.stopped {cites, request}. Step B: the writer's fire (already delisted is done). Step C, one transaction: the consume with{reason, transaction}andfire.done {cites, transaction}. - The fire's own events cite the decision under
cites, notdecision, so#spentcounts 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.endagainst a run that may outlive the first one. - W-N1
#firing: afire.stoppedwith nofire.donefor the samecites. It refuses instance-firing at the human and tester verbs (#launchable), at the take of a launch or regenerate (#takeRefusal), and at the chief'srole.launch(#authorityFor). #organizationFailpasses 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.lifecycleViewshowskill: truewhen 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.jsonwithinRole gainsrole.hireandrole.fire.packages/business/src/vocabulary.mjsandpackages/bus/src/broker.mjsmove both from the hard waits to ROUTED.roles/chief-of-staff.mdandroles/pm.mddrop them from the hard-wait list, whichworkflow-roles.test.mjs:113pins to GATED_ONLY. No other shipped role gains either action, androle.test.mjspins that. - The call takes the reason form, as
role.enddoes:instance.hire {name, definition, vars?, reason}andinstance.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.raiseargumentskey 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,#checkAuthoritywith{target: name}, instance-kept.role-not-chiefcomes before the authority check, so a non-chief role refusesrole-not-chiefeven when its file grants the actions. A chief whose file lacks the grant refusesdecision-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
#firingnow goes by order, since the reason form has no decision id: afire.stoppedof the subject with no laterfire.done(seqafter it) for the same subject.fire.stoppedcarries{action, request}andfire.done{action, transaction}. - Harness: the typed
hireandfiretools (verbsinstance.hireandinstance.fire) come forward from S4, because a held action with no typed tool failsuntypedActions. Each tool follows its own grant.roles/chief-of-staff.mdalready 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 inseat/src/sandbox.mjsRUNNER_FILES. The runner imports broker.mjs, so a session died with ERR_MODULE_NOT_FOUND: clilauncher.test.mjsfailed 15 of 29 and seatsandbox.test.mjs4 of 10 at089b92ca3. A bisect showed S2c passes 29 of 29. I had not run those two files for S2d. Fix:unhireableActionmoves 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: seatsandbox.test.mjs, clilauncher.test.mjs, clithread-launch.test.mjsand cligit-socket.test.mjs. - Mutant (sage-1497): broker.mjs imports
unhireableActionfrom 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.mjsS2d block rewritten to the reason form (39 pass), with new tests for the reason bounds,role-not-chiefwith the grant,decision-requiredfor 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.mjsgains 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,
#firingwithout the order and without the subject, the reason unchecked, the writer given the reason, the context without it, no#checkAuthority, norole-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.mjslifecyclePassruns on the tick after the thread and land passes. It lists the open requests (host oplifecycleRequests) and carries each underserialize: the take, the act, thenlifecycleDone. A take that answers no closes nothing more here. A refusal goes in the launch log asrefusedwith the request id. A done that throws leaves the request taken with no done, which S3b's CLI reports asoutcome-unknown. The log line names a run that did start.- Launch:
check()andstart()gainhuman. The human's request may name any declared instance, as V2 asks. The listing limits the chief. launch.by still takes thepmbranch (N8). A tester request launches withlaunchedBy: "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-endentry. The exit handler writes the oneendrecord. 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,lifecycleRequestMsfor tests) it closesregenerate-timeoutand starts nothing. A closing host closes ithost-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-runningagainst the registry first, thenlifecycleDelete(the writer's commit), then the rename ofworkspaces/<b>/I/toworkspaces/<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 closesworkspace-rename-failedwith 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, andactcloses each requestlaunches-disabledbefore any signal, start or rename. The thread and land passes stay off, solauncher.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-fullwith 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-timeoutwith a SIGSTOPped runner,regenerate-threadat 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]andbusiness veto BUSINESS keep|unkeep INSTANCE|nohire|hire DEFINITION --revision REV.--waittakes 0 to 600 s and defaults to 30. Every usage error exits 4 before any bus call. add and set refuse--waitand--reason.- Launch, regenerate and delete print the request id on stderr, then poll
instance.requestevery 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--waitsays so and names the 60 s expiry. Taken with no done isoutcome-unknownand 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
--waittimeout 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
instancesread gainsrunningper 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 markskeptandrunning r1, r2and addsretired:andthe chief may not hire:lines. scripts/mosaicsendsbusiness vetoto packages/cli with instances and instance.- Tests: cli
instances.test.mjs+8: the V6 veto ops and kept-delete refusal, therunningread, launch and regenerate printing the run, a closed refusal with its id (exit 3),--wait 0still open and takenoutcome-unknown, request-cancelled, terminate output andnot-held, delete to its transaction andworkspace-rename-failednaming it, usage errors (exit 4, no call), and the scripts/mosaic veto route. The existing route test gains the veto assertions. clilifecycle.test.mjs+5: the CLI end to end through the pass (launch, regenerate,capacity-fullwith the request id, terminate, delete), T2a (launch-revokedat 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 businessacme: a launch, a terminate and a delete in one leave the other's files byte-identical). buschief-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-startingand the V5 refusal order are bus tests from S2c.check()refusing a retired name is shown through the verb and the take, sincecheck()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
instancesguard 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'sinstanceis 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.mjsgains the T8 end to end: the chief's session hiressec-2(definition chief-information-security-officer, the shipped definitionhireableaccepts: message.send and record.write, no credential) withTOOL hire, a launch refusesno-route, the human sets the route withinstances.setop change, the same session launches it (launchedBy chief),NOT A NAMEandreviewerrefuseinstance-not-listed, thenTOOL fireends its run, leaves itlaunched: falseand a later launch refusesinstance-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, androles/chief-of-staff.mdsays so. Sage ruled (b) in sage-1521: the human route step stays, and the manifest notes say that launch refusesno-routeuntil 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-launchhost runs only the lifecycle pass,lifecycleDeleteanswers the transaction on a retry).roles/chief-of-staff.mdis 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/requestevery 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
sendfalse 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/instancestakes opdeletewith the revision and the instance alone and answers the request./api/agent/launch,/regenerate,/terminateand/api/vetoeach haveinstanceSend's boundary and one call toagentWrite, 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--sendexception.cli.mjs:agentWritereaches onlyinstance.launch,instance.regenerate,instance.end,veto.setandinstance.request(agentCallerthrows 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/instancesis refused ininstanceSendwith 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.aliveevent, 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) andagents-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-190agents/megavolt/work/queue-199/DESIGN.md:295-307) has row 194 lift that refusal for the human's create only. The agent is created withstorereferences 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, andinstances.setanswersrestart: 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 answersrestart: truewith no new runtime code.credentials.issuethen refusescredential-unknown, because the broker's copy of the business doesn't hold the agent (broker.mjs:1443on 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):
instance-settings.mjs: a humanaddof a credentialed definition writesstorereferences with the date fields row 199 requires and no files. Hire keeps the refusal. The human's create isinstances.setopadd(agents.js posts it to /api/instances), so it already answersrestart. The runtime comment atruntime.mjs:115-116("Create and hire add neither") changes to say a human create can.agents.jscreateAgent: the receipt carries the answer's restart text, as bus.js does. Today it says "Created X." and dropsrestart.- T19, bus test: a human create of a credentialed definition answers
restart: true. Issue refusescredential-unknownbefore a restart. After a bus restart over the same directories, issue works and the status readsvalid. Hire of the same definition still refuses. A browser assertion covers the restart text in the Agents receipt. - Mutants: M61 the create refusal stays for the human (T19 fails at create). M62
fixedRoleignorescredentials, so the reload goes live and answersrestart: 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 areplanDelete(instance-settings.mjs:189-193). A delete writes in the launcher's pass, throughsetopdeleteafter the take (comment atinstance-settings.mjs:187-188), so its revoke goes there, afterplanDeleteand 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 runsissueagain 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.instanceswrites nothing (instance-settings.mjs:262) and calls no revoke. - A revoke for an agent the running bus has no entry for (sage-1592):
credential-unknowncounts 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 withcredential-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#knownfirst (credentials.mjs:197-198), and#knownrefusescredential-unknownfrom 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: oncredential-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), andremove(instance)revokes every service and then clears the agent's directory (credentials.mjs:211-218). The broker verbcredentials.revoketakesagent, 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 callsremove(instance). Both count as the revoke for the order, the refusal on failure, T20 and M64-M67. If the landed code differs from86963d281, follow it and note the difference here. - What that does to the sage-1592 check, as of
86963d281:removetakes 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, refusingcredential-filefor 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-failedand 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-unknownas 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
- Item 7 dates, sage-1605 pick (a). The human create writes the UTC create day as
rotateByandexpires(instance-settings.mjs:118-120).checkCredentialRefskips 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 ininstance-settings.test.mjs(validate exits 0 with "not issued"). - 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 inbus/tests/reload.test.mjs(add, restart, issue, then a vars change goes live with restart false). The T14 date-only test stays. - Delete takes all-store references, sage-1605.
deleteRefusalrefuses only when some reference isn'tstore: true(instance-settings.mjs:43-49). A file or env reference refusesorganization-credentials-out-of-scopeat the request, before any take (T20 delete case 2). - Fire on a file or env reference revokes in memory, sage-1605. The result's
credentials.outsideStorenames the services whose reference is outside the store (runtime.mjs:122-124, 141-142). T20 fire case 1 checksoutsideStore: ['vikunja']for coder. - The
credential-revoke-failedstore-dir check stays, sage-1605 (runtime.mjs:132-140). It now runs after delete'sremovetoo.removealready leaves no token, so on delete it only refuses a nameremovewould itself have refused. - 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 andtracker.sync) run on the add path (instance-settings.mjs:88-124,organization-tracker-bot-taken).changewith a tracker refuses (instance-settings.mjs:214). The broker passestrackerthrough (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_KEYSin serve.mjs is unchanged, the Agents and Settings forms still list credentialed definitions as not accepted, and a vikunja add from the Console answersorganization-tracker-requiredwith a reason that names the CLI (webui/src/serve.mjs:332-333,webui/tests/instances.test.mjs). - 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).lifecycleDeletereturnsbot(broker.mjs:1386-1387), the launcher carries it tolifecycleDone(cli/src/launcher.mjs:752-767),lifecycleDonetakesbotchecked against BOT_NAME (broker.mjs:132, 1392-1393), andlifecycleViewshows 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 incli/tests/lifecycle.test.mjs, andagents-browser.test.mjs. - No Vikunja account. The create makes none and the delete removes none. Q188 and the
account.createhard wait aren't touched (sage-1610). - 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'ttester(instance-settings.mjsadd, thedefinitionForcall). The broker reserves that name: no human or role may be calledtester(broker.mjs:234, 237). W2 has a new case, a tester add with a valid tracker, which still refusesorganization-credentials-out-of-scope. - The commit hook is
beforeWrite(instance-settings.mjs:332-360). A first draft named itbeforebeside alet before, a SyntaxError, so I renamed it. - A failed delete revokes first.
Credentials.removerevokes every service it knows before it reads the directory (row 199bus/src/credentials.mjsremove). When the directory then holds a nameremoverefuses, 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. - 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.mjsandbus/src/credentials.mjsare unchanged fromcb5fdb064. The broker's#requestCredential(broker.mjs:1679-1709) is row 199's text as merged in the589ae2c1dsquash. b3's own credential hunks: the runtime revoke callback (runtime.mjs:109-143) and the broker'scredential-revoke-failedmapping (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
fixedRoleignorescredentials(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-unknowncounts 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
checkCredentialRefdrops 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
deleteRefusalrefuses 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.mjspinswebui/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.mjspins the broker's vocabulary import. b2 importedunhireableActionand 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.mjsW2 caught the tester binding credentials through a tracker add. The writer now lifts the credential refusal only when the actor is nottester(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-failedhas a reason in the CLI (cli/src/instances.mjsREASONS) and in the Console (webui/src/serve.mjsAGENT_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/testsfile 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:361runsbeforeWrite(the revoke), then:362runswriter.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 atbroker.mjs:1382andinstance-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 refusesrevision-conflictwith 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 runbeforeWriteinsidewriter.writeafter the:188check; and in either case giverevision-conflictandoutcome-unknowntext 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.mjscommit(): beforebeforeWrite, compare the requested revision withwriter.read(business).revisionand refuserevision-conflicton a difference, without callingbeforeWrite. It runs for every caller (add, set, veto, hire, fire, delete). Every caller already read the file withwriter.read(it validates), so a file that went invalid in between refusesBusinessError, 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 andcommit(). 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.mjsT21. A delete raised on revision R and taken; a credential issue for rev-3 writes the file; the delete refusesrevision-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-conflictsays the file changed since that revision (CLIinstances.mjsREASONS, Consoleserve.mjsINSTANCE_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 throughdefinitionForfor the human's add and change, and hire useddefinitionFor. Hire now refusesdefinition-not-hireablefor any CREATIVE_DEFINITIONS entry, andcheckHireablerefuses a creative name in a role's hireable list at load. - Reload and library projects (C2-04). A library entry has no
rootand its mirror is ensured at start, butfixedAtStart().rootstook every project's root, so a changed mapping reloaded live.rootsnow holds the non-library roots, the Credentials store's set, andlibrariesfixes each library entry whole (reload T14). - Creative delete (sage-2164).
deleteRefusalrefuses END_PROTECTED, creative definitions included, while the listing'sprotectedis false for a creative instance, so the Agents page offered a Delete the server refuses. The listing addsendProtected, the same checkdeleteRefusalruns, 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.setwithlaunch: falsedelists it. - Row 213's companion listing test (
instance-settings.test.mjs:274) didn't have 194'skept; it now pinskeptandendProtected. - DEFERRED C1 (Duckworth N2 on row 196):
testerNoSignInbound the next row touchingserve.mjs. 194 touches it, so the dead parameter is removed fromserve.mjs,webui/src/cli.mjsandsso.test.mjs. - Manifest notes for Webby (sage-2166): C1 is closed by 194 (the three
testerNoSignInlines), anddocs/plans/DEFERRED.mdis 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
646cd493aplus the tree): 312 pass, 2 fail. Both came from combining rows on thec3d88840cbase, so committed646cd493afails both too:instance-settings.test.mjs:283(row 213) expected the writer'srestarttrue 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.mjsT16 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 usesstartTesterSocketunder a temp data root andtesterRequest, astester-socket.test.mjsandsso.test.mjsdo.
- 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.mjstook 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.mjsaddsrestart: !liveto the answer of every instance-writer verb:instances.set,veto.set,instance.hireandinstance.fire(INSTANCE_VERBS). Before, onlyinstances.setcarried 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/vetotreats an answer without a booleanrestartas 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 callsinstance.fireCheckbefore step A. That runs the writer'sfireChecksagainst the file with no write, so the fire refuses before it stops anything.fireruns the samefireChecksat 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-keptwith the file unchanged, the run still held and noinstance-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 theinstance.fireCheckcall, 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})fromprocessing-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, thewebui/src/bus.mjspin. 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.mjsmerged clean. Row 194's two tracker STATUS entries and row 254's six library entries sit in separate lines, with no duplicate key.DEFERRED.mdmerged 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
keptHumanOnlycall 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 addedprocessing = null, and 194 addedtesterOn = null, roleFile = null. The merged signature endsprocessing = null, testerOn = null, roleFile = null. 191's#processingfield and#processingState()sit beside 194's fields.bus/src/runtime.mjs, startBroker. 191 turnedconst broker = new Broker({into an assignment (broker = new Broker({) and addedbroker.reconcileProcessing(); processing.start();after it. 194'sroleFileandtesterOnarguments stay inside that call.cli/src/launcher.mjs, start(). 191 addedadmissionGeneration, and 194 addedhuman = false. The merged parameters endthread = null, human = false, admissionGeneration. 191'sadmission(...)calls and the tick'stry { admission(); } catch { return; }are unchanged.webui/src/cli.mjs, the main startServer call. 191 passesprocessingand 194 passesagentWrite, which gives..., routesWrite, agentWrite, processing. 191'slet processing = null;is kept.webui/src/public/index.html, the head. 191'sprocessing.csslink andprocessing.jsscript come first. 194'sagents.jscomes afterchat.js, beforebus.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 = nullcomes before 191'sprocessing = null. 191's/api/processingroute 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 dropstesterNoSignIn: ssoOn(C1), and 191 addsprocessing: { read: () => call('processing.status') }on the next line. Both are kept.webui/src/serve.mjs, startServer's signature. 194 dropstesterNoSignIn = false, and 191'sprocessing = nullstays.
b6:
bus/src/runtime.mjs, the imports. 194'sINSTANCE_VERBSconstant 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: truenow goes through 191'sstopRun, using the fire's own options: reasonfireand 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 asstops. lifecycleDone writeskilledas lifecycle.end.killed andaliveas lifecycle.end.alive, each with [subject, request, run, pid]. The instances read setsalivefrom 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#launchablerefusesprocessing-haltedat the verb. A halt during a fire's stop joins that stop through stopRun'sstoppingmap. 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'sat. Spawn time comes from the launcher'sspawnedAtmap, orstartedAtwhen the map has no entry. A run spawned after the verb survives, and the next end takes it. Clock caveat:atis 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 intakenspawns 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, notlauncher.test.mjsas 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 throughinstances.<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
^skipand# 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
unknownto{ outcome: "alive", reason: "unknown" }. The fire-stop log keeps the rawunknown. - Bus. lifecycleDone accepts an optional
reasonon a stop, and onlyunknownon an alive stop. The lifecycle.end.alive event carries it asreasonin its body. The event has no detail field, so the reason goes in as a body key, and it adds no new event type.#aliveAfterFireand 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.
- 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,
- 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) isMath.max(Date.now(), clock + 1), so an end'satcan 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 instancesin the CLI doesn't printalive. 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.
#aliveAfterFirechecks onlysession.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 buildsnew Broker({ store: null, roleFile: 'roles' })and expectsroleFile must be a function. Row 191 (4bdc7e0e8) readsdirname(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.#storeand 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 closeslaunch-failedwith whyrunning 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), addss.launcher.registry.remove("acme", "reviewer-run")with N6's comment. That's the convention at N6:763-764, and it's test-only. standIn'st.afterremoval still runs, and a second remove of a missing run is the same no-op it already is in N6.