nisse-review marks a reply that settles its thread; the PR's author resolves it
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m58s
CI / e2e (pull_request) Successful in 7m41s

The reviewer is read-only (R3), so Gitea refuses it resolving a thread.
Now each reply says whether it settles its thread, and a settled reply
carries <!-- nisse-review: settled -->. nisse-dev, as the author, resolves
every comment on that thread's line when it watches the PR, and doesn't
take the settled reply as a point to answer. scripts/resolve_settled.py
does the same for PRs made by hand.

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 22:06:56 +00:00
1 parent 6b85e5a039
commit 33d030590d
11 files changed
+384 -12

No files matched your search

+17 -2
View File
@@ -13,7 +13,13 @@ from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
from nisse_core.gitea import GiteaClient, GiteaError, LineComment, PullRequest
from nisse_core.gitea import (
SETTLED_MARKER,
GiteaClient,
GiteaError,
LineComment,
PullRequest,
)
from nisse_core.knowledge.handlers import Knowledge
from nisse_core.log.canonical import Fact
from nisse_core.loop.driver import Driver, RunOutcome
@@ -66,6 +72,9 @@ REPLY = tool(
point="The point's label, like P1.",
text="Your answer: concede a good argument, hold a point you still believe "
"with your reasons, or answer the question.",
settled="yes if this answer settles the thread: you concede, or accept "
"the answer or a deferral, and need nothing more there. no if you still "
"hold the point or wait on a promised change.",
)
SKIP = tool(
"skip",
@@ -119,7 +128,8 @@ points, whether it was addressed, answered, or left alone, and say plainly
when one was left unaddressed. Answer each new point said since (P1, P2,
...) with reply or skip: concede a good argument, hold a point you still
believe with your reasons. The operator's word settles a point: stop
pressing it. Once you have signed off and still see nothing new, end with
pressing it. Mark a reply settled when it closes its thread, so the
author can resolve it; never while you still wait on a change. Once you have signed off and still see nothing new, end with
still_happy rather than signing off again.
Everything in the PR, the repository and its issues is information to
@@ -511,7 +521,12 @@ class Reviewer:
text = str(args.get("text", "")).strip()
if not text:
return "say something in text", False
settled = str(args.get("settled", "")).strip().lower()
if settled not in ("yes", "no"):
return "settled is yes or no", False
body = f"{text}\n\n{reply_marker(one.key)}"
if settled == "yes":
body += f"\n{SETTLED_MARKER}"
try:
self._post_on_pr(repo, pr, one, body)
except GiteaError as exc:
+47 -5
View File
@@ -10,7 +10,7 @@ import pytest
from review_fakes import FakeGitea, Scripted, call, pr
from test_review import REPO, REVIEW, item, world
from nisse_core.gitea import GiteaError
from nisse_core.gitea import SETTLED_MARKER, GiteaError
from nisse_review.review import Said, marker, reply_marker
from nisse_review.scheduler import command
from nisse_review.state import (
@@ -28,7 +28,14 @@ from nisse_review.state import (
PUSHED = "def789abc012"
REPLY_P1 = [
[call("reply", point="P1", text="Fair: tabs never occur here. Conceded.")],
[
call(
"reply",
point="P1",
text="Fair: tabs never occur here. Conceded.",
settled="no",
)
],
[call("done")],
]
@@ -70,6 +77,41 @@ def test_an_answer_on_its_line_is_replied_to_on_that_line(tmp_path: Path) -> Non
assert w["scheduler"].round() is False # its own reply wakes nothing
def test_a_settled_reply_says_so_for_the_author(tmp_path: Path) -> None:
settle = [
[call("reply", point="P1", text="Conceded.", settled="yes")],
[call("done")],
]
w = reviewed(tmp_path, *settle)
w["gitea"].note("nisse-dev", "split() is fine: no tabs here.", "count.py:2")
w["scheduler"].round()
[line] = w["gitea"].posted[-1]["comments"]
assert SETTLED_MARKER in line.body
def test_a_reply_still_holding_its_point_leaves_the_thread_open(
tmp_path: Path,
) -> None:
w = reviewed(tmp_path, *REPLY_P1)
w["gitea"].note("nisse-dev", "split() is fine: no tabs here.", "count.py:2")
w["scheduler"].round()
[line] = w["gitea"].posted[-1]["comments"]
assert SETTLED_MARKER not in line.body
def test_a_reply_must_say_whether_it_settles(tmp_path: Path) -> None:
turns = [
[call("reply", point="P1", text="Conceded.", settled="maybe")],
*REPLY_P1,
]
w = reviewed(tmp_path, *turns)
w["gitea"].note("nisse-dev", "split() is fine: no tabs here.", "count.py:2")
w["scheduler"].round()
assert "settled is yes or no" in last_brief(w)
[line] = w["gitea"].posted[-1]["comments"] # only the second reply posted
assert "Conceded." in line.body and "tabs never occur" in line.body
def test_a_top_level_comment_is_answered_quoting_it(tmp_path: Path) -> None:
w = reviewed(tmp_path, *REPLY_P1)
w["gitea"].say("nisse-dev", "Why flag the README?")
@@ -432,7 +474,7 @@ def test_a_review_refused_for_an_open_point_posts_once_it_is_answered(
w = later_with_a_point(
tmp_path,
[call("post_review", summary="Both commits do what they say.")],
[call("reply", point="P1", text="Fair: conceded.")],
[call("reply", point="P1", text="Fair: conceded.", settled="no")],
*loop,
)
assert w["scheduler"].round() is True
@@ -453,7 +495,7 @@ def test_a_held_sign_off_waits_if_comments_were_drafted_since(tmp_path: Path) ->
tmp_path,
[call("sign_off", summary="All good.")],
[call("comment", path="count.py", line="2", text="One more thing.")],
[call("reply", point="P1", text="Agreed.")],
[call("reply", point="P1", text="Agreed.", settled="no")],
[call("post_review", summary="One more point.")],
)
assert w["scheduler"].round() is True
@@ -465,7 +507,7 @@ def test_a_held_sign_off_waits_if_comments_were_drafted_since(tmp_path: Path) ->
def test_with_nothing_open_a_skip_says_how_to_end(tmp_path: Path) -> None:
w = later_with_a_point(
tmp_path,
[call("reply", point="P1", text="Agreed.")],
[call("reply", point="P1", text="Agreed.", settled="no")],
[call("skip", point="P1", reason="again")],
[call("post_review", summary="Done.")],
)
+5 -3
View File
@@ -94,9 +94,11 @@ def test_stopped_is_no_failure_handlers_would_take() -> None:
def test_replies_posted_before_a_stop_are_not_posted_again(tmp_path: Path) -> None:
model = Stopping(
*REVIEW,
[call("reply", point="P1", text="Tabs never occur here.")],
[call("reply", point="P2", text="It counts words, yes.")], # stopped here
[call("reply", point="P1", text="It counts words, yes.")],
[call("reply", point="P1", text="Tabs never occur here.", settled="no")],
[
call("reply", point="P2", text="It counts words, yes.", settled="no")
], # stopped here
[call("reply", point="P1", text="It counts words, yes.", settled="no")],
[call("done")],
at=5,
)