hermes: separate review findings from blockers

This commit is contained in:
jenkins 2026-08-16 11:54:26 -03:00
parent 42ac76fe67
commit 1ea8e16286
4 changed files with 43 additions and 4 deletions

View File

@ -25,7 +25,7 @@ spec:
ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback
ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available
ai.bstein.dev/config-rev: "20260816-goal-completion-judge-v2"
ai.bstein.dev/config-rev: "20260816-review-findings-contract-v3"
prometheus.io/scrape: "true"
prometheus.io/path: /metrics
prometheus.io/port: "9010"

View File

@ -70,7 +70,22 @@ RESULT_SCHEMA: dict[str, Any] = {
"changed_files": {"type": "array", "items": {"type": "string"}},
"tests_run": {"type": "array", "items": {"type": "string"}},
"artifacts": {"type": "array", "items": {"type": "string"}},
"blockers": {"type": "array", "items": {"type": "string"}},
"findings": {
"type": "array",
"items": {"type": "string"},
"description": (
"Defects, risks, or observations discovered by a review or diagnosis. "
"Findings do not prevent the assigned review or diagnosis from completing."
),
},
"blockers": {
"type": "array",
"items": {"type": "string"},
"description": (
"Concrete obstacles that prevent completion of the assigned task itself. "
"This must be empty when status is completed; review findings belong in findings."
),
},
},
}
@ -277,7 +292,7 @@ Workspace: {workspace}
Operate autonomously inside the workspace. Inspect before editing, preserve unrelated user changes, run proportionate tests, and do not claim completion without evidence. You have owner-level Kubernetes access in every namespace. Prefer Flux-tracked manifests for durable changes, but use kubectl, Flux, exec, port-forwarding, rollout operations, and existing Vault workflows when the objective or incident requires them. Persist any desired-state mutation back to Git. Do not force-push, hard-reset, clean untracked files, or expose credentials.
Return a final JSON object matching the supplied schema. Use status=incomplete when required work, tests, commands, commits, pushes, or verification are still running or remain to be done. Use status=blocked only for a concrete unresolved blocker. Never use status=completed for a progress report. List changed files, tests run, durable artifact paths, and blockers explicitly.
Return a final JSON object matching the supplied schema. Use status=incomplete when required work, tests, commands, commits, pushes, or verification are still running or remain to be done. Use status=blocked only when an obstacle prevents completion of the assigned task itself. Never use status=completed for a progress report. For review or diagnostic tasks, put defects and risks in findings; those findings can make the reviewed change unfit to ship without blocking completion of the review. The blockers array must be empty whenever status is completed. List changed files, tests run, durable artifact paths, findings, and task blockers explicitly.
"""
@ -964,7 +979,13 @@ def execute_claim(board: str, task_id: str) -> None:
"goal_turn": goal_turn,
}
if structured:
for key in ("changed_files", "tests_run", "artifacts", "blockers"):
for key in (
"changed_files",
"tests_run",
"artifacts",
"findings",
"blockers",
):
value = structured.get(key)
metadata[key] = value if isinstance(value, list) else []

View File

@ -53,6 +53,15 @@ def test_completed_pending_state_test_name_is_not_a_false_positive():
assert goal.unfinished_result_reason(result) is None
def test_completed_review_findings_are_not_task_blockers():
result = _result(
summary="Review complete; the pull request is not merge-ready.",
findings=["A casting-table variant fails open."],
)
assert goal.unfinished_result_reason(result) is None
class JudgeResponse:
def __init__(self, verdict: str, reason: str):
self.body = json.dumps(

View File

@ -946,6 +946,15 @@ def test_provider_commands_are_structured_unattended_and_capped(tmp_path: Path):
assert "max" not in claude
def test_worker_contract_separates_review_findings_from_task_blockers(tmp_path: Path):
prompt = lanes.build_prompt("Review the change.", tmp_path)
assert "put defects and risks in findings" in prompt
assert "blockers array must be empty whenever status is completed" in prompt
assert "findings" in lanes.RESULT_SCHEMA["properties"]
assert "assigned task itself" in lanes.RESULT_SCHEMA["properties"]["blockers"]["description"]
@pytest.mark.parametrize(
"command",
[