Give calls outside any work item an agent-level scope
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 50s

Letting the model choose which ready item to switch to adds a model
call that belongs to no work item. Duty runs before a finding already
had the same gap. A10 now defines an agent-level scope for these calls,
kept apart from work item transcripts in both directions. The switch
call sees only code-rendered summaries of the ready items, runs as the
executor, and falls back to the oldest item if it fails. A9 gains a
work_item.switch event, and A18 accepts that an injected alert can at
worst reorder work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-09-23 21:57:05 +00:00
1 parent 854bddc680
commit 5b163f0c2e
1 file changed
+28 -7
+28 -7
View File
@@ -163,12 +163,22 @@ come back to something. Components throughout this document refer to them.
When the active work item is waiting on the operator (an open gate, a pending When the active work item is waiting on the operator (an open gate, a pending
question, or a halt in `AWAIT_INPUT`) and other work items are ready, the agent question, or a halt in `AWAIT_INPUT`) and other work items are ready, the agent
switches to one of them and posts a notice naming both items and why it chose switches to one of them and posts a notice naming both items and why it chose
that one. The model chooses. This is one of the few judgments it is trusted with that one. An alert must not sit for hours behind an unanswered prompt. The agent
outright, because choosing grants nothing: every item still meets its own gates, never switches away from an item that is working. The item it left stays
and a poor choice costs time, not safety. An alert must not sit for hours behind waiting; switching answers nothing on its behalf.
an unanswered prompt. The agent never switches - **The model chooses which ready item to switch to.** This is one of the few
away from an item that is working. The item it left stays waiting; switching judgments it is trusted with outright, because choosing grants nothing: every
answers nothing on its behalf. item still meets its own gates, and a poor choice costs time, not safety.
- *It is an agent-level call,* belonging to no work item (A10), made by the
`executor` role at the current config (A11 pins roles per item, and this call
has none).
- *It sees summaries rendered by code from the state store,* one per ready item:
source, age, severity, current state, title. It never sees another item's
transcript, so work items stay isolated (A10).
- *It falls back to the oldest ready item* if the call fails or its answer does
not name one of them. Switching never waits on a model.
- *The switch is one state change* (A9 `work_item.switch`), naming both items,
the reason, and whether the model, the fallback, or the operator chose.
- **Parking gives up authority.** Parking a work item unmounts any elevated MCP - **Parking gives up authority.** Parking a work item unmounts any elevated MCP
instances (F3) its approved plan mounted. They are remounted when it becomes instances (F3) its approved plan mounted. They are remounted when it becomes
active again, if its approval has not expired (C2). An approved plan's active again, if its approval has not expired (C2). An approved plan's
@@ -433,7 +443,8 @@ by replaying the operational trail:
- `task_id` — the enclosing work item's ID (0.2), minted when the work item - `task_id` — the enclosing work item's ID (0.2), minted when the work item
opens (`use <project>`, ticket pickup, an operator question, a duty's finding) opens (`use <project>`, ticket pickup, an operator question, a duty's finding)
and threaded through every event it causes; `null` only outside any work item and threaded through every event it causes; `null` only outside any work item
(e.g. boot, or a duty run before it yields a finding). A milestone is a phase (e.g. boot, a duty run before it yields a finding, or choosing which item to
switch to). A milestone is a phase
within a work item, so starting one does not mint a new ID. within a work item, so starting one does not mint a new ID.
- `cause` — what triggered the mutation: operator command, approval - `cause` — what triggered the mutation: operator command, approval
(`rev-N` + channel), escalation trigger, reconciliation verdict, or crash (`rev-N` + channel), escalation trigger, reconciliation verdict, or crash
@@ -445,6 +456,7 @@ by replaying the operational trail:
|---|---| |---|---|
| `state.transition` | `from`, `to`, `project` | | `state.transition` | `from`, `to`, `project` |
| `intent.begin` / `intent.commit` / `intent.abort` | `intent_id`, `kind` | | `intent.begin` / `intent.commit` / `intent.abort` | `intent_id`, `kind` |
| `work_item.switch` | `from`, `to`, `reason`, `chosen_by` (`model` / `fallback` / `operator`) |
| `gate.open` / `gate.approve` / `gate.reject` | `gate`, `rev`, `channel` | | `gate.open` / `gate.approve` / `gate.reject` | `gate`, `rev`, `channel` |
| `boot.reconcile` | `action`, per-intent verdicts | | `boot.reconcile` | `action`, per-intent verdicts |
| `schema.migrate` | `from_version`, `to_version` | | `schema.migrate` | `from_version`, `to_version` |
@@ -475,6 +487,12 @@ Rules:
- **The log is scoped to a work item (0.2).** Every event carries its work item's ID, - **The log is scoped to a work item (0.2).** Every event carries its work item's ID,
and a model call for one work item renders only that item's events. Evaluation and a model call for one work item renders only that item's events. Evaluation
capture (A16) may read across items. capture (A16) may read across items.
- **Some calls belong to no work item,** and their events go to an **agent-level
scope**, with no work item ID: a duty run until it yields a finding, and the
choice of which item to switch to (0.2). Each such call renders only its own
events plus what code gives it. Agent-level events are never rendered into a
work item's call, and a work item's events reach an agent-level call only as a
code-rendered summary.
- **Canonical tool-call IDs are minted locally.** Provider IDs are mapped at the - **Canonical tool-call IDs are minted locally.** Provider IDs are mapped at the
boundary and never used as a join key. Providers differ in both ID format and in boundary and never used as a join key. Providers differ in both ID format and in
where tool calls sit structurally (content blocks vs. a separate array on the where tool calls sit structurally (content blocks vs. a separate array on the
@@ -826,6 +844,9 @@ Requirements:
- **Parse intake into structured fields in code** wherever the source allows it, - **Parse intake into structured fields in code** wherever the source allows it,
e.g. alert name, severity, host, from Alertmanager's JSON. Less free text then e.g. alert name, severity, host, from Alertmanager's JSON. Less free text then
reaches the model at all, and what reaches it is labelled by where it came from. reaches the model at all, and what reaches it is labelled by where it came from.
- **Some injection is accepted because it can only reorder work.** An alert title
can tell the switch decision (0.2) to pick it first. The worst case is ordering,
and the switch notice makes it visible.
- **Check each agent against the lethal trifecta:** access to private data, exposure - **Check each agent against the lethal trifecta:** access to private data, exposure
to untrusted content, and a channel to send data out. Damage needs all three. to untrusted content, and a channel to send data out. Damage needs all three.
- `nisse-dev` reads untrusted content and holds little private data: its Gitea - `nisse-dev` reads untrusted content and holds little private data: its Gitea