- 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>
os.kill(pid, 0) succeeds on a zombie, and a killed orphan stays a
zombie until reaped — which never happens inside a CI container with
no init. The test read that as "still alive" and failed there while
passing locally. It now reads the process state from /proc, with a
control assertion so a broken liveness check cannot pass silently.
The group kill itself was correct.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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>