From 5b163f0c2e0dfe8807ef95fdf6860448f899b1ae Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Wed, 23 Sep 2026 21:57:05 +0000 Subject: [PATCH] Give calls outside any work item an agent-level scope 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) --- docs/Architecture-Build-Spec.md | 35 ++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/docs/Architecture-Build-Spec.md b/docs/Architecture-Build-Spec.md index cf422f1..a6f8bc6 100644 --- a/docs/Architecture-Build-Spec.md +++ b/docs/Architecture-Build-Spec.md @@ -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 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 - that one. The model chooses. This is one of the few judgments it is trusted with - outright, because choosing grants nothing: every item still meets its own gates, - and a poor choice costs time, not safety. An alert must not sit for hours behind - an unanswered prompt. The agent never switches - away from an item that is working. The item it left stays waiting; switching - answers nothing on its behalf. + that one. An alert must not sit for hours behind an unanswered prompt. The agent + never switches away from an item that is working. The item it left stays + waiting; switching answers nothing on its behalf. +- **The model chooses which ready item to switch to.** This is one of the few + judgments it is trusted with outright, because choosing grants nothing: every + 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 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 @@ -433,7 +443,8 @@ by replaying the operational trail: - `task_id` — the enclosing work item's ID (0.2), minted when the work item opens (`use `, ticket pickup, an operator question, a duty's finding) 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. - `cause` — what triggered the mutation: operator command, approval (`rev-N` + channel), escalation trigger, reconciliation verdict, or crash @@ -445,6 +456,7 @@ by replaying the operational trail: |---|---| | `state.transition` | `from`, `to`, `project` | | `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` | | `boot.reconcile` | `action`, per-intent verdicts | | `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, and a model call for one work item renders only that item's events. Evaluation 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 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 @@ -826,6 +844,9 @@ Requirements: - **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 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 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