Completes the B1 loop past EXECUTING, and answers the question you raised reviewing #6 — where the EXECUTING → IDLE edge lives and whether a FINISHED state is needed. It isn't: PR acceptance is the milestone's final gate, so completion returns to milestone planning or to IDLE.
publish.py — MilestonePublisher commits the work on a task-namespaced branch (<task-id>/milestone-N, so re-running a project against the same repo cannot collide with an earlier run), pushes, and opens a PR carrying the approved plan in its body.
The A2/A3 ordering is the substance. Intent is recorded before the first externally visible act, so a crash anywhere in push-then-open leaves a durable record, and boot reconciliation asks Gitea whether the PR exists rather than opening a second one. A test simulates the dangerous window directly: PR opened, process dies before the intent is committed, next boot must answer "already done".
A configured publisher always contributes its own reconciler at boot, rather than relying on a caller to remember to register it — the failure mode otherwise is silent and only shows up after a crash.
Nothing to commit is reported, not raised. The executor may legitimately conclude there was nothing to change; that is a fact for the operator, not an error.
A publish failure gates to the operator instead of retrying into a possible duplicate — and a test confirms the operator can get back out of that gate, guarding the deadlock we fixed in #12.
PR_OPEN with EXECUTING → PR_OPEN and PR_OPEN → {MILESTONE_PLANNING, IDLE}, driven by next milestone and project complete.
Still to come in 6b: reacting to review comments (the polling intake), and Gitea as an approval source. The PR body has a slot for a running test service's URL per B2, but nothing reports one yet — that arrives with the browser tool.
16 new unit tests; 157 unit + 10 integration green locally.
Completes the B1 loop past `EXECUTING`, and answers the question you raised reviewing #6 — where the `EXECUTING → IDLE` edge lives and whether a FINISHED state is needed. It isn't: **PR acceptance is the milestone's final gate**, so completion returns to milestone planning or to `IDLE`.
- **`publish.py`** — `MilestonePublisher` commits the work on a task-namespaced branch (`<task-id>/milestone-N`, so re-running a project against the same repo cannot collide with an earlier run), pushes, and opens a PR carrying the approved plan in its body.
- **The A2/A3 ordering is the substance.** Intent is recorded *before* the first externally visible act, so a crash anywhere in push-then-open leaves a durable record, and boot reconciliation asks Gitea whether the PR exists rather than opening a second one. A test simulates the dangerous window directly: PR opened, process dies before the intent is committed, next boot must answer "already done".
- **A configured publisher always contributes its own reconciler at boot**, rather than relying on a caller to remember to register it — the failure mode otherwise is silent and only shows up after a crash.
- **Nothing to commit is reported, not raised.** The executor may legitimately conclude there was nothing to change; that is a fact for the operator, not an error.
- **A publish failure gates to the operator** instead of retrying into a possible duplicate — and a test confirms the operator can get back *out* of that gate, guarding the deadlock we fixed in #12.
- **`PR_OPEN`** with `EXECUTING → PR_OPEN` and `PR_OPEN → {MILESTONE_PLANNING, IDLE}`, driven by `next milestone` and `project complete`.
Still to come in 6b: reacting to review comments (the polling intake), and Gitea as an approval source. The PR body has a slot for a running test service's URL per B2, but nothing reports one yet — that arrives with the browser tool.
16 new unit tests; 157 unit + 10 integration green locally.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
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.
Completes the B1 loop past
EXECUTING, and answers the question you raised reviewing #6 — where theEXECUTING → IDLEedge lives and whether a FINISHED state is needed. It isn't: PR acceptance is the milestone's final gate, so completion returns to milestone planning or toIDLE.publish.py—MilestonePublishercommits the work on a task-namespaced branch (<task-id>/milestone-N, so re-running a project against the same repo cannot collide with an earlier run), pushes, and opens a PR carrying the approved plan in its body.PR_OPENwithEXECUTING → PR_OPENandPR_OPEN → {MILESTONE_PLANNING, IDLE}, driven bynext milestoneandproject complete.Still to come in 6b: reacting to review comments (the polling intake), and Gitea as an approval source. The PR body has a slot for a running test service's URL per B2, but nothing reports one yet — that arrives with the browser tool.
16 new unit tests; 157 unit + 10 integration green locally.
🤖 Generated with Claude Code