Adopt an already-published PR instead of publishing again
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m12s

Fixes counterexample 5 from specs/orchestrator.qnt. A crash after the
PR was opened but before PR_OPEN was recorded left the agent in
EXECUTING; the restart question's 'continue' and 'start' then published
again, Gitea refused a second PR, and there was no way back to PR_OPEN.
On the live run the executor model happened to notice the existing
work; recovery must not depend on that.

- Boot (A3 step 4): in EXECUTING, if Gitea shows the milestone's PR
  open, adopt it, watch it from the start, and resume in PR_OPEN with a
  notice. Gitea unreachable or the PR closed: the restart question, as
  before.
- MilestonePublisher.publish is idempotent: after pushing, an open PR
  for the branch is adopted (and so updated) rather than a second one
  opened. This also covers a PR opened whose response was lost.
- The model adopts too, gains the lost-response outcome, and now counts
  PRs opened per milestone (one_pr_per_milestone was a flag the fixed
  model could never set). A mutant without adoption fails the new
  scenario; the properties held over 200,000 traces each.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-09-29 19:58:19 +00:00
1 parent 6fdec6922b
commit 8bd1ac0fa2
6 files changed
+165 -24

No files matched your search

+32 -1
View File
@@ -16,6 +16,7 @@ class FakeGitea:
def __init__(self, *, fail: bool = False) -> None:
self.opened: list[dict[str, Any]] = []
self.existing: list[str] = []
self.closed: set[str] = set()
self._fail = fail
def open_pull_request(
@@ -33,7 +34,8 @@ class FakeGitea:
def find_pull_request(self, repo: str, head: str) -> PullRequest | None:
if head in self.existing:
return PullRequest(number=1, state="open", head=head, url="u")
state = "closed" if head in self.closed else "open"
return PullRequest(number=1, state=state, head=head, url="u")
return None
@@ -212,3 +214,32 @@ def test_a_followup_the_executor_committed_itself_is_pushed(
# Nothing new since that push: no second push, and it says so.
assert publisher.push_followup(task_id="t", milestone=1, message="m") is None
assert len(git.pushed) == 2
def test_publishing_again_adopts_the_open_pr_instead_of_opening_another(
store: StateStore, tmp_path: Path
) -> None:
# E.g. Gitea opened the PR but the response was lost, and the milestone
# was published again: the PR is adopted and updated, never duplicated.
git = FakeGit(tmp_path / "work")
client = FakeGitea()
publisher = build(store, git, client)
publisher.publish(task_id="t", milestone=1, project="demo", plan="p")
(tmp_path / "work" / "more.txt").write_text("more")
result = publisher.publish(task_id="t", milestone=1, project="demo", plan="p")
assert len(client.opened) == 1
assert result.pull_request is not None and result.pull_request.number == 1
assert "already open" in result.note
assert git.pushed == ["t/milestone-1", "t/milestone-1"] # updated it
assert store.pending_intents() == []
def test_a_closed_pr_is_not_adopted(store: StateStore, tmp_path: Path) -> None:
git = FakeGit(tmp_path / "work")
client = FakeGitea()
client.existing.append("t/milestone-1")
client.closed.add("t/milestone-1")
publisher = build(store, git, client)
assert publisher.published_pull_request("t", 1) is None
publisher.publish(task_id="t", milestone=1, project="demo", plan="p")
assert len(client.opened) == 1 # the operator closed it; a new one opens