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

+58 -5
View File
@@ -88,12 +88,14 @@ def test_switching_parks_the_work_and_resuming_finds_it(
agent.watch()
agent._schedule()
# Switching to 14 parked 15: its half-done work is committed and pushed.
# Switching to 14 parked 15: its half-done work is pushed as a stash to
# a side branch (new files in its third parent), never onto 15's branch.
b_branch = loop_of(item(world, 15)).branch
pushed = git(
"--git-dir", str(world["bare"]), "show", f"refs/heads/{b_branch}:lines.py"
)
assert pushed == "L = 1"
bare = ["--git-dir", str(world["bare"])]
parked = f"refs/heads/{item(world, 15).id}-parked"
assert git(*bare, "show", f"{parked}^3:lines.py") == "L = 1"
# The branch has no commits of its own, so it wasn't pushed at all.
assert b_branch not in git(*bare, "branch", "--list")
# 14's fix went to its own branch, without 15's work.
assert loop_of(item(world, 14)).wait == MERGE
files = git("--git-dir", str(world["bare"]), "ls-tree", "--name-only", a_branch)
@@ -105,6 +107,8 @@ def test_switching_parks_the_work_and_resuming_finds_it(
say(transport, world, 15, "one per line")
assert world["repo"].current_branch() == b_branch
assert (world["repo"].root / "lines.py").read_text() == "L = 1\n"
# Put back, so the side branch is gone.
assert item(world, 15).id + "-parked" not in git(*bare, "branch", "--list")
assert loop_of(item(world, 15)).wait == MERGE
assert len(forge.prs) == 2
@@ -190,3 +194,52 @@ def test_status_shows_the_line(world: dict[str, Any]) -> None:
assert "#15 Add count_lines: ready, 2 of 2 in line" in agent.status()
agent.handle(InboundMessage(DON, "do #15 next"), None)
assert "#15 Add count_lines: ready, 1 of 2 in line" in agent.status()
def test_a_fix_half_done_when_its_pr_merges_survives_a_switch(
world: dict[str, Any],
) -> None:
"""Found by the walk: work on a review fix, parked after the PR merged,
was pushed onto the merged branch, where nobody would see it. Now it
comes back with the item and goes out in its next PR."""
forge: Forge = world["forge"]
two_issues(forge)
model = ScriptedModel(
[call("restate", text="count_words")], # 14
[call("restate", text="count_lines")], # 15
[ # 14: a PR
call("start_milestone", name="words"),
call("write_file", path="words.py", content="W = 1\n"),
call("finish_milestone", summary="words", final="yes"),
],
[ # 14, a review comment: half a fix, then a question
call("write_file", path="fix.py", content="F = 1\n"),
call("ask_human", question="fix it in words.py too?"),
],
[ # 15 runs meanwhile
call("start_milestone", name="lines"),
call("write_file", path="lines.py", content="L = 1\n"),
call("finish_milestone", summary="lines"),
],
[call("finish_milestone", summary="the fix", final="yes")], # 14 again
)
agent, transport = loop_agent(world, model)
agent.intake()
say(transport, world, 14, "go")
[pr] = forge.prs
forge.comments[pr] = [Comment(7, "don", "add fix.py", "t")]
agent.watch()
agent._schedule()
assert loop_of(item(world, 14)).wait == ANSWER
say(transport, world, 15, "go") # parks 14's half-done fix
forge.merge(pr)
agent.watch()
agent._schedule() # 14 resumes; its fix goes out as a new PR
bare = ["--git-dir", str(world["bare"])]
merged_head = forge.prs[pr]["head"]
assert "fix.py" not in git(*bare, "ls-tree", "--name-only", merged_head).split()
newest = max(
n for n, p in forge.prs.items() if p["head"].startswith(item(world, 14).id)
)
assert newest != pr and forge.files(newest) == ["fix.py"]