From 349ec794f2c220cfa7fd7ee7ec5fa7ed48d3b0f9 Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Fri, 2 Oct 2026 18:30:32 +0000 Subject: [PATCH] Fix inconsistencies and gaps found re-reading the architecture Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE --- docs/Architecture.md | 125 +++++++++++++++++++++++++++---------------- 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 0a79143..45264c4 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -227,9 +227,10 @@ Every agent's work is organised into three kinds of unit. | **waiting on the operator** | an answer, a review or an approval | | **blocked** | something else to finish, e.g. another issue to close | -- **Blocked is read from external truth,** such as a Gitea issue dependency (D1), - and becomes ready only when that dependency actually closes, never by a model's - judgement (P6). The operator can unblock by removing the dependency. +- **Blocked is read from external truth:** a Gitea issue dependency (D1), or a + request to another agent (a ticket, see below) that hasn't closed. It becomes + ready only when the dependency actually closes, never by a model's judgement + (P6). The operator can unblock by removing the dependency. - **An agent that finds a blocker mid-work** says so and proposes recording it (e.g. "blocked by #12"); with the operator's agreement it records it, parks the item as blocked and moves on. @@ -237,7 +238,12 @@ Every agent's work is organised into three kinds of unit. **Park-and-switch: one active work item per agent.** - Other work items are *parked*, with their full position held durably. - **Switching happens only at safe points,** when the active item is waiting on - the operator or has closed, and never while intents are unresolved (L8). + the operator, is blocked, or has closed, and never while intents are unresolved + (L8). +- **Conversation is not work.** Answering a message, about any item and in any + thread (L5), happens between turns without switching the active item. A reply + may change an item's state (an answer makes it ready, `abandon` closes it), but + only the active item has tools run for it. - **The operator can always switch.** The agent may switch on its own to a *ready* item (one it can act on without the operator) when the active item is waiting on the operator, after a grace period (default 10 minutes, so a watching operator @@ -273,8 +279,9 @@ What is fixed is small: Rules for the loop: - **The agent never waits in a state it can't explain or leave.** Every wait is - either for the operator (with a question or proposal they can see) or for work - it has started. Anything else is a bug; the model checker (`specs/`) guards this. + for the operator (with a question, proposal or PR they can see), for work it has + started, or for a recorded dependency to close (L1). Anything else is a bug; the + model checker (`specs/`) guards this. - **Questions never change state** (L5). An operator's question gets an answer, never an interpretation as approval, rejection or command. - **`stop` works in every state,** for every agent: it ends the current turn or @@ -303,21 +310,24 @@ An approval grants a **permission with a scope**: "work on this issue", **What always needs discussion first:** any class-3 action not named by an approved step sequence or by the operator in conversation (P3). -**Approvals bind to exactly what was approved.** -- Every plan revision carries a tag, numbered across the whole work item, so - `rev-N` names exactly one plan. +**Approvals bind to exactly what was approved.** This covers everything the agent +puts up for agreement: a dev restatement awaiting a go-ahead (D1), a design +(`design`, D2), an ops step sequence (O2). +- Every revision carries a tag, numbered across the whole work item, so `rev-N` + names exactly one proposal. - **A reply that names no revision is about the one up when it arrived:** a bare - `approve` or any feedback is stamped on arrival with the revision last put up - (in Matrix, the one whose request had been sent; in Gitea, the latest plan - comment older than the reply). If that isn't the current revision, the reply is - not applied, and the agent says which revision is up. A repeated approval, an + `approve` or `go`, or any feedback, is stamped on arrival with the revision last + put up (in Matrix, the one whose message had been sent; on a Gitea issue or a + Vikunja ticket, the latest proposal comment older than the reply). If that isn't + the current revision, the reply is not applied, and the agent says which revision + is up. A repeated approval, an approval on two channels, or an afterthought therefore can never land on the next plan. (Found by the model checker.) - **Approval messages are rendered by code from what will actually happen,** never from a model's description of it (L11). - **Approvals expire** where the world can change underneath them (ops, O2). -**Approval sources.** Matrix (L5), Gitea comments (D2) and Vikunja comments (O5) +**Approval sources.** Matrix (L5), Gitea comments (D1) and Vikunja comments (O5) are approval sources; each authenticates the approver against its own operator allowlist. The first valid approval wins and is echoed to the other channels. The knowledge vault (L10) is **never** an approval source or an instruction channel. @@ -340,10 +350,9 @@ An independent check that sorts an action the fast path doesn't cover. - **It judges the action, not the actor's story.** Its input is the structured action (tool, command, target, a summary of the change) and the **agreed scope** (the work item's task and what was agreed, or the approved step sequence). Never - the actor's - transcript or reasoning, and never untrusted content such as issue text or web - pages. Otherwise injected text, or the actor's own rationalisation, can argue past - it. + the actor's transcript or reasoning, and never untrusted content such as issue + text or web pages. Otherwise injected text, or the actor's own rationalisation, + can argue past it. - **It fails closed.** An error, a timeout or an unclear verdict counts as *needs discussion*. - **It is independent of the actor:** its own prompt and context, ideally a @@ -364,15 +373,18 @@ interface (send, ask, receive), so another chat system is one new class. Existing Matrix clients are the UI. **What the operator can say.** Plain language, plus a small command vocabulary -that works in every state: `status`, `stop`, `abandon`, `approve [rev-N]`, and -work-item selection (e.g. `use `, "do #14 next"). +that works in every state: `status`, `stop`, `abandon`, `approve [rev-N]`, `go` +(the go-ahead after a restatement, D1), and work-item selection (e.g. `use +`, "do #14 next"). - **Free-flowing conversation can open a work item.** A conversation about work ("I'm having trouble with X…") is kept as a work item from its first message; it may end with a project, an issue, or just an answer. - **A thread per work item.** The agent opens each work item's conversation as a Matrix thread. A reply in a thread goes to that item, even while it is parked - (answering makes it ready); top-level messages go to the agent in general or to - the active item. + (answering makes it ready). Commands in a thread (`stop`, `abandon`, `go`) apply + to that item; at top level, `stop` applies to the active item and `status` to the + agent. Any other top-level message starts a new conversation in its own thread; + if it clearly belongs to an existing item, the agent offers to continue there. - **Documents as input.** The operator can paste a document, attach a text or Markdown file, or point to a note in the knowledge vault (L10) ("my sketch is in the vault as `recipes idea`"). The message is the instruction; the document is @@ -542,7 +554,7 @@ Build artifacts, workspaces and shipped-out logs are **not** in it. 3. **Adopt what external truth shows:** a published PR for a milestone that was still executing, an approval posted while the agent was down. 4. **Resume waiting points as they were:** a pending question is asked again, an - open plan is announced again, watches on external threads resume from their + open proposal is announced again, watches on external threads resume from their cursors. 5. **Interrupted work resumes only on evidence.** - *A delegation* resumes automatically when the cut is clean (last event a @@ -601,8 +613,8 @@ a model name or a vague "weaker model". autonomously, and hands back. - **One harness, not one per vendor.** Vendor agent SDKs would bring back the lock-in the design avoids. -- **Scoped by the agreed plan.** A delegation can't widen what the work item was - agreed to do; its actions pass through the same three layers (P4). +- **Scoped by the work item's scope** (L3). A delegation can't widen what the work + item was agreed to do; its actions pass through the same three layers (P4). - **Branch at delegation start;** abandoning it discards the branch. - **Hard caps** on turns, tokens and wall-clock time. - **Checkpointed as it goes,** so it can resume after a crash (L8). @@ -742,9 +754,9 @@ Tool calls come in three kinds, and the framework allows no others. | Kind | Examples | Where it runs | |---|---|---| -| **Control tools** | `submit_plan`, `ask_human`, `finish`, `ask_expert` | In the harness. They act only on the agent's own state and conversation | +| **Control tools** | `ask_human`, `ask_expert`, `start_milestone`, `finish_milestone`, reading and writing notes (L10) | In the harness. They act only on the agent's own state, conversation and records | | **Execution tools** | Running a command; reading, writing and listing files | On a remote host, over SSH, through `execute(host, …)` | -| **MCP tools** | Proxmox, UniFi, Gitea APIs (X3) | In MCP server containers beside the agent | +| **MCP tools** | Proxmox and UniFi APIs (X3) | In MCP server containers beside the agent | - **`execute(host, command, *, secrets, stdin)` is the only way to run anything.** There is no local execution path in the framework, so the agent host never runs a @@ -777,7 +789,14 @@ Work arrives through Gitea issues or conversation, and leaves as reviewed PRs. **Intake: an issue assigned to `dev-agent`.** Assignment says whose job it is. - **The operator's issue becomes the work item's record;** the agent doesn't open a second one. Only issues filed or assigned by allowlisted operators are acted on - (L11). + (L11). When someone else wrote the issue, the operator's assignment is the + instruction and the issue's text stays data. +- **Work that starts in conversation gets a record too.** When a conversation + turns into work on an existing repository, the agent opens an issue for it, + assigned to `dev-agent` and linked to the work item. Intake ignores issues + already linked to a work item, so a record is never picked up twice. (This is the + one case where the agent assigns itself, and only for work the operator has + already agreed to.) - **Unassigned issues are backlog,** never work. The agent can discuss them when asked. - **An issue with open Gitea dependencies is blocked** (L1) until they close. @@ -788,7 +807,9 @@ Work arrives through Gitea issues or conversation, and leaves as reviewed PRs. **restates its understanding** in the item's Matrix thread (L5): what it takes the request to mean, how it fits the architecture, and its first step. Then: - **An issue labelled `bug`** proceeds after a short grace period (as in L1), unless - something below says to talk first. + something below says to talk first. During the grace period the item is waiting + on the operator, so the agent may work on something else; when it expires the + item is ready. - **Anything else** (a feature, a change, a question) waits for the operator's go-ahead after the restatement; the conversation is where the design is agreed. - **Talk first when:** the issue is labelled `discuss`; the agent finds the report @@ -807,8 +828,9 @@ request to mean, how it fits the architecture, and its first step. Then: project, the agent says what it needs provisioned: a repository, its protection and the agent's access (D4), a build VM, and the project's configuration. The operator provisions these (later `nisse-ops`, X4). The agent then opens a tracking - issue in the new repository ("implement the build plan"), and the architecture - lands in one or more PRs, followed by the build plan's milestones. + issue in the new repository ("implement the build plan", a record as above), and + the architecture lands in one or more PRs, followed by the build plan's + milestones. - **Progress** on a tracking issue is posted as it happens: facts by the harness (milestone started, PR opened or merged), prose by the agent when there's something worth saying. @@ -820,7 +842,8 @@ item, after checking what is left open (D5). conversation, the agent may file an issue against any project where `dev-agent` is a collaborator, when the operator asks or agrees to its suggestion. - **Never self-assigned:** a filed issue is backlog until the operator assigns it, - so the agent can't create work for itself. + so the agent can't create work for itself. (Records of agreed work, above, are + the only issues it assigns to itself.) - **Duplicates first:** it searches the project's open issues and offers to add to an existing one instead. - **Provenance:** the issue says it was filed from a conversation, when, and with @@ -878,8 +901,8 @@ commands run. and reported to the operator. - Build artifacts and caches in the workspace never survive into a new task. -The milestones of one work item share that workspace, since they build on each -other, often before earlier PRs are merged. +The milestones of one work item share that workspace; each starts from the default +branch after the previous milestone's PR has merged (D4). **Toolchain preflight.** When the workspace is prepared, the agent determines what the project needs (from its files, e.g. `Cargo.toml`, `pyproject.toml`, @@ -917,6 +940,11 @@ a corrupt `.pyc`. the previous milestone's PR has merged, read from Gitea; until then the work item waits on the operator and the agent works on something else (L1). Stacked PRs are a possible later addition (Q). + - **A PR closed without merging** makes the item ready with that fact; the agent + asks the operator what next (revise, re-plan or abandon) rather than waiting + for a merge that won't come. + - **A review comment** on the PR makes the item ready, so the review loop (D5) + runs even while the item is parked. - **Parking commits and pushes the item's branch,** so another work item on the same project starts from a fresh checkout (D3) and the parked one resumes from its branch. @@ -957,8 +985,10 @@ a corrupt `.pyc`. required approvals are recorded; the branch protection must then require an operator approval the agent's account can't satisfy, so the merge rule is enforced by Gitea, not by the agent. -- **Finishing a work item** closes its Gitea issue with a note (completed or - abandoned), after checking what is left open. +- **Finishing a work item:** the last PR closes its issue on merge ("Closes #N"; + earlier milestone PRs say "Part of #N"). When no merge closes it (abandoned, or a + task that changed no code), the agent closes the issue with a note. Either way it + first checks what is left open. ### D6. `nisse-dev`'s action classes @@ -1022,8 +1052,8 @@ they take effect. | Scope source | the assigned issue and its conversation | approved step sequences (O2) | | Default for class 3 | discuss | discuss, and exact-step approval | | Work shape | milestones → PRs | short reactive tasks and scheduled checks → live changes | -| Driver of work | the operator in Matrix, Gitea | Vikunja tickets, alerts, duties, the operator | -| Placement | sealed, own VLAN | Tailscale, credentials, broad reach | +| Driver of work | issues assigned in Gitea, conversation in Matrix | Vikunja tickets, alerts, duties, the operator | +| Placement | sealed build VMs, own VLAN | Tailscale, credentials, broad reach | The two are separate deployments over the same library; one instance playing both roles would collapse the isolation. @@ -1044,7 +1074,7 @@ exactly the steps written. This is `nisse-ops`' scope source for the classifier Prose may explain the plan; it is never scope. - **The approval message is rendered by code from the steps,** flagging at the top anything irreversible, anything touching a protected target, and anything that - takes down the agent's own host. + takes down the agent host (which runs every agent, `nisse-ops` included). - **Execution is matched against the steps.** Read-only calls are always allowed. An elevated call runs only if it matches the next unfinished step exactly (arguments normalised against the tool's schema). Anything else halts and asks: @@ -1095,6 +1125,10 @@ a routine fix approved by hand often enough). (`approve rev-N`). Live prompts stay in Matrix, with outcomes written back. - **Action-triggering tickets are operator-scoped:** a ticket is an instruction channel into an agent that changes infrastructure. +- **Requests from other agents** (L1), such as `nisse-dev` asking for a build VM, + arrive as tickets and are picked up like duty findings: the agent may look and + propose, but any write needs a step sequence the operator approves (O2). Until + standing approvals exist, the operator approves each provisioning request. ### O6. Deployment @@ -1209,10 +1243,11 @@ agents. log and renderers, and image tokens in the context budget. Measured when that work starts. - **More than one active work item per agent.** Park-and-switch first; concurrency - costs per-item serialisation, a scheduler for the shared inference host, strict - approval threading, separate workspaces per item, and per-item intent ownership. -- **When a work item must have an external record.** Plan-driven dev work and - ticket-driven ops work already do; whether every item with an approval must. + costs per-item serialisation, a scheduler for the shared inference host, and + per-item intent ownership. Build VMs per project (D3) already separate workspaces + across projects, and threads (L5) already separate conversations. +- **When a work item must have an external record.** Dev work on a repository and + ticket-driven ops work already do (D1, O5); whether pure conversations must. - **Fallback browser access to network gear:** standing, or handed off per task. - **Relocating stateful MCP servers** off the agent host, one at a time, if one ever justifies losing per-agent attribution. @@ -1259,5 +1294,5 @@ history): - **Kept, largely as they were:** the work model, durability and reconciliation, LiteLLM and the canonical log, delegation, prompt injection, identity, and `nisse-ops`' approved step sequences. -- **Moved to the Development Plan (to be written):** build order, supporting-service - status, and interim arrangements. +- **Moved to the Development Plan** (`docs/Development-Plan.md`): build order, + supporting-service status, and interim arrangements.