diff --git a/agents/honker/work/queue-194/DESIGN.md b/agents/honker/work/queue-194/DESIGN.md index 874729505..ba12d5ec5 100644 --- a/agents/honker/work/queue-194/DESIGN.md +++ b/agents/honker/work/queue-194/DESIGN.md @@ -12,15 +12,20 @@ Related designs read: row 190 `agents/darkwing/work/queue-190/DESIGN.md` (r2, af - One verb family on the CLI, `mosaic business instance BUSINESS INSTANCE`, and one Console section. Create and delete change the business file. Launch, terminate and regenerate act on runs. -- Every human verb goes through the bus with the human proof. Launch and regenerate go through the - launcher's one `start()`, so `--no-launch`, the limits, `launch.revoke` and row 191's switch all apply. +- Every human verb goes through the bus with the human proof. Launch, regenerate and delete are requests the + launcher takes once and carries out under `serialize`. Launches go through the one `start()`, so + `--no-launch`, the limits, `launch.revoke` and row 191's switch all apply. - The chief hires (adds and lists an instance) and fires (ends its runs and delists it). It never deletes. - The human's veto is two lists in the business file: `launch.keep` (instances the chief can't touch) - and `launch.noHire` (definitions the chief can't hire). Only a human verb writes them. -- The broker reloads a business from its file after each committed organization write. Create, delete, - hire, fire and veto changes then count without a bus restart. Hand edits still need one. -- No bus schema change and no migration. One business-file schema change (D2.3) and one role file change - (D2.1, Q1) are called out for review. + `role.hire` and `role.fire` are new hard waits, so Jason resolves each one (Mos, mos-1017). The routed + form waits on a PRD 1.7 draft in this packet. +- A hire floor in code: the chief can never hire a definition that holds launch, end, organization or hire + authority, or `review.verdict`. +- The per-agent forbid is two lists in the business file that only the human writes: `launch.keep` + (instances the chief can't fire, replace or end) and `launch.noHire` (definitions the chief can't hire). +- The broker reloads a business from its file after each committed write. Create, delete, hire, fire and + veto changes then count without a bus restart. Hand edits still need one, except `bus.tester`. +- No bus schema change and no migration. Named for review: two new vocabulary actions (hard waits), the + role-file `hireable` key, `"hireable": []` in `roles/chief-of-staff.json`, and three business-file keys. ## What exists today (checked at 53bb59462) @@ -51,156 +56,287 @@ actor. The CLI usage joins `INSTANCES_USAGE` (`cli/src/instances.mjs:9`). |---|---|---|---| | V1 create | `business instance add B I --definition DEF [--vars FILE] [--launch] --revision REV` | `instances.set` op `add` | as today, with no restart (D3) | | V2 launch | `business instance launch B I [--wait SECONDS]` | `instance.launch` | one run, through the launcher | -| V3 terminate | `business instance terminate B I [--reason TEXT]` | `instance.end` | ends every run of I | +| V3 terminate | `business instance terminate B I [--reason TEXT]` | `instance.end` | ends every run of I and cancels I's open requests | | V4 regenerate | `business instance regenerate B I [--wait SECONDS]` | `instance.regenerate` | V3, then V2 once the run has exited | -| V5 delete | `business instance delete B I --revision REV` | `instances.set` op `delete` | removes I from the business file | +| V5 delete | `business instance delete B I --revision REV [--wait SECONDS]` | `instances.set` op `delete` | a request. The launcher removes I from the business file and renames its workspace | | V6 veto | `business veto B (keep\|unkeep) I` and `business veto B (nohire\|hire) DEF`, each `--revision REV` | `veto.set` | edits `launch.keep` and `launch.noHire` | `mosaic business instances B` (read) gains, per instance: `running` (run ids from the bus claims), `kept` and `retired`. The top level gains `noHire`. `mosaic stop RUN` stays as it is. It is the same-uid operator tool, and V3 is the human verb. +### D1.1 Requests (V2, V4, V5) + +The broker never starts a process or touches a workspace. Launch, regenerate and delete are requests the +launcher carries out, as E9 already does for the human's threads. A request has three states, open, taken +and closed, all recorded as `action.allowed` events with reserved action names: + +| Event | Written by | Body | +|---|---|---| +| `lifecycle.launch`, `lifecycle.regenerate`, `lifecycle.delete` | the broker, at the verb | `{ request, via }`, subject I, actor the human or `tester`. A `human.input` event goes with it, as `launch.revoke` does | +| `lifecycle.taken` | host op `lifecycleTake` | `{ request }` | +| `lifecycle.done` | host op `lifecycleDone` | `{ request, run? , refusal? }` | +| `lifecycle.cancelled` | V3, for each open request of I | `{ request, by }` | + +1. The launcher's new `lifecyclePass` runs on the same tick as `threadPass` and `landPass`. It lists open + requests with host op `lifecycleRequests`. +2. For each one, under `serialize`, it calls `lifecycleTake(request)`. That op runs in one store + transaction. It re-checks, in order: the request is still open (not taken, cancelled or closed), it is + less than 60 s old (else it closes as `request-expired`), the business isn't revoked (`launch_state`, + `launch-revoked`), I isn't retired, row 191's switch is off (`processing-halted`) for launch and + regenerate, and for a tester request `bus.tester` is true in the file (D4.1). Any failure closes the + request with that code. Otherwise it writes `lifecycle.taken` and answers go. +3. Only after go does the pass act: `check(I, { human: true })` then `start()` for a launch, the V5 steps + for a delete. +4. The pass then calls `lifecycleDone`. A taken request never runs again, whatever happens after the take. + If `lifecycleDone` throws, the request stays taken with no done event. The CLI reports that as + `outcome-unknown` with the request id, and the launcher log names the run. +5. The CLI polls a read verb, `instance.request`, until the request closes or `--wait` runs out (default + 30 s). A timeout exits 3 with the request id. An untaken request can still be taken within its 60 s. + +So the human's `launch.revoke` stops every untaken request, not only new ones (B4), and a bus restart +can't start a request older than 60 s. + ### V2 launch -1. The broker checks that I exists, isn't a thread instance (`not-thread-launch`: threads start on a - message), isn't retired, and that the business isn't revoked (`launch-revoked`). The human's own brake - applies to the human until they restore it. When row 191 is present, a halted instance refuses - `processing-halted` here, so no launch waits for a resume. -2. The broker writes one `action.allowed` event, action `lifecycle.launch`, subject I, actor the human, - body `{ request, via }`, where `request` is a generated id. It also writes a `human.input` event, as - `launch.revoke` does. The answer is `{ request }`. -3. A new launcher pass, `lifecyclePass`, runs on the same tick as `threadPass` and `landPass`. It reads the - open requests through a new host op, `lifecycleRequests`. For each request it runs, under `serialize`, - `check(I, { human: true })` and then `start({ instance: I, launchedBy: b.human })`. - `human: true` lets the human launch an instance that isn't in `launch.instances`. The listing limits - the chief, not the human. `launch.by` launches with `pm: true`, as `--pm` does today. Every other step - of `check()` and `start()` is unchanged. -4. The pass reports the outcome with host op `lifecycleDone`. That op writes `action.allowed`, action - `lifecycle.done`, citing the request and naming the run or the refusal code. A request with a done - event is closed. -5. A request still open 60 s after it was written closes as `request-expired` and never launches. A bus - restart therefore can't start runs a human asked for before it. -6. The CLI polls a read verb, `instance.request`, until the request closes or `--wait` runs out (default - 30 s). It prints the run or the refusal. A timeout exits 3 with the request id, and the launch may still - happen within the 60 s window. - -The broker never starts a process. The launcher keeps that, as E9 already does for the human's threads. +1. At the verb the broker checks that I exists, isn't a thread instance (`not-thread-launch`: threads start + on a message), isn't retired, and that the business isn't revoked and, with row 191, isn't halted. These + repeat at the take (D1.1 item 2). +2. After the take, `check(I, { human: true })` and `start({ instance: I, launchedBy })`, where + `launchedBy` is the business human or `tester`. `human: true` lets the human launch an instance that + isn't in `launch.instances`. The listing limits the chief, not the human. `launch.by` still goes through + the `pm: true` branch (`launcher.mjs:235`), as `--pm` does today. Every other step of `check()` and + `start()` is unchanged. ### V3 terminate -1. The broker revokes every claim I holds (`#claimEnd(s, h, 'revoke', null, reason, s.human)`). A thread - instance has all its thread claims revoked, which is exactly `assistant.stop`. No claim held answers - `not-held`. -2. It writes `lifecycle.end` with the request id and the runs. The launcher pass sends SIGTERM to each - run's runner after checking its start time, as the idle stop does (`launcher.mjs:526`). The existing - exit handler records the end, so the launch journal and `session.ended` are unchanged. -3. Terminate is allowed while row 191's switch is on. It starts nothing. +1. The broker revokes every claim I holds (`#claimEnd(s, h, 'revoke', null, reason, actor)`). A thread + instance has all its thread claims revoked, which is exactly `assistant.stop`. +2. In the same transaction it writes `lifecycle.cancelled` for every open (untaken) request of I, so no + launch the human asked for before the terminate starts after it (B5). +3. It writes `lifecycle.end` with the runs. The launcher pass sends SIGTERM to each run's runner after + checking its start time, as the idle stop does (`launcher.mjs:526`). It also signals any run of I in the + launcher's registry that has spawned but not yet bound, which holds no claim yet. The existing exit + handler records the end, so the launch journal and `session.ended` are unchanged. +4. Nothing held, nothing open and nothing in the registry answers `not-held`. +5. Terminate is allowed while row 191's switch is on. It starts nothing. + +A taken launch request is past cancelling. Its run reaches the registry under `serialize`, and the next +terminate ends it. The CLI's terminate output says so when a taken request exists. + +There is no SIGKILL escalation in this row. A run that ignores SIGTERM keeps running. `mosaic stop` and the +limits are the operator's tools for it (N6). ### V4 regenerate -A fresh session with the same identity and role. It is V3 then V2 under one request. The pass waits for -the old run's exit handler, then runs `check()` and `start()`. The workspace stays, as it does across every -launch today. The session is new because every non-thread launch gets a new run dir (`seat/src/session.mjs:107`, newRun). +A fresh session with the same identity and role. It is V3 then V2 under one request. The pass takes the +request, waits for the old run's exit handler, then runs `check()` and `start()`. The workspace stays, as +it does across every launch today. The session is new because every non-thread launch gets a new run dir +(`seat/src/session.mjs:107`, newRun). If the old run hasn't exited by the request's 60 s mark, the request +closes as `regenerate-timeout`, nothing starts, and the old run is still running (N6). A thread instance (the assistant) refuses `regenerate-thread`. Its context lives in the thread's session directory, and clearing that is T1 item 7 (clear context). This row doesn't take it. ### V5 delete -`instances.set` gains op `delete`. The writer refuses, in this order: +`instances.set` gains op `delete`. At the verb the broker and the writer refuse, in this order: | Refusal | When | |---|---| +| `tester-refused` | the caller is the tester (B6) | | `organization-definition-refused` | I is END_PROTECTED | | `organization-instance-referenced` | I is `launch.by` or an arbiter. The message names the reference | | `organization-instance-kept` | I is in `launch.keep`. The human runs `veto unkeep` first. Delete never clears a veto silently | -| `organization-credentials-out-of-scope` | I holds credentials or a tracker bot. That stays a hand edit until the credentials row (T1 item 6, not yet filed) | +| `organization-credentials-out-of-scope` | I holds credentials or a tracker bot. That stays a hand edit until the credentials row (row 199) | | `instance-running` | the broker sees a claim for I. The human terminates first | +| `instance-starting` | an open or taken request for I has no done event | | BusinessError | I is the last name in `launch.instances` (E1). The message says so | -Otherwise the writer removes `roles.I` and the references that exist only for I: its `launch.instances` -entry, its `launch.limits.tokens.instances` entry and its per-instance model entry. It adds I to a new -top-level list, `retired`. The build lists every place `validateBusinessDocument` reads a role instance -name and puts each one in one of the two groups above. A reference the build finds that this table doesn't -name stops the build and goes back to Webby. +The checks pass with no write, and the broker records a `lifecycle.delete` request. After the take, the +launcher pass, under `serialize` so no `start()` of I runs alongside it: + +1. Re-checks the registry for I (`instance-running`) and re-runs the writer's checks against the file as it + stands. The `--revision` the human gave must still match, else `revision-conflict`. +2. Commits the file through the organization writer. It removes `roles.I` and the references that exist + only for I: its `launch.instances` entry, its `launch.limits.tokens.instances` entry and its + per-instance model entry. It adds I to a new top-level list, `retired`. The build lists every place + `validateBusinessDocument` reads a role instance name and puts each one in one of the two groups (refuse + or remove). A reference the table doesn't name stops the build and goes back to Webby. +3. Renames `workspaces//I/` to `workspaces//.retired/I-/`, mode 0700. No workspace is + no rename. +4. Calls `lifecycleDone` with the transaction. + +The file goes first. Once it is committed, `check()` refuses I as unknown, so no launch can open the old +workspace even if step 3 fails. A failed rename closes the request with `workspace-rename-failed`, the +transaction and the path left behind. A second `delete` of a retired name skips steps 1 and 2 and retries +step 3 only. The file and evidence are already right, so this is the whole recovery. What stays: - Run evidence (`launches///`, `launches/.jsonl`) and bus rows are untouched. They are write-once. -- The workspace `workspaces//I/` is renamed to `workspaces//.retired/I-/`, mode 0700. - This row deletes no file. A prune with a receipt is a later act. -- `retired` stops a later create from reusing the name. Decisions bind to the role instance and outlive - restarts (`broker.mjs:783-788`). A new `coder` would otherwise inherit an approval raised by the old one. - The tradeoff is a name that can't come back without a hand edit. I chose that over voiding decisions, - which would touch a second store. +- The workspace, under `.retired`. This row deletes no file. A prune with a receipt is a later act. +- `retired` stops a later create or hire from reusing the name. Decisions bind to the role instance and + outlive restarts (`broker.mjs:783-788`). A new `coder` would otherwise inherit an approval raised by the + old one. The tradeoff is a name that can't come back without a hand edit. I chose that over voiding + decisions, which would touch a second store. ### V6 veto `veto.set` args: `{ revision, op: "keep" | "unkeep" | "nohire" | "hire", name }`. It goes through the organization writer, so it is logged with actor and diff in `changes.jsonl`. `keep` needs a declared, -non-END_PROTECTED instance. `nohire` needs a definition in `rolesDir`. A no-op answers +non-END_PROTECTED instance. `nohire` needs a name that matches the id pattern. A no-op answers `organization-no-change`. It is human-only and isn't in `TESTER_ORGANIZATION_VERBS`. ## D2. Chief hire and fire -### D2.1 What the chief may do +### D2.1 Authority (Mos, mos-1015 and mos-1017, through Sage, sage-1362 and sage-1365) + +This row adds `role.hire` and `role.fire` to `ACTIONS` (`vocabulary.mjs:11`). Under PRD REQ-WAIT-3 a new +action is a hard wait until a PRD version says it isn't (`vocabulary.mjs:52-57`), so both ship as hard +waits. Jason resolves each one. There is no PRD change and no grant in a role file: `role.mjs:42` refuses +a withinRole that lists a hard wait, and REQ-WAIT-2 forbids it. + +The flow in this row: + +1. The chief raises a `role.hire` or `role.fire` decision whose body carries the arguments (business, + name, definition, vars, or name and reason). A hard wait goes to the human only. No arbiter resolves it. +2. Jason resolves it in the Console or the CLI. +3. The chief calls `instance.hire` or `instance.fire` citing the decision. The broker checks it with + `#consumeAuthority(s, 'role.hire', { decision, target })`, as the `role.revoke` decision branch does, + and the arguments must equal the decision body. Every check below runs again at this call, so a veto + set after the resolve still refuses. + +The routed form, where the chief hires and fires without a decision each time, needs a PRD version that +moves both actions to `ROUTED`. My draft is `agents/honker/work/queue-194/PRD-1.7-DRAFT.md`. It quotes +TARGET.md line 66 and carries Mos's bounds. Jason approves it as a `prd.approve` decision in the Console, +and Sage opens that decision. When it lands, a later commit adds the two actions to both `ROUTED` lists +(`vocabulary.mjs`, `broker.mjs:43-69`) and to `roles/chief-of-staff.json` withinRole. The broker then +takes the `reason` form instead of a decision. The verbs and every check below stay the same. This row +builds and tests the hard-wait form only. It writes the `hireable` key and its floor now (D2.3), because +both forms use them. + +### D2.2 What the chief may do | Act | Bus verb | Rule | |---|---|---| | launch | `launch` (host) | as today: listed instances only | -| end a run | `role.revoke` + reason | as today, and refused `instance-kept` when the target is kept | -| hire | `instance.hire { name, definition, vars? }` | adds and lists a new instance. Refused when the definition is in `noHire`, END_PROTECTED or credentialed, or the name exists or is retired | -| fire | `instance.fire { name, reason }` | ends I's runs and removes I from `launch.instances`. Refused when I is kept, END_PROTECTED, `launch.by` or the last listed instance | +| end a run | `role.revoke` + reason | as today, and refused `instance-kept` when the target is kept (D2.4) | +| hire | `instance.hire { name, definition, vars?, decision }` | adds and lists an instance. Refusals below | +| fire | `instance.fire { name, reason, decision }` | ends I's runs, cancels I's open requests, and removes I from `launch.instances`. Refusals below | -The chief never deletes. A fired instance stays in `roles` and can be listed again by a hire of the same -name (that is a `change` with `launch: true`, so no new name is spent) or by the human. Delete stays a -human act because it retires the name. +Only the business's chief, the instance named by `launch.by`, may call either verb. Any other caller +refuses `role-not-chief`, even with a resolved decision, as `#classification` already limits +`role.launch`. The broker reads `hireable` from that chief's own role file. Both act only in the chief's +own business. Neither verb takes a business argument. The broker uses the +cap's business, and a decision raised in another business refuses `decision-mismatch`. Row 191's switch +refuses both while halted (D6). Every hire and fire writes `changes.jsonl` through the organization writer +with actor the chief's instance and run, and the decision id. -Hire and fire need authority the chief doesn't have. That is Q1. The two options: +Hire refuses, in order: -- O1, recommended. Add `role.hire` and `role.fire` to `ACTIONS` (`vocabulary.mjs:11`) and to - `roles/chief-of-staff.json` withinRole. The broker checks them with `#consumeAuthority`, as it does - `role.end`. This is a role file change (AGENTS.md invariant 7) and widens what a chief can do. It is what - Case 1 asks for ("hire/fire authority for agents as needed; unless user forbids per agent"). - Sage's bounded form (sage-1355): the role file also carries an allowlist of definitions the chief may - hire, for example `hireable: ["security-reviewer"]`, checked before `noHire`. Hires stay uncredentialed. - Every hire and fire writes `changes.jsonl` with actor the chief's instance and run. Row 191's switch - refuses both (D6). The allowlist key is part of the role file change. -- O2. No role file change. The chief raises a gated `role.hire` or `role.fire` decision, and the human - resolves it. This follows the brief's "verbs, not new role authority" literally, but Case 1's authority - then sits with the human. +1. `definition-not-hireable` when the definition fails the floor (D2.3), whatever any list says. +2. `definition-not-hireable` when the definition isn't in the chief role file's `hireable` list, or is in + the business's `launch.noHire`. +3. `organization-definition-refused` for END_PROTECTED, `organization-credentials-out-of-scope` for a + credentialed definition (until the credentials row, row 199), `invalid-request` for version 1. +4. `organization-instance-retired` when the name is retired. +5. When the name exists: a fired instance (in `roles`, not listed) is listed again only if `definition` + equals its current one and `vars` is absent. Any other definition or vars refuses + `organization-instance-exists`, and so does a listed name. A re-hire never moves an instance's + decisions to a different role (B3). -The brief says this row adds verbs, not authority. Case 1 says the chief hires and fires. Both can't hold, -so the choice is Mos's (Sage sent it with an O1 recommendation, sage-1355). The rest of this design works under either option. Only the broker's -authority check and the role file differ. +Hire's `vars` may hold only the keys the chief can already set on an instance through `organization.set` +(the model key, `broker.mjs:1266-1269`) and narrowing `limits.*` keys. Anything else, such as `harness` +or a route, refuses `invalid-request`. The human sets those (N3). -### D2.2 The veto +Fire refuses `instance-kept` for a kept instance, `organization-definition-refused` for END_PROTECTED, +`organization-instance-referenced` for `launch.by`, and a BusinessError for the last listed instance. A +fired instance stays in `roles`. The chief never deletes. Delete stays a human act because it retires the +name. -- `launch.keep`: instance names. The broker refuses every chief act aimed at a kept instance except launch: - `role.end` (E6 shows end then launch is a replace), fire, and a hire that would change it. The chief - can't stop a runaway kept instance. The human and the limits still can. I chose the wider rule because - "can't fire or replace it" is otherwise open to end-then-relaunch. -- `launch.noHire`: definition names. `instance.hire` refuses `definition-not-hireable`. -- Default, per Jason's wording "unless user forbids per agent": everything the structural rules allow is - hireable and nothing is kept. The structural rules are uncredentialed, not END_PROTECTED, and a valid - version 2 role. +### D2.3 The hire floor (B2, sage-1365 item 4) -### D2.3 Where it lives and how the broker enforces it +A definition the chief can never hire, whatever `hireable` says. It fails the floor when its withinRole or +crossRole holds any of: -The veto lives in the business file, under `launch`, beside the membership it limits: -`launch.keep: [instance]`, `launch.noHire: [definition]`, and top-level `retired: [instance]`. That file is -the human's. The chief reaches it only through `organization.set`, whose keys (`ORGANIZATION_KEYS`, -`broker.mjs:145`) exclude `launch` and `retired`. A test pins that (T13). +| Floor action | Why | +|---|---| +| `role.launch` | launch authority | +| `role.end`, `role.revoke` | end authority | +| `organization.change` | organization authority | +| `role.hire`, `role.fire` | hire authority | +| `review.verdict` | a chief can't pick its own reviewers, so author ≠ reviewer holds | + +`roles/chief-of-staff.json` fails it (role.launch, role.end, organization.change), so the chief never hires +a chief. Reviewer and security-reviewer fail it (review.verdict). A credentialed definition is refused by +D2.2 item 3 until row 199, and the floor list is the place a credentialed action joins if row 199 needs it. + +The floor is a constant in `business/src/vocabulary.mjs`, `HIRE_FLOOR`. It is checked twice: + +- At load. `role.mjs` validates the chief role file's `hireable` list: distinct definition names, each + present in `rolesDir`, version 2, and not failing the floor. A list naming one refuses the role file + (`hireable names , which holds `). The check reads only the named file's authority lists, + never its `hireable`, so a list can't recurse. `hireable` is an optional key on any role file. This is a + role schema change, named for review. +- At hire, in the broker, against the definition as loaded then. A role file edited after load is caught + here. + +`hireable` in `roles/chief-of-staff.json` starts empty in this row (`[]`). Filling it is a reviewed role +file commit, as AGENTS.md invariant 7 requires. With an empty list the chief can hire nothing, even with +Jason's resolve, until a reviewed commit names a kind. I chose that over a default list because the +choice of role kinds is a policy call, not this row's. + +### D2.4 The per-agent forbid and the veto (sage-1365 item 3) + +TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as needed; unless user forbids +per agent". The forbid is two lists in the business file that only the human writes: + +- `launch.keep`: instance names. The chief can't fire, replace or end a kept instance. The broker refuses + `instance-kept` for every chief act aimed at it except launch: + - `instance.fire`, even with a resolved `role.fire` decision. Mos's limit is that Jason's veto applies + to every hire and fire, so the human unkeeps before a fire. + - a hire of that name. + - `role.revoke` in the reason branch (`broker.mjs:1607-1613`). End then launch is a replace (E6). + - an `organization.set` write of that instance's model key (B7). A model change is a replace at the next + launch. + + The `role.revoke` decision branch (`broker.mjs:1614-1615`) still ends a kept run. It ends a run, it + doesn't fire or replace, and it cites a decision the human resolved (Webby's Q2 ruling). The human's V3 + and the limits also still end a kept run. +- `launch.noHire`: definition names the chief can't hire into this business (D2.2 item 2). + +Why the chief can't change the forbid: + +- Only `veto.set` writes the lists. It is human-only (`#human(s)` then the D4 hook) and refuses the tester. +- `organization.set`, the chief's one write verb, reaches only `ORGANIZATION_KEYS` (`broker.mjs:145`), + which exclude `launch` and `retired`. +- `instances.set` is human or tester only (`broker.mjs:1248-1298`). +- Hire and fire carry no field that writes either list. + +T18 tests each of those paths from a chief session and checks the business file bytes are unchanged. + +END_PROTECTED instances can't be kept (N7). That is intended: Q169 lets the chief end the assistant's +runs, and the assistant's own stop is `assistant.stop`. Default, per "unless user forbids": nothing is +kept and nothing is on `noHire`. + +### D2.5 Where it lives and how the broker enforces it + +The forbid lives in the business file, under `launch`, beside the membership it limits: +`launch.keep: [instance]`, `launch.noHire: [definition]`, and top-level `retired: [instance]`. `hireable` +lives in the chief's role file, because it bounds the role, not one business. `checkLaunch` (`business.mjs:118`) gains the two keys. Each list is distinct. `keep` names declared, -non-protected instances. `noHire` names definitions present in `rolesDir`. `retired` names none of -`roles`. This is the business-file schema change for review. Old files without the keys load unchanged. An -old bus loader rejects a file that has them (`rejectUnknownKeys`), so a rollback past this row needs the -keys removed first. The activation plan has to say so (R4). +non-protected instances. `noHire` entries need only the id pattern (N2). A stale deny entry is harmless, +and a reviewed commit that removes a role file must not stop every business that lists it. `retired` +names none of `roles`. This is the business-file schema change for review. Old files without the keys load +unchanged. An old bus loader rejects a file that has them (`rejectUnknownKeys`), so a rollback past this +row needs the keys removed first. The activation plan has to say so (R4). The broker enforces from its live copy of the business, which D3 keeps current. The checks sit in the -`role.revoke` reason branch and in the two new chief verbs, before any write. +`role.revoke` reason branch, in `organization.set`'s model write and in the two chief verbs, before any +write. ## D3. Live changes @@ -210,23 +346,29 @@ add and redefine. 1. `Broker.reloadBusiness(id, document)` swaps in one business's validated document. It is synchronous, so no request sees half of it. It refuses an unknown id and a document whose `human` changed (that is row 193's ground). -2. The runtime's organization callback (`runtime.mjs:121-133`) calls it after every committed write by - `instances.set`, `veto.set`, `instance.hire`, `instance.fire`, `limits.set` and `organization.set`. It - loads from the file through the same loader `bootConfig` uses (`cli/src/config.mjs:88-105`), so the - broker holds exactly what a restart would give it. Then `limits.refresh(id)` runs, as `plugins.set` does - now. +2. The runtime's organization callback (`runtime.mjs:121-133`) calls it in one place, after every verb + that commits a write, whatever the verb (N1). Today that is `limits.set`, `instances.set`, `routes.set` + and `plugins.set`, and after this row `veto.set`, `instance.hire`, `instance.fire` and the launcher's + delete through the same writer. It loads from the file through the loader `bootConfig` uses + (`cli/src/config.mjs:88-105`), so the broker holds exactly what a restart would give it. Then + `limits.refresh(id)` runs, as `plugins.set` does now. 3. Other startup consumers of `businesses` in `startBroker` (`runtime.mjs:75-81`): credential references, usage, the web services and the tasks adapter's bots. A create or hire adds no credentials, no tracker bot and no web grant, because credentialed definitions are refused. The build checks each consumer against that claim. Any change that would need one of them still answers `restart: true` and doesn't reload. 4. A definition change on a running instance refuses `instance-running`. A live cap must not change - authority under its run. This is new: today the change waits for the restart. + authority under its run. This is new: today the change waits for the restart. Existing tests that pin + `restart: true` change with it (N4): `business/tests/instance-settings.test.mjs`, + `bus/tests/instances-verb.test.mjs`, `cli/tests/instances.test.mjs:76-94` (the `RESTART_NEEDED` text and + the restart line) and `webui/tests/instances.test.mjs:25, 71, 74, 140-145`. The build names each + changed assertion in its manifest. 5. The chief's `launch` tool (E5) takes `instance` as a string with the id pattern instead of the start-time enum. Its description says the host refuses an unlisted instance. `check()` already refuses one (`launcher.mjs:235`). Without this, the chief can't launch its own hire until its session restarts. `tools.mjs` may carry hash pins in tests. The build greps for them (R5). -6. Hand edits of the business file still need a restart. Nothing watches the file. +6. Hand edits of the business file still need a restart. Nothing watches the file. The one exception is + `bus.tester` (D4.1), read from the file at each tester call. After this, `restart` is false for create, delete, hire, fire and veto changes, and true only under item 3. @@ -240,18 +382,26 @@ use it. Row 193 replaces the hook body and nothing else. ### D4.1 The tester (Sage, sage-1355) -`instance.launch` and `instance.end` are also tester verbs. Every other new write verb refuses -`tester-refused`. The tester's cap is bound to one business, and only when that business sets `bus.tester` -to true (`broker.mjs:542-546`). That stays Jason's switch. Two checks run on each tester call, before any -effect: +What the tester may do after this row: + +| Verb | Tester | +|---|---| +| `instance.launch`, `instance.end` | yes, new | +| `instances.set` ops `add`, `change` | yes, as today (row 175, `broker.mjs:103`, `serve.mjs:537`) | +| `instances.set` op `delete` | no, `tester-refused` in the broker (B6) | +| `instance.regenerate`, `veto.set` | no, `tester-refused` | + +The tester's cap is bound to one business, and only when that business sets `bus.tester` to true +(`broker.mjs:542-546`). That stays Jason's switch, and no bus verb writes it. Two checks run on each tester +lifecycle call, before any effect, and again at the take (D1.1): 1. The request's business is the cap's business. Any other refuses `tester-refused`. -2. `bus.tester` is still exactly true in the broker's live copy. After D3 a running bus can see the switch - turned off, and the cap must stop working then, not at the next restart. Off refuses `tester-off`. +2. `bus.tester` is exactly true in the business file as it stands, read through `settings.current(id)` as + `harnessOn` does (`runtime.mjs:136-142`), not from the broker's copy (B8). An unreadable file refuses + `tester-off`. So a hand edit turning testing off counts at the next tester call, without a restart. -The request and done events record actor `tester`. The tester gets no regenerate, delete, create or veto. -Its Q173 tests of the Agents section then cover launch and terminate on a scratch business, and the rest -through fixtures. +The request and done events record actor `tester`. The tester's Q173 tests of the Agents section then cover +launch and terminate on a scratch business, and the rest through fixtures. ## D5. One stack instance @@ -266,9 +416,10 @@ the other byte-identical (T11). - Launch and regenerate end in `start()`, which row 191 gates (its D3 inventory, `launcher.mjs:329-457`). The new pass adds no second start path. - The broker refuses `instance.launch` and `instance.regenerate` with `processing-halted` while the switch - is on (V2 step 1), so no request is waiting when a human resumes. + is on (V2 step 1), and the take checks again (D1.1), so no request is waiting when a human resumes. - `instance.hire` and `instance.fire` are agent work verbs, so row 191's default refuses them while halted. - The build adds them to row 191's work table, not its read list. + The build adds them to row 191's work table, not its read list. A hire Jason resolved before a halt + still refuses while halted, and the chief calls it again after the resume. - Terminate, create, delete and veto start nothing and stay allowed. - If row 191 hasn't landed when this row builds, the build chains on its candidate (Q177). The two halted checks are written against its reader and tested with its fixture. @@ -289,6 +440,8 @@ signed-in user, exact Origin, same-origin Fetch Metadata, JSON within 16 KiB, st through the human transport, and `outcome-unknown` on an unexpected answer. Launch and regenerate show the request, then poll `GET /api/agent/request?id=` until it closes. +`agents.js` joins the static file list (`serve.mjs:28`) and `index.html` (N5). + When `/api/config` says `send` is false, every action button shows as unavailable with the text "Agent actions are unavailable here. Use mosaic business instance from the instance CLI." This is the live state until Q175 phase 2. Every write test uses a fixture server and fixture writers, as rows 165 and 191 do. No @@ -303,23 +456,28 @@ Every test uses scratch roots under `TMPDIR` and the existing fixtures (`busines | Test | Shows | File | |---|---|---| | T1 | V1 create on a running bus: a launch of the new instance binds with no restart, and the answer says `restart: false` | cli `instances.test.mjs` | -| T2 | V2: a human launch of an unlisted instance runs. A thread instance, a retired one and a revoked business refuse. A request older than 60 s never launches. A refused `check()` closes the request with its code | cli `lifecycle.test.mjs` (new) | +| T2 | V2: a human launch of an unlisted instance runs. `launch.by` goes through the `pm` branch (N8). A thread instance, a retired one and a revoked business refuse at the verb. A request older than 60 s never launches. A refused `check()` closes the request with its code | cli `lifecycle.test.mjs` (new) | +| T2a | Take: a `launch.revoke` between the request and the pass closes it `launch-revoked` with no process. A `lifecycleDone` that throws once leaves the request taken, and the next tick starts nothing. A taken request is never taken twice | same | | T3 | V2 with `--no-launch` and with each limit refusal: no process, request closed with the code | same | -| T4 | V3: claims revoked, runner SIGTERMed after the start-time check, one end record. A mismatched start time sends no signal | same | -| T5 | V4: the new run starts only after the old exit, with a new run dir and the same workspace. The assistant refuses `regenerate-thread` | same | -| T6 | V5: each refusal in the V5 table, in order. A clean delete leaves the evidence bytes unchanged, renames the workspace, adds `retired`, and a create of the same name refuses | business `instance-settings.test.mjs` | -| T7 | V6: each op, the no-op refusal, and a `changes.jsonl` entry with actor `human` | business | -| T8 | Chief hire: added and listed, then launched by the same chief session (D3 item 5). `noHire`, credentialed, END_PROTECTED, existing and retired each refuse. Under O1, a definition outside `hireable` refuses `definition-not-hireable` | bus `chief-of-staff.test.mjs` | -| T9 | Chief fire: runs end, delisted, still in `roles`. Kept, `launch.by` and last-listed refuse | same | -| T10 | Keep: chief `role.end`, fire and hire aimed at a kept instance refuse `instance-kept`. The human's V3 on it works | same | -| T11 | Two scratch stack instances with the same business id: V2, V3 and V5 in A leave B's root and config byte-identical | cli `lifecycle.test.mjs` | -| T12 | Agent and reader sessions refuse every human verb before any effect. The tester refuses every one except launch and terminate. The D4 hook is called with the verb | bus `instances-verb.test.mjs` | -| T12a | Tester launch and terminate work on the cap's business with `bus.tester` true, record actor `tester`, refuse `tester-refused` for a second business on the same bus, and refuse `tester-off` after a live write turns `bus.tester` off | bus `instances-verb.test.mjs` | +| T4 | V3: claims revoked, runner SIGTERMed after the start-time check, one end record. A mismatched start time sends no signal. An open request is cancelled and never starts. A spawned, unbound run in the registry is signalled | same | +| T5 | V4: the new run starts only after the old exit, with a new run dir and the same workspace. A run that ignores SIGTERM closes the request `regenerate-timeout` with nothing started. The assistant refuses `regenerate-thread` | same | +| T6 | V5: each refusal in the V5 table, in order, including `instance-starting` with an open request. A clean delete leaves the evidence bytes unchanged, commits the file before the rename, adds `retired`, and a create or hire of the same name refuses. A failed rename closes `workspace-rename-failed`, `check()` refuses the name, and a second delete renames only | business `instance-settings.test.mjs` and cli `lifecycle.test.mjs` | +| T7 | V6: each op, the no-op refusal, an unknown `noHire` name accepted, and a `changes.jsonl` entry with actor `human` | business | +| T8 | Chief hire (hard-wait form): no decision refuses. A decision raised by the chief goes to the human only, no arbiter. After the resolve, the hire adds and lists the instance, logs actor and decision, and the same chief session launches it (D3 item 5). Arguments that differ from the decision body refuse. A non-chief caller with a resolved decision refuses `role-not-chief`. Outside `hireable`, in `noHire`, credentialed, END_PROTECTED, retired, and extra vars each refuse | bus `chief-of-staff.test.mjs` | +| T8a | Re-hire (B3): a fired name with the same definition and no vars is listed again. Another definition or vars refuses `organization-instance-exists` | same | +| T8b | Hire floor (D2.3): one case per floor action. A fixture definition, clean when the chief's role file loads and listed in `hireable`, is rewritten to hold only that action before the hire. The hire refuses `definition-not-hireable`. chief-of-staff and reviewer refuse | same | +| T8c | Floor at load: a role file whose `hireable` names a definition holding each floor action refuses at `loadRole`, one case per action. A clean list loads. A missing, version 1 or duplicate name refuses | business `role.test.mjs` | +| T9 | Chief fire: runs end, open requests cancelled, delisted, still in `roles`. Kept (even with a resolved decision), END_PROTECTED, `launch.by` and last-listed refuse | bus `chief-of-staff.test.mjs` | +| T10 | Keep: chief `role.revoke` with a reason, fire, hire of that name and an `organization.set` model write aimed at a kept instance each refuse `instance-kept`. A `role.revoke` citing a human-resolved decision ends the kept run (Q2). The human's V3 works | same | +| T11 | Two scratch stack instances with the same business id: V2, V3 and V5 in A leave B's root and config byte-identical. A chief of business A can't hire into business B on the same bus | cli `lifecycle.test.mjs`, bus | +| T12 | Agent and reader sessions refuse every human verb before any effect. The tester gets exactly the D4.1 table, including `tester-refused` for `instances.set` op `delete` (B6). The D4 hook is called with the verb | bus `instances-verb.test.mjs` | +| T12a | Tester launch and terminate work on the cap's business with `bus.tester` true, record actor `tester`, and refuse `tester-refused` for a second business on the same bus. A file edit setting `bus.tester` to false, with no restart, refuses the next call and the take `tester-off`. An unreadable file refuses `tester-off` | bus `instances-verb.test.mjs` | | T13 | `organization.set` with `launch`, `retired`, `keep` or `noHire` keys refuses | bus | -| T14 | Live reload: after each write the broker answers from the new document. A definition change on a running instance refuses `instance-running` | bus | -| T15 | Kill switch (with row 191's fixture): launch and regenerate refuse `processing-halted`, hire and fire refuse, terminate works | bus and cli | -| T16 | Console: each route's boundary refusals, a fixture write per action, `outcome-unknown` on a bad shape, unavailable buttons when `send` is false, request polling | webui `agents.test.mjs` (new) | +| T14 | Live reload: after each committed verb, `plugins.set` and `routes.set` included, the broker answers from the new document. A definition change on a running instance refuses `instance-running` | bus | +| T15 | Kill switch (with row 191's fixture): launch and regenerate refuse `processing-halted` at the verb and at the take, hire and fire refuse, terminate works | bus and cli | +| T16 | Console: each route's boundary refusals, a fixture write per action, `outcome-unknown` on a bad shape, unavailable buttons when `send` is false, request polling, and `agents.js` served from the static list | webui `agents.test.mjs` (new) | | T17 | Business file: the new keys validate, each bad entry refuses, and a file without them loads byte for byte as before | business `business.test.mjs` | +| T18 | The forbid can't be changed by the chief (D2.4): from a chief session, `veto.set`, `organization.set` with each list key, `instances.set`, and hire and fire with extra fields all refuse, and the business file bytes are unchanged | bus `chief-of-staff.test.mjs` | ### Mutants @@ -330,15 +488,15 @@ sha256 checked after each one. |---|---|---| | M1 | `instance.launch` skips `#human` | T12 | | M2 | the pass calls `check()` without `human: true` | T2 | -| M3 | the pass starts a request older than 60 s | T2 | -| M4 | the revoked-business check is skipped for the human | T2 | +| M3 | the take accepts a request older than 60 s | T2 | +| M4 | the take skips the `launch_state` check | T2a | | M5 | terminate signals without the start-time check | T4 | | M6 | regenerate starts before the old exit | T5 | | M7 | delete skips the running check | T6 | | M8 | delete skips `retired` | T6 | | M9 | delete clears `keep` instead of refusing | T6 | | M10 | delete removes the workspace instead of renaming it | T6 | -| M11 | the `keep` check is removed from `role.end` | T10 | +| M11 | the `keep` check is removed from the `role.revoke` reason branch | T10 | | M12 | `noHire` is not read by hire | T8 | | M13 | fire deletes the instance from `roles` | T9 | | M14 | `reloadBusiness` isn't called after `instances.set` | T1, T14 | @@ -347,43 +505,87 @@ sha256 checked after each one. | M17 | the Console route skips the Origin check | T16 | | M18 | `veto.set` joins `TESTER_ORGANIZATION_VERBS` | T12 | | M19 | the tester's business scope check is dropped | T12a | -| M20 | the tester's live `bus.tester` check is dropped | T12a | -| M21 | under O1, hire skips the role file's `hireable` list | T8 | +| M20 | the tester's `bus.tester` file read is dropped | T12a | +| M21 | hire skips the role file's `hireable` list | T8 | +| M22 | the take doesn't write `lifecycle.taken`, so a request can be taken twice | T2a | +| M23 | terminate doesn't cancel open requests | T4 | +| M24 | delete skips the `instance-starting` check | T6 | +| M25 | delete renames before it commits the file | T6 | +| M26 | the tester's `instances.set` op `delete` refusal is dropped | T12 | +| M27 | the `keep` check is dropped from `organization.set`'s model write | T10 | +| M28 | the `keep` check is added to the `role.revoke` decision branch | T10 | +| M29 | a re-hire accepts a different definition | T8a | +| M30 to M36 | each floor action removed from `HIRE_FLOOR` in the broker's hire check, one mutant per action: `role.launch`, `role.end`, `role.revoke`, `organization.change`, `role.hire`, `role.fire`, `review.verdict` | T8b | +| M37 to M43 | the same seven, removed from the load check in `role.mjs` only | T8c | +| M44 | fire skips `instance-kept` when a decision is cited | T9 | +| M45 | `instance.hire` skips the `launch.by` caller check | T8 | +| M46 | `veto.set` skips `#human` | T18 | ## Scope and boundaries -- In the row: the verbs and passes above in `business` (instance-settings, business validation), `bus` - (broker verbs, reload, runtime callback), `cli` (instances command, launcher pass, host ops), `harness` - (the launch tool's schema, and the hire and fire tools under O1), `webui` (routes and the Agents - section), the tests, and the package READMEs. `docs/TOOLS.md` text goes to Sage. -- Role file change: `roles/chief-of-staff.json` under O1 only. Business file schema: D2.3. Both are named - for review. -- Not in the row: credentialed hires and deletes (the credentials row), clear context for the assistant (T1 item 7), - human roles (row 193), and any live activation or Console start. +- In the row: the verbs and passes above in `business` (vocabulary, role validation, instance-settings, + business validation), `bus` (broker verbs, reload, runtime callback), `cli` (instances command, launcher + pass, host ops), `harness` (the launch tool's schema, and the hire and fire tools), `webui` (routes and + the Agents section), the tests, the package READMEs and the PRD draft in this packet. `docs/TOOLS.md` + text goes to Sage. +- Named for review: + - Vocabulary: `role.hire` and `role.fire` join `ACTIONS` as hard waits, under REQ-WAIT-3. The commit + message names them as hard waits, as REQ-WAIT-3 requires. + - Role schema: the optional `hireable` key, validated in `role.mjs` (D2.3). + - Role file: `roles/chief-of-staff.json` gains `"hireable": []`. No authority change. + - Business file schema: `launch.keep`, `launch.noHire`, `retired` (D2.5). +- Not in the row: the routed form (after PRD 1.7), filling `hireable`, credentialed hires and deletes (row + 199), clear context for the assistant (T1 item 7), human roles (row 193), SIGKILL escalation, and any + live activation or Console start. ## Risks -- R1. Case 1's working roles (coder, reviewer, pm, cto) all declare credentials (E11). Neither the human - verb nor the chief can create or delete them until the credentials row (T1 item 6, not yet filed) binds - credentials. Until then this row covers - uncredentialed roles only. That is the largest gap against Case 1, and it isn't this row's to close. +- R1. Case 1's working roles (coder, reviewer, pm, cto) all declare credentials (E11), and reviewer is + below the hire floor anyway. Neither the human verb nor the chief can create or delete credentialed + roles until the credentials row (row 199). That is the largest gap against Case 1, and it isn't this + row's to close. - R2. The human proof is path-bound and same-uid (P19 333). Any same-uid process that runs the tree's `human-cli.mjs` can launch and delete. Row 190b's separate uid and row 193 narrow it. This row adds no check of its own. - R3. The 60 s request window means a slow `check()` (checkouts) can still start a run up to that point after the CLI gave up. The CLI says so on timeout. -- R4. Rollback past this row needs `keep`, `noHire` and `retired` removed from each business file, or the - old loader refuses it. The activation plan states this. -- R5. `tools.mjs`, `serve.mjs` and `broker.mjs` may carry sha256 pins in other packages' tests. The build - greps the tests for every changed path plus a 64-hex value before in-review. +- R4. Rollback past this row needs `keep`, `noHire` and `retired` removed from each business file, and + `hireable` from the role file, or the old loaders refuse them. The activation plan states this. +- R5. `tools.mjs`, `serve.mjs`, `broker.mjs`, `vocabulary.mjs` and `role.mjs` may carry sha256 pins in + other packages' tests. The build greps the tests for every changed path plus a 64-hex value before + in-review, without a `head`. +- R6. With `hireable` empty, the chief can hire nothing until a reviewed role file commit names a kind. + Case 1's "as needed" waits on that commit and on PRD 1.7. -## Questions +## Questions and rulings -- Q1, for Mos (sent by Sage, sage-1355): chief hire and fire authority, O1 (role file change) or O2 (gated - decisions)? D2.1. Sage recommends O1 bounded: uncredentialed roles only, an allowlist of role kinds in - the role file, every hire and fire logged, and the kill switch covering both. Both options stay in this - design until Mos rules. -- Q2, for Webby: keep blocks the chief's `role.end` on that instance, not only fire and replace (D2.2). Is - the wider rule right? -- Q3, settled by Sage (sage-1355): launch and terminate are tester verbs scoped to the tester's own +- Q1, ruled by Mos (mos-1015, mos-1017, through sage-1365): `role.hire` and `role.fire` ship as hard + waits Jason resolves. The routed form needs PRD 1.7, drafted in `PRD-1.7-DRAFT.md`. D2.1. +- Q2, ruled by Webby (webby-0003): keep blocks the chief's `role.revoke` reason branch, not its decision + branch. D2.4. +- Q3, ruled by Sage (sage-1355): launch and terminate are tester verbs scoped to the tester's own business, behind `bus.tester`. D4.1. +- Q4, for Sage: should `hireable` start empty (my choice, D2.3), or should this row name a first kind for + review? + +## r2 changes against webby-0003 + +| Finding | Where | +|---|---| +| B1 | D2.1: hard waits under REQ-WAIT-3, no role grant. The routed form is PRD 1.7 (packet draft) | +| B2 | D2.3: the floor from sage-1365 item 4, checked at load and at hire, T8b, T8c, M30 to M43 | +| B3 | D2.2 hire item 5: a re-hire keeps the definition, T8a, M29 | +| B4 | D1.1: one `lifecycleTake` transaction re-checks and writes `lifecycle.taken`, T2a, M4, M22 | +| B5 | V3 cancels open requests and signals unbound runs. V5 refuses `instance-starting`, runs under `serialize`, commits the file before the rename, and states the recovery. T4, T6, M23 to M25 | +| B6 | V5 table and D4.1: the tester's delete refuses, T12, M26 | +| B7 | D2.4: keep covers `organization.set`'s model write, T10, M27 | +| B8 | D4.1: `bus.tester` is read from the file at each call and at the take, T12a, M20 | +| Q2 | D2.4, T10, M28 | +| N1 | D3 item 2: one reload after every committed write | +| N2 | D2.5: `noHire` takes any id-pattern name | +| N3 | D2.2: hire's vars limited to the model key and narrowing `limits.*` | +| N4 | D3 item 4 names the tests that change | +| N5 | D7 and T16: `agents.js` joins `serve.mjs:28` and `index.html` | +| N6 | V3 and V4: no SIGKILL, `regenerate-timeout` | +| N7 | D2.4: intended, Q169 | +| N8 | V2 item 2 and T2 | diff --git a/agents/honker/work/queue-194/PRD-1.7-DRAFT.md b/agents/honker/work/queue-194/PRD-1.7-DRAFT.md new file mode 100644 index 000000000..c40c4d13c --- /dev/null +++ b/agents/honker/work/queue-194/PRD-1.7-DRAFT.md @@ -0,0 +1,73 @@ +# PRD 1.7 draft: the chief of staff's hire and fire authority + +Draft by Honker for row 194 (#1692), under Mos's ruling mos-1017 (through Sage, sage-1365). Not approved. +It changes nothing until Jason approves it as a `prd.approve` decision in the Console. Sage opens that +decision and sends Mos its id. Base: `docs/prd/mosaic-stack.md` version 1.6 at 53bb59462. + +## What it changes + +Row 194 adds `role.hire` and `role.fire` to the vocabulary. Under REQ-WAIT-3 they are hard waits, so today +Jason resolves every hire and every fire. This version routes them for the chief of staff within fixed +bounds, so the chief hires and fires as needed and the user's per-agent forbid still holds. + +When this version lands, a reviewed commit: + +1. adds `role.hire` and `role.fire` to `ROUTED` in `packages/business/src/vocabulary.mjs` and in + `packages/bus/src/broker.mjs`. +2. adds both to `roles/chief-of-staff.json` withinRole. +3. switches the broker's check on `instance.hire` and `instance.fire` from a cited decision to the + `reason` form, as `role.end` works for REQ-ROLE-2. + +Every other check stays as row 194 builds it. Filling `hireable` stays a separate reviewed role file +commit. + +## Text for the PRD + +Status line, appended to the current paragraph: + +> Version 1.7 routes the chief of staff's hire and fire actions, approved by Jason in decision +> . + +Source, a new bullet above "Version 1.6 source": + +> - Version 1.7 source, TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as +> needed; unless user forbids per agent" + +Under "Added in version 1.2", after REQ-COS-2: + +> Added in version 1.7: +> +> - **REQ-COS-3.** The chief of staff hires and fires role instances without a decision each time. Hiring +> adds a role instance to the organization file and lists it for launch. Firing ends the instance's +> runs and removes it from the launch list. It stays in the organization file. Only the user deletes +> an instance. These actions are `role.hire` and `role.fire`. They leave the hard waits and route +> under this requirement, within these bounds: +> - The chief acts only in its own organization. +> - The chief hires only role definitions that need no credentials, until a later version adds +> credentialed roles. +> - The chief hires only the role kinds its role file lists. Changing that list is a reviewed commit. +> - The chief never hires a role that holds launch, end, organization or hire authority, or the +> review verdict. So the chief never hires a chief and never picks its own reviewers, and the +> author is never the reviewer. This holds whatever the list says. +> - The user can forbid per agent. The user marks an instance so the chief can't fire, replace or end +> it, and marks role kinds the chief can't hire. Only the user changes these marks, through the +> WebUI or the CLI. +> - The kill switch stops every hire and fire while it is on. +> - Every hire and fire is logged with the chief's instance, its run and the reason. + +REQ-WAIT-3, appended: + +> `role.hire` and `role.fire` were added as hard waits and route under REQ-COS-3 since version 1.7. + +## Notes for the reviewer and for Mos + +- REQ-COS-1 (1.2) already says the chief "hires and terminates role instances". REQ-COS-3 gives that + sentence its actions and bounds. It doesn't widen REQ-COS-1. +- "Replace" in the forbid covers ending a kept instance's run with a reason and changing its model. A + user-resolved decision still ends a kept run (row 194 D2.4). +- The kill switch is row 191's. If row 191 hasn't landed when this version is approved, the bound still + holds through row 194's chain on its candidate. +- The credentials bound names no row number, so the text doesn't go stale when row 199 lands. A later + version lifts it. +- This draft follows the PRD's own style: plain words, no semicolons in new sentences, and the status + paragraph never edited in place except to append.