diff --git a/hermes_cli/goals.py b/hermes_cli/goals.py index bf2e25ef70..2047c81c54 100644 --- a/hermes_cli/goals.py +++ b/hermes_cli/goals.py @@ -1646,7 +1646,15 @@ def run_kanban_goal_loop( _log(f"kanban goal loop: task {task_id} status={status!r}; stopping") return _result("stopped", f"status={status}") - verdict, reason, _parse_failed, _wait, _transport_failed = judge_goal(goal_text, last_response) + # The between-turns judge runs outside any agent turn: bind the per-task relay-affinity + # scope (same shape as the handoff gates) so the relay does not reject the call (#113669). + from agent.portal_tags import get_affinity_scope, reset_affinity_scope, set_affinity_scope + affinity_token = None if get_affinity_scope() else set_affinity_scope(f"kanban:{task_id}") + try: + verdict, reason, _parse_failed, _wait, _transport_failed = judge_goal(goal_text, last_response) + finally: + if affinity_token is not None: + reset_affinity_scope(affinity_token) if verdict == "wait": verdict = "continue" _log(f"kanban goal loop: turn {turns_used}/{max_turns} verdict={verdict} reason={_truncate(reason, 120)}") diff --git a/tests/hermes_cli/test_kanban_goal_judge_affinity.py b/tests/hermes_cli/test_kanban_goal_judge_affinity.py index c2d3e7d0c9..3e8d40bc31 100644 --- a/tests/hermes_cli/test_kanban_goal_judge_affinity.py +++ b/tests/hermes_cli/test_kanban_goal_judge_affinity.py @@ -85,3 +85,28 @@ def test_tool_gate_binds_per_task_affinity_scope(): kanban_tools._goal_gate("kanban_complete", _task("task-9"), "task-9", "ev") assert seen == ["kanban:task-9"] assert get_affinity_scope() is None + + +def test_goal_loop_judge_binds_per_task_affinity_scope(): + """The between-turns judge in run_kanban_goal_loop runs under kanban: too.""" + from hermes_cli import goals + + seen = [] + + def fake_judge(goal, last_response): + seen.append(get_affinity_scope()) + return ("continue", "not yet", False, None, False) + + with patch.object(goals, "judge_goal", side_effect=fake_judge): + result = goals.run_kanban_goal_loop( + task_id="task-7", + goal_text="goal", + run_turn=lambda prompt: "still working", + task_status_fn=lambda: "running", + block_fn=lambda msg: None, + max_turns=2, + first_response="first", + ) + assert result["outcome"] == "blocked_budget" + assert seen == ["kanban:task-7", "kanban:task-7"] + assert get_affinity_scope() is None