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.
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)
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>
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?
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.
> 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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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-devbot account, room, and credentials exist.transport.py— the A1Transportinterface (send_message,send_approval_request,ask_question,on_message) plusInMemoryTransportfor tests and the stub loop. No platform SDK outside an implementation.gates.py— A4 approval primitives: monotonicrev-Ntags, approvals bind to a revision (a stale tag gets a notice and is never applied), the typed wordapproveonly ("approve, but…" is rejection feedback), per-channel operator allowlist. One deviation from the plan as written: instead of a vacuousApprovalSourceprotocol, the seam isGate.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, plusAWAIT_INPUT). Gating is structural: the agent's only actions areSubmitPlan(ends its turn) andAskHuman; there is no proceed tool. Gate state, revision counters,task_id, and pending questions all checkpoint throughStateStore; boot resumes an open gate with a notice, or gates toAWAIT_INPUT— never blind-resumes.EXECUTINGis a stub endpoint for now.gate.open/gate.approve/gate.rejectevents complete the A9 taxonomy.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
@@ -0,0 +26,4 @@EXECUTING = "EXECUTING"AWAIT_INPUT = "AWAIT_INPUT"TRANSITIONS = {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:So the eventual additions to
TRANSITIONSare(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 nextuse <project>.Code review feedback is the
PR_OPENreview 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 keptTRANSITIONSlimited 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_OPENedges so the shape of the full machine is visible at the definition site.🤖 Generated with Claude Code