Multi-milestone work and documents on the new loop (M2.4)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 3m45s
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:
1 parent
da32cac5cf
commit
cd9ab2559e
10 files changed
+539
-53
No files matched your search
@@ -25,6 +25,7 @@ from dataclasses import replace
|
||||
from nisse_dev.cancel import Cancel
|
||||
from nisse_dev.canonical import Fact, PlanSubmitted, UserMessage
|
||||
from nisse_dev.gates import command_form
|
||||
from nisse_dev.git import GitError
|
||||
from nisse_dev.gitea import GiteaClient, GiteaError, Issue
|
||||
from nisse_dev.loop.driver import Driver, RunOutcome
|
||||
from nisse_dev.loop.items import (
|
||||
@@ -81,16 +82,28 @@ the project, and your first step. Wait for their go-ahead. If the request is
|
||||
unclear, ask_human instead of guessing: a restatement asks no questions, so
|
||||
the operator can agree to it as it stands.
|
||||
|
||||
After the go-ahead, work in a milestone: call start_milestone before
|
||||
changing anything (never create or switch branches yourself), make the
|
||||
change, run the project's tests, then call finish_milestone with a summary.
|
||||
The harness commits, pushes and opens a PR, and you wait for review. Review
|
||||
comments arrive as messages: make the changes within the same milestone (don't
|
||||
start another), run the tests, and call finish_milestone again to push the fix. When the PR has merged, call
|
||||
close_item with what was done, what was verified, and what is left open. If
|
||||
the PR is closed without merging, ask the operator what next. When a review
|
||||
needs no change from you, or you are only waiting for the operator to merge
|
||||
(you can't merge it yourself), call wait_for_review.
|
||||
The project's documents are in its repository: read ARCHITECTURE.md and any
|
||||
build plans under docs/plans/ before you restate.
|
||||
|
||||
After the go-ahead, work in milestones, one PR each. Call start_milestone
|
||||
before changing anything (never create or switch branches yourself; each
|
||||
milestone starts from the latest default branch), make the change, run the
|
||||
project's tests, then call finish_milestone with a summary, saying whether it
|
||||
is the final milestone. The harness commits, pushes and opens a PR, and you
|
||||
wait for review. Review comments arrive as messages: make the changes within
|
||||
the same milestone (don't start another), run the tests, and call
|
||||
finish_milestone again to push the fix. When a review needs no change from
|
||||
you, or you are only waiting for the operator to merge (you can't merge it
|
||||
yourself), call wait_for_review. The next milestone starts only after the
|
||||
previous PR has merged. When the last PR has merged, call close_item with
|
||||
what was done, what was verified, and what is left open. If a PR is closed
|
||||
without merging, ask the operator what next.
|
||||
|
||||
Work longer than about one PR gets a build plan: a Markdown file under
|
||||
docs/plans/ giving, per milestone, the goal, the interfaces and behaviour, the
|
||||
edge cases, and the tests that are its acceptance criteria. Commit it with the
|
||||
first milestone, and mark each milestone done in it in that milestone's PR.
|
||||
Keep ARCHITECTURE.md current: a design change updates it in the same PR.
|
||||
|
||||
{labels}Never label the issue yourself; you may suggest a label to the operator.
|
||||
|
||||
@@ -808,6 +821,20 @@ class LoopAgent:
|
||||
loop,
|
||||
str(args.get("summary", "")).strip(),
|
||||
str(args.get("title", "")).strip(),
|
||||
# Unsaid, it's final: the PR closes the issue, as before.
|
||||
str(args.get("final", "yes")).strip().lower() != "no",
|
||||
)
|
||||
elif outcome.kind == "wait" and outcome.tool == "close_item" and loop.pr:
|
||||
# Closing leaves nothing open (L2): an open PR is waited out first.
|
||||
self._fact(
|
||||
item,
|
||||
f"close_item waits: PR #{loop.pr} is still open; the item closes "
|
||||
"after it merges",
|
||||
)
|
||||
self._wait(
|
||||
item,
|
||||
replace(loop, wait=MERGE, note=f"PR #{loop.pr}"),
|
||||
f"I'll close this once PR #{loop.pr} merges: {loop.pr_url}",
|
||||
)
|
||||
elif outcome.kind == "wait" and outcome.tool == "close_item":
|
||||
summary = str(args.get("summary", "")).strip()
|
||||
@@ -828,6 +855,23 @@ class LoopAgent:
|
||||
item = self._store.work_item(item_id)
|
||||
assert item is not None
|
||||
loop = loop_of(item)
|
||||
if loop.pr:
|
||||
# D4: the next milestone waits for the merge, read from Gitea now,
|
||||
# not from the last watch round.
|
||||
try:
|
||||
pr = self._gitea.pull_request(self._repo, loop.pr)
|
||||
except GiteaError as exc:
|
||||
return f"couldn't check PR #{loop.pr}: {exc}; try again", False
|
||||
if pr.state == "open":
|
||||
wait = (
|
||||
f"milestone {loop.milestone}'s PR #{loop.pr} hasn't merged yet; "
|
||||
"the next milestone starts after it merges. If you're only "
|
||||
"waiting for that, call wait_for_review."
|
||||
)
|
||||
return wait, False
|
||||
how = "merged" if pr.merged else "was closed without merging"
|
||||
self._fact(item, f"PR #{loop.pr} {how}")
|
||||
loop = replace(loop, pr=0, pr_url="", branch="")
|
||||
if loop.branch:
|
||||
busy = f"milestone {loop.milestone} is still open; finish it first"
|
||||
return busy, False
|
||||
@@ -835,7 +879,10 @@ class LoopAgent:
|
||||
return "milestones aren't available here", False
|
||||
number = loop.milestone + 1
|
||||
branch = f"{item.id}-milestone-{number}-{uuid.uuid4().hex[:4]}"
|
||||
self._milestones.start(branch)
|
||||
try:
|
||||
self._milestones.start(branch)
|
||||
except GitError as exc:
|
||||
return f"couldn't start the milestone: {exc}", False
|
||||
name = str(args.get("name", "")).strip()
|
||||
self._fact(item, f"milestone {number} started ({name}) on branch {branch}")
|
||||
save(
|
||||
@@ -869,7 +916,12 @@ class LoopAgent:
|
||||
)
|
||||
|
||||
def _finish(
|
||||
self, item: WorkItem, loop: Loop, summary: str, title: str = ""
|
||||
self,
|
||||
item: WorkItem,
|
||||
loop: Loop,
|
||||
summary: str,
|
||||
title: str = "",
|
||||
final: bool = True,
|
||||
) -> None:
|
||||
"""finish_milestone: publish the milestone, or push a review fix."""
|
||||
if self._milestones is None:
|
||||
@@ -918,7 +970,12 @@ class LoopAgent:
|
||||
# D4: finishing without a start cuts the branch now.
|
||||
number = loop.milestone + 1
|
||||
branch = f"{item.id}-milestone-{number}-{uuid.uuid4().hex[:4]}"
|
||||
self._milestones.start(branch)
|
||||
try:
|
||||
self._milestones.start(branch)
|
||||
except GitError as exc:
|
||||
note = f"I couldn't start milestone {number}: {exc}"
|
||||
self._wait(item, replace(loop, wait=ANSWER, note=note), note)
|
||||
return
|
||||
loop = replace(loop, milestone=number, branch=branch)
|
||||
published = self._milestones.publish(
|
||||
item_id=item.id,
|
||||
@@ -926,6 +983,7 @@ class LoopAgent:
|
||||
title=title or f"{item.title} (milestone {loop.milestone})",
|
||||
summary=summary,
|
||||
issue=loop.issue,
|
||||
final=final,
|
||||
)
|
||||
left = (
|
||||
f" (left out {len(published.left_out)} build artifact(s))"
|
||||
@@ -984,6 +1042,11 @@ class LoopAgent:
|
||||
)
|
||||
save(self._store, item.id, CLOSED, loop, cause=outcome)
|
||||
self._store.close_work_item(item.id, outcome=outcome, cause=note)
|
||||
if self._milestones is not None:
|
||||
try:
|
||||
self._milestones.drop_parked(f"{item.id}-")
|
||||
except GitError:
|
||||
pass # a leftover side branch; nothing depends on it
|
||||
if loop.issue:
|
||||
try:
|
||||
self._gitea.post_comment(self._repo, loop.issue, f"{outcome}: {note}")
|
||||
|
||||
Reference in new issue
Block a user