Add control loop, gating primitives, and transport interface (A4/B1) #6

Merged
dongar merged 1 commits from control-loop-gating into main 2026-09-06 15:10:24 +00:00
Collaborator

Step-2 PR A from the approved plan: the approval loop end to end with a stub agent — no LLM, no network. PR B (MatrixTransport) follows once the @nisse-dev bot account, room, and credentials exist.

  • transport.py — the A1 Transport interface (send_message, send_approval_request, ask_question, on_message) plus InMemoryTransport for tests and the stub loop. No platform SDK outside an implementation.
  • gates.py — A4 approval primitives: monotonic rev-N tags, approvals bind to a revision (a stale tag gets a notice and is never applied), the typed word approve only ("approve, but…" is rejection feedback), per-channel operator allowlist. One deviation from the plan as written: instead of a vacuous ApprovalSource protocol, the seam is Gate.evaluate(sender, text, channel) — the Gitea/Vikunja pollers will simply call it with their channel name; an interface with real methods can grow when the second source exists.
  • control.py — the B1 state machine subset (IDLE → PLANNING_ARCH → AWAIT_ARCH_SIGNOFF → MILESTONE_PLANNING → AWAIT_MILESTONE_SIGNOFF → EXECUTING, plus AWAIT_INPUT). Gating is structural: the agent's only actions are SubmitPlan (ends its turn) and AskHuman; there is no proceed tool. Gate state, revision counters, task_id, and pending questions all checkpoint through StateStore; boot resumes an open gate with a notice, or gates to AWAIT_INPUT — never blind-resumes. EXECUTING is a stub endpoint for now.
  • gate.open / gate.approve / gate.reject events complete the A9 taxonomy.
  • CLAUDE.md — records the plan-first working convention we adopted.

Operator commands wired: use <project>, approve / approve rev-N, free-text rejection feedback, start milestone <n> (only after milestone-plan approval), status, and free text as answers to agent questions.

Tests: 15 new unit tests (gate semantics, full happy path, reject-and-revise, stale approvals, unauthorized senders, ask_human flow), 2 integration tests (restart mid-gate resumes with revision history and task correlation intact; an unresolved intent gates the whole boot). 45 unit + 5 integration green locally.

🤖 Generated with Claude Code

Step-2 **PR A** from the approved plan: the approval loop end to end with a stub agent — no LLM, no network. PR B (MatrixTransport) follows once the `@nisse-dev` bot account, room, and credentials exist. - **`transport.py`** — the A1 `Transport` interface (`send_message`, `send_approval_request`, `ask_question`, `on_message`) plus `InMemoryTransport` for tests and the stub loop. No platform SDK outside an implementation. - **`gates.py`** — A4 approval primitives: monotonic `rev-N` tags, approvals bind to a revision (a stale tag gets a notice and is never applied), the typed word `approve` only ("approve, but…" is rejection feedback), per-channel operator allowlist. One deviation from the plan as written: instead of a vacuous `ApprovalSource` protocol, the seam is `Gate.evaluate(sender, text, channel)` — the Gitea/Vikunja pollers will simply call it with their channel name; an interface with real methods can grow when the second source exists. - **`control.py`** — the B1 state machine subset (`IDLE → PLANNING_ARCH → AWAIT_ARCH_SIGNOFF → MILESTONE_PLANNING → AWAIT_MILESTONE_SIGNOFF → EXECUTING`, plus `AWAIT_INPUT`). Gating is structural: the agent's only actions are `SubmitPlan` (ends its turn) and `AskHuman`; there is no proceed tool. Gate state, revision counters, `task_id`, and pending questions all checkpoint through `StateStore`; boot resumes an open gate with a notice, or gates to `AWAIT_INPUT` — never blind-resumes. `EXECUTING` is a stub endpoint for now. - **`gate.open` / `gate.approve` / `gate.reject`** events complete the A9 taxonomy. - **CLAUDE.md** — records the plan-first working convention we adopted. Operator commands wired: `use <project>`, `approve` / `approve rev-N`, free-text rejection feedback, `start milestone <n>` (only after milestone-plan approval), `status`, and free text as answers to agent questions. Tests: 15 new unit tests (gate semantics, full happy path, reject-and-revise, stale approvals, unauthorized senders, ask_human flow), 2 integration tests (restart mid-gate resumes with revision history and task correlation intact; an unresolved intent gates the whole boot). 45 unit + 5 integration green locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dev-agent added 1 commit 2026-09-05 21:28:21 +00:00
Add control loop, gating primitives, and transport interface (A4/B1)
CI / checks (push) Successful in 9s
CI / checks (pull_request) Successful in 9s
a251104a44
Step-2 PR A: the approval loop end to end with a stub agent, no LLM
and no network.

- transport.py: Transport interface per A1 (send_message,
  send_approval_request, ask_question, on_message) with an
  InMemoryTransport; no platform SDK outside an implementation
- gates.py: PlanRevision with monotonic rev-N tags; Gate.evaluate
  enforces revision binding (stale tags refused with a notice),
  operator allowlisting, and approve-word-only semantics; any other
  reply is rejection feedback. Channel seam ready for Gitea/Vikunja
  approval sources
- control.py: B1 state machine subset with structural gating — the
  agent's only actions are SubmitPlan (ends its turn) and AskHuman;
  no proceed tool exists. Gate state, revision counters, task_id,
  and pending questions checkpoint through StateStore, and boot
  resumes an open gate or gates to AWAIT_INPUT, never blind-resumes
- gate.open/approve/reject A9 events complete the log taxonomy
- CLAUDE.md: record the plan-first working convention
- Tests: 15 unit (gates + orchestrator flows), 2 integration
  (mid-gate restart with revisions intact; unresolved intents gate
  the whole boot)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -0,0 +26,4 @@
EXECUTING = "EXECUTING"
AWAIT_INPUT = "AWAIT_INPUT"
TRANSITIONS = {
Owner

Should there be a transition from EXECUTING to IDLE? Or some kind of FINISHED or FINAL APPROVAL state?

For example, how will code review feedback be handled?

Should there be a transition from EXECUTING to IDLE? Or some kind of FINISHED or FINAL APPROVAL state? For example, how will code review feedback be handled?
Author
Collaborator

Should there be a transition from EXECUTING to IDLE? Or some kind of FINISHED or FINAL APPROVAL state? For example, how will code review feedback be handled?

Yes — but via PR_OPEN, which this PR deliberately stops short of. The full B1 machine in the spec continues:

EXECUTING → PR_OPEN → (review loop) → on acceptance → next milestone, or project complete

So the eventual additions to TRANSITIONS are (EXECUTING, PR_OPEN), (PR_OPEN, MILESTONE_PLANNING) for the next milestone, and (PR_OPEN, IDLE) for project completion. There's no separate FINISHED state in the spec: PR acceptance in Gitea is the final approval for a milestone — the merged PR is the durable record — and completion just returns to IDLE, ready for the next use <project>.

Code review feedback is the PR_OPEN review loop, and it belongs to the Gitea layer (B2, build-order step 6): the poller ingests PR comments and commits as structured JSON (never through a chat bridge), and the agent reacts with fix commits while the PR stays open. That work also needs the executor (step 5), which is why EXECUTING is a stub endpoint here — I kept TRANSITIONS limited to edges the orchestrator can actually traverse today, so the table stays truthful rather than aspirational.

If you'd like, I can add a short comment in control.py at the TRANSITIONS table noting the deferred PR_OPEN edges so the shape of the full machine is visible at the definition site.

🤖 Generated with Claude Code

> Should there be a transition from EXECUTING to IDLE? Or some kind of FINISHED or FINAL APPROVAL state? For example, how will code review feedback be handled? Yes — but via `PR_OPEN`, which this PR deliberately stops short of. The full B1 machine in the spec continues: ``` EXECUTING → PR_OPEN → (review loop) → on acceptance → next milestone, or project complete ``` So the eventual additions to `TRANSITIONS` are `(EXECUTING, PR_OPEN)`, `(PR_OPEN, MILESTONE_PLANNING)` for the next milestone, and `(PR_OPEN, IDLE)` for project completion. There's no separate FINISHED state in the spec: PR acceptance in Gitea *is* the final approval for a milestone — the merged PR is the durable record — and completion just returns to IDLE, ready for the next `use <project>`. Code review feedback is the `PR_OPEN` review loop, and it belongs to the Gitea layer (B2, build-order step 6): the poller ingests PR comments and commits as structured JSON (never through a chat bridge), and the agent reacts with fix commits while the PR stays open. That work also needs the executor (step 5), which is why EXECUTING is a stub endpoint here — I kept `TRANSITIONS` limited to edges the orchestrator can actually traverse today, so the table stays truthful rather than aspirational. If you'd like, I can add a short comment in control.py at the TRANSITIONS table noting the deferred `PR_OPEN` edges so the shape of the full machine is visible at the definition site. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dongar approved these changes 2026-09-06 15:10:20 +00:00
dongar merged commit b05a402c02 into main 2026-09-06 15:10:24 +00:00
dongar deleted branch control-loop-gating 2026-09-06 15:10:24 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dev/nisse#6