- Dockerfile: built with uv into /opt/nisse (installed, not editable), run on python:3.12-slim with openssh-client as user nisse; stateless, with state in a /var/lib/nisse-dev volume, config mounted at /etc/nisse-dev/nisse-dev.yaml, secrets as environment variables, keys mounted read-only. 202 MB. Its `test` stage adds the tests and their tools over the installed package (UV_NO_SYNC, so `uv run` never swaps in the source tree); the default target is the agent. - scripts/e2e.sh --image REF runs the suite inside an image's test stage, on its own sshd. - CI builds both images on every PR (nothing pushed); release.yml pushes gitea.dongar.net/dongar/nisse-dev:<tag> and :latest on a v* tag, with the REGISTRY_TOKEN secret. - scripts/bringup.sh builds the code under test into an image and runs it in the systemd unit (root, $E2E mounted at its own path); `plan` prints what it would run; the build-VM set-aside reads the projects: config. Checked: the image refuses to start without a config, runs as uid 10001; one e2e test passed from the test stage (2m45s, $0). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
422 lines
24 KiB
Markdown
422 lines
24 KiB
Markdown
# 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 | partial | `control.py`, a fixed state machine with no-stranding, `stop`, `abandon`. Replaced by the new loop (Phase 2) |
|
||
| **L3** Authority | partial | Revision binding and arrival stamping (`gates.py`); scope is still plan approval, not the issue and its conversation |
|
||
| **L4** Classifier | none | |
|
||
| **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); no `classifier_verdict`, `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 | `orchestrator.qnt` (current loop), `loop.qnt` (the architecture, #91) |
|
||
|
||
---
|
||
|
||
## 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:** executor `qwen3.8-27b`, planner and helper
|
||
`claude-opus-5-5`, expert `claude-haiku-4-5`. Phase 2 moves to driver and helpers.
|
||
- **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`.
|
||
- **The end-to-end test runs on every PR** (M1.6): the real agent, every role on a
|
||
local model (so it costs nothing per call), a repository of the run's own and a
|
||
real build host, driven by a scripted operator. CI runs it as its own job on pull
|
||
requests, after the checks pass. There is one runner, and a run a newer push
|
||
supersedes has to be cancelled by hand. 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 models move with the loop:** `orchestrator.qnt` while `control.py`
|
||
runs, `loop.qnt` for the new loop, whose properties become tests as each piece
|
||
lands (Phase 2).
|
||
- **`control.py` gets fixes only,** and only for what blocks a live run. New
|
||
behaviour goes into the new loop.
|
||
- **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` (unset: 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}`. The old loop runs exactly one project and refuses
|
||
more. On the new loop 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.* **Built** (this PR);
|
||
accepted once a tag's image passes the e2e suite.
|
||
- 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`. CI builds both on every PR;
|
||
`release.yml` pushes `gitea.dongar.net/dongar/nisse-dev:<tag>` and `:latest`
|
||
(`REGISTRY_TOKEN`).
|
||
- 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.* `nisse-dev` runs the new
|
||
loop; `control.py`, its tests and `orchestrator.qnt` are removed.
|
||
|
||
### Phase 3 — Authority
|
||
|
||
**M3.1 Action classes and the fast path.** *Needs discussion.* Harmless, contained
|
||
and dangerous actions per agent (P3, D6), static fast-path rules.
|
||
|
||
**M3.2 The classifier.** *Needs discussion.* L4, its model, fail-closed verdicts,
|
||
overrides as an evaluation set.
|
||
|
||
**M3.3 The trust dial.** *Needs discussion.* Per project (D8); agent merging behind
|
||
protection it can't satisfy, verified by a test (D4, D5, #65).
|
||
|
||
### 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]`, `[executor]` 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.
|