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

+40 -2
View File
@@ -79,7 +79,11 @@ class PolicyModel:
return [
call("start_milestone", name=f"m{self.n}"),
call("write_file", path=f"f{self.n}.py", content=f"x = {self.n}\n"),
call("finish_milestone", summary=f"work {self.n}"),
call(
"finish_milestone",
summary=f"work {self.n}",
final=self.rng.choice(["yes", "no"]),
),
]
@@ -308,6 +312,7 @@ class Walk:
f"one_active (nothing left running): {where}"
)
self._parked_work_safe(where)
self._milestones_start_fresh(where)
heads = [p["head"] for p in forge.prs.values()]
assert len(heads) == len(set(heads)), f"one_pr_per_milestone: {where}"
bare = self.world["bare"]
@@ -325,6 +330,29 @@ class Walk:
)
assert landed, f"review_fixes_land (PR {n}): {where}"
def _milestones_start_fresh(self, where: str) -> None:
"""D4: a milestone starts from the latest default branch, so one
redone after a closed PR never carries the rejected work."""
forge: Forge = self.world["forge"]
bare = self.world["bare"]
for item in self.items():
mine = sorted(
(n, p)
for n, p in forge.prs.items()
if p["head"].startswith(f"{item.id}-milestone-")
and "-fix-" not in p["head"]
)
for i, (n, earlier) in enumerate(mine):
if earlier["merged"]:
continue
rejected = _rev(bare, f"refs/heads/{earlier['head']}")
if _is_ancestor(bare, rejected, "refs/heads/main"):
continue
for m, later in mine[i + 1 :]:
assert not _is_ancestor(
bare, rejected, f"refs/heads/{later['head']}"
), f"milestones_start_fresh: PR {m} carries PR {n}: {where}"
def _parked_work_safe(self, where: str) -> None:
"""L1, D4: nothing of a parked item lives only on the build VM: what
every branch of an open item that isn't on the workspace points at is
@@ -367,7 +395,17 @@ class Walk:
for path in written:
if here and (root / path).exists():
continue
history = _git(bare, "log", "--all", "--format=%H", "--", path)
history = _git(
# Full history: a parked stash keeps new files in its
# third parent, which plain log simplifies away.
bare,
"log",
"--all",
"--full-history",
"--format=%H",
"--",
path,
)
assert history, f"parked_work_safe: {path} of #{item.id} lost: {where}"