nisse-review: a review refused for an open point posts once the point is answered
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m54s
CI / e2e (pull_request) Successful in 5m10s

On #166 the reviewer called post_review with two points unanswered, was
refused, answered them, then only skipped already-answered points until the
40-turn cap: the review went out as partial, its summary lost. A refused
ending call is now held, and the driver ends the run with it as soon as the
last point is settled (a new due hook in nisse-core's driver; nisse-dev
doesn't use it). A held call that is wrong for another reason by then
(sign_off over drafted comments) is left to the model. With nothing open,
reply and skip say which tool ends the run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-19 20:11:40 +00:00
1 parent be81adc185
commit 26ef125d1e
5 files changed
+221 -22

No files matched your search

+69
View File
@@ -81,3 +81,72 @@ def test_a_tool_not_named_waiting_doesnt_end_the_run(tmp_path: Path) -> None:
)
assert ran.tool == "post_review"
assert results(log)[0].startswith("no such tool: ask_human")
NOTE = tool("note", "Note something.", text="The note.")
def test_a_held_call_goes_ahead_once_due_says_so(tmp_path: Path) -> None:
# post_review is refused while a point is open and held by the agent;
# the handled call that settles the point ends the run with it.
open_points = ["P1"]
held: dict[str, Any] = {}
def refuse(args: dict[str, Any]) -> str | None:
if open_points:
held["call"] = ("post_review", args)
return "answer P1 first"
return None
def settle(args: dict[str, Any]) -> tuple[str, bool]:
open_points.clear()
return "answered P1", True
def due() -> tuple[str, dict[str, Any], str] | None:
if "call" in held and not open_points:
tool_, args = held.pop("call")
return tool_, args, "your held post_review went ahead"
return None
model = Scripted(
[call("post_review", body="the summary")],
[call("note", text="P1"), call("read_file", path="a.py")],
# never reached: the run ended on the note
[call("note", text="again")],
)
log = transcript(tmp_path)
reader = Reader()
ran = Driver(model, "m").run(
log,
system="s",
tools=[READ, POST, NOTE],
waiting={"post_review"},
workspace=reader,
handlers={"note": settle},
checks={"post_review": refuse},
due=due,
)
assert (ran.kind, ran.tool, ran.arguments) == (
"wait",
"post_review",
{"body": "the summary"},
)
assert results(log) == [
"answer P1 first",
"answered P1\n\n[harness] your held post_review went ahead; your turn has ended",
"not run: the turn had ended",
]
assert reader.calls == [] # the read after it never ran
def test_without_due_a_settled_point_leaves_the_run_going(tmp_path: Path) -> None:
model = Scripted([call("note", text="x")], [call("post_review", body="b")])
ran = Driver(model, "m").run(
transcript(tmp_path),
system="s",
tools=[POST, NOTE],
waiting={"post_review"},
workspace=None,
handlers={"note": lambda args: ("noted", True)},
)
assert (ran.tool, ran.arguments) == ("post_review", {"body": "b"})