Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions hermes_cli/kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
54 changes: 48 additions & 6 deletions tests/hermes_cli/test_kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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
):
Expand Down