Commit Graph
7 Commits
Author SHA1 Message Date
dongarandClaude Opus 5 d508e9a9b7 Externalise plans to Gitea and accept approvals there (A4, Part H)
CI / checks (push) Successful in 43s
CI / checks (pull_request) Successful in 41s
- approvals.py: PlanBoard opens one issue per project and posts each
  plan revision as a comment carrying its rev tag. The state store
  keeps only a pointer, so gate state can be re-derived from Gitea
  after a state-volume loss — external truth wins
- Gitea comments are a second approval source: revision binding, the
  typed word approve, and first-approval-wins work exactly as over
  transport, with a per-channel allowlist because a Gitea login is
  not a Matrix MXID
- Outcomes are echoed to the other channel, so the two records cannot
  silently disagree about what was approved
- A Gitea outage degrades durability of the record, not the ability
  to approve: planning proceeds and Matrix remains a complete channel
- Live test now asserts the server refuses the agent's own merge and
  its push to the protected branch. That property was previously
  true only because our code did not attempt it; branch protection
  makes it structural, and this test fails if the protection is ever
  relaxed
- Fixed a race in the process-group test: killing a group is
  asynchronous and the grandchild is an orphan, so its exit is not
  synchronised with our return; the test now waits for death

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 22:15:08 +00:00
dongarandClaude Opus 5 affdb9e8aa Add PR comment intake and the review loop (B2) — step 6b
CI / checks (push) Successful in 42s
CI / checks (pull_request) Successful in 43s
- poller.py: CommentPoller polls PR comments rather than taking
  webhooks, keeping the VM outbound-only (B3). On an outage it backs
  off exponentially, caps, and retries indefinitely while logging:
  polling is pure intake, so an outage delays a reaction but never
  fails a task or forces a transition
- prime() marks the existing thread as seen without delivering it, so
  starting a watch does not re-address feedback already handled
- Review loop: a comment in PR_OPEN enters the canonical transcript,
  the executor responds, and the fix is pushed to the same branch with
  a reply on the PR. No intent is recorded for a follow-up push — the
  PR already exists, so a repeated push is harmless, unlike opening a
  second PR
- Comment authors are checked against a separate Gitea operator
  allowlist: a PR comment is an instruction channel into an agent that
  writes code, so it is operator-scoped, and a Gitea login is not a
  Matrix MXID (A4 per-channel allowlists)
- Orchestrator now holds a lock: transport and the poller deliver on
  separate threads, and state transitions must not interleave
- Backoff timings are injectable, so the outage path is tested in
  milliseconds instead of being untestable

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 21:59:50 +00:00
dongarandClaude Opus 5 3117565928 Publish finished milestones as PRs (B2/A3) — step 6a, part 2
CI / checks (push) Successful in 41s
CI / checks (pull_request) Successful in 38s
Completes the B1 loop past EXECUTING, and answers the question raised
reviewing #6: PR acceptance is a milestone's final gate, so there is
no separate FINISHED state — completion returns to milestone planning
or to IDLE.

- publish.py: MilestonePublisher commits the work on a task-namespaced
  branch, pushes, and opens a PR carrying the approved plan. Intent is
  recorded before the first externally visible act, so a crash
  anywhere in the sequence leaves a durable record and boot
  reconciliation asks Gitea whether the PR exists rather than opening
  a second one
- Nothing to commit is reported, not raised: the executor may
  legitimately conclude there was nothing to change
- control.py: PR_OPEN state with EXECUTING -> PR_OPEN and
  PR_OPEN -> {MILESTONE_PLANNING, IDLE}; operator commands
  'next milestone' and 'project complete'
- A configured publisher always contributes its own reconciler at
  boot, so an interrupted publish is settled against external truth
  without the caller having to remember to register it
- A publish failure gates to the operator rather than retrying into a
  possible duplicate

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 20:43:20 +00:00
dongarandClaude Opus 5 bacf65dbe2 Fix AWAIT_INPUT deadlock and make the live milestone test robust
CI / checks (push) Successful in 36s
CI / checks (pull_request) Successful in 29s
Found while chasing a flaky live test, but the flakiness was a real
bug: only the ask_human path recorded a pending item, so AWAIT_INPUT
reached any other way — planning failure, stopped milestone, boot
reconciliation — refused every operator reply with "no pending
question to answer" and the agent could never be recovered. The
operator had no way back short of wiping state.

Every path into AWAIT_INPUT now records what a reply should resume,
and answering dispatches on it: planning failures and questions
resume planning, a stopped milestone returns to its still-approved
gate, and the fallback says what commands work instead of
dead-ending. Two regression tests cover the recovery paths.

The live test also scripted a fixed approve/approve/start sequence,
which a real model breaks by legitimately calling ask_human at a
gate. It now plays the operator adaptively, responding to whatever
the agent actually did. Five consecutive live runs pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:46:07 +00:00
dongarandClaude Opus 5 0fd0b2e4a5 Add executor tool loop (A12/A15) — step 5b
CI / checks (push) Failing after 13s
CI / checks (pull_request) Failing after 13s
- tools.py: four flat tools (read_file, write_file, list_dir,
  run_command) per A15's reduced manifest. Deliberately unsandboxed:
  the VM is the trust boundary (B3) and tool use is auto-approved
  inside an approved unit of work (A4), so path confinement would add
  failure modes without adding safety. What is enforced is
  operational — timeout with process-group kill so a backgrounded
  server cannot outlive its command, output truncation so a build log
  cannot eat the context window, closed stdin so prompts fail fast,
  and a scrubbed environment so project code never sees the agent's
  credentials
- executor.py: renders the log, calls the executor role with the
  reduced manifest, dispatches tool calls, and checkpoints every
  result (A13). Escalation is observable, never self-assessed (A12):
  consecutive command failures and a hard turn cap. Failed reads do
  not count — looking around is not failing
- Until delegation exists (step 7), escalation gates to the human
  with the failures attached
- control.py: start milestone now runs the executor; a stopped
  milestone gates to AWAIT_INPUT

Two bugs found by running it live against real models:

- A turn-ending gate tool (submit_plan) left a tool_call with no
  tool_result, which providers reject outright, so every planning
  turn after the first failed with a 400. The planner now records the
  gate as the call's outcome, which is also the truthful transcript
- The renderer now emits a placeholder for any tool_call lacking a
  result, so a crash between call and result (A3) cannot poison the
  transcript permanently

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:35:39 +00:00
dongarandClaude Opus 5 cf85648719 Add LLM-backed planning agent (A4/A11/A13) — step 5a
CI / checks (push) Successful in 12s
CI / checks (pull_request) Successful in 11s
- planner.py: LLMPlanningAgent offers exactly two tools, submit_plan
  and ask_human, so gating stays structural at the tool layer; no
  proceed tool exists. System prompt requires explicit, low-inference
  plans because a weaker model executes them (A13)
- Model output is appended to the canonical log as it arrives, before
  being interpreted, so a crash mid-planning leaves a truthful
  transcript
- A bad turn (prose instead of a tool call, malformed arguments, or a
  failed call) retries once, then raises PlanningError; the
  orchestrator gates to AWAIT_INPUT rather than inventing a plan
- Orchestrator: appends every operator message, plan_submitted, and
  gate_opened to the transcript, so re-planning sees rejection
  feedback as conversation rather than out-of-band parameters; pins
  the A11 role mapping at task start
- llm.py: non-JSON or non-object tool arguments now raise LLMError
  instead of escaping as JSONDecodeError — observed from llama3.2,
  which emits truncated argument strings
- Live planning test is opt-in on NISSE_LIVE_PLANNER_MODEL: planning
  escalates unconditionally (A12) and the local model does not hold
  the tool-call contract, so it awaits a frontier route

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 14:47:12 +00:00
dongarandClaude Fable 5 a251104a44 Add control loop, gating primitives, and transport interface (A4/B1)
CI / checks (push) Successful in 9s
CI / checks (pull_request) Successful in 9s
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>
2026-09-05 21:28:07 +00:00