diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index c8c53dba7ecb..7d4b4ef60898 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -5889,13 +5889,19 @@ def check_respawn_guard(conn: sqlite3.Connection, task_id: str) -> Optional[str] return "recent_success" # 4. GitHub PR URL in a recent comment — prior worker already opened a PR. - pr_cutoff = now - _RESPAWN_GUARD_PR_WINDOW - for c in conn.execute( - "SELECT body FROM task_comments WHERE task_id = ? AND created_at >= ?", - (task_id, pr_cutoff), - ).fetchall(): - if c["body"] and _RESPAWN_GUARD_PR_URL_RE.search(c["body"]): - return "active_pr" + # DISABLED BY DEFAULT: in this deployment every PR is human-reviewed and + # human-merged, so a re-spawn cannot silently land a duplicate/merged PR. + # The guard only created friction (tasks stuck `ready` after producing a PR, + # with no `--force` flag in this Hermes version). Re-enable by setting + # HERMES_KANBAN_RESPAWN_GUARD_ACTIVE_PR=1. + if os.environ.get("HERMES_KANBAN_RESPAWN_GUARD_ACTIVE_PR", "").strip() in ("1", "true", "True"): + pr_cutoff = now - _RESPAWN_GUARD_PR_WINDOW + for c in conn.execute( + "SELECT body FROM task_comments WHERE task_id = ? AND created_at >= ?", + (task_id, pr_cutoff), + ).fetchall(): + if c["body"] and _RESPAWN_GUARD_PR_URL_RE.search(c["body"]): + return "active_pr" return None diff --git a/tests/hermes_cli/test_kanban_db.py b/tests/hermes_cli/test_kanban_db.py index 94295f2b63ab..b0ddffb4be6e 100644 --- a/tests/hermes_cli/test_kanban_db.py +++ b/tests/hermes_cli/test_kanban_db.py @@ -1710,8 +1710,24 @@ def test_respawn_guard_stale_success_not_guarded(kanban_home): assert reason is None -def test_respawn_guard_active_pr_in_comment(kanban_home): - """A GitHub PR URL in a recent comment triggers active_pr.""" +def test_respawn_guard_active_pr_disabled_by_default(kanban_home): + """active_pr guard is OFF by default: every PR is human-merged here, so a + re-spawn cannot silently land a duplicate/merged PR. A PR URL in a recent + comment must NOT defer the task unless the guard is explicitly re-enabled.""" + with kb.connect() as conn: + t = kb.create_task(conn, title="has-pr", assignee="alice") + kb.add_comment( + conn, t, "worker", + "PR created: https://github.com/totemx-AI/subsidysmart/pull/42", + ) + reason = kb.check_respawn_guard(conn, t) + assert reason is None + + +def test_respawn_guard_active_pr_in_comment_when_enabled(kanban_home, monkeypatch): + """When HERMES_KANBAN_RESPAWN_GUARD_ACTIVE_PR=1, a GitHub PR URL in a recent + comment triggers active_pr (legacy opt-in behaviour).""" + monkeypatch.setenv("HERMES_KANBAN_RESPAWN_GUARD_ACTIVE_PR", "1") with kb.connect() as conn: t = kb.create_task(conn, title="has-pr", assignee="alice") kb.add_comment( @@ -1722,8 +1738,10 @@ def test_respawn_guard_active_pr_in_comment(kanban_home): assert reason == "active_pr" -def test_respawn_guard_old_pr_comment_not_guarded(kanban_home): - """A GitHub PR URL in a comment older than the PR window does not block.""" +def test_respawn_guard_old_pr_comment_not_guarded(kanban_home, monkeypatch): + """A GitHub PR URL in a comment older than the PR window does not block, + even when the guard is explicitly enabled.""" + monkeypatch.setenv("HERMES_KANBAN_RESPAWN_GUARD_ACTIVE_PR", "1") with kb.connect() as conn: t = kb.create_task(conn, title="old-pr", assignee="alice") old_ts = int(time.time()) - kb._RESPAWN_GUARD_PR_WINDOW - 60 @@ -1809,9 +1827,11 @@ def fake_spawn(task, workspace): def test_dispatch_respawn_guard_skips_active_pr( - kanban_home, all_assignees_spawnable + kanban_home, all_assignees_spawnable, monkeypatch ): - """dispatch_once skips (but does not block) a task with an active PR comment.""" + """With the active_pr guard explicitly enabled, dispatch_once skips (but does + not block) a task with an active PR comment.""" + monkeypatch.setenv("HERMES_KANBAN_RESPAWN_GUARD_ACTIVE_PR", "1") spawned_ids = [] def fake_spawn(task, workspace): @@ -1832,6 +1852,28 @@ def fake_spawn(task, workspace): assert kb.get_task(conn, t).status == "ready" +def test_dispatch_active_pr_spawns_by_default( + kanban_home, all_assignees_spawnable +): + """By default (guard off) a task with a PR comment is NOT skipped — it + spawns normally, since PRs are human-merged and re-spawn is safe.""" + spawned_ids = [] + + def fake_spawn(task, workspace): + spawned_ids.append(task.id) + + with kb.connect() as conn: + t = kb.create_task(conn, title="has-pr", assignee="alice") + kb.add_comment( + conn, t, "worker", + "Opened https://github.com/totemx-AI/subsidysmart/pull/99", + ) + res = kb.dispatch_once(conn, spawn_fn=fake_spawn) + + assert (t, "active_pr") not in res.respawn_guarded + assert t in spawned_ids + + def test_dispatch_respawn_guard_dry_run_no_auto_block( kanban_home, all_assignees_spawnable ):