A design milestone holds documents only, enforced
Found by the first local e2e run: qwen3.8-27b put a new project's code in the same PR as its ARCHITECTURE.md, with no build plan. D1 says a `design` item agrees its design before any code, and that rested on wording only. - Until a PR of a `design` item has merged, finish_milestone is sent back if the milestone changes anything but ARCHITECTURE.md and docs/, naming the paths to take out. The merge is recorded (design_merged) and code may follow. - The label's instruction and the new project's brief say so plainly: documents first, in a PR of their own; code after it has merged. Tests: a design milestone with code is sent back and only the documents are published, then code after the merge; the walk's model follows the rule. 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
ad2d090143
commit
88c7d2e8c9
6 files changed
+101
-7
No files matched your search
@@ -150,9 +150,11 @@ LABELS = {
|
||||
"discuss": "This issue is labelled discuss: talk it through with the "
|
||||
"operator (ask_human) before restating, and restate only once you agree "
|
||||
"on what is wanted.",
|
||||
"design": "This issue is labelled design: agree the design with the "
|
||||
"operator before any code. Your restatement puts the design to them; "
|
||||
"code waits for their go-ahead on it.",
|
||||
"design": "This issue is labelled design: the design comes before any "
|
||||
"code. Your first milestone is documents only: ARCHITECTURE.md and a "
|
||||
"build plan under docs/plans/, in a PR of their own (if you restate, put "
|
||||
"the design to the operator). Code starts in a later milestone, after that "
|
||||
"PR has merged; the harness refuses a design milestone with anything else.",
|
||||
}
|
||||
|
||||
|
||||
@@ -519,6 +521,9 @@ class LoopAgent:
|
||||
if pr.merged:
|
||||
self._fact(item, f"PR #{loop.pr} merged")
|
||||
loop, position = replace(loop, pr=0, pr_url="", branch=""), READY
|
||||
if "design" in loop.labels and not loop.design_merged:
|
||||
self._fact(item, "the design has merged: code may follow")
|
||||
loop = replace(loop, design_merged=True)
|
||||
elif pr.state != "open":
|
||||
self._fact(item, f"PR #{loop.pr} was closed without merging")
|
||||
loop, position = replace(loop, pr=0, pr_url="", branch=""), READY
|
||||
@@ -1047,7 +1052,8 @@ class LoopAgent:
|
||||
project.repo,
|
||||
title,
|
||||
f"{brief}\n\nFrom a conversation with the operator, agreed there "
|
||||
f"as rev-{loop.rev}. First: ARCHITECTURE.md and the build plan."
|
||||
f"as rev-{loop.rev}. First: ARCHITECTURE.md and the build plan, "
|
||||
"in a PR of documents only; code after it has merged."
|
||||
f"\n\n<!-- nisse-item: {new_id} -->",
|
||||
)
|
||||
self._gitea.assign_issue(project.repo, number, self._account)
|
||||
@@ -1211,6 +1217,32 @@ class LoopAgent:
|
||||
)
|
||||
self._last_project = here
|
||||
|
||||
def _design_problem(self, item_id: str) -> str | None:
|
||||
"""D1, D2: a `design` item agrees its design before any code. Until
|
||||
a PR of it has merged, a milestone holds documents only."""
|
||||
item = self._store.work_item(item_id)
|
||||
if item is None:
|
||||
return None
|
||||
loop = loop_of(item)
|
||||
milestones = self._at(item).milestones
|
||||
if "design" not in loop.labels or loop.design_merged or milestones is None:
|
||||
return None
|
||||
try:
|
||||
changed = milestones.changed()
|
||||
except GitError:
|
||||
return None # publishing reports what's wrong with the workspace
|
||||
code = [
|
||||
p for p in changed if p != "ARCHITECTURE.md" and not p.startswith("docs/")
|
||||
]
|
||||
if not code:
|
||||
return None
|
||||
return (
|
||||
"not published: this milestone is the design, so it holds documents "
|
||||
"only (ARCHITECTURE.md, docs/); code starts in a later milestone, "
|
||||
"after this one's PR has merged. Take these out of it (they can "
|
||||
"wait in the plan): " + ", ".join(code)
|
||||
)
|
||||
|
||||
def _converse_run(self, item: WorkItem) -> None:
|
||||
"""A conversation's turn (L1, L5): it talks, and may propose work on
|
||||
a project. No workspace, nothing to run."""
|
||||
@@ -1329,7 +1361,10 @@ class LoopAgent:
|
||||
tools=tools,
|
||||
workspace=workspace,
|
||||
handlers={"start_milestone": lambda args: self._start(item.id, args)},
|
||||
checks={"restate": _restatement_problem},
|
||||
checks={
|
||||
"restate": _restatement_problem,
|
||||
"finish_milestone": lambda args: self._design_problem(item.id),
|
||||
},
|
||||
)
|
||||
finally:
|
||||
self._running = False
|
||||
|
||||
@@ -55,6 +55,7 @@ class Loop:
|
||||
proposal_title: str = ""
|
||||
proposal_kind: str = "" # "work" on a project, or a new "project"
|
||||
new_project: str = "" # a new project being provisioned (D1)
|
||||
design_merged: bool = False # a `design` item's design PR has merged (D1, D2)
|
||||
# Where the operator last spoke (D5: answer where asked): "" for the
|
||||
# Matrix thread, or the PR it came from, with the inline location and a
|
||||
# little of what they said, for quoting.
|
||||
|
||||
@@ -161,6 +161,10 @@ class Milestones:
|
||||
if git.remote_sha(self._remote, parked, credential=self._credential):
|
||||
git.delete_remote_branch(self._remote, parked, credential=self._credential)
|
||||
|
||||
def changed(self) -> list[str]:
|
||||
"""What the current milestone changes, from the default branch."""
|
||||
return self._git.changed_paths(f"origin/{self._base}")
|
||||
|
||||
# -- a new project's checks (D1, D4) -----------------------------------------
|
||||
|
||||
def verify_access(self, account: str) -> list[str]:
|
||||
|
||||
Reference in new issue
Block a user