On the first live run the executor committed its milestone itself, as
its plan asked. The publisher only published a commit it made, found a
clean tree, reported "no changes to publish", and left the agent in
EXECUTING, where every message is answered "busy".
- publish(): with nothing left to commit, publish anyway when the branch
has commits origin/<base> lacks; report no changes only when it has
none.
- push_followup(): same for review fixes, comparing HEAD with what the
remote branch holds (ls-remote; pushes go by URL, so there is no
tracking ref).
- Nothing to publish now gates to AWAIT_INPUT, and the reply returns to
the still-approved milestone plan.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
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>