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

+69 -26
View File
@@ -91,38 +91,73 @@ class Milestones:
# -- parking and resuming (L1, D4) -----------------------------------------
def park(self, *, keep: tuple[str, ...]) -> str | None:
"""Before another item uses the workspace: commit whatever the item
on it left, and push its branch, so nothing of a parked item lives
only on the build VM. Only a harness branch of an open item (one
starting with a ``keep`` prefix) is parked; returns its name."""
"""Before another item uses the workspace: nothing of the item on it
may live only on the build VM. Its uncommitted work is stashed and
pushed to ``<item>-parked``, never committed onto its branch, which
may be a PR's that has merged meanwhile; resuming puts it back
uncommitted, for wherever the work goes next. Commits the harness
made but couldn't push are pushed. Only an open item's harness branch
(one starting with a ``keep`` prefix, ``<item>-``) is parked; returns
its name."""
git = self._git
branch = git.current_branch()
if not keep or not branch.startswith(keep):
prefix = next((k for k in keep if branch.startswith(k)), None)
if prefix is None:
return None
git.commit_all("WIP: parked while another work item runs")
if git.remote_sha(self._remote, branch, credential=self._credential) != (
git.head_sha()
sha = git.stash_all(f"parked from {branch}")
if sha is not None:
git.push_commit(
self._remote, sha, f"{prefix}parked", credential=self._credential
)
remote = git.remote_sha(self._remote, branch, credential=self._credential)
if remote != git.head_sha() and (
remote is not None or git.commits_since(f"origin/{self._base}") > 0
):
git.push(self._remote, branch, credential=self._credential)
return branch
def resume(self, branch: str, *, keep: tuple[str, ...], mine: str = "") -> None:
"""Back to a parked item: park whatever is on the workspace now, then
check out the item's branch. The local branch wins when it exists (it
can only be ahead: parking pushed it); otherwise it comes from origin.
A workspace already on one of the item's own branches (``mine``, a
prefix) is where it left off, and stays."""
def resume(self, branch: str, *, keep: tuple[str, ...], mine: str) -> None:
"""Back to a parked item: park whatever is on the workspace now,
check out the item's branch, and put its parked work back. The local
branch wins when it exists (it can only be ahead: parking pushed it);
otherwise it comes from origin. A workspace already on one of the
item's own branches (``mine``, its ``<item>-`` prefix) stays."""
git = self._git
current = git.current_branch()
if current == branch or (mine and current.startswith(mine)):
if current != branch and not current.startswith(mine):
self.park(keep=keep)
if branch in git.local_branches():
git.checkout_existing_branch(branch)
git.reset_to(branch, branch)
else:
git.fetch(credential=self._credential)
pushed = git.remote_sha(
self._remote, branch, credential=self._credential
)
# A branch never pushed held nothing beyond the default branch.
start = f"origin/{branch}" if pushed else f"origin/{self._base}"
git.reset_to(branch, start)
# Even when it stayed: a crash may have come between parking and the
# next item's start.
self.unpark(mine)
def unpark(self, mine: str) -> None:
parked = f"{mine}parked"
git = self._git
sha = git.remote_sha(self._remote, parked, credential=self._credential)
if sha is None:
return
self.park(keep=keep)
if branch in git.local_branches():
git.checkout_existing_branch(branch)
git.reset_to(branch, branch)
else:
if not git.has_commit(sha):
git.fetch(credential=self._credential)
git.reset_to(branch, f"origin/{branch}")
git.apply_stash(sha)
git.delete_remote_branch(self._remote, parked, credential=self._credential)
def drop_parked(self, mine: str) -> None:
"""A closed item's parked work, if any, goes: closing set it aside."""
parked = f"{mine}parked"
git = self._git
if git.remote_sha(self._remote, parked, credential=self._credential):
git.delete_remote_branch(self._remote, parked, credential=self._credential)
def _ahead_of_origin(self) -> bool:
current = self._git.current_branch()
@@ -136,8 +171,12 @@ class Milestones:
# -- milestones (D4) -------------------------------------------------------
def start(self, branch: str) -> str:
"""Cut the milestone's branch where the item is; returns its start."""
self._git.checkout_new_branch(branch)
"""Cut the milestone's branch from the latest default branch (D4):
a milestone after a merge builds on it, and one redone after a closed
PR doesn't carry the rejected work. Uncommitted work comes along.
Returns where it starts."""
self._git.fetch(credential=self._credential)
self._git.checkout_new_branch(branch, f"origin/{self._base}")
return self._git.head_sha()
def publish(
@@ -148,6 +187,7 @@ class Milestones:
title: str,
summary: str,
issue: int,
final: bool = True,
) -> Published:
git = self._git
try:
@@ -184,7 +224,7 @@ class Milestones:
head=branch,
base=self._base,
title=title,
body=_body(summary, issue, left_out),
body=_body(summary, issue, left_out, final),
)
self._store.commit_intent(intent, cause=f"opened PR #{pr.number}")
return Published("opened", f"opened PR #{pr.number}", pr, left_out)
@@ -227,10 +267,13 @@ class Milestones:
return Fixed("failed", f"pushing the fix failed: {exc}")
def _body(summary: str, issue: int, left_out: tuple[str, ...]) -> str:
def _body(
summary: str, issue: int, left_out: tuple[str, ...], final: bool = True
) -> str:
parts = [summary]
if issue:
parts += ["", f"Closes #{issue}"]
# D1: the last PR closes the issue on merge; earlier ones are part of it.
parts += ["", f"Closes #{issue}" if final else f"Part of #{issue}"]
if left_out:
parts += ["", "## Left out", "", "Build artifacts, never part of the work:"]
parts += [f"- `{path}`" for path in left_out]