Skip to content

fix(kanban): treat a phantom/deleted PR URL as not_found so it never wedges active_pr - #100

Merged
cwest merged 1 commit into
cwest/integrationfrom
fix/active-pr-not-found
Aug 1, 2026
Merged

cwest merged 1 commit into
cwest/integrationfrom
fix/active-pr-not-found

Conversation

@cwest

@cwest cwest commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Problem

A PR URL that GitHub definitively does not have — one that was never created, or was deleted — makes gh pr view exit nonzero with the GraphQL signature Could not resolve to a PullRequest. _resolve_pr_state failed open to "unknown" on any nonzero exit, and the active_pr respawn guard treats "unknown" as still-active (conservative). So a phantom/stale PR URL sitting in a task comment guarded the card on every dispatcher tick, indefinitely — dozens of respawn_guarded {active_pr} events with no worker ever spawning.

This is an orthogonal residual hole not covered by the earlier active_pr fixes (NousResearch#46204 explicit-unblock cutoff, NousResearch#46549 review-lane bypass, #77 recovery-actor re-arming comment): all of those still route through _resolve_pr_state, which fails open on the phantom-PR case.

Root cause

Conflating a definitive not-found (terminal — a nonexistent PR can never be active work) with a transient gh failure (network / auth / gh-missing — genuinely unresolvable, must stay fail-open).

Fix

  • New module-level _PR_NOT_FOUND_STDERR_RE (could not resolve to a pullrequest, case-insensitive).
  • _resolve_pr_state returns a new terminal state "not_found" when gh exits nonzero and stderr carries that signature. Every other nonzero exit still returns "unknown" — fail-open behavior is preserved for genuine transient failures.
  • check_respawn_guard's dup-PR decision now consults a single-source-of-truth _RESPAWN_GUARD_INACTIVE_PR_STATES = frozenset({"closed", "merged", "not_found"}). A not-found PR no longer guards, exactly like a closed/merged one; open and unknown still guard.

Minimal, single-file production change in hermes_cli/kanban_db.py.

Tests

RED-then-GREEN, in tests/hermes_cli/test_kanban_db.py:

  • _resolve_pr_state maps the GraphQL not-found stderr → not_found.
  • A generic nonzero exit with no not-found signature still → unknown (fail-open intact).
  • A not-found PR-URL comment from a real builder → guard clears to None.

Plus one hermetic-fixture fix in tests/hermes_cli/test_kanban_route_feedback_to_author.py: the kanban_home fixture now stubs _resolve_pr_state→"open", because its placeholder PR URL is non-existent and would otherwise now resolve not_found and clear the very guard those routing tests set out to exercise. The routing logic under test is orthogonal to live PR-state resolution.

Verification

Real kanban_db, temp HERMES_HOME, no mocks. Isolated kanban surface green (310 passed across the four core kanban test files run per-file). Zero new failures — the ~26 cross-test-pollution failures seen in a whole--k kanban single-process run reproduce identically on the clean base branch and are unrelated to this change (base: 892 passed / 26 failed; this branch: 895 passed / 26 failed = exactly the 3 new tests added).

PATCHES.md updated with the carried-patch manifest row (permanent-local; fork-internal dispatcher invariant).

…r wedges active_pr

A PR URL that GitHub definitively does not have — never created, or deleted
— makes `gh pr view` exit nonzero with the GraphQL signature "Could not
resolve to a PullRequest". `_resolve_pr_state` failed open to "unknown" on
ANY nonzero exit, and the active_pr respawn guard treats "unknown" as still-
active, so a phantom PR URL in a task comment guarded the card on every tick
indefinitely (dozens of respawn_guarded {active_pr} events, no worker ever
spawned).

Root cause: conflating a DEFINITIVE not-found (terminal — a nonexistent PR
can never be active work) with a TRANSIENT gh failure (network / auth /
gh-missing — genuinely unresolvable, must stay fail-open).

Add a terminal "not_found" state: _resolve_pr_state returns it when gh exits
nonzero AND stderr carries the not-found signature (_PR_NOT_FOUND_STDERR_RE);
every other nonzero exit still returns "unknown" (fail-open preserved).
check_respawn_guard's dup-PR decision consults a single-source-of-truth
_RESPAWN_GUARD_INACTIVE_PR_STATES = {closed, merged, not_found}, so a
not-found PR no longer guards while open/unknown still do.

Tests: 3 new behavior-contract tests (not-found stderr maps to not_found; a
generic nonzero exit still maps to unknown; a not-found PR-URL comment from a
real builder clears the guard). Also makes the route-feedback fixture stub
_resolve_pr_state->open so its placeholder PR URL is deterministic instead of
resolving not_found against live GitHub.
@cwest
cwest merged commit fefc899 into cwest/integration Aug 1, 2026
37 checks passed
@cwest
cwest deleted the fix/active-pr-not-found branch August 1, 2026 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant