honker: row 194 design r2 (webby-0003 B1-B8, Q2, N1-N8; Mos mos-1017 hard waits, hire floor, per-agent forbid) and PRD 1.7 draft

This commit is contained in:
jason.woltje committed 2026-10-20 14:19:49 -05:00
1 parent 14a6f5f9d5
commit c0a35f47a0
2 files changed
+434 -159

No files matched your search

+361 -159
View File
@@ -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 <verb> BUSINESS INSTANCE`, and one Console section. - One verb family on the CLI, `mosaic business instance <verb> BUSINESS INSTANCE`, and one Console section.
Create and delete change the business file. Launch, terminate and regenerate act on runs. 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 - Every human verb goes through the bus with the human proof. Launch, regenerate and delete are requests the
launcher's one `start()`, so `--no-launch`, the limits, `launch.revoke` and row 191's switch all apply. 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 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) `role.hire` and `role.fire` are new hard waits, so Jason resolves each one (Mos, mos-1017). The routed
and `launch.noHire` (definitions the chief can't hire). Only a human verb writes them. form waits on a PRD 1.7 draft in this packet.
- The broker reloads a business from its file after each committed organization write. Create, delete, - A hire floor in code: the chief can never hire a definition that holds launch, end, organization or hire
hire, fire and veto changes then count without a bus restart. Hand edits still need one. authority, or `review.verdict`.
- No bus schema change and no migration. One business-file schema change (D2.3) and one role file change - The per-agent forbid is two lists in the business file that only the human writes: `launch.keep`
(D2.1, Q1) are called out for review. (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) ## 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) | | 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 | | 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 | | 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` | | 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` `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 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. 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 ### V2 launch
1. The broker checks that I exists, isn't a thread instance (`not-thread-launch`: threads start on a 1. At the verb the broker checks that I exists, isn't a thread instance (`not-thread-launch`: threads start
message), isn't retired, and that the business isn't revoked (`launch-revoked`). The human's own brake on a message), isn't retired, and that the business isn't revoked and, with row 191, isn't halted. These
applies to the human until they restore it. When row 191 is present, a halted instance refuses repeat at the take (D1.1 item 2).
`processing-halted` here, so no launch waits for a resume. 2. After the take, `check(I, { human: true })` and `start({ instance: I, launchedBy })`, where
2. The broker writes one `action.allowed` event, action `lifecycle.launch`, subject I, actor the human, `launchedBy` is the business human or `tester`. `human: true` lets the human launch an instance that
body `{ request, via }`, where `request` is a generated id. It also writes a `human.input` event, as isn't in `launch.instances`. The listing limits the chief, not the human. `launch.by` still goes through
`launch.revoke` does. The answer is `{ request }`. the `pm: true` branch (`launcher.mjs:235`), as `--pm` does today. Every other step of `check()` and
3. A new launcher pass, `lifecyclePass`, runs on the same tick as `threadPass` and `landPass`. It reads the `start()` is unchanged.
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.
### V3 terminate ### V3 terminate
1. The broker revokes every claim I holds (`#claimEnd(s, h, 'revoke', null, reason, s.human)`). A thread 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`. No claim held answers instance has all its thread claims revoked, which is exactly `assistant.stop`.
`not-held`. 2. In the same transaction it writes `lifecycle.cancelled` for every open (untaken) request of I, so no
2. It writes `lifecycle.end` with the request id and the runs. The launcher pass sends SIGTERM to each launch the human asked for before the terminate starts after it (B5).
run's runner after checking its start time, as the idle stop does (`launcher.mjs:526`). The existing 3. It writes `lifecycle.end` with the runs. The launcher pass sends SIGTERM to each run's runner after
exit handler records the end, so the launch journal and `session.ended` are unchanged. checking its start time, as the idle stop does (`launcher.mjs:526`). It also signals any run of I in the
3. Terminate is allowed while row 191's switch is on. It starts nothing. 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 ### V4 regenerate
A fresh session with the same identity and role. It is V3 then V2 under one request. The pass waits for A fresh session with the same identity and role. It is V3 then V2 under one request. The pass takes the
the old run's exit handler, then runs `check()` and `start()`. The workspace stays, as it does across every request, waits for the old run's exit handler, then runs `check()` and `start()`. The workspace stays, as
launch today. The session is new because every non-thread launch gets a new run dir (`seat/src/session.mjs:107`, newRun). 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 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. directory, and clearing that is T1 item 7 (clear context). This row doesn't take it.
### V5 delete ### 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 | | Refusal | When |
|---|---| |---|---|
| `tester-refused` | the caller is the tester (B6) |
| `organization-definition-refused` | I is END_PROTECTED | | `organization-definition-refused` | I is END_PROTECTED |
| `organization-instance-referenced` | I is `launch.by` or an arbiter. The message names the reference | | `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-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-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 | | 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` The checks pass with no write, and the broker records a `lifecycle.delete` request. After the take, the
entry, its `launch.limits.tokens.instances` entry and its per-instance model entry. It adds I to a new launcher pass, under `serialize` so no `start()` of I runs alongside it:
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 1. Re-checks the registry for I (`instance-running`) and re-runs the writer's checks against the file as it
name stops the build and goes back to Webby. 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/<b>/I/` to `workspaces/<b>/.retired/I-<transaction>/`, 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: What stays:
- Run evidence (`launches/<b>/<run>/`, `launches/<b>.jsonl`) and bus rows are untouched. They are - Run evidence (`launches/<b>/<run>/`, `launches/<b>.jsonl`) and bus rows are untouched. They are
write-once. write-once.
- The workspace `workspaces/<b>/I/` is renamed to `workspaces/<b>/.retired/I-<transaction>/`, mode 0700. - The workspace, under `.retired`. This row deletes no file. A prune with a receipt is a later act.
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
- `retired` stops a later create from reusing the name. Decisions bind to the role instance and outlive outlive restarts (`broker.mjs:783-788`). A new `coder` would otherwise inherit an approval raised by the
restarts (`broker.mjs:783-788`). A new `coder` would otherwise inherit an approval raised by the old one. old one. The tradeoff is a name that can't come back without a hand edit. I chose that over voiding
The tradeoff is a name that can't come back without a hand edit. I chose that over voiding decisions, decisions, which would touch a second store.
which would touch a second store.
### V6 veto ### V6 veto
`veto.set` args: `{ revision, op: "keep" | "unkeep" | "nohire" | "hire", name }`. It goes through the `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, 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`. `organization-no-change`. It is human-only and isn't in `TESTER_ORGANIZATION_VERBS`.
## D2. Chief hire and fire ## 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 | | Act | Bus verb | Rule |
|---|---|---| |---|---|---|
| launch | `launch` (host) | as today: listed instances only | | 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 | | 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? }` | adds and lists a new instance. Refused when the definition is in `noHire`, END_PROTECTED or credentialed, or the name exists or is retired | | hire | `instance.hire { name, definition, vars?, decision }` | adds and lists an instance. Refusals below |
| 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 | | 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 Only the business's chief, the instance named by `launch.by`, may call either verb. Any other caller
name (that is a `change` with `launch: true`, so no new name is spent) or by the human. Delete stays a refuses `role-not-chief`, even with a resolved decision, as `#classification` already limits
human act because it retires the name. `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 1. `definition-not-hireable` when the definition fails the floor (D2.3), whatever any list says.
`roles/chief-of-staff.json` withinRole. The broker checks them with `#consumeAuthority`, as it does 2. `definition-not-hireable` when the definition isn't in the chief role file's `hireable` list, or is in
`role.end`. This is a role file change (AGENTS.md invariant 7) and widens what a chief can do. It is what the business's `launch.noHire`.
Case 1 asks for ("hire/fire authority for agents as needed; unless user forbids per agent"). 3. `organization-definition-refused` for END_PROTECTED, `organization-credentials-out-of-scope` for a
Sage's bounded form (sage-1355): the role file also carries an allowlist of definitions the chief may credentialed definition (until the credentials row, row 199), `invalid-request` for version 1.
hire, for example `hireable: ["security-reviewer"]`, checked before `noHire`. Hires stay uncredentialed. 4. `organization-instance-retired` when the name is retired.
Every hire and fire writes `changes.jsonl` with actor the chief's instance and run. Row 191's switch 5. When the name exists: a fired instance (in `roles`, not listed) is listed again only if `definition`
refuses both (D6). The allowlist key is part of the role file change. equals its current one and `vars` is absent. Any other definition or vars refuses
- O2. No role file change. The chief raises a gated `role.hire` or `role.fire` decision, and the human `organization-instance-exists`, and so does a listed name. A re-hire never moves an instance's
resolves it. This follows the brief's "verbs, not new role authority" literally, but Case 1's authority decisions to a different role (B3).
then sits with the human.
The brief says this row adds verbs, not authority. Case 1 says the chief hires and fires. Both can't hold, Hire's `vars` may hold only the keys the chief can already set on an instance through `organization.set`
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 (the model key, `broker.mjs:1266-1269`) and narrowing `limits.*` keys. Anything else, such as `harness`
authority check and the role file differ. 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: ### D2.3 The hire floor (B2, sage-1365 item 4)
`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 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: | Floor action | Why |
`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`, | `role.launch` | launch authority |
`broker.mjs:145`) exclude `launch` and `retired`. A test pins that (T13). | `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 <def>, which holds <action>`). 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, `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 non-protected instances. `noHire` entries need only the id pattern (N2). A stale deny entry is harmless,
`roles`. This is the business-file schema change for review. Old files without the keys load unchanged. An and a reviewed commit that removes a role file must not stop every business that lists it. `retired`
old bus loader rejects a file that has them (`rejectUnknownKeys`), so a rollback past this row needs the names none of `roles`. This is the business-file schema change for review. Old files without the keys load
keys removed first. The activation plan has to say so (R4). 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 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 ## 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 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 no request sees half of it. It refuses an unknown id and a document whose `human` changed (that is row
193's ground). 193's ground).
2. The runtime's organization callback (`runtime.mjs:121-133`) calls it after every committed write by 2. The runtime's organization callback (`runtime.mjs:121-133`) calls it in one place, after every verb
`instances.set`, `veto.set`, `instance.hire`, `instance.fire`, `limits.set` and `organization.set`. It that commits a write, whatever the verb (N1). Today that is `limits.set`, `instances.set`, `routes.set`
loads from the file through the same loader `bootConfig` uses (`cli/src/config.mjs:88-105`), so the and `plugins.set`, and after this row `veto.set`, `instance.hire`, `instance.fire` and the launcher's
broker holds exactly what a restart would give it. Then `limits.refresh(id)` runs, as `plugins.set` does delete through the same writer. It loads from the file through the loader `bootConfig` uses
now. (`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, 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 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 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 against that claim. Any change that would need one of them still answers `restart: true` and doesn't
reload. reload.
4. A definition change on a running instance refuses `instance-running`. A live cap must not change 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 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 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. (`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). `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. 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) ### D4.1 The tester (Sage, sage-1355)
`instance.launch` and `instance.end` are also tester verbs. Every other new write verb refuses What the tester may do after this row:
`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 | Verb | Tester |
effect: |---|---|
| `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`. 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 2. `bus.tester` is exactly true in the business file as it stands, read through `settings.current(id)` as
turned off, and the cap must stop working then, not at the next restart. Off refuses `tester-off`. `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. The request and done events record actor `tester`. The tester's Q173 tests of the Agents section then cover
Its Q173 tests of the Agents section then cover launch and terminate on a scratch business, and the rest launch and terminate on a scratch business, and the rest through fixtures.
through fixtures.
## D5. One stack instance ## 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`). - 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 new pass adds no second start path.
- The broker refuses `instance.launch` and `instance.regenerate` with `processing-halted` while the switch - 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. - `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. - 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 - 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. 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 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. 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 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 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 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 | | 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` | | 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 | | 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 | | 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. The assistant refuses `regenerate-thread` | 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. 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` | | 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, and a `changes.jsonl` entry with actor `human` | business | | 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: 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` | | 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` |
| T9 | Chief fire: runs end, delisted, still in `roles`. Kept, `launch.by` and last-listed refuse | same | | 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 |
| T10 | Keep: chief `role.end`, fire and hire aimed at a kept instance refuse `instance-kept`. The human's V3 on it works | 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 |
| 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` | | 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` |
| 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` | | 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` |
| 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` | | 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 | | 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 | | 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`, hire and fire refuse, terminate works | bus and cli | | 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 | webui `agents.test.mjs` (new) | | 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` | | 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 ### Mutants
@@ -330,15 +488,15 @@ sha256 checked after each one.
|---|---|---| |---|---|---|
| M1 | `instance.launch` skips `#human` | T12 | | M1 | `instance.launch` skips `#human` | T12 |
| M2 | the pass calls `check()` without `human: true` | T2 | | M2 | the pass calls `check()` without `human: true` | T2 |
| M3 | the pass starts a request older than 60 s | T2 | | M3 | the take accepts a request older than 60 s | T2 |
| M4 | the revoked-business check is skipped for the human | T2 | | M4 | the take skips the `launch_state` check | T2a |
| M5 | terminate signals without the start-time check | T4 | | M5 | terminate signals without the start-time check | T4 |
| M6 | regenerate starts before the old exit | T5 | | M6 | regenerate starts before the old exit | T5 |
| M7 | delete skips the running check | T6 | | M7 | delete skips the running check | T6 |
| M8 | delete skips `retired` | T6 | | M8 | delete skips `retired` | T6 |
| M9 | delete clears `keep` instead of refusing | T6 | | M9 | delete clears `keep` instead of refusing | T6 |
| M10 | delete removes the workspace instead of renaming it | 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 | | M12 | `noHire` is not read by hire | T8 |
| M13 | fire deletes the instance from `roles` | T9 | | M13 | fire deletes the instance from `roles` | T9 |
| M14 | `reloadBusiness` isn't called after `instances.set` | T1, T14 | | 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 | | M17 | the Console route skips the Origin check | T16 |
| M18 | `veto.set` joins `TESTER_ORGANIZATION_VERBS` | T12 | | M18 | `veto.set` joins `TESTER_ORGANIZATION_VERBS` | T12 |
| M19 | the tester's business scope check is dropped | T12a | | M19 | the tester's business scope check is dropped | T12a |
| M20 | the tester's live `bus.tester` check is dropped | T12a | | M20 | the tester's `bus.tester` file read is dropped | T12a |
| M21 | under O1, hire skips the role file's `hireable` list | T8 | | 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 ## Scope and boundaries
- In the row: the verbs and passes above in `business` (instance-settings, business validation), `bus` - In the row: the verbs and passes above in `business` (vocabulary, role validation, instance-settings,
(broker verbs, reload, runtime callback), `cli` (instances command, launcher pass, host ops), `harness` business validation), `bus` (broker verbs, reload, runtime callback), `cli` (instances command, launcher
(the launch tool's schema, and the hire and fire tools under O1), `webui` (routes and the Agents pass, host ops), `harness` (the launch tool's schema, and the hire and fire tools), `webui` (routes and
section), the tests, and the package READMEs. `docs/TOOLS.md` text goes to Sage. the Agents section), the tests, the package READMEs and the PRD draft in this packet. `docs/TOOLS.md`
- Role file change: `roles/chief-of-staff.json` under O1 only. Business file schema: D2.3. Both are named text goes to Sage.
for review. - Named for review:
- Not in the row: credentialed hires and deletes (the credentials row), clear context for the assistant (T1 item 7), - Vocabulary: `role.hire` and `role.fire` join `ACTIONS` as hard waits, under REQ-WAIT-3. The commit
human roles (row 193), and any live activation or Console start. 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 ## Risks
- R1. Case 1's working roles (coder, reviewer, pm, cto) all declare credentials (E11). Neither the human - R1. Case 1's working roles (coder, reviewer, pm, cto) all declare credentials (E11), and reviewer is
verb nor the chief can create or delete them until the credentials row (T1 item 6, not yet filed) binds below the hire floor anyway. Neither the human verb nor the chief can create or delete credentialed
credentials. Until then this row covers roles until the credentials row (row 199). That is the largest gap against Case 1, and it isn't this
uncredentialed roles only. That is the largest gap against Case 1, and it isn't this row's to close. 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 - 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 `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. check of its own.
- R3. The 60 s request window means a slow `check()` (checkouts) can still start a run up to that point - 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. 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 - R4. Rollback past this row needs `keep`, `noHire` and `retired` removed from each business file, and
old loader refuses it. The activation plan states this. `hireable` from the role file, or the old loaders refuse them. The activation plan states this.
- R5. `tools.mjs`, `serve.mjs` and `broker.mjs` may carry sha256 pins in other packages' tests. The build - R5. `tools.mjs`, `serve.mjs`, `broker.mjs`, `vocabulary.mjs` and `role.mjs` may carry sha256 pins in
greps the tests for every changed path plus a 64-hex value before in-review. 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 - Q1, ruled by Mos (mos-1015, mos-1017, through sage-1365): `role.hire` and `role.fire` ship as hard
decisions)? D2.1. Sage recommends O1 bounded: uncredentialed roles only, an allowlist of role kinds in waits Jason resolves. The routed form needs PRD 1.7, drafted in `PRD-1.7-DRAFT.md`. D2.1.
the role file, every hire and fire logged, and the kill switch covering both. Both options stay in this - Q2, ruled by Webby (webby-0003): keep blocks the chief's `role.revoke` reason branch, not its decision
design until Mos rules. branch. D2.4.
- Q2, for Webby: keep blocks the chief's `role.end` on that instance, not only fire and replace (D2.2). Is - Q3, ruled by Sage (sage-1355): launch and terminate are tester verbs scoped to the tester's own
the wider rule right?
- Q3, settled by Sage (sage-1355): launch and terminate are tester verbs scoped to the tester's own
business, behind `bus.tester`. D4.1. 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 |
@@ -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
> <prd.approve decision id>.
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.