Merge pull request 'Define the work model: duties, work items, scopes' (#38) from work-model into main
CI / checks (push) Successful in 1m1s

Reviewed-on: dongar/nisse#38
Reviewed-by: Don Garrett <dgarrett@acm.org>
This commit was merged in pull request #38.
This commit is contained in:
dongar committed 2026-09-23 20:31:29 +00:00
commit 182216296d
1 file changed
+131 -8
+131 -8
View File
@@ -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, consumers that don't exist yet. Build the Dev Agent as if the library were internal,
then lift the seams that proved real. 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 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 extraction step in the build order is a package split within one tree, not a repo
split. 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`) ## PART A — THE AGENT LIBRARY (`nisse-core`)
The shared spine. Every component here is consumed by both agents. This is the part 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 process restart. This is what makes the whole system safe to redeploy and reboot; it
blocks everything else. blocks everything else.
Must persist: active project/task, per-project state-machine position, approvals Must persist: the active work item and each work item's state-machine position
granted, pending questions, session continuity, (dev) git clone locations. (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, Disposable (never on the state volume): agent code, build artifacts/test containers,
shipped-out logs. shipped-out logs.
@@ -203,6 +286,11 @@ Requirements:
for copy-paste. Any other reply/comment is `reject <feedback>`. Reactions/emoji are for copy-paste. Any other reply/comment is `reject <feedback>`. Reactions/emoji are
never an approval path. Approvals are consumed at first sight; later edits to an never an approval path. Approvals are consumed at first sight; later edits to an
approval comment are ignored and logged. 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 - **Autonomy inside an approved unit of work:** all tool/command use auto-approved
(Claude Agent SDK `bypassPermissions` or an allow-all `canUseTool`). Escalation to (Claude Agent SDK `bypassPermissions` or an allow-all `canUseTool`). Escalation to
the human happens *only* via `ask_human`. the human happens *only* via `ask_human`.
@@ -311,9 +399,11 @@ by replaying the operational trail:
- `ts` — UTC ISO-8601 timestamp - `ts` — UTC ISO-8601 timestamp
- `agent` — `nisse-dev` | `nisse-ops` - `agent` — `nisse-dev` | `nisse-ops`
- `event` — dotted type from the taxonomy below - `event` — dotted type from the taxonomy below
- `task_id` — correlation ID for the enclosing unit of work, minted when the - `task_id` — the enclosing work item's ID (0.2), minted when the work item
unit starts (`use <project>`, milestone start, ticket pickup) and threaded opens (`use <project>`, ticket pickup, an operator question, a duty's finding)
through every event it causes; `null` only outside any unit (e.g. boot). 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 - `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
recovery. 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) | | `knowledge_retrieved` | document text as read, document ID, revision, retrieved-at (A17) |
Rules: 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 - **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
@@ -691,6 +784,10 @@ intended.
A thin app over `nisse-core`. Plans and implements software projects; produces PRs. 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
<project>` 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) ### B1. Workflow (state machine)
``` ```
@@ -713,7 +810,8 @@ PR_OPEN
### B2. Requirements ### 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 <project>`, `approve`, - **Operator commands** (over transport): `use <project>`, `approve`,
`reject <feedback>`, `start milestone <n>`, `status`, plus free text during `reject <feedback>`, `start milestone <n>`, `status`, plus free text during
planning (forwarded to the agent as answers). planning (forwarded to the agent as answers).
@@ -845,8 +943,12 @@ Special cases fold into the tiers:
### C4. Ticket-Driven Control (Vikunja) ### C4. Ticket-Driven Control (Vikunja)
The sysadmin analog of "dev agent driven by Gitea." The sysadmin analog of "dev agent driven by Gitea."
- **Vikunja ticket = task of record** (durable, has status/queue/history). **Matrix = - **Vikunja ticket = task of record** (durable, has status/queue/history): the
live conversation about it** (questions, confirm-first prompts, progress). 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 - **Flow:** ticket filed → agent picks up → discuss/confirm over Matrix → update
status as it progresses → close with result. status as it progresses → close with result.
- **Mechanics:** Vikunja REST API + webhooks (same two-direction, JSON-only pattern - **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. 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 Instance-per-agent-VM is the starting position. Migrate one server at a time, when
a specific server justifies it. 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.
--- ---