Multi-milestone work and documents on the new loop (M2.4)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 3m45s

- Every milestone branch is cut from the latest default branch: the next
  milestone builds on the merged previous one, and a redo after a closed PR
  drops the rejected work. Uncommitted work comes along.
- While the item's PR is open, start_milestone is refused, with the PR's
  state read from Gitea at that moment, not the last watch round.
- finish_milestone(final): earlier PRs say "Part of #N", the last "Closes
  #N"; unsaid, it's final, as before.
- close_item with the item's PR still open waits for the merge (L2: a
  closed item leaves nothing open).
- The driver's instructions cover ARCHITECTURE.md and build plans under
  docs/plans/ (D2); the harness never parses a plan.
- Parking stashes uncommitted work and pushes it to <item>-parked, put back
  on resume, instead of committing it onto the item's branch. The walk
  found a half-done review fix parked onto a PR that had merged meanwhile,
  where it was lost. Architecture D4's parking line says so.

Tests: unit tests for fresh milestone branches, the merge wait (including
a merge not yet watched), redo after a closed PR, Part of/Closes, close
waiting on an open PR, and the parked-fix regression; the walk's new
milestones_start_fresh property (fork-from-current fails 9 seeds); an e2e
two-milestone feature.

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-10-05 20:57:37 +00:00
1 parent da32cac5cf
commit cd9ab2559e
10 files changed
+539 -53

No files matched your search

+56
View File
@@ -425,3 +425,59 @@ def test_a_plain_yes_is_a_go_ahead(
prs.append(pr)
finally:
operator.tidy(issues, prs)
def test_a_two_milestone_feature(
running: tuple[RunningLoop, InMemoryTransport, str],
) -> None:
"""M2.4: work in two milestones. The first PR carries the build plan and
is part of the issue; the second starts from the merged first and closes
it; the item closes after both merge."""
agent, transport, run = running
operator = Operator(agent, transport)
gitea, repo = agent.gitea, agent.config.gitea_repo
as_operator = operator_gitea(agent.config.gitea_url)
issues: list[int] = []
prs: list[int] = []
try:
body = (
f"{task(run)}\n\nThen, separately, {dependent_task(run)[0].lower()}"
f"{dependent_task(run)[1:]}\n\nDo this in two milestones, one PR each: "
f"vowels_{run} first, then consonants_{run} after the first has merged. "
"Write a short build plan for it."
)
issues.append(as_operator.create_issue(repo, f"letters_{run}", body))
as_operator._request(
"PATCH",
f"/repos/{repo}/issues/{issues[0]}",
json={"assignees": [agent.account]},
)
operator.tick()
# --- milestone 1: the plan and vowels; part of the issue ---
item = operator.drive_until(MERGE, issues[0])
first = loop_of(item).pr
prs.append(first)
pr = gitea._request("GET", f"/repos/{repo}/pulls/{first}")
assert f"Part of #{issues[0]}" in pr["body"], pr["body"]
files = [
f["filename"]
for f in gitea._request("GET", f"/repos/{repo}/pulls/{first}/files")
]
assert any(f.startswith("docs/plans/") for f in files), files
merge(as_operator, repo, first)
operator.tick()
# --- milestone 2: from the merged first; closes the issue ---
item = operator.drive_until(MERGE, issues[0])
second = loop_of(item).pr
assert second != first
prs.append(second)
pr = gitea._request("GET", f"/repos/{repo}/pulls/{second}")
assert f"Closes #{issues[0]}" in pr["body"], pr["body"]
diff = gitea._http.get(f"/repos/{repo}/pulls/{second}.diff").text
assert f"consonants_{run}" in diff
assert f"\n+def vowels_{run}" not in diff # built on the merged first
operator.merge_and_close(issues[0])
finally:
operator.tidy(issues, prs)