Files
nisse/docs/Development-Plan.md
T
dongarandClaude Opus 5.5 ab469b60bb
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m29s
CI / e2e (pull_request) Successful in 11m23s
M3.3: the trust dial (the agent merges approved PRs)
A project's trust.merge is operator (the default) or approved (L3, D5,
#65). Set to approved, the harness, never the model, merges a PR in the
PR watch once an approving review from a gitea.operators account is on
its current commit, its checks pass (none at all counts; pending
waits) and it's mergeable; the item then goes on as after any merge.
Each reason to hold off (failed checks, conflicts, a refused merge) is
said once per commit.

Gitea enforces it: protection requires approvals from an operators-only
whitelist and dismisses stale ones. At start, for each approved
project, the agent checks by behaviour that its own unapproved merge is
refused, with a throwaway branch and PR made through the API (never the
workspace); if it isn't refused, the operator merges until a restart.

- gitea: approvals, combined check status, a PR's head commit and
  mergeability, API-made branches and their deletion.
- Test and bring-up repositories get the protection.
- Tests: the decision (unit), the hard wall on a real Gitea
  (test_trust_live), an e2e test where the operator only approves, and
  a walk property, merges_only_approved, checked against a mutant that
  ignores which commit was approved.

Closes #65

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-10-08 13:48:16 +00:00

30 KiB
Raw Blame History

Nisse — Development Plan

Status: agreed (PR #84); reworked in PR #92 after the architecture revision (PR #90). How to get from the code as it is to the architecture in docs/Architecture.md, in what order, and how each step is accepted. The architecture says what and why; this document says how and when, and is revised as work lands.

The priority is a minimal new loop, running live. Work is ordered to reach the smallest end-to-end version of the conversational loop (M2.1) as soon as possible; everything not needed for it waits. Each milestone is checked by an automated end-to-end test against real resources, not by hand.


1. Where we are

A survey of main (2026-10-02) against the architecture. Done means it exists and matches; partial, something exists but needs changing; none, not started.

Only nisse-dev has real code; nisse-core and nisse-ops are placeholders.

Section Status Notes
L1 Work model mostly done Work items with an active pointer (M1.1); issue intake (M2.1); queue, parking and blocked items (M2.2) on the new loop; no grace period yet
L2 Agent loop mostly done The loop (Phase 2), the only one since the switch-over (M2.8): driver runs, waits, stop, abandon, question runs
L3 Authority partial Go-aheads bound to the restatement's revision, on the issue and its conversation (M2.1); action classes and the fast path, with allow bound to one dangerous action (M3.1); the trust dial, trust.merge (M3.3)
L4 Classifier mostly done On the classifier role (the local model), for what the fast path doesn't know; fails closed; overrides export as an evaluation set (M3.2). Promoting its verdicts into rules is by hand
L5 Conversation partial Matrix transport, mentions, lenient commands; on the new loop, a thread per item, named and ambiguous commands, plain-language go-aheads (M2.3); little explanation
L6 Context partial Context fitting, per-role views and orchestrator facts (M1.2); no scope or agent memory
L7 Canonical log partial Work item and actor on every event, fact (M1.1, M1.2); action_verdict (M3.1); no knowledge_retrieved
L8 Durability mostly done WAL store, intents, boot reconciliation, PR adoption, comment redelivery; to move onto the new loop
L9 Models and roles mostly done LiteLLM, eval capture; on the new loop: driver/helper/expert/classifier pinned per item, capability profiles in briefings, delegation and ask_expert with triggers (M2.6)
L10 Knowledge vault none
L11 Injection partial Operator allowlists on every channel; no classifier
L12 Secrets, logging partial Structured state-change log; secrets in .env; no per-operation handout or redaction
L13 Execution none Tools run locally in the agent's process. The token leaks into the clone (#89)
D1–D2 Workflow partial On the new loop: issue intake, restatement and go-ahead (M2.1), labels (M2.3); no documents in the repo
D3–D4 Workspace, git partial Idempotent publishing; no fresh workspace (#62), git add -A (#80)
D5 Review partial PR conversation comments only (#69), watch stall (#70)
D7–D8 Deployment, projects partial Runs from a venv as a systemd unit on this box; project entries in config (M1.4); egress not enforced
O, X none No container image yet
specs/ done for now loop.qnt (the architecture, #91); orchestrator.qnt went with control.py (M2.8)

2. The environment today

  • Supporting services (X5): Matrix, Gitea, LiteLLM, Vaultwarden, LLDAP with Authelia. The infrastructure repository exists in Gitea.
  • Local inference: llama-swap on an RTX 4090 host (320 W cap). The benchmark (PR #55) chose qwen3.8-27b for local work.
  • Models by role today: driver and helper claude-opus-5-5, expert claude-haiku-4-5. qwen3.8-27b has had no role since the executor's went (M2.8); the driver benchmark measures where it fits.
  • Bring-up runs on this development VM (scripts/bringup.sh), against test resources: a test repository, the test Matrix room, a budget-capped LiteLLM key; configuration in /root/nisse-e2e/.
  • Test repositories are made per run by the test operator (nisse-testing-operator, NISSE_OPERATOR_TOKEN), as an operator provides a project's: seeded from tests/integration/seed/, the agent a collaborator, main protected, a deploy key of its own; deleted afterwards (scripts/e2e-cleanup.sh for leftovers). They replaced the shared sandbox repository, whose state accumulated across runs.
  • A build VM for bring-up is provided by the operator by hand until Terraform (Phase 6); the agent reaches it over SSH with a key from /root/nisse-e2e/ssh/.

3. How we work

  • One PR per milestone, each starting with a short implementation plan per CLAUDE.md. A milestone marked needs discussion starts with a design discussion first.
  • Every change comes with tests, through scripts/check.sh.
  • Testing has three levels (M1.6): every PR runs the checks (unit tests across the runner's cores, 16 property walks from a moving seed, integration) and the end-to-end smoke tests; a PR labelled e2e:full, a push to main and a manual run add all 48 walks and the whole end-to-end suite; a published release runs that full set before its image is built. The end-to-end tests drive the real agent, every role on a local model (so they cost nothing per call), in a repository of the run's own, with a scripted operator; CI runs them after the checks pass. There is one runner, so a newer run for a PR or branch cancels the one it supersedes (#126). Locally only the tests being written (-k) or diagnosed (--lf); scripts/e2e.sh reports what a run spent. Manual live runs are for the new loop's first runs and phase ends. Findings are filed as issues and either fixed before moving on or deliberately deferred.
  • The Quint model moves with the loop: loop.qnt models the architecture, and its properties are tests (the property walk), changed in the same PR as a rule they cover.
  • Bring-up runs on this box as root, against test resources only, with today's gates on until the classifier and trust dial are proven (Phase 3).
  • This plan is revised as we go, in the same PR as the work that changes it.

4. Phases

Phase 0 — Housekeeping (done)

M0.1 Retire the old spec. Done. M0.2 A bring-up script. Done.

Phase 1 — Execution over SSH, and an end-to-end test

M1.1 Work items in the state store. Done.

M1.2 Transcript views. Done. (#78 confirmed live, and closed.)

M1.3 Execution over SSH. Pre-approved. Done (#93).

  • execute(host, command, *, secrets, stdin) as the only way to run anything; file tools over SSH; the host bound by the harness from the project (L13).
  • The workspace lives on the build VM: cloned there, with no token in its configuration; the push key handed to each push only (D4, #89).
  • Every secret handed out is redacted from tool output (L12).
  • Tests against a throwaway sshd container.
  • Acceptance: the existing executor and delegation tests pass over SSH; a live run of one milestone on the operator's build VM.
  • Also landed, from that run (bring-up run 3): the push credential as an HTTPS token or an SSH deploy key; flat harness branch names, after the executor made a branch named after the task; publishing only the work (#80), brought forward from M2.1; credentials kept out of every repr.

M1.6 The end-to-end test. Pre-approved. Done (#95): scripts/e2e.sh.

  • Automates bring-up run 3: the assembled agent, real models, the Gitea sandbox and a real build host, with a scripted operator that answers whatever the agent asks.
  • Asserts what was checked by hand: a PR on the sandbox containing only the work, pushed with the push credential; no credential in the clone; the planner's next turn citing the facts rather than the executor's turns.
  • Runs against the current loop now, and against the new loop from M2.1.

Phase 1 acceptance: the end-to-end test passes against the current loop. Done: it passes on the bring-up build VM.

Moved after M2.1: M1.4 (projects) and M1.5 (the container image). Neither is needed for a minimal loop running live; both are done before the switch-over (M2.8), when several projects and deployment start to matter.

Phase 2 — The new loop, as a thin slice first

Built alongside control.py, reusing the state store, log, views, publisher, pollers and recovery. The first milestone is the smallest end-to-end path; each later one widens it and extends the end-to-end test (M1.6) to cover it.

M2.1 The slice. Needs discussion (the loop's shape in code). Done (#96, #97, #98, #99; first live run: sandbox #255, follow-ups in #100).

  • An issue assigned to dev-agent → a work item (D1); a restatement in the item's thread; a go-ahead bound to its revision (L3, L5); one milestone through start_milestone / finish_milestone on a harness branch (D4); the PR; a review comment answered (D5); the merge; the item closed.
  • Includes what carries over: a fresh workspace at item start and harness-owned branches (D3, D4, #62); review intake, inline comments and the watch stall (#69, #70); the review-fix check after merge. (Publishing only the work, #80, landed early in M1.3.)
  • loop.qnt's properties for these paths become tests.
  • Acceptance: the end-to-end test runs that path on the new loop, plus a crash mid-publish; then a first manual live run.

M2.2 The queue. Pre-approved. Done (this PR). Several items: states, park-and-switch with branches pushed, next-item order, blocked items from Gitea dependencies, a closed PR handled (L1, D4). End-to-end: two issues and a dependency.

  • Switching happens as soon as the active item waits; the grace period (§6, L1) is left until a watching operator misses it. Order is "do #N next"; priority labels and driver-chosen order wait for a need.
  • The property walk covers several issues, dependencies and the operator's order, and checks that parked work is never lost (every file an open item wrote is on the workspace or in the forge).

M2.3 Threads and labels. Pre-approved. Done (this PR). Routing by thread, ambiguous commands asked about (L5); bug grace periods, discuss, design; questions and plain-language go-aheads (D1). End-to-end: a bug and a feature, labelled and not.

  • Commands naming their item ("abandon #14") apply to it anywhere; go, approve and abandon alone at top level are asked about. stop is never an answer.
  • A plain-language go-ahead is read by one model call (GoAheadReader, on the driver's model until the classifier role, M2.6); questions never reach it.
  • bug waits tuning.loop.bug_grace_s (default 10 minutes) after its restatement; any reply stops the countdown. discuss isn't offered restate until the operator has spoken; design is steered by the driver's instructions.
  • Labels are read at intake; a label changed later is not picked up yet.

M2.4 Multi-milestone work and documents. Pre-approved. Done (this PR). Build plans in the repository, ARCHITECTURE.md kept current, waiting for merges between milestones (D2, D4). End-to-end: a two-milestone feature.

  • Every milestone branch is cut from the latest default branch, so the next one builds on the merged previous and a redo drops a closed PR's work. While the item's PR is open, start_milestone is refused, the PR's state read from Gitea at that moment. finish_milestone(final): earlier PRs say "Part of #N", the last "Closes #N".
  • close_item with the item's PR still open waits for the merge.
  • Documents are the driver's instructions only; the harness never parses a plan.
  • Parking stashes uncommitted work and pushes it to <item>-parked, put back on resume, instead of committing it onto the item's branch: the walk found a half-done review fix parked onto a PR that had merged meanwhile, where it was lost.
  • Deferred: a milestone with no changes that wasn't expected to change code is still raised with the operator (D4).

M2.5 Conversations and new projects. Done (#106 and this PR). Needs discussion; the design was agreed in this PR (general scope, L1, L5; the new-project sequence, D1). Work started in conversation, record issues carrying their item's ID, the new-project flow and its project entry (D1, D8, #29). After M1.4. End-to-end: a small new project, in a repository the test operator creates under its own account for the run and deletes afterwards.

  • PR A, conversations — done (this PR).
    • A top-level message that isn't a command is a general item, threaded under it; the driver gets reply, ask_human and propose_work, with no workspace.
    • Agreed work (a go-ahead, or a plain yes, on the proposal) is filed under an intent as a record issue carrying the new item's ID. Its item starts agreed, with no second restatement. Intake skips any issue carrying an ID, and boot settles a filing cut off by a crash.
    • A conversation closes after tuning.loop.general_idle_s without a message; a reply in its thread then starts a new one that continues it.
  • PR B, new projects — done (this PR).
    • propose_project(name, purpose, decisions) goes up for a go-ahead.
    • On go, the checklist: repository, protection, collaborator, build VM, deploy key, and the project entry as YAML to fill in.
    • On "done", the agent reads its configuration again and adds the new project live (only additions). It then checks the access: the repository's permissions and protection as the agent sees them, then in practice on a throwaway branch with an empty commit (a branch push must succeed, a push to the default branch and merging its own PR must be refused); the branch and PR are removed. Anything that fails is said, and nothing starts.
    • When all is well: the tracking issue with the distilled decisions, and the project's item, agreed and labelled design, whose first milestone is ARCHITECTURE.md and the build plan. A conversation waiting on provisioning doesn't close as idle.

M2.6 Roles and capability profiles. Pre-approved. Done (this PR). driver, helper, expert, classifier (L9); escalation triggers; delegation and ask_expert on the new loop.

  • roles.driver (required since M2.8; until then, unset meant the planner's model) and roles.classifier (unset: the expert's), pinned per item; the driver and the go-ahead reader run on the item's.
  • profiles: in configuration (tools, strengths, weaknesses), used in briefings: the driver is told what its helper and expert are good at, the helper who will read its handback; never a model's name.
  • delegate(task) and ask_expert(question) for the driver. A helper works on a branch off the milestone's; its work comes back as uncommitted changes on a handback and is discarded otherwise, so harness branches never hold commits only the clone has (the walk found a fix stranded that way when a PR merged mid-help).
  • Triggers: failure_limit failed commands in a row, or the turn cap, mid- milestone, call a helper in once per milestone; if it fails too, the item waits on the operator as before. A delegation cut off by a crash is discarded at boot.
  • Evaluation capture: delegations, ask_expert and the reader's verdicts.
  • Deferred: the context-won't-fit trigger (L6); the action classifier (M3.2).

M2.7 Conversation. Pre-approved. Done (this PR). Explanations, progress in prose, asking mid-work (L5, #42, #77).

  • Questions never change state: a message ending in ? to a waiting project item is answered in a question run (reply and look-only tools), and the item goes back to the very wait it was in, so a restatement up stays up and a go binds to it.
  • A restatement gives its reasons and the main alternative; a revision says what changed (restate(changed), sent back without it).
  • Progress: the driver's and a helper's own words for a turn are posted, trimmed, at most once a minute per item; details shows the last tool calls. A message arriving mid-run is acknowledged at once.
  • Deferred: streaming (#42); the question property in specs/loop.qnt (the walk checks it against the code).

M1.4 Projects (D8). Pre-approved. Done (this PR).

  • The project entry in configuration: repository, default branch, build VM, label defaults, secrets by name with their visibility, egress additions.
  • Acceptance: the bring-up configuration is one project entry; a second project needs only a second entry.
  • projects: {name: entry}. Each project has its own clone, workspace, push credential and milestones. Issues are named project#14 when two projects could both have #14, and a bare #14 that matches two is asked about. Switching projects parks the one worked on last.
  • Secrets are by ${VAR} reference only, commands or harness. commands secrets reach run_command as environment variables, redacted from output; harness secrets are held for later. Egress is recorded, not enforced (D7).
  • Deferred: config reload without a restart (with M2.5's entry step if wanted); the trust dial's configuration (M3.3).

M1.5 The agent as a container image. Pre-approved. Done (#115; accepted 2026-10-07: v0.1.0's published image passed the whole e2e suite, 9 of 9, run with the release's own tests over it).

  • A Dockerfile for nisse-dev, stateless with a volume for state; a Gitea Actions workflow that builds it and pushes to Gitea's registry on each release tag (X4).
  • bringup.sh runs the image instead of a venv.
  • Acceptance: a tagged release produces an image; the end-to-end test runs from it.
  • The image runs as its own user; state in /var/lib/nisse-dev, configuration mounted at /etc/nisse-dev/nisse-dev.yaml, secrets as environment variables, keys mounted read-only. Its test stage carries the tests over the installed package, for scripts/e2e.sh --image REF. Images are built only on release: release.yml pushes gitea.dongar.net/dev/nisse-dev:<version> and :latest on a published release (REGISTRY_TOKEN, the dev organization's).
  • Acceptance runs the tests over a published image: docker build --target test --build-arg TEST_BASE=gitea.dongar.net/dev/nisse-dev:<version> from the release's checkout, then scripts/e2e.sh --image; the test stage adds only the test tools, so the agent under test is the one published.
  • The live bring-up's switch to the image waits for the next live test (it needs its own test repository too); bringup.sh plan shows what it will run.

M2.8 Switch over. Pre-approved, after acceptance. Done (this PR, after the live session's fixes in #119 and #122). nisse-dev runs only the loop.

  • Removed: control.py, planner.py, executor.py, approvals.py, gates.py, publish.py, poller.py, the old assembly (agent.py, folded into loop/build.py), the state store's state-machine view, their tests, the old end-to-end and live planning tests, and orchestrator.qnt with its scenarios.
  • Configuration: loop:, roles.planner, roles.executor and tuning.executor are refused with what to use instead; roles.driver is required, and tuning.driver takes the driver's turn cap and failure limit.
  • The benchmark and the capability matrix measure the driver: the loop's own system prompt and tools, the task's plan as the agreed restatement. Task plans end with finish_milestone, so earlier runs show as (old).
  • A delegation on the loop records what its helper's alias resolved to (L9), as the old path's did.

Phase 3 — Authority

M3.1 Action classes and the fast path. Needs discussion. Done (this PR). Harmless, contained and dangerous actions per agent (P3, D6), static fast-path rules.

  • nisse_core.actions (the shared part, nisse-core's first code): Action, Verdict, and reading shell commands statically, segment by segment (cd, wrappers like sudo and timeout, sh -c); what can't be read that way is unknown, and a line is as bad as its worst command.
  • nisse_dev.loop.actions, the rule table: reading, checks and tests harmless; workspace edits, local git and installs on the build VM contained; pushing, other hosts, uploads, publishing, the forge's tools, deleting outside the workspace and wrecking the VM dangerous; changing branches by hand refused.
  • Every execution call is screened before it runs, the driver's and the helper's. A dangerous one isn't run: the item waits on the operator with a question rendered from the action, and allow lets exactly that action run once. A helper can't ask, so it is refused and told to hand back.
  • Every verdict is an action_verdict event (no model sees it) and an oplog line; details shows the counts and every action not simply run.
  • Decided: unknown actions run, logged, until the classifier (M3.2), since this is a project in progress, not production.
  • Deferred: the classifier and in-scope judgement (M3.2), the trust dial (M3.3), egress enforcement (X2).

M3.2 The classifier. Needs discussion. Done (this PR). L4, its model, fail-closed verdicts, overrides as an evaluation set.

  • nisse_dev.loop.classifier: one model call per action the fast path calls unknown, on the item's pinned classifier role. It sees the action and the agreed scope (project, repository, issue number, the restatement the operator said go to), built in code: never the transcript, the issue's text, tool output or the driver's reasoning. Its verdict is a tool call: safe runs, in_scope runs and is reported in the thread, needs_discussion asks as a dangerous action does. An error, no verdict or an unknown one fails closed.
  • Decided: no different-model requirement (separate context is what L4's independence needs; the verdicts record the model, so correlated misses can be measured); the local model to start with; verdicts cached per item, agreed revision and action.
  • action_verdict gains source (fast path or classifier), model and the classifier's exact input; details shows the source. An allow after the classifier asked is an override. scripts/classifier-evals.py exports the judgements from a store (read-only); tests/integration/classifier_cases.yaml is the seed set, run per model with --capability-models (qwen3.8-27b-nothink and claude-haiku-4-5: 9 of 9 each).
  • The fast path's delete rule: on the build VM, deleting is contained; only what breaks the host stays dangerous (decided with the operator).
  • Deferred: promoting verdicts the classifier keeps agreeing on into rules (by hand, from exported cases); the trust dial (M3.3).

M3.3 The trust dial. Needs discussion. Done (this PR). Per project (D8); agent merging behind protection it can't satisfy, verified by a test (D4, D5, #65).

  • trust.merge per project: operator (the default) or approved: the harness (never the model) merges a PR in the PR watch once an approving review from a gitea.operators account is on the PR's current commit, its checks pass (no checks at all counts as passing; pending waits) and it's mergeable; then the item goes on as after any merge. Each reason to hold off is said once.
  • Protection, verified at start per approved project: a throwaway branch and PR made through the API (never the workspace), whose merge must be refused; if it isn't, the operator merges until a restart. Real Gitea is tested too (test_trust_live.py): refused unapproved, merged once approved, refused again after a commit lands on the approved PR.
  • Test and bring-up repositories get the protection: approvals from an operators-only whitelist, stale approvals dismissed, the agent on the merge whitelist.
  • Decided: two levels only (no merging on checks alone, D5); verification by behaviour, since a collaborator can't read protection and must never set it. Deferred: a project-setup script for the operator (protection, labels) and a label check, #130.

Phase 4 — The environment

M4.1 Toolchain preflight, M4.2 recording installs, M4.3 environment repair. Pre-approved. All on the build VM (D3).

Phase 5 — Memory

M5.1 The vault. Pre-approved. The folder, per-agent mounts, control tools, atomic writes and provenance (L10).

M5.2 Agent memory (needs discussion) and M5.3 scope memory (pre-approved): when notes are written and read, verbatim corrections (L6).

M5.4 The operator's copy. Pre-approved. Syncthing, directions per folder.

Phase 6 — Production

M6.1 The agent host. Needs discussion. Containers, volumes, snapshots, per-agent networks (X1, X2, X4).

M6.2 Build VMs by Terraform. Needs discussion. The template, VLAN and egress (D7, X2), collection when idle.

M6.3 Secrets and identity. Pre-approved. Per-agent secret sets on the host, LLDAP principal (L12).

M6.4 Delivery. Pre-approved. Watchtower, restarts gated on an idle flag (X4).

M6.5 The browser tool. Pre-approved. On the internal test network (D7).

Phase 7 — nisse-core and nisse-ops

M7.1 Extract nisse-core. M7.2 MCP servers (#37). M7.3 Approved step sequences (O2). M7.4 Ops intake and duties from the infrastructure repository (O5). M7.5 The ops tool VM and deployment (O6). M7.6 Requests between agents: nisse-dev asking for build VMs (L1, O5). All needs discussion except M7.1.

Not yet scheduled

From the architecture's open questions and older issues: filing issues from conversation; stacked PRs; releases; nisse-ops issuing credentials; a server database; recovering the agent host (an ops issue); more than one active work item; vision; one local model for every agent; several forges (#26); a room per project (#28).


5. Open issues and where they land

Issue Lands in
#26 multiple forges Not yet scheduled
#27 per-project state, clone on use M1.3 (clone on the build VM), M1.4 (projects)
#28 room per project Not yet scheduled (threads, M2.3, may make it unnecessary)
#29 create a project from scratch M2.5 (ownership settled by D4, D8)
#37 MCP servers M7.2
#42 per-turn commentary M2.7
#62 branch at execution start M2.1
#65 agent merges approved PRs M3.3
#69 inline review comments M2.1
#70 watch stall M2.1
#77 conversational agent M2.3 (questions), M2.7

6. Implementation notes

Mechanism detail moved out of docs/Architecture.md in PR #90, which keeps only decisions and their reasons. Keyed by the architecture section each note implements; a milestone touching that section uses the note, and changes it here if the implementation differs.

  • L1 switching: the grace period before switching away from an item waiting on the operator defaults to 10 minutes. When the driver chooses the next ready item and the choice fails or is invalid, fall back to the oldest.
  • L3 revision stamping: in Matrix, a reply is stamped with the revision whose message had been sent when it arrived; on a Gitea issue, with the latest proposal comment older than the reply.
  • L5 transport: the Transport interface is send, ask and receive. Intake backoff is exponential, capped at a few minutes.
  • L6 fitting order: first shorten old tool results to their first and last lines, then drop the oldest events with a note saying so; never the system prompt, the current plan or the most recent turns. View labels: [orchestrator], [operator, Matrix], [operator, comment on PR #N], [helper report], [driver] narration.
  • L7 rendering: content is stored as bytes or local paths and uploaded or inlined at render time, never as provider handles. Token budgeting and prompt-cache breakpoints are render-time concerns, never stored. Compaction summaries are first-class events so a model change never inherits opaquely compacted history. A reasoning "session" is a delegation, including one resumed after a crash.
  • L8 state store: SQLite in WAL mode, each change committed and fsynced before the loop continues. A delegation's cut is clean when its last event is a completed result, its branch is at the expected commit, its caps are unbreached and external truth matches the log. "Anything else" offers continue, re-plan or abandon.
  • L9 local models: request tool_choice only of models whose server declares it can enforce it; otherwise the loop enforces the contract (an explicit finish, prose treated as a violation). Briefings use the capability profile, never a model name or "weaker model".
  • L10 vault: atomic writes are write-then-rename. Obsidian's settings folder (and .git, if history is kept) are not synced. A failed write is retried, or recorded and reconciled later.
  • L12: secrets reach a command by environment or stdin. Headless agents get an Authelia single-factor exception scoped by source address, group and target. Logs are shipped to the host journal, not kept in containers.
  • D3 preflight: a project's needs are read from its files (Cargo.toml, pyproject.toml, package.json) or a declaration in the repository.
  • D4 publishing: standard excludes are configured in each clone, and only what the work produced is committed. The push credential is handed to each push only: a deploy key over stdin to a short-lived ssh-agent on the build VM, or a push-only account's HTTPS token as that command's secrets, read by a credential helper that names environment variables, never values.
  • D7: test services use ports 18000–18099, so opening access later is a firewall change. The inference host is behind LiteLLM, so it is not a second egress exception.
  • X4: the agent host's state dataset is ZFS, snapshotted on a schedule and on each approval. Tool VM templates use Terraform's bpg/proxmox provider with cloud-init. Containers restart unless stopped.
  • X5: the local inference host is llama-swap on an RTX 4090 machine.