From 2c9a082e5bd5c2757f931b2c4ce6ba70a262bc64 Mon Sep 17 00:00:00 2001 From: Don Garrett Date: Wed, 23 Sep 2026 20:26:24 +0000 Subject: [PATCH] Define the work model: duties, work items, scopes "Project" was carrying several things at once: the repo, the effort through arch plan and milestones, and the transcript's scope. The Sysadmin Agent needs the same distinctions in a different shape, with standing duties that produce many efforts over one scope. New section 0.2 names them: - Duty: a standing responsibility that produces work items, and owns deduplication of its findings - Work item: the unit of state, identified by a locally minted id (A9's task_id), optionally linked to an external record - Scope: what a work item operates on, shared across work items - Park-and-switch: one active work item, switching only at safe points, nothing lost while parked; the schema does not assume one 0.1 records this as a bounded exception to extract-after-it-works: identity is designed ahead, behaviour is not. A2, A4, A9, A10, B1/B2 and C4 now speak in these terms; the transcript is scoped per work item, and a milestone no longer mints a task_id. Part H parks concurrency with its costs, and leaves open when a work item must have an external record. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/Architecture-Build-Spec.md | 139 ++++++++++++++++++++++++++++++-- 1 file changed, 131 insertions(+), 8 deletions(-) diff --git a/docs/Architecture-Build-Spec.md b/docs/Architecture-Build-Spec.md index 74bb959..c39387e 100644 --- a/docs/Architecture-Build-Spec.md +++ b/docs/Architecture-Build-Spec.md @@ -64,6 +64,14 @@ Both are thin applications over a shared **`nisse-core`** library. The library i consumers that don't exist yet. Build the Dev Agent as if the library were internal, then lift the seams that proved real. +There is one deliberate, bounded exception: the **work model's identity scheme** +(0.2) is settled for both agents ahead of extraction. Identifiers end up in the state +schema, branch names, the operational log (A9), the transcript (A10), and external +records. Changing those after extraction would mean migrating two agents and their +stored state. Only identity is designed ahead. Behaviour is still built by each +agent as it needs it, so nothing in 0.2 requires the Dev Agent to implement duties, +deduplication, or concurrency. + The three artifacts live in **one repository** (decided — Part H), so the extraction step in the build order is a package split within one tree, not a repo split. @@ -86,6 +94,80 @@ split. --- +## 0.2 The Work Model: Duties, Work Items, and Scopes + +Every agent's work is organised into three kinds of unit. They define what state +belongs where, how work is identified, and what the operator is resuming when they +come back to something. Components throughout this document refer to them. + +| Concept | What it is | Lifetime | Dev Agent | Sysadmin Agent | +|---|---|---|---|---| +| **Duty** | A standing responsibility that produces work items | Indefinite, recurring | None yet; the operator is the only source | Scheduled checks, log monitoring, alert intake | +| **Work item** | One effort with a lifecycle: opened, worked (often through gates), closed | Opened → closed | Building a feature through arch plan → milestones → PRs | Diagnosing one failure; answering one question | +| **Scope** | What a work item operates on, and the context that persists across work items there | Indefinite | A repo | A site and service, e.g. `garrett-hall` Proxmox | + +### Duty + +- **A duty produces work items. It is not one itself.** A run of a duty (one log + analysis, one scheduled check) is read-only and yields findings. A finding that + needs attention becomes a work item. +- **The operator is always an implicit source** of work items, for every agent. +- **Deduplication is a duty's responsibility.** When a later run finds the same + failure, it attaches to the existing open work item instead of opening a second + one. That needs a stable fingerprint per finding, checked against open work items. + This is the same idempotency concern A2/A3 address for side effects. + +### Work item + +- **The unit of state.** A work item owns its state-machine position, gates and plan + revisions, pending questions, pinned role mapping (A11), and its transcript (A10). +- **Identity is minted locally.** Every work item gets a local ID when it opens. + This is the `task_id` of A9: a work item's ID and its correlation ID are one value. +- **An external record is optional, and linked.** A work item *may* be tied to a + durable external record: a Gitea issue for the Dev Agent, a Vikunja ticket for the + Sysadmin Agent. Many will not be; a general question to either agent is a work + item with no record. A work item can also gain a record partway through, for + example a question that turns into a change to make. +- **Where the spec requires a record, it has one.** Dev plan gates are externalized + to Gitea (Part H), and ticket-driven ops work has its ticket (C4). When a record is + linked, external truth wins on divergence (A2), and gate state can be re-derived + from it after state loss. Whether *every* gated work item must have a record is + open (Part H). +- **Work items are not nested.** A milestone is a phase *within* a dev work item, and + a delegation (A13) is an episode within one. Neither is a work item of its own. + +### Scope + +- **A scope holds context shared by the work items that operate on it:** for a repo, + the clone and the project's conventions; for infrastructure, the A17 world model + of that site or service. +- **Many work items may share one scope.** A feature waiting on review and a bug fix + in the same repo are two work items on one scope. So are two failures found in the + same log run. +- **A work item has zero or more scopes.** A dev work item has exactly one repo for + now. A general question may have none. +- **Scope identifiers follow existing naming.** Repos use `owner/repo`. Sites use the + site identifier (Part 0), combined with the service. + +### Park-and-switch: one active work item per agent + +- **Each agent has at most one active work item.** Others are *parked*, with their + full position held in the state store (A2). +- **Switching happens only at safe points,** where the loop is waiting on a human + (an open gate, a pending question, a PR in review) or is idle. It never happens + mid-turn. Because turns run to completion, every point at which a command can + arrive is a safe point, with one exception: switching is refused while intents + are unresolved (A2). That keeps every pending intent owned by the active work + item, so boot reconciliation (A3) stays single-item. +- **Nothing is lost while parked.** Intake for a parked work item, such as comments + on its Gitea issue or PR, is read from a durable per-thread cursor when it becomes + active again. It is not dropped, and it is not replayed from the beginning. +- **The state schema must not assume one item.** Work items live in their own table, + and "active" is a pointer to one of them. Allowing more than one active item later + (Part H) is then a control-loop change, not a migration of every stored item. + +--- + ## PART A — THE AGENT LIBRARY (`nisse-core`) The shared spine. Every component here is consumed by both agents. This is the part @@ -121,8 +203,9 @@ canonical log (A10) is the sole source of truth. process restart. This is what makes the whole system safe to redeploy and reboot; it blocks everything else. -Must persist: active project/task, per-project state-machine position, approvals -granted, pending questions, session continuity, (dev) git clone locations. +Must persist: the active work item and each work item's state-machine position +(0.2), approvals granted, pending questions, session continuity, and scope state such +as (dev) git clone locations. Disposable (never on the state volume): agent code, build artifacts/test containers, shipped-out logs. @@ -203,6 +286,11 @@ Requirements: for copy-paste. Any other reply/comment is `reject `. Reactions/emoji are never an approval path. Approvals are consumed at first sight; later edits to an approval comment are ignored and logged. +- **Approvals bind to a work item, too (0.2).** A bare `approve` binds to the + *active* work item's latest revision, under the unambiguity rule above. An + approval arriving on a *parked* item's external record (a Gitea or Vikunja + comment) is delivered when that item next becomes active. It is never applied to + the active item. - **Autonomy inside an approved unit of work:** all tool/command use auto-approved (Claude Agent SDK `bypassPermissions` or an allow-all `canUseTool`). Escalation to the human happens *only* via `ask_human`. @@ -311,9 +399,11 @@ by replaying the operational trail: - `ts` — UTC ISO-8601 timestamp - `agent` — `nisse-dev` | `nisse-ops` - `event` — dotted type from the taxonomy below - - `task_id` — correlation ID for the enclosing unit of work, minted when the - unit starts (`use `, milestone start, ticket pickup) and threaded - through every event it causes; `null` only outside any unit (e.g. boot). + - `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 + 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 recovery. @@ -351,6 +441,9 @@ The durable record is a **canonical event log** in SQLite, not a provider-shaped | `knowledge_retrieved` | document text as read, document ID, revision, retrieved-at (A17) | 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. - **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 @@ -691,6 +784,10 @@ intended. A thin app over `nisse-core`. Plans and implements software projects; produces PRs. +In 0.2's terms, a **project** below is a dev work item on a repo scope. `use +` opens or resumes a work item on a repo, and `project complete` closes the +work item. The repo, its clone, and any other work items on it continue. + ### B1. Workflow (state machine) ``` @@ -713,7 +810,8 @@ PR_OPEN ### B2. Requirements -- **One active project at a time.** Operator selects it. +- **One active project at a time** (park-and-switch, 0.2). Operator selects it; + other projects stay parked at their last position and resume where they left off. - **Operator commands** (over transport): `use `, `approve`, `reject `, `start milestone `, `status`, plus free text during planning (forwarded to the agent as answers). @@ -845,8 +943,12 @@ Special cases fold into the tiers: ### C4. Ticket-Driven Control (Vikunja) The sysadmin analog of "dev agent driven by Gitea." -- **Vikunja ticket = task of record** (durable, has status/queue/history). **Matrix = - live conversation about it** (questions, confirm-first prompts, progress). +- **Vikunja ticket = task of record** (durable, has status/queue/history): the + external record of a work item (0.2). **Matrix = live conversation about it** + (questions, confirm-first prompts, progress). +- **Scheduled checks and alert intake are duties (0.2).** Tickets they generate are + duty findings, deduplicated against open tickets, and they route through + confirm-first (C2) like any auto-generated ticket. - **Flow:** ticket filed → agent picks up → discuss/confirm over Matrix → update status as it progresses → close with result. - **Mechanics:** Vikunja REST API + webhooks (same two-direction, JSON-only pattern @@ -1113,6 +1215,27 @@ The rest resolve when reached, not now. attribution property that makes instance-per-agent-VM (F3) clean. Instance-per-agent-VM is the starting position. Migrate one server at a time, when a specific server justifies it. +- **Parked: more than one active work item (0.2).** Park-and-switch is the starting + position. The pressure to relax it will come first from the Sysadmin Agent: an + alert arriving mid-diagnosis, or a duty's scheduled run coming due while a work + item is active, has to wait for a safe point. Allowing concurrent items would + cost: + - *Serialization in the state store and control loop.* Transitions for different + items interleave, and the single orchestrator lock becomes per-item. + - *A scheduler.* Something has to decide which item's turn runs, especially with + one local inference host serving all of them. + - *Approval binding.* Two confirm-first prompts open at once make a bare `approve` + ambiguous everywhere, so per-item rooms or strict reply threading become + mandatory rather than convenient. + - *Shared-scope conflicts.* Two active work items on one repo need separate + working trees. Two on one infrastructure scope could make changes that + interfere with each other. + - *Intent ownership.* Boot reconciliation has to sort pending intents by item, and + refusing to switch no longer keeps them single-item. +- **Open: when a work item must have an external record (0.2).** Plan-gated dev work + and ticket-driven ops work already require one. Whether every gated work item + needs one (for instance, an ops question that turns into a confirm-first change) + is best settled alongside the C2 taxonomy. ---