Fix inconsistencies and gaps found re-reading the architecture
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m27s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-02 18:30:32 +00:00
1 parent acf7ce191c
commit 349ec794f2
1 file changed
+80 -45
+80 -45
View File
@@ -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 <project>`, "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
<project>`, "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.