708 lines
52 KiB
Markdown
708 lines
52 KiB
Markdown
# Row 194 design: agent lifecycle from the Console and the CLI, with a per-agent veto
|
|
|
|
Brief: `docs/plans/2026-10-20_agent-lifecycle.md` (blob a907109409ac). Gitea #1692. MVP T1 item 5 (mos-0974,
|
|
mos-0977, DECISIONS Q176). Owner Honker, reviewer Webby, gate owner Sage. Base: refactor at 53bb59462.
|
|
Branch `honker/row194`.
|
|
|
|
Related designs read: row 190 `agents/darkwing/work/queue-190/DESIGN.md` (r2, after mos-0986), row 191
|
|
`agents/rocko/work/queue-191/DESIGN-r2.md`. Row 193 is still in design. All line numbers below are at
|
|
53bb59462.
|
|
|
|
## Summary
|
|
|
|
- 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.
|
|
- 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.
|
|
`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 allowlist in code: the chief can hire only a definition whose every action is on `HIRE_ALLOWED`
|
|
(message, record, review request, assigned-task updates, and two actions that matter after row 199).
|
|
Launch, end, organization, naming, hire, verdict, merge and every later action stay out.
|
|
- 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)
|
|
|
|
| # | Fact | Where |
|
|
|---|---|---|
|
|
| E1 | Instances are keys of the business file's `roles`. `launch.instances` lists those the chief may launch. The list must be non-empty | `business.mjs:121-132` |
|
|
| E2 | The human adds an instance and changes its definition, vars and listing. No delete. Credentialed definitions are refused | `instance-settings.mjs:1-5, 22-31, 75-156` |
|
|
| E3 | The broker copies the businesses once at start and refuses a role it doesn't know | `broker.mjs:208, 446` |
|
|
| E4 | The chief launches over `launch.sock`. `fromSession` checks identity, then `check()` (which reloads the file), then `authorizeLaunch`, then `start()` | `launcher.mjs:232-327, 624-668` |
|
|
| E5 | The chief's `launch` tool takes an enum of the instances listed when its bundle was built | `harness/src/tools.mjs:116-119` |
|
|
| E6 | The chief ends a run with `role.revoke` plus a reason (`role.end`). Ending then launching again is already a replace | `broker.mjs:1601-1628`, `tools.mjs:167` |
|
|
| E7 | `mosaic stop RUN` is a same-uid SIGTERM. It has no bus call and no human proof | `cli.mjs:447-454`, `seat/src/session.mjs:249-270` |
|
|
| E8 | The Console stops only the assistant (`assistant.stop`) and writes instances through `instances.set` | `serve.mjs:282-289, 315-363, 535` |
|
|
| E9 | The launcher already starts runs for the human on its 2 s tick: threads with pending messages, and merge holders | `launcher.mjs:133, 487-597` |
|
|
| E10 | `launch.revoke` and `launch.restore` are human-only. A revoked business refuses `role.launch` | `broker.mjs:770-774, 2087-2095` |
|
|
| E11 | Only the assistant is END_PROTECTED. Coder, reviewer, pm and cto all declare gitea and vikunja credentials | `vocabulary.mjs:135`, `roles/*.json` |
|
|
|
|
The chief's veto today comes from the human-only membership in E2. The chief can only launch what the human
|
|
listed. It has no way to create an instance, and the human has no way to stop the chief ending a given
|
|
instance's runs.
|
|
|
|
## D1. The verbs
|
|
|
|
All of them are human verbs. Each is a broker verb behind `#human(s)` and then the D4 hook. Each logs its
|
|
actor. The CLI usage joins `INSTANCES_USAGE` (`cli/src/instances.mjs:9`).
|
|
|
|
| Verb | CLI | Bus verb | Effect |
|
|
|---|---|---|---|
|
|
| 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 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 [--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. 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, 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 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`. 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 (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 |
|
|
|
|
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/<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:
|
|
|
|
- Run evidence (`launches/<b>/<run>/`, `launches/<b>.jsonl`) and bus rows are untouched. They are
|
|
write-once.
|
|
- 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 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 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 field by field (N1, webby-0010). For hire that is
|
|
`name`, `definition` and `vars` (vars compared with `canonicalJson`, absent equal to absent only). For
|
|
fire it is `name` and `reason`. A key in the call that the body lacks, or a body key the call lacks,
|
|
refuses `decision-mismatch`. 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 allowlist 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 (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 |
|
|
|
|
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 refuses, in order:
|
|
|
|
1. `definition-not-hireable` when the definition holds an action outside `HIRE_ALLOWED` (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).
|
|
|
|
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).
|
|
|
|
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.
|
|
|
|
Fire ends the fired instance's processes, not only its claims (F4, Megavolt through sage-1374). Every run
|
|
gets the shared model token at start: `ENV_ALLOW` (`packages/seat/src/session.mjs:83-94`) passes
|
|
`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN` and the other provider variables into the sandbox. A revoke
|
|
can't take that token back, so a fired agent whose process lives on still holds it.
|
|
|
|
The delist is a business file commit through the organization writer, and the revoke and events are store
|
|
rows. No transaction covers both (webby-0011 B2), so fire runs in a fixed order, store side first:
|
|
|
|
A. One store transaction: steps 1 to 3 below, plus `fire.stopped` with the decision id. The decision isn't
|
|
consumed yet.
|
|
B. The delist through the organization writer (`launch.instances` loses I, logged in `changes.jsonl`).
|
|
C. One store write consuming the decision and recording `fire.done`.
|
|
|
|
If B or C fails, fire answers the step's code and the decision stays unconsumed. A second fire citing the
|
|
same decision runs again. A finds nothing to revoke, cancel or stop, and that isn't a refusal for fire
|
|
(unlike V3's `not-held`). B finds I already delisted and counts as done. C consumes. So a fire is safe to
|
|
repeat until it's consumed. The other order would leave a delisted agent with a live process and its model
|
|
token when the store step fails, which is the case F4 exists to stop. A halt between A and B doesn't stop
|
|
B or C, because neither starts anything. A second fire while halted refuses (D6), and the halt's own stop
|
|
covers the runs.
|
|
|
|
Steps 1 to 3 are V3's, with one change:
|
|
|
|
1. It revokes every claim the instance holds.
|
|
2. It writes `lifecycle.cancelled` for each of the instance's open requests.
|
|
3. It writes `lifecycle.end` with the runs and `kill: true`. The launcher pass stops each runner, spawned
|
|
but unbound registry runs included, with escalation (sage-1380 item 1):
|
|
- SIGTERM after the identity check.
|
|
- After the grace, SIGKILL to any runner still alive, after the same identity check again, and
|
|
`lifecycle.end.killed` with the run and pid.
|
|
- A runner still alive after SIGKILL and the observe wait gets `lifecycle.end.alive`. The Console's
|
|
Agents section shows it to the human.
|
|
A runner whose identity no longer matches is never signalled. Every check, the recheck after the
|
|
grace included, uses the identity (start ticks plus boot id), never a bare pid, so a reused pid never
|
|
counts as the fired run (webby-0011 N2).
|
|
|
|
Fire's answer lists the runs it is stopping. The delist stops any new chief launch (`launch` takes listed
|
|
instances only). A human launch request already taken is past cancelling, as in V3. The human made it, the
|
|
fire answer names it, and the human's terminate ends its run.
|
|
|
|
The stop is row 191's per-run stop, not a second one, so the two can't fight (sage-1380 item 1). Row 191's
|
|
design r2 (`agents/rocko/work/queue-191/DESIGN-r2.md`, "Stop policy and completion") sends SIGTERM, waits
|
|
5 s, sends SIGKILL to verified groups and namespaces, and observes for 2 s. Its identity check is start
|
|
ticks plus boot id, which replaces the start-time check at `launcher.mjs:526`. Fire calls that routine for
|
|
one instance's runs and uses its constants, so the grace is row 191's 5 s, not the 10 s in r3. The order:
|
|
|
|
- Fire is refused while halted (D6), so a halt sweep never meets a fire that started after it.
|
|
- A halt that comes during a fire's stop finds those runs already stopping. Each run has one stop receipt
|
|
(row 191's), and the first stop owns it. The halt sweep joins it instead of sending a second SIGTERM or
|
|
restarting the deadline, and counts the run in its remaining or quiescent totals.
|
|
- The record says which stop owned it: reason `fire` with the decision, or row 191's `kill-switch` with
|
|
its generation.
|
|
|
|
V3 terminate is unchanged. It still sends SIGTERM only (N6). Only fire escalates, because only a fired
|
|
agent must lose the token at once. If row 191 hasn't landed when this row builds, the build chains on its
|
|
candidate (Q177), as D6 already says.
|
|
|
|
### D2.3 The hire allowlist (B2, sage-1365 item 4, webby-0010 B1)
|
|
|
|
r2 and r3 had a deny list, `HIRE_FLOOR`. Webby's B1 showed it missed routed actions (`git.merge.protected`,
|
|
which `roles/merge-gate.json` holds with no credentials, `decision.resolve.technical`, `policy.change`,
|
|
`prd.approve`), and that any action added later would be hireable by default. That isn't fail-closed. r4
|
|
replaces it with an allowlist.
|
|
|
|
A definition is hireable only if every action in its withinRole and crossRole is on `HIRE_ALLOWED`:
|
|
|
|
| Allowed action | Held today by |
|
|
|---|---|
|
|
| `message.send`, `record.write` | every v2 role file |
|
|
| `review.request`, `task.update.assigned` | coder, cto, pm, reviewer |
|
|
| `task.scope.change`, `git.push.working` | coder, cto, pm (credentialed, so they matter only after row 199) |
|
|
|
|
Everything else in `ACTIONS` fails, including the actions of the old floor (`role.launch`, `role.end`,
|
|
`role.revoke`, `organization.change`, `instance.name`, `review.verdict`, `role.hire`, `role.fire`) and those
|
|
B1 named. An action added to `ACTIONS` later fails until a reviewed commit adds it here.
|
|
`git.push.remote` (row 199, routed to coder by PRD 1.7) isn't on the list. A coder holding it stays
|
|
unhireable until a reviewed commit adds it, after the credentials bound is lifted.
|
|
|
|
Against today's role files: chief-of-staff, cto, merge-gate, pm, reviewer and security-reviewer fail.
|
|
Assistant, researcher, coder and the five C-suite files pass the allowlist. Assistant is END_PROTECTED and
|
|
coder is credentialed, so D2.2 item 3 refuses them. The C-suite files hold only `message.send` and
|
|
`record.write`, so hiring one gives no authority beyond its name. `hireable` still has to list it.
|
|
|
|
The list is a constant in `business/src/vocabulary.mjs`, `HIRE_ALLOWED`. 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 every action on `HIRE_ALLOWED`. A list naming one that isn't 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` stays empty in this row (`[]`, Q4, sage-1374). Naming the first
|
|
role kind is a separate policy commit in `roles/`, made after Jason resolves the `prd.approve` decision, as
|
|
AGENTS.md invariant 7 requires. With an empty list the chief can hire nothing, even with Jason's resolve,
|
|
until that commit. The row's tests use scratch role files whose `hireable` names a kind.
|
|
|
|
### 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` 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, in `organization.set`'s model write and in the two chief verbs, before any
|
|
write.
|
|
|
|
## D3. Live changes
|
|
|
|
Today the broker's `#businesses` is fixed at start (E3), and `instances.set` answers `restart: true` for
|
|
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 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. 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. 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.
|
|
|
|
## D4. RBAC until row 193
|
|
|
|
Every human verb above calls `#human(s)` and then one hook, `#mayHuman(s, verb, target)`. Until row 193
|
|
lands, the hook passes for the business's one `human`, which is the owner. The verbs are listed once, in
|
|
an exported `LIFECYCLE_HUMAN_VERBS` (`instance.launch`, `instance.end`, `instance.regenerate`, `veto.set`,
|
|
and `instances.set` ops `add`, `change`, `delete`), so row 193 maps each verb to the human roles that may
|
|
use it. Row 193 replaces the hook body and nothing else.
|
|
|
|
### D4.1 The tester (Sage, sage-1355)
|
|
|
|
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 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'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
|
|
|
|
Row 190 carries the stack instance as the config `MOSAIC_CONFIG` names. These verbs take no instance name.
|
|
The CLI reaches the broker socket under that config's `dataRoot`. The broker acts on its own businesses,
|
|
launcher and data root. A business id that exists in two stack instances is two separate businesses. A test
|
|
runs two scratch roots with the same business id and shows a launch, a terminate and a delete in one leave
|
|
the other byte-identical (T11).
|
|
|
|
## D6. The kill switch
|
|
|
|
- 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), 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. 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.
|
|
|
|
## D7. The Console
|
|
|
|
A new Settings section, "Agents", in its own `agents.js` file. Settings itself isn't rewritten. It lists
|
|
each instance with definition, listed, kept, retired and running runs, and flags a run with a
|
|
`lifecycle.end.alive` event as "still running after fire" (D2.2). It has these actions:
|
|
|
|
- Create (definition select, from `definitions`), Delete (with a confirm naming what stays: the evidence
|
|
and the workspace under `.retired`).
|
|
- Launch, Terminate (with an optional reason), Regenerate (with a confirm).
|
|
- Keep and Unkeep per instance, and a No-hire toggle per definition.
|
|
|
|
The routes are `POST /api/instances` (op `delete` joins `add` and `change`), `POST /api/agent/launch`,
|
|
`/api/agent/terminate`, `/api/agent/regenerate` and `POST /api/veto`. Each uses `instanceSend`'s boundary:
|
|
signed-in user, exact Origin, same-origin Fetch Metadata, JSON within 16 KiB, strict keys, one bus call
|
|
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
|
|
route gets a `--send` exception.
|
|
|
|
## D8. Tests
|
|
|
|
Every test uses scratch roots under `TMPDIR` and the existing fixtures (`business/tests/helpers.mjs`,
|
|
`cli/tests/helpers.mjs`, `webui/tests/tester-fixture.mjs`). None touches `~/.mosaic-dev`,
|
|
`~/.config/mosaic-dev`, a live unit or a slot 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` |
|
|
| 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. 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 allowlist (D2.3): one case per action in `ACTIONS` that is neither on `HIRE_ALLOWED` nor a hard wait (`role.mjs` already refuses those), looped from the lists, not typed by hand. A fixture definition, clean when the chief's role file loads and listed in `hireable`, is rewritten to hold that action before the hire. The hire refuses `definition-not-hireable`. A definition holding one allowed and one refused action refuses. One holding only allowed actions hires. A pin test fixes `HIRE_ALLOWED` to the six actions | same |
|
|
| T8c | Allowlist at load: a role file whose `hireable` names a definition holding each refused action refuses at `loadRole`, one case per action, looped the same way. The mixed case refuses. 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` |
|
|
| T9a | Fire ends processes (F4, sage-1380 item 1): with a bound run and a spawned but unbound registry run of I, both fixture runners holding a stand-in model variable, fire answers both runs. After the launcher pass, neither runner pid is alive and I holds no claim. A fixture runner that ignores SIGTERM gets SIGKILL after the grace, is gone, and has `lifecycle.end.killed` with its run and pid. A stand-in whose kill is stubbed to fail gets `lifecycle.end.alive`. A runner whose identity changed before the SIGTERM, or between SIGTERM and SIGKILL, gets no signal. V3 terminate of a SIGTERM-ignoring runner sends no SIGKILL | cli `launcher.test.mjs` |
|
|
| T9b | Fire and row 191's halt: a halt during a fire's stop sends no second SIGTERM, keeps the fire's deadline, writes one stop receipt with reason `fire` and the decision, and counts the run in its totals. A fire while halted refuses (D6) | cli `launcher.test.mjs`, on row 191's fixture |
|
|
| T9c | Fire order (webby-0011 B2): the organization writer stubbed to fail after A commits. Fire answers the writer's code, I's claims are revoked and its runs stopped, I is still listed, and the decision is unconsumed. A second fire citing the same decision delists I and consumes it. A third fire refuses `decision-consumed`. With C stubbed to fail, the second fire finds I delisted and only consumes | 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 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
|
|
|
|
Each one must fail at least one test. They run against the candidate, and the file is restored and its
|
|
sha256 checked after each one.
|
|
|
|
| # | Change | Killer |
|
|
|---|---|---|
|
|
| M1 | `instance.launch` skips `#human` | T12 |
|
|
| M2 | the pass calls `check()` without `human: true` | 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 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 |
|
|
| M15 | the tool enum is kept | T8 |
|
|
| M16 | the halted check is dropped from `instance.launch` | T15 |
|
|
| 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 `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 | the broker's hire check accepts one action outside `HIRE_ALLOWED`, one mutant per action: `role.launch`, `role.end`, `organization.change`, `instance.name`, `review.verdict`, `git.merge.protected`, `decision.resolve.technical` | T8b |
|
|
| M37 to M43 | the same seven, accepted by 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 |
|
|
| M47 | the broker's hire check needs some action on `HIRE_ALLOWED` instead of every action | T8b |
|
|
| M48 | the same in `role.mjs` only | T8c |
|
|
| M49 | fire revokes the claims but writes no `lifecycle.end`, so no runner is signalled | T9a |
|
|
| M50 | fire's signal skips spawned but unbound registry runs | T9a |
|
|
| M51 | fire's stop drops the SIGKILL escalation | T9a |
|
|
| M52 | SIGKILL is sent without the second identity check | T9a |
|
|
| M53 | a killed runner gets no `lifecycle.end.killed` | T9a |
|
|
| M54 | V3 terminate escalates to SIGKILL as fire does | T9a |
|
|
| M55 | the halt sweep sends its own SIGTERM and restarts the deadline on a run fire is stopping | T9b |
|
|
| M56 | fire delists before the store step | T9c |
|
|
| M57 | fire consumes the decision in step A | T9c |
|
|
| M58 | a second fire with nothing to revoke refuses `not-held` | T9c |
|
|
|
|
## Scope and boundaries
|
|
|
|
- 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), and pm, cto and
|
|
reviewer fail the hire allowlist 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, 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 the policy commit in `roles/` that follows
|
|
the `prd.approve` resolve names a kind (Q4). Case 1's "as needed" waits on that commit and on PRD 1.7.
|
|
- R7. Fire's SIGKILL closes the SIGTERM-ignoring case (sage-1380). A runner still alive after SIGKILL
|
|
(an unkillable process, or a stop whose identity check fails) keeps the shared model token until the
|
|
human acts on `lifecycle.end.alive`. Only a per-agent model token (row 199 Q-M4, a later proxy row)
|
|
would let a fire revoke the token itself.
|
|
|
|
## Questions and rulings
|
|
|
|
- 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, ruled by Sage (sage-1374): `hireable` stays empty in this row. Naming the first role kind is a
|
|
separate policy commit in `roles/` after Jason resolves the `prd.approve` decision. Tests use scratch role
|
|
files that name a kind. D2.3.
|
|
|
|
## 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 (an allowlist since r4) |
|
|
| 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 |
|
|
|
|
## r3 changes against sage-1374, sage-1377 and Megavolt's F4
|
|
|
|
| Item | Where |
|
|
|---|---|
|
|
| Q4 ruled: `hireable` stays empty, the first kind is a later policy commit in `roles/` | D2.3, R6, Questions |
|
|
| `instance.name` joins the floor (Webby B2, mos-1023 item 6) | D2.3 table, T8b, T8c, M47, M48 |
|
|
| F4: fire ends the fired agent's processes, since `ENV_ALLOW` gives every run the shared model token | D2.2, D7, T9a, M49 to M51, R7 |
|
|
| `git.push.remote` routed to coder only (mos-1028 item 3, row 199 r3) | `PRD-1.7-DRAFT.md`, REQ-LAND-2 and the REQ-WAIT-3 append |
|
|
|
|
## r4 changes against webby-0010, sage-1380 and megavolt-0008
|
|
|
|
| Item | Where |
|
|
|---|---|
|
|
| webby-0010 B1: the deny-list floor becomes the allowlist `HIRE_ALLOWED`, so merge-gate, `decision.resolve.technical`, `policy.change`, `prd.approve` and any later action are refused | D2.3, Summary, R1, T8b, T8c, M30 to M43, M47, M48, PRD REQ-COS-3 bound |
|
|
| webby-0010 N1: field-by-field compare of the call against the decision body, extra or missing keys refuse | D2.1 item 3 |
|
|
| webby-0010 N2: the model write is in REQ-COS-3's text | `PRD-1.7-DRAFT.md` |
|
|
| sage-1380 item 1: fire escalates to SIGKILL through row 191's per-run stop, and the order against a halt is fixed | D2.2, T9a, T9b, M51 to M55, R7 |
|
|
| sage-1380 item 2 and megavolt-0008: REQ-LAND-2 binds one commit oid, and Megavolt's three text changes against row 199 r4 (e1aa6216c) | `PRD-1.7-DRAFT.md` |
|
|
| webby-0011 B1: same as webby-0010 B1, M47 and M48 now allowlist mutants | D2.3 |
|
|
| webby-0011 B2: fire runs store side first, then the delist, then the consume, safe to repeat | D2.2 order A to C, T9c, M56 to M58 |
|
|
| webby-0011 N1: the remote-owner host check at each push | sent to Megavolt for row 199 |
|
|
| webby-0011 N2: identity, not bare pid, at every check | D2.2 step 3 |
|
|
| Q4: Webby's view matches Sage's ruling (empty) | no change |
|