A design milestone holds documents only, enforced
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 5m4s
CI / e2e (pull_request) Successful in 24m22s

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:
dongarandClaude Opus 5.5 committed 2026-10-07 15:39:00 +00:00
1 parent ad2d090143
commit 88c7d2e8c9
6 files changed
+101 -7

No files matched your search

+40 -5
View File
@@ -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