hermes: finalize completed review verdicts in the goal judge

The local goal judge scored every worker report against "did the reviewed
implementation reach a shippable state". A read-only reviewer that returned a
completed BLOCK verdict with findings was therefore resumed turn after turn with
an instruction to repair code it was forbidden to touch (observed live on
t_dbdcd739), burning subscription capacity and risking an unbounded loop.

Completion is now judged against the action the card assigned:

* Cards declare their role explicitly with Hermes-Task-Role / Hermes-Expected-
  Output metadata. Pre-contract cards fall back to a narrow inference that needs
  a read-only scope, a requested verdict, no requested mutation deliverable, and
  a report that changed no files.
* Role resolution reads only the card itself. Prior attempts, parent results,
  cross-task history and comments appended to the worker context can no longer
  reassign the role.
* Review, audit and diagnostic cards finalize deterministically on a truthful
  SHIP or BLOCK verdict with evidence, and fail closed on a missing,
  unrecognized or self-contradictory verdict, on a BLOCK without findings, and
  on a verdict without evidence. Every rejection reason carries the read-only
  guard, so a resumed review is never told to edit the reviewed implementation.
* Implementation cards keep the fail-closed model judge unchanged, including the
  unfinished-work heuristic and judge-unavailable rejection.
* All judge reasons are bounded, single-line and secret-redacted before they
  reach Kanban metadata, comments and continuation prompts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hermes Agent 2026-08-17 14:49:33 +00:00
parent d8f2d818b9
commit b080b5f622
3 changed files with 1157 additions and 19 deletions

View File

@ -1,5 +1,24 @@
#!/usr/bin/env python3
"""Fail-closed completion checks for durable external Kanban workers."""
"""Role-aware completion checks for durable external Kanban workers.
Completion is judged against the action the card assigned, not against the
health of whatever the card asked the worker to look at.
* Implementation cards keep the fail-closed local goal judge: a report is
accepted only when the model agrees every explicit requirement finished.
* Review, audit, and diagnostic cards deliver a *verdict*. A completed report
carrying a truthful SHIP or BLOCK verdict with evidence finalizes the card
even when the verdict says the reviewed artifact must not ship. Findings are
the deliverable, not a task blocker, so the card is never resumed with an
instruction to repair an implementation the reviewer may not touch.
The task role comes from explicit card metadata (``Hermes-Task-Role: review``)
whenever the card author supplies it. Cards written before that contract fall
back to a deliberately narrow inference that requires a read-only scope, a
requested verdict, no requested mutation deliverable, and a report that changed
no files. Review completion is fail-closed on a missing, unrecognized, or
self-contradictory verdict and on missing evidence.
"""
from __future__ import annotations
@ -24,6 +43,30 @@ GOAL_JUDGE_TIMEOUT_SECONDS = max(
min(180, int(os.environ.get("HERMES_GOAL_JUDGE_TIMEOUT_SECONDS", "120"))),
)
RESULT_STATUSES = frozenset({"completed", "blocked", "incomplete"})
REVIEW_ROLE = "review"
IMPLEMENTATION_ROLE = "implementation"
TASK_ROLES = frozenset({REVIEW_ROLE, IMPLEMENTATION_ROLE})
ROLE_ALIASES = {
"audit": REVIEW_ROLE,
"assessment": REVIEW_ROLE,
"diagnosis": REVIEW_ROLE,
"diagnostic": REVIEW_ROLE,
"inspection": REVIEW_ROLE,
"review": REVIEW_ROLE,
"reviewer": REVIEW_ROLE,
"verdict": REVIEW_ROLE,
"build": IMPLEMENTATION_ROLE,
"change": IMPLEMENTATION_ROLE,
"implement": IMPLEMENTATION_ROLE,
"implementation": IMPLEMENTATION_ROLE,
"repair": IMPLEMENTATION_ROLE,
}
JUDGE_REASON_LIMIT = 600
MIN_REVIEW_SUMMARY_CHARS = 80
READ_ONLY_GUARD = (
"the assigned action is a read-only review: report the verdict and evidence, "
"and do not modify the reviewed implementation"
)
GOAL_JUDGE_SCHEMA: dict[str, Any] = {
"type": "object",
"additionalProperties": False,
@ -43,6 +86,60 @@ UNFINISHED_EVIDENCE = re.compile(
r"\b(?:tests?|checks?|build|validation|verification|commit|push)\b)",
re.IGNORECASE | re.DOTALL,
)
# The worker context appends prior attempts, parent results, cross-task history
# and comments after the card the worker was actually given. Role resolution
# reads only the card itself so an unrelated sentence quoted from an earlier run
# cannot reassign the task role.
HISTORY_SECTION = re.compile(
r"^##\s+(?:Prior attempts\b|Parent task results\b|Recent work by\b|Comment thread\b)",
re.IGNORECASE | re.MULTILINE,
)
# Explicit, machine-readable card metadata. This is the supported contract;
# every pattern below it only keeps pre-contract cards working.
ROLE_DIRECTIVE = re.compile(
r"^[\s>#*\-]*(?:hermes[\s_-]*)?task[\s_-]*role\s*[:=]\s*([A-Za-z_-]+)\s*$",
re.IGNORECASE | re.MULTILINE,
)
OUTPUT_DIRECTIVE = re.compile(
r"^[\s>#*\-]*(?:hermes[\s_-]*)?expected[\s_-]*output\s*[:=]\s*([A-Za-z_-]+)\s*$",
re.IGNORECASE | re.MULTILINE,
)
READ_ONLY_SCOPE = re.compile(
r"(?:\b(?:do|does|must|should|shall|may|will)\s+not\s+(?:\w+\s+){0,3}?"
r"(?:edit|modify|change|alter|patch|rewrite|implement|repair)\b"
r"|\bdon'?t\s+(?:\w+\s+){0,3}?(?:edit|modify|change|alter|patch)\b"
r"|\bwithout\s+(?:editing|modifying|changing|altering|patching)\b"
r"|\bno\s+(?:code|file|source|implementation)\s+(?:edits?|changes?|modifications?)\b"
r"|\bmake\s+no\s+(?:code|file|source)?\s*changes\b"
r"|\bread[\s-]?only\s+(?:scope|review|audit|analysis|assessment|inspection|diagnos\w+)\b)",
re.IGNORECASE,
)
VERDICT_DELIVERABLE = re.compile(
r"(?:\bship\s+or\s+block\b|\bblock\s+or\s+ship\b|\bship\s*/\s*block\b"
r"|\breturn\s+(?:a\s+|one\s+|the\s+)?(?:strict\s+)?verdict\b"
r"|\b(?:review|audit|release|diagnostic)\s+verdict\b|\bverdict\s*[:=])",
re.IGNORECASE,
)
MUTATION_DELIVERABLE = re.compile(
r"(?:\bpushed\s+(?:sha|commit|head|branch|revision)\b"
r"|\bpush\s+(?:the\s+|your\s+|one\s+)?(?:branch|commit|change\w*|fix\w*)\b"
r"|\bcommit\s+and\s+push\b"
r"|\bopen\s+(?:a|an|one|the)\s+(?:new\s+)?(?:draft\s+)?(?:pr\b|pull\s+request)"
r"|\bdraft\s+pr\s+only\b)",
re.IGNORECASE,
)
VERDICT_DIRECTIVE = re.compile(
r"\b(?:final\s+|strict\s+|overall\s+)?(?:verdict|recommendation|decision)\s*"
r"(?:is\s*)?[:=—-]\s*[*_`\s]*(ship|block)\b",
re.IGNORECASE,
)
VERDICT_TOKEN = re.compile(r"(?<![A-Za-z])(SHIP|BLOCK)(?![A-Za-z])")
CONTROL_CHARACTERS = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
SECRET_LIKE = re.compile(
r"(?:gh[pousr]_[A-Za-z0-9]{16,}|xox[abprs]-[A-Za-z0-9-]{10,}"
r"|sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}"
r"|eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,})"
)
def _bounded(value: str, limit: int) -> str:
@ -55,17 +152,138 @@ def _bounded(value: str, limit: int) -> str:
return f"{value[:head]}{marker}{value[-(remaining - head):]}"
def unfinished_result_reason(result: dict[str, Any]) -> str | None:
def sanitize_reason(value: Any, limit: int = JUDGE_REASON_LIMIT) -> str:
"""Return one bounded, single-line, secret-free judge reason."""
text = SECRET_LIKE.sub("[redacted]", str(value or ""))
text = " ".join(CONTROL_CHARACTERS.sub(" ", text).split())
if not text:
return "local judge supplied no reason"
return text if len(text) <= limit else f"{text[: limit - 3].rstrip()}..."
def _strings(value: Any) -> list[str]:
"""Return the non-empty string entries of a reported evidence list."""
if not isinstance(value, list):
return []
return [str(item).strip() for item in value if str(item).strip()]
def card_scope(objective: str) -> str:
"""Return the card the worker was assigned, without appended run history."""
text = str(objective or "")
match = HISTORY_SECTION.search(text)
return text[: match.start()] if match else text
def _declared_role(objective: str) -> str | None:
"""Return the role a card declared through explicit worker metadata."""
for pattern in (ROLE_DIRECTIVE, OUTPUT_DIRECTIVE):
roles = {
ROLE_ALIASES[value]
for value in (
match.group(1).strip().lower() for match in pattern.finditer(objective)
)
if value in ROLE_ALIASES
}
if len(roles) == 1:
return roles.pop()
if roles:
# A card that declares two different roles has no usable contract.
# Hold it to the stricter implementation regime rather than letting
# the more permissive review path win an ambiguous declaration.
return IMPLEMENTATION_ROLE
return None
def task_role(objective: str, result: dict[str, Any] | None = None) -> tuple[str, str]:
"""Resolve the assigned task role and how that resolution was reached."""
text = card_scope(objective)
declared = _declared_role(text)
if declared is not None:
return declared, "directive"
changed = _strings((result or {}).get("changed_files"))
if (
not changed
and READ_ONLY_SCOPE.search(text)
and VERDICT_DELIVERABLE.search(text)
and not MUTATION_DELIVERABLE.search(text)
):
return REVIEW_ROLE, "inferred"
return IMPLEMENTATION_ROLE, "default"
def review_verdict(result: dict[str, Any]) -> tuple[str | None, str | None]:
"""Return the declared SHIP/BLOCK verdict, or why it cannot be trusted."""
declared: set[str] = set()
field = result.get("verdict")
if field is not None:
value = str(field).strip().upper()
if value not in {"SHIP", "BLOCK"}:
return None, f"review verdict field is not SHIP or BLOCK: {value[:40]!r}"
declared.add(value)
summary = str(result.get("summary") or "")
declared.update(match.upper() for match in VERDICT_DIRECTIVE.findall(summary))
if len(declared) > 1:
return None, "review report declares both SHIP and BLOCK verdicts"
if declared:
return declared.pop(), None
tokens = {match.upper() for match in VERDICT_TOKEN.findall(summary)}
if len(tokens) > 1:
return None, "review summary names both SHIP and BLOCK without a verdict line"
if tokens:
return tokens.pop(), None
return None, "review report declares no explicit SHIP or BLOCK verdict"
def review_completion_problem(result: dict[str, Any]) -> str | None:
"""Fail closed on a review report that does not carry a usable verdict."""
verdict, problem = review_verdict(result)
if problem or verdict is None:
return problem or "review report declares no explicit SHIP or BLOCK verdict"
summary = str(result.get("summary") or "").strip()
if len(summary) < MIN_REVIEW_SUMMARY_CHARS:
return f"{verdict} verdict carries no reviewable rationale in its summary"
findings = _strings(result.get("findings"))
if verdict == "BLOCK" and not findings:
return "BLOCK verdict reports no findings to justify it"
if not findings and not _strings(result.get("tests_run")) and not _strings(
result.get("artifacts")
):
return f"{verdict} verdict reports no findings, tests, or artifacts as evidence"
return None
def _reported_role(result: dict[str, Any]) -> str:
"""Classify a report shape when no objective is available to the caller."""
if _strings(result.get("changed_files")):
return IMPLEMENTATION_ROLE
if result.get("verdict") is not None or VERDICT_DIRECTIVE.search(
str(result.get("summary") or "")
):
return REVIEW_ROLE
return IMPLEMENTATION_ROLE
def unfinished_result_reason(
result: dict[str, Any],
*,
role: str | None = None,
) -> str | None:
"""Reject self-contradictory completion reports without model inference."""
status = str(result.get("status") or "")
summary = str(result.get("summary") or "").strip()
blockers = result.get("blockers")
if status == "incomplete":
return summary or "worker explicitly reported incomplete work"
return sanitize_reason(summary or "worker explicitly reported incomplete work")
if status != "completed":
return None
if isinstance(blockers, list) and any(str(item).strip() for item in blockers):
return "worker reported blockers while claiming completion"
if (role or _reported_role(result)) == REVIEW_ROLE:
# A review's prose describes the reviewed artifact, so the unfinished
# work heuristic below would read the artifact's state as the review's
# own. The verdict contract is the deterministic gate instead.
return review_completion_problem(result)
tests = result.get("tests_run")
evidence = [summary]
if isinstance(tests, list):
@ -76,18 +294,14 @@ def unfinished_result_reason(result: dict[str, Any]) -> str | None:
return None
def judge_goal_completion(
objective: str,
result: dict[str, Any],
*,
url: str = GOAL_JUDGE_URL,
open_request: Callable[..., Any] = urllib.request.urlopen,
) -> tuple[bool, str]:
"""Use the local model as a fail-closed judge for explicit goal cards."""
deterministic_reason = unfinished_result_reason(result)
if deterministic_reason:
return False, deterministic_reason
payload = {
def _completion_claimed(result: dict[str, Any]) -> bool:
"""Report whether the worker actually claimed a finished task."""
return str(result.get("status") or "") == "completed"
def _judge_payload(objective: str, result: dict[str, Any], role: str) -> dict[str, Any]:
"""Build the fail-closed local judge request for an implementation card."""
return {
"model": GOAL_JUDGE_MODEL,
"messages": [
{
@ -98,13 +312,16 @@ def judge_goal_completion(
"Return complete only when the report gives concrete evidence that every "
"explicit requirement finished. Return continue when any work, test, command, "
"commit, push, review, or verification is pending, in progress, omitted, or "
"inconclusive. Do not trust the report status field."
"inconclusive. Defects the worker reports about some other artifact are "
"findings, not unfinished work on this task. Do not trust the report status "
"field."
),
},
{
"role": "user",
"content": json.dumps(
{
"task_role": role,
"objective": _bounded(objective, 6000),
"worker_report": _bounded(
json.dumps(result, default=str), 4000
@ -126,9 +343,40 @@ def judge_goal_completion(
},
},
}
def judge_goal_completion(
objective: str,
result: dict[str, Any],
*,
url: str = GOAL_JUDGE_URL,
open_request: Callable[..., Any] = urllib.request.urlopen,
) -> tuple[bool, str]:
"""Judge one worker report against the action its card actually assigned."""
role, source = task_role(objective, result)
deterministic_reason = unfinished_result_reason(result, role=role)
if deterministic_reason is None and not _completion_claimed(result):
deterministic_reason = (
f"worker reported status {str(result.get('status') or 'unknown')!r} "
"rather than a finished task"
)
if deterministic_reason:
if role == REVIEW_ROLE:
return False, sanitize_reason(f"{READ_ONLY_GUARD}; {deterministic_reason}")
return False, sanitize_reason(deterministic_reason)
if role == REVIEW_ROLE:
# The requested deliverable is present and self-consistent. A verdict
# that the reviewed artifact must not ship is a finished review, so the
# card finalizes instead of resuming a worker that may not repair it.
verdict, _ = review_verdict(result)
findings = len(_strings(result.get("findings")))
return True, sanitize_reason(
f"review deliverable complete: {verdict} verdict with {findings} finding(s); "
f"task role {role} resolved by {source}"
)
request = urllib.request.Request(
url,
data=json.dumps(payload).encode("utf-8"),
data=json.dumps(_judge_payload(objective, result, role)).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
@ -139,7 +387,7 @@ def judge_goal_completion(
verdict = json.loads(content)
if verdict.get("verdict") not in {"complete", "continue"}:
raise ValueError("local judge returned an invalid verdict")
reason = str(verdict.get("reason") or "local judge supplied no reason")
reason = sanitize_reason(verdict.get("reason"))
return verdict.get("verdict") == "complete", reason
except (
AttributeError,
@ -151,4 +399,6 @@ def judge_goal_completion(
json.JSONDecodeError,
urllib.error.URLError,
) as error:
return False, f"local completion judge unavailable: {type(error).__name__}: {error}"
return False, sanitize_reason(
f"local completion judge unavailable: {type(error).__name__}: {error}"
)

View File

@ -0,0 +1,461 @@
"""Task-role and verdict-contract tests for the Hermes goal judge."""
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
import pytest
SCRIPT = Path(__file__).parents[2] / "services/hermes/scripts/cli_lane_goal.py"
SPEC = importlib.util.spec_from_file_location("cli_lane_review_goal_test", SCRIPT)
assert SPEC and SPEC.loader
goal = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = goal
SPEC.loader.exec_module(goal)
REVIEW_CARD = """# Kanban task t_review: Independent read-only review of the worker pool
Assignee: cli-claude-xhigh
## Body
Perform a fresh independent read-only release review of the pool implementation.
Do not edit files, comment, commit, push, merge, publish, or deploy.
Return strict SHIP or BLOCK with exact file/line and reproducible inputs.
"""
IMPLEMENTATION_CARD = """# Kanban task t_impl: Repair the pool blockers
Assignee: cli-auto
## Body
Repair every reported blocker, run the focused suites, and open a draft PR.
Return exact pushed SHA and evidence.
"""
POLLUTED_CARD = REVIEW_CARD + """
## Recent work by @cli-claude-xhigh
- t_prior: Implemented and pushed the pool. PR #18 is open at the exact pushed SHA.
## Comment thread
- shepherd: commit and push the repairs before returning.
"""
def _review(**overrides):
value = {
"status": "completed",
"summary": (
"Independent read-only review of PR #18 at head 2000252. Verdict: BLOCK. "
"Five reproducible P0 defects fire on the coordinator's first tick and the "
"review worktree was left pristine."
),
"changed_files": [],
"tests_run": ["pytest testing/tests/test_pool.py: 82 passed"],
"artifacts": ["/opt/data/workspace/evidence/t_review/RUN.md"],
"findings": ["P0-1 BLOCKER - coordinator.py:248 compares int to str run ids."],
"blockers": [],
}
value.update(overrides)
return value
def _implementation(**overrides):
value = {
"status": "completed",
"summary": "All acceptance criteria passed and the branch was pushed.",
"changed_files": ["src/example.py"],
"tests_run": ["pytest -q: 12 passed"],
"artifacts": [],
"findings": [],
"blockers": [],
}
value.update(overrides)
return value
def _no_judge(*_args, **_kwargs):
raise AssertionError("the model judge must not be consulted for a review card")
def test_completed_block_review_finalizes_without_consulting_the_model():
accepted, reason = goal.judge_goal_completion(
REVIEW_CARD,
_review(),
open_request=_no_judge,
)
assert accepted is True
assert "BLOCK verdict with 1 finding(s)" in reason
assert "resolved by inferred" in reason
def test_completed_ship_review_finalizes_on_its_own_evidence():
accepted, reason = goal.judge_goal_completion(
REVIEW_CARD,
_review(
summary=(
"Independent read-only review of PR #18 at head 2000252. Verdict: SHIP. "
"Every prioritized boundary was exercised and no defect survived."
),
findings=[],
),
open_request=_no_judge,
)
assert accepted is True
assert "SHIP verdict with 0 finding(s)" in reason
@pytest.mark.parametrize(
("role_line", "expected"),
[
("Hermes-Task-Role: review", goal.REVIEW_ROLE),
("Task-Role: audit", goal.REVIEW_ROLE),
("- Hermes Task Role = diagnostic", goal.REVIEW_ROLE),
("Hermes-Expected-Output: verdict", goal.REVIEW_ROLE),
("Hermes-Task-Role: implementation", goal.IMPLEMENTATION_ROLE),
],
)
def test_explicit_card_metadata_decides_the_task_role(role_line, expected):
card = IMPLEMENTATION_CARD.replace("## Body\n", f"## Body\n{role_line}\n")
assert goal.task_role(card, _review())[0] == expected
assert goal.task_role(card, _review())[1] == "directive"
def test_explicit_review_metadata_survives_a_changed_file_report():
card = REVIEW_CARD.replace("## Body\n", "## Body\nHermes-Task-Role: review\n")
accepted, reason = goal.judge_goal_completion(
card,
_review(changed_files=["reports/review.md"]),
open_request=_no_judge,
)
assert accepted is True
assert "resolved by directive" in reason
def test_appended_run_history_cannot_reassign_the_card_role():
assert goal.task_role(POLLUTED_CARD, _review()) == (goal.REVIEW_ROLE, "inferred")
assert "Recent work by" not in goal.card_scope(POLLUTED_CARD)
assert "Do not edit files" in goal.card_scope(POLLUTED_CARD)
def test_implementation_card_never_infers_a_review_role():
assert goal.task_role(IMPLEMENTATION_CARD, _review()) == (
goal.IMPLEMENTATION_ROLE,
"default",
)
@pytest.mark.parametrize(
("result", "expected"),
[
(_review(summary="Verdict: BLOCK."), "carries no reviewable rationale"),
(_review(findings=[]), "BLOCK verdict reports no findings"),
(
_review(summary=_review()["summary"].replace("Verdict: BLOCK.", "")),
"declares no explicit SHIP or BLOCK verdict",
),
(
_review(verdict="SHIP"),
"declares both SHIP and BLOCK verdicts",
),
(
_review(verdict="maybe"),
"review verdict field is not SHIP or BLOCK",
),
(
_review(
summary=(
"The reviewed change is fine overall. SHIP is defensible but BLOCK "
"is also arguable given the unresolved capacity question."
),
),
"names both SHIP and BLOCK without a verdict line",
),
(
_review(
summary=(
"Independent read-only review of PR #18. Verdict: SHIP. Every "
"prioritized boundary was exercised and nothing survived."
),
findings=[],
tests_run=[],
artifacts=[],
),
"no findings, tests, or artifacts as evidence",
),
],
)
def test_malformed_review_completion_fails_closed(result, expected):
accepted, reason = goal.judge_goal_completion(
REVIEW_CARD,
result,
open_request=_no_judge,
)
assert accepted is False
assert expected in reason
assert reason.startswith(goal.READ_ONLY_GUARD)
def test_rejected_review_never_demands_an_implementation_edit():
accepted, reason = goal.judge_goal_completion(
REVIEW_CARD,
_review(findings=[]),
open_request=_no_judge,
)
assert accepted is False
assert "do not modify the reviewed implementation" in reason
def test_incomplete_review_turn_is_resumed_under_the_read_only_guard():
accepted, reason = goal.judge_goal_completion(
REVIEW_CARD,
_review(status="incomplete", summary="Only two of six boundaries were read."),
open_request=_no_judge,
)
assert accepted is False
assert reason.startswith(goal.READ_ONLY_GUARD)
assert "Only two of six boundaries were read." in reason
def test_blocked_review_reports_its_own_obstacle_not_a_verdict():
result = _review(
status="blocked",
summary="The pull request head could not be resolved from any SCM mirror.",
blockers=["Gitea API is unreachable from the review worktree."],
)
assert goal.unfinished_result_reason(result, role=goal.REVIEW_ROLE) is None
assert goal.unfinished_result_reason(result) is None
accepted, reason = goal.judge_goal_completion(
REVIEW_CARD, result, open_request=_no_judge
)
assert accepted is False
assert "rather than a finished task" in reason
def test_conflicting_role_directives_hold_the_card_to_implementation():
card = REVIEW_CARD.replace(
"## Body\n",
"## Body\nHermes-Task-Role: review\nHermes-Task-Role: implementation\n",
)
assert goal.task_role(card, _review()) == (goal.IMPLEMENTATION_ROLE, "directive")
def test_incomplete_review_summary_is_bounded_before_it_leaves_the_judge():
accepted, reason = goal.judge_goal_completion(
REVIEW_CARD,
_review(status="incomplete", summary="pending\n" + "y" * 4000),
open_request=_no_judge,
)
assert accepted is False
assert reason.startswith(goal.READ_ONLY_GUARD)
assert len(reason) <= goal.JUDGE_REASON_LIMIT
def test_review_claiming_completion_with_blockers_is_rejected():
accepted, reason = goal.judge_goal_completion(
REVIEW_CARD,
_review(blockers=["the diff could not be resolved"]),
open_request=_no_judge,
)
assert accepted is False
assert "worker reported blockers while claiming completion" in reason
def test_review_prose_about_the_reviewed_artifact_is_not_unfinished_work():
result = _review(
summary=(
"Independent read-only review. Verdict: BLOCK. The reviewed branch's CI "
"build is still running and its migration remains pending, which is why "
"the change is unfit to ship."
),
tests_run=["upstream pipeline: build is still running on the reviewed head"],
)
assert goal.unfinished_result_reason(result, role=goal.REVIEW_ROLE) is None
accepted, _reason = goal.judge_goal_completion(
REVIEW_CARD,
result,
open_request=_no_judge,
)
assert accepted is True
def test_implementation_prose_about_unfinished_work_is_still_rejected():
result = _implementation(summary="The broad rerun remains active before the push.")
assert goal.unfinished_result_reason(result)
assert goal.unfinished_result_reason(result, role=goal.IMPLEMENTATION_ROLE)
class JudgeResponse:
def __init__(self, verdict: str, reason: str):
self.body = json.dumps(
{"choices": [{"message": {"content": json.dumps(
{"verdict": verdict, "reason": reason}
)}}]}
).encode()
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def read(self):
return self.body
def test_implementation_card_keeps_the_fail_closed_model_judge():
observed = {}
def request(req, timeout):
observed["payload"] = json.loads(req.data)
observed["timeout"] = timeout
return JudgeResponse("continue", "the remote head was never verified")
accepted, reason = goal.judge_goal_completion(
IMPLEMENTATION_CARD,
_implementation(),
open_request=request,
)
assert accepted is False
assert reason == "the remote head was never verified"
role = json.loads(observed["payload"]["messages"][1]["content"])["task_role"]
assert role == goal.IMPLEMENTATION_ROLE
assert observed["timeout"] == 120
def test_implementation_completion_still_finalizes_on_an_accepting_judge():
accepted, reason = goal.judge_goal_completion(
IMPLEMENTATION_CARD,
_implementation(),
open_request=lambda *_a, **_k: JudgeResponse("complete", "every criterion has evidence"),
)
assert accepted is True
assert reason == "every criterion has evidence"
def test_ambiguous_card_without_a_verdict_report_stays_on_the_model_judge():
ambiguous = """# Kanban task t_mixed: Review and repair the pool
## Body
Review the pool, then repair what you find. Do not modify unrelated files.
Return a verdict and open a draft PR with the repairs.
"""
accepted, _reason = goal.judge_goal_completion(
ambiguous,
_review(),
open_request=lambda *_a, **_k: JudgeResponse("continue", "the repairs are missing"),
)
assert goal.task_role(ambiguous, _review())[0] == goal.IMPLEMENTATION_ROLE
assert accepted is False
def test_judge_reason_is_bounded_sanitized_and_secret_free():
leaky = "line one\r\n\x07 token ghp_" + "A" * 24 + " " + "x" * 900
accepted, reason = goal.judge_goal_completion(
IMPLEMENTATION_CARD,
_implementation(),
open_request=lambda *_a, **_k: JudgeResponse("continue", leaky),
)
assert accepted is False
assert len(reason) <= goal.JUDGE_REASON_LIMIT
assert "ghp_" not in reason
assert "[redacted]" in reason
assert "\n" not in reason and "\r" not in reason and "\x07" not in reason
def test_unavailable_judge_still_fails_closed_for_implementation_cards():
accepted, reason = goal.judge_goal_completion(
IMPLEMENTATION_CARD,
_implementation(),
open_request=lambda *_a, **_k: JudgeResponse("unknown", "bad"),
)
assert accepted is False
assert "local completion judge unavailable" in reason
def test_bare_uppercase_verdict_token_is_accepted_as_a_last_resort():
result = _review(
summary=(
"Independent read-only review of PR #18 at head 2000252 concludes BLOCK "
"because five reproducible P0 defects fire on the first coordinator tick."
),
)
assert goal.review_verdict(result) == ("BLOCK", None)
accepted, _reason = goal.judge_goal_completion(
REVIEW_CARD, result, open_request=_no_judge
)
assert accepted is True
def test_unknown_role_directive_falls_through_to_the_next_contract_surface():
card = IMPLEMENTATION_CARD.replace(
"## Body\n",
"## Body\nHermes-Task-Role: gardener\nHermes-Expected-Output: verdict\n",
)
assert goal.task_role(card, _review()) == (goal.REVIEW_ROLE, "directive")
def test_unrecognized_directives_leave_the_role_to_inference():
card = REVIEW_CARD.replace("## Body\n", "## Body\nHermes-Task-Role: gardener\n")
assert goal.task_role(card, _review()) == (goal.REVIEW_ROLE, "inferred")
def test_malformed_evidence_fields_are_read_without_raising():
assert goal._strings("not-a-list") == []
result = _implementation(tests_run="pytest -q", summary="Everything shipped.")
assert goal.unfinished_result_reason(result) is None
def test_implementation_rejection_reason_carries_no_read_only_guard():
accepted, reason = goal.judge_goal_completion(
IMPLEMENTATION_CARD,
_implementation(blockers=["the remote was unreachable"]),
open_request=_no_judge,
)
assert accepted is False
assert reason == "worker reported blockers while claiming completion"
def test_oversized_objectives_and_empty_reasons_stay_bounded():
compacted = goal._bounded("a" * 9000, 6000)
assert len(compacted) == 6000
assert "[goal evidence compacted]" in compacted
assert goal.sanitize_reason(" ") == "local judge supplied no reason"
def test_judging_never_mutates_the_reported_result():
result = _review()
snapshot = json.dumps(result, sort_keys=True)
goal.judge_goal_completion(REVIEW_CARD, result, open_request=_no_judge)
assert json.dumps(result, sort_keys=True) == snapshot

View File

@ -0,0 +1,427 @@
"""Lane-level goal-loop behaviour for review and implementation cards."""
from __future__ import annotations
import importlib.util
import json
import sys
from contextlib import nullcontext
from pathlib import Path
from types import SimpleNamespace
SCRIPTS = Path(__file__).parents[2] / "services/hermes/scripts"
sys.path.insert(0, str(SCRIPTS))
def _load(name: str):
spec = importlib.util.spec_from_file_location(name, SCRIPTS / f"{name}.py")
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
lanes = _load("cli_lane_runner")
# The single-claim loop lives in the runner today and in ``cli_lane_execution``
# once the lane modules are split. Patch whichever module owns it so this file
# describes lane behaviour rather than one file layout.
execution = sys.modules.get("cli_lane_execution", lanes)
goal = sys.modules["cli_lane_goal"]
REVIEW_CARD = """# Kanban task t_review: Independent read-only review of the worker pool
## Body
Perform a fresh independent read-only release review of the pool implementation.
Do not edit files, comment, commit, push, merge, publish, or deploy.
Return strict SHIP or BLOCK with exact file/line and reproducible inputs.
"""
IMPLEMENTATION_CARD = """# Kanban task t_impl: Repair the pool blockers
## Body
Repair every reported blocker, run the focused suites, and open a draft PR.
Return exact pushed SHA and evidence.
"""
BLOCK_REVIEW = {
"status": "completed",
"summary": (
"Independent read-only review of PR #18 at head 2000252. Verdict: BLOCK. "
"Five reproducible P0 defects fire on the coordinator's first tick and the "
"review worktree was left pristine."
),
"changed_files": [],
"tests_run": ["pytest testing/tests/test_pool.py: 82 passed"],
"artifacts": [],
"findings": ["P0-1 BLOCKER - coordinator.py:248 compares int to str run ids."],
"blockers": [],
}
class _Connection:
def close(self) -> None:
return None
class _Lane:
"""One instrumented direct-CLI-lane claim over a fake Kanban board."""
def __init__(self, tmp_path: Path, monkeypatch, *, card: str, task, reports):
self.calls: list[tuple[str, dict]] = []
self.comments: list[str] = []
self.prompts: list[str] = []
self.routes: list[tuple[str, dict]] = []
self.heartbeats: list[tuple[str, object]] = []
self.reports = list(reports)
self.task = task
self.state_root = tmp_path / "cli-lanes"
self.workspace = tmp_path
self.db = SimpleNamespace(
scoped_current_board=lambda _board: nullcontext(),
connect=lambda board=None, **_kwargs: _Connection(),
get_task=lambda _conn, _task_id: self.task,
worker_log_path=lambda _task_id, board: tmp_path / "worker.log",
_resolve_worktree_workspace=lambda _task, board: (tmp_path, "wt/t_card"),
set_branch_name=lambda *_args: None,
set_workspace_path=lambda *_args: None,
build_worker_context=lambda *_args: card,
heartbeat_worker=lambda _conn, _task_id, *, note, expected_run_id: (
self.heartbeats.append((note, expected_run_id)) or True
),
add_comment=lambda _conn, _task_id, _author, body: self.comments.append(body),
complete_task=self._complete_task,
block_task=self._block_task,
reclaim_task=self._reclaim_task,
)
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=self.db))
# The lane derives durable journal identity from ``STATE_ROOT``, so
# relocate the root itself rather than stubbing ``state_path``.
for module in list(sys.modules.values()):
if getattr(module, "__name__", "").startswith("cli_lane") and hasattr(
module, "STATE_ROOT"
):
monkeypatch.setattr(module, "STATE_ROOT", self.state_root)
monkeypatch.setattr(execution, "fresh_unavailable_provider", lambda: None)
monkeypatch.setattr(execution, "select_route", self._select_route)
monkeypatch.setattr(execution, "run_provider", self._run_provider)
def _complete_task(
self,
_conn,
task_id,
*,
result,
summary,
metadata,
expected_run_id=None,
replay_ended_run_id=None,
):
self.calls.append(
(
"complete",
{
"task_id": task_id,
"result": result,
"summary": summary,
"metadata": metadata,
"expected_run_id": expected_run_id or replay_ended_run_id,
},
)
)
self.task = SimpleNamespace(
**{
**vars(self.task),
"status": "done",
"result": result,
"completed_run_id": expected_run_id or replay_ended_run_id,
}
)
return True
def _block_task(self, _conn, task_id, *, reason, kind, expected_run_id=None):
self.calls.append(
(
"block",
{
"task_id": task_id,
"reason": reason,
"kind": kind,
"expected_run_id": expected_run_id,
},
)
)
return True
def _reclaim_task(self, _conn, _task_id, *, reason=None, expected_run_id=None):
return True
@property
def state_file(self) -> Path:
return self.state_root / "titan-iac/t_card.json"
@staticmethod
def _route(provider: str, effort: str = "xhigh"):
return lanes.Route(
provider,
f"{provider}-model",
effort,
f"{provider}-{effort}",
"switchyard",
"routed",
1,
(),
)
def _select_route(self, _prompt, assignee, **kwargs):
self.routes.append((assignee, kwargs))
if assignee.startswith("cli-codex"):
return self._route("codex")
return self._route("claude")
def _run_provider(self, *args, **_kwargs):
self.prompts.append(args[1])
args[6]("working")
return self.reports.pop(0)
@property
def terminal(self) -> tuple[str, dict]:
assert len(self.calls) == 1, self.calls
return self.calls[0]
@property
def state(self) -> dict:
return json.loads(self.state_file.read_text(encoding="utf-8"))
def rejections(self) -> list[str]:
return [item for item in self.comments if "Goal completion rejected" in item]
def _task(**overrides):
value = {
"id": "t_card",
"status": "running",
"current_run_id": 23,
"completed_run_id": None,
"result": None,
"assignee": "cli-claude-xhigh",
"max_runtime_seconds": 600,
"goal_mode": True,
"goal_max_turns": 8,
}
value.update(overrides)
return SimpleNamespace(**value)
def _result(**overrides):
value = dict(BLOCK_REVIEW)
value.update(overrides)
return lanes.ProcessResult(0, "review turn", value, False)
def _no_judge(*_args, **_kwargs):
raise AssertionError("the model judge must not be consulted for a review card")
def test_completed_block_review_finalizes_on_its_first_turn(tmp_path, monkeypatch):
monkeypatch.setattr(goal.urllib.request, "urlopen", _no_judge)
lane = _Lane(
tmp_path,
monkeypatch,
card=REVIEW_CARD,
task=_task(),
reports=[_result()],
)
execution.execute_claim("titan-iac", "t_card")
action, kwargs = lane.terminal
assert action == "complete"
assert kwargs["expected_run_id"] == 23
assert json.loads(kwargs["result"])["findings"] == BLOCK_REVIEW["findings"]
assert kwargs["metadata"]["goal_turn"] == 1
assert "BLOCK verdict with 1 finding(s)" in kwargs["metadata"]["goal_judge_reason"]
assert lane.rejections() == []
assert len(lane.prompts) == 1
assert lane.reports == []
def test_review_verdict_reaches_kanban_without_hidden_mutation(tmp_path, monkeypatch):
monkeypatch.setattr(goal.urllib.request, "urlopen", _no_judge)
lane = _Lane(
tmp_path,
monkeypatch,
card=REVIEW_CARD,
task=_task(),
reports=[_result()],
)
execution.execute_claim("titan-iac", "t_card")
_action, kwargs = lane.terminal
stored = json.loads(kwargs["result"])
assert stored["status"] == "completed"
assert stored["summary"] == BLOCK_REVIEW["summary"]
assert stored["blockers"] == []
assert kwargs["summary"] == BLOCK_REVIEW["summary"]
assert kwargs["metadata"]["findings"] == BLOCK_REVIEW["findings"]
assert len(kwargs["metadata"]["goal_judge_reason"]) <= goal.JUDGE_REASON_LIMIT
def test_reviewer_provider_fallback_still_finalizes_one_verdict(tmp_path, monkeypatch):
monkeypatch.setattr(goal.urllib.request, "urlopen", _no_judge)
lane = _Lane(
tmp_path,
monkeypatch,
card=REVIEW_CARD,
task=_task(assignee="cli-auto"),
reports=[
lanes.ProcessResult(1, "usage limit reached", None, True),
_result(),
],
)
execution.execute_claim("titan-iac", "t_card")
action, kwargs = lane.terminal
assert action == "complete"
assert kwargs["metadata"]["provider"] == "codex"
assert kwargs["metadata"]["goal_turn"] == 1
assert any("Provider fallback: claude -> codex" in item for item in lane.comments)
assert lane.rejections() == []
assert len(lane.prompts) == 2
def test_review_restart_resumes_the_next_turn_and_completes_once(tmp_path, monkeypatch):
monkeypatch.setattr(goal.urllib.request, "urlopen", _no_judge)
lane = _Lane(
tmp_path,
monkeypatch,
card=REVIEW_CARD,
task=_task(),
reports=[_result()],
)
lane.state_file.parent.mkdir(parents=True, exist_ok=True)
lane.state_file.write_text(
json.dumps({"goal_turn": 3, "current_route": {"provider": "codex"}}),
encoding="utf-8",
)
execution.execute_claim("titan-iac", "t_card")
action, kwargs = lane.terminal
assert action == "complete"
assert kwargs["metadata"]["goal_turn"] == 4
assert lane.state["goal_turn"] == 4
assert any("Restart-time provider change" in item for item in lane.comments)
def test_malformed_review_is_resumed_without_demanding_an_edit(tmp_path, monkeypatch):
monkeypatch.setattr(goal.urllib.request, "urlopen", _no_judge)
lane = _Lane(
tmp_path,
monkeypatch,
card=REVIEW_CARD,
task=_task(goal_max_turns=2),
reports=[
_result(summary="Review done.", findings=[]),
_result(),
],
)
execution.execute_claim("titan-iac", "t_card")
action, kwargs = lane.terminal
assert action == "complete"
assert kwargs["metadata"]["goal_turn"] == 2
assert len(lane.rejections()) == 1
assert goal.READ_ONLY_GUARD in lane.rejections()[0]
assert "do not modify the reviewed implementation" in lane.prompts[1]
assert "Goal-loop continuation" in lane.prompts[1]
def test_exhausted_review_budget_blocks_with_a_read_only_reason(tmp_path, monkeypatch):
monkeypatch.setattr(goal.urllib.request, "urlopen", _no_judge)
lane = _Lane(
tmp_path,
monkeypatch,
card=REVIEW_CARD,
task=_task(goal_max_turns=1),
reports=[_result(summary="Review done.", findings=[])],
)
execution.execute_claim("titan-iac", "t_card")
action, kwargs = lane.terminal
assert action == "block"
assert kwargs["expected_run_id"] == 23
assert goal.READ_ONLY_GUARD in kwargs["reason"]
def test_implementation_goal_loop_is_preserved(tmp_path, monkeypatch):
verdicts = iter([(False, "the remote head was never verified"), (True, "all criteria hold")])
contexts: list[str] = []
def judge(objective, *_args, **_kwargs):
contexts.append(objective)
return next(verdicts)
monkeypatch.setattr(goal, "judge_goal_completion", judge)
implementation = {
"status": "completed",
"summary": "Focused tests passed and the branch was pushed.",
"changed_files": ["src/a.py"],
"tests_run": ["pytest focused: passed"],
"artifacts": [],
"findings": [],
"blockers": [],
}
lane = _Lane(
tmp_path,
monkeypatch,
card=IMPLEMENTATION_CARD,
task=_task(assignee="cli-auto", goal_max_turns=3),
reports=[
lanes.ProcessResult(0, "turn one", dict(implementation), False),
lanes.ProcessResult(0, "turn two", dict(implementation), False),
],
)
execution.execute_claim("titan-iac", "t_card")
action, kwargs = lane.terminal
assert action == "complete"
assert kwargs["metadata"]["goal_turn"] == 2
assert len(lane.rejections()) == 1
assert "the remote head was never verified" in lane.rejections()[0]
assert "prior rejected reports" in contexts[1]
def test_incomplete_implementation_never_finalizes(tmp_path, monkeypatch):
lane = _Lane(
tmp_path,
monkeypatch,
card=IMPLEMENTATION_CARD,
task=_task(assignee="cli-auto", goal_max_turns=1),
reports=[
lanes.ProcessResult(
0,
"turn one",
{
"status": "incomplete",
"summary": "The focused suite is still running.",
"changed_files": ["src/a.py"],
"tests_run": [],
"artifacts": [],
"findings": [],
"blockers": [],
},
False,
)
],
)
execution.execute_claim("titan-iac", "t_card")
action, kwargs = lane.terminal
assert action == "block"
assert "The focused suite is still running." in kwargs["reason"]