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

+42
View File
@@ -19,6 +19,7 @@ from test_loop_milestones import (
from test_loop_milestones import world as world # noqa: PLC0414 -- the fixture
from nisse_dev.canonical import ToolCall, ToolResult
from nisse_dev.gitea import Issue
from nisse_dev.loop.items import MERGE, WAITING, loop_of
@@ -174,3 +175,44 @@ def test_an_unsaid_final_closes_the_issue(world: dict[str, Any]) -> None:
say(transport, world, "go")
[pr] = forge.prs
assert "Closes #14" in forge.prs[pr]["body"]
def test_a_design_milestone_holds_documents_only(world: dict[str, Any]) -> None:
"""D1, D2: a `design` item agrees its design before any code; the
harness refuses a design milestone with code in it."""
forge: Forge = world["forge"]
forge.issues[0] = Issue(14, "Add count_words", "count words", "don", ("design",))
model = ScriptedModel(
RESTATE,
[
call("start_milestone", name="design"),
call("write_file", path="ARCHITECTURE.md", content="# arch\n"),
call("write_file", path="docs/plans/words.md", content="# plan\n"),
call("write_file", path="words.py", content="W = 1\n"), # too soon
call("finish_milestone", summary="design", final="no"),
],
[
call("run_command", command="rm words.py"),
call("finish_milestone", summary="design", final="no"),
],
milestone("words.py", final="yes"), # after the design merged
)
agent, transport = loop_agent(world, model)
agent.intake()
say(transport, world, "go")
[design] = forge.prs
assert sorted(forge.files(design)) == ["ARCHITECTURE.md", "docs/plans/words.md"]
refused = [
e.event.result
for e in world["store"].transcript(item(world).id, "driver").read()
if isinstance(e.event, ToolResult)
and "this milestone is the design" in e.event.result
]
assert refused and "words.py" in refused[0]
forge.merge(design)
agent.watch()
agent._schedule()
code = max(forge.prs)
assert code != design and forge.files(code) == ["words.py"]
assert loop_of(item(world)).design_merged
+7 -2
View File
@@ -63,6 +63,11 @@ class PolicyModel:
self.n += 1
offered = {t["function"]["name"] for t in tools}
facts = [getattr(e.event, "text", "") for e in events]
# A `design` item writes documents until its design has merged.
design = "labelled design" in (system or "") and not any(
"the design has merged" in t for t in facts
)
path = f"docs/d{self.n}.md" if design else f"f{self.n}.py"
roll = self.rng.random()
if roll < 0.08:
return [call("ask_human", question=f"question {self.n}?")]
@@ -98,7 +103,7 @@ class PolicyModel:
# Half done, then a question: work a switch must park.
return [
call("start_milestone", name=f"m{self.n}"),
call("write_file", path=f"f{self.n}.py", content=f"x = {self.n}\n"),
call("write_file", path=path, content=f"x = {self.n}\n"),
call("ask_human", question=f"halfway {self.n}?"),
]
merged = any("merged" in t for t in facts[-6:])
@@ -106,7 +111,7 @@ class PolicyModel:
return [call("close_item", summary=f"done {self.n}")]
return [
call("start_milestone", name=f"m{self.n}"),
call("write_file", path=f"f{self.n}.py", content=f"x = {self.n}\n"),
call("write_file", path=path, content=f"x = {self.n}\n"),
call(
"finish_milestone",
summary=f"work {self.n}",