Fixes from the first live session of the new loop
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m33s
CI / e2e (pull_request) Successful in 10m38s

Found live on 2026-10-07 (bring-up on nisse-testing-operator/nisse-bringup,
Opus as driver):

- The driver's prose nudge went into the system prompt, so the retry ended
  on the model's own reply. Opus 5.5 refuses that ("does not support
  assistant message prefill. The conversation must end with a user
  message"). The nudge is now a harness fact in the conversation. The
  delegation harness's prose path did the same and is fixed likewise.
- A helper's first request ended with its own delegation_started event,
  rendered as an assistant message: every delegation would have failed on
  Opus. The helper now sees it as the harness handing over its task.
- model_contract checks that every request ends on the user's side, so
  scripted tests catch this from now on (each fix is mutation-checked).
- `status` (and `details`) in an item's thread were taken as replies: they
  woke the driver and cancelled a bug's countdown. They now answer at once
  and change nothing.
- The driver had no way to answer a remark without changing the item, so it
  answered in prose. Every run woken by a message (or a review comment) now
  offers `reply`, which answers where asked and returns to the very wait
  that was interrupted.

Also: scripts/bringup-repo.sh makes the bring-up's test repository
(make_repo/delete_repo, split out of fresh_repo), and made repositories get
the agent's labels (bug, discuss, design).

Tests: test_loop_live_fixes.py (one per finding); the walk sends `status`
into threads (status_changes_nothing) and its scripted driver sometimes
answers remarks with reply.

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-08 00:02:50 +00:00
1 parent 572bd9f8eb
commit 659915d309
9 files changed
+341 -42

No files matched your search

+154
View File
@@ -0,0 +1,154 @@
"""What the first live session of the new loop found (2026-10-07), each as a
test: a request ending on the assistant's side (refused by models without
prefill: the driver's prose nudge, a helper's first request), `status` in an
item's thread taken as a reply, and no way for the driver to answer a remark.
"""
from pathlib import Path
from typing import Any
from model_contract import assert_renderable
from test_loop import RESTATED, FakeGitea, Harness, ScriptedModel, call, issue
from test_loop_help import ROLES, hand_back
from test_loop_milestones import RESTATE, Forge, loop_agent, say
from test_loop_milestones import item as the_item
from test_loop_milestones import world as world # noqa: PLC0414 -- the fixture
from nisse_dev.canonical import AssistantText, Fact
from nisse_dev.delegation import Caps
from nisse_dev.gitea import Comment
from nisse_dev.loop.help import Helpers
from nisse_dev.loop.items import GO_AHEAD, MERGE, WAITING, loop_of
class Clock:
def __init__(self) -> None:
self.now = 1000.0
def __call__(self) -> float:
return self.now
def test_the_prose_nudge_keeps_requests_ending_on_the_user_side(
tmp_path: Path,
) -> None:
# ScriptedModel checks every request as the API would, now including
# "the conversation must end with a user message".
h = Harness(
tmp_path,
ScriptedModel([AssistantText(text="Let me think.", model="m")], RESTATED),
FakeGitea(issue()),
)
h.agent.intake()
assert loop_of(h.item()).wait == GO_AHEAD # the nudge worked
facts = [
e.event.text
for e in h.store.transcript(h.item().id, "orchestrator").read()
if isinstance(e.event, Fact)
]
assert any("Your last reply was prose" in f for f in facts)
class ContractHelper:
"""A helper that checks its requests as the API would."""
def __init__(self, *turns: list[Any]) -> None:
self.turns = list(turns)
def complete(self, events, tools, model, system=None) -> list[Any]:
assert_renderable(events)
return self.turns.pop(0)
def test_a_helpers_requests_end_on_the_user_side(world: dict[str, Any]) -> None:
helper = ContractHelper(
[AssistantText(text="Looking around first.", model="h")], # prose
[call("write_file", path="helped.py", content="H = 1\n")],
[hand_back()],
)
model = ScriptedModel(
RESTATE,
[
call("start_milestone", name="words"),
call("delegate", task="write helped.py"),
call("finish_milestone", summary="with help", final="yes"),
],
)
helpers = Helpers(
client=helper, # type: ignore[arg-type]
roles=ROLES,
client_for=lambda model: helper, # type: ignore[arg-type,return-value]
caps=Caps(turns=5),
)
agent, transport = loop_agent(world, model, helpers=helpers)
agent.intake()
say(transport, world, "go")
forge: Forge = world["forge"]
[pr] = forge.prs
assert forge.files(pr) == ["helped.py"]
def test_status_in_a_thread_is_not_a_reply(tmp_path: Path) -> None:
clock = Clock()
model = ScriptedModel(RESTATED) # called once only: status runs nothing
h = Harness(
tmp_path,
model,
FakeGitea(issue(labels=("bug",))),
clock=clock,
bug_grace_s=600,
)
h.agent.intake()
grace = loop_of(h.item()).grace_until
h.say("status")
assert h.posted()[-1].startswith("#14 Add count_words: waiting for go-ahead")
loop = loop_of(h.item())
assert (h.item().position, loop.wait, loop.grace_until) == (
WAITING,
GO_AHEAD,
grace,
)
h.say("details")
assert loop_of(h.item()).grace_until == grace
def test_a_remark_gets_a_reply_and_the_restatement_stays_up(tmp_path: Path) -> None:
h = Harness(
tmp_path,
ScriptedModel(
RESTATED,
[call("reply", text="Noted: I'll keep it to text.py.")],
),
FakeGitea(issue()),
)
h.agent.intake()
h.say("just a thought: keep it small")
loop = loop_of(h.item())
assert (loop.wait, loop.rev) == (GO_AHEAD, 1)
assert h.posted()[-1] == "Noted: I'll keep it to text.py."
def test_a_review_comment_can_be_answered_without_a_fix(
world: dict[str, Any],
) -> None:
forge: Forge = world["forge"]
model = ScriptedModel(
RESTATE,
[
call("start_milestone", name="words"),
call("write_file", path="w.py", content="W = 1\n"),
call("finish_milestone", summary="words", final="yes"),
],
[call("reply", text="It's a constant on purpose, for the test.")],
)
agent, transport = loop_agent(world, model)
agent.intake()
say(transport, world, "go")
[pr] = forge.prs
forge.comments[pr] = [Comment(7, "don", "why a constant", "t")]
agent.watch()
agent._schedule()
loop = loop_of(the_item(world))
assert (loop.wait, loop.pr) == (MERGE, pr)
assert forge.posted[-1][0] == pr # answered on the PR, where asked
assert "It's a constant on purpose" in forge.posted[-1][1]