Publish finished milestones as PRs (B2/A3) — step 6a, part 2 #16

Merged
dongar merged 1 commits from pr-open into main 2026-09-07 20:45:57 +00:00
Collaborator

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

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)
dev-agent added 1 commit 2026-09-07 20:43:35 +00:00
Publish finished milestones as PRs (B2/A3) — step 6a, part 2
CI / checks (push) Successful in 41s
CI / checks (pull_request) Successful in 38s
3117565928
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>
dongar approved these changes 2026-09-07 20:45:53 +00:00
dongar merged commit 914bc26221 into main 2026-09-07 20:45:57 +00:00
dongar deleted branch pr-open 2026-09-07 20:45:57 +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#16