Skip to content

🐛 fix(kanban): clear active_pr respawn guard on unblock from triage - #61

Merged
cwest merged 1 commit into
cwest/integrationfrom
topic/kanban-unblock-from-triage
Jul 11, 2026
Merged

🐛 fix(kanban): clear active_pr respawn guard on unblock from triage#61
cwest merged 1 commit into
cwest/integrationfrom
topic/kanban-unblock-from-triage

Conversation

@cwest

@cwest cwest commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Why

The outer feedback loop — a card bounced back to its author for a revision while it carries an open PR — had no automatic active_pr respawn-guard-clearing path. Only the inner review-bounce loop self-heals (auto_route_review_bounce reassigns + unblocks, emitting the unblocked cutoff event that check_respawn_guard honors to clear active_pr).

unblock_task matched only WHERE status IN ('blocked', 'scheduled'). When repeated churn on the outer loop inflated block_recurrences past BLOCK_RECURRENCE_LIMIT, the block-loop breaker escalated the card to triage — where unblock_task matched zero rows, returned False, and emitted no unblocked event. Since check_respawn_guard uses the latest unblocked event as the cutoff that clears the active_pr guard, a triaged card carrying an open-PR comment stayed respawn-guarded forever: the dispatcher refused to spawn the author every tick (respawn_guarded {active_pr}), and the standard block→unblock recovery silently no-oped from triage. Only a hand reset_block_recurrences + status=blocked + unblock_task rescued it.

What

Add 'triage' to both WHERE status IN (...) clauses in unblock_task (hermes_cli/kanban_db.py) — the stale-run-pointer SELECT and the status UPDATE. A card escalated to triage now transitions back to ready/todo (parent gate re-checked) and emits the unblocked cutoff, clearing active_pr exactly like a normal block→unblock.

block_recurrences is deliberately still not reset on unblock, so the loop breaker is fully preserved: a genuine same-finding loop still escalates, and the counter reset remains an explicit operator action (reset_block_recurrences / --reset-loop) or a completion.

Tests (RED → GREEN)

  • RED first: test_triage_card_with_open_pr_is_wedged_before_unblock documents the exact wedge (a triage card with an inflated counter + open-PR comment is active_pr-guarded).
  • GREEN: test_unblock_from_triage_emits_cutoff_and_clears_active_pr — unblock from triage flips to ready, emits the unblocked event, and check_respawn_guard returns None.
  • test_unblock_from_triage_rechecks_parent_gate — an undone parent sends the card to todo, not ready (parent-completion invariant preserved).
  • test_unblock_from_triage_preserves_block_recurrences — the loop counter survives the unblock (breaker preserved).
  • Existing test_cli_unblock_reset_loop_recovers_card_stuck_in_triage now asserts the card returns to ready (behavior strengthened); stale CLI comments/messages synced.

Verification: 451 kanban tests pass, 0 regressions. The 5 new behavior-contract tests reproduce today's exact wedge. Unrelated pre-existing failures (WSL / systemd / qwen-oauth / signal-timing) were confirmed identical on the pristine base tip — this change introduces none.

Carried as a PATCHES.md row (permanent-local) composing with the block-loop-breaker + reset-recovery rows. Base commit SHA unchanged (no rebase onto new upstream main).

The outer feedback loop (feedback -> author on a card carrying an open
PR) had no automatic active_pr respawn-guard-clearing path — only the
inner review-bounce loop did. unblock_task matched only
`status IN ('blocked','scheduled')`, so a card the block-loop breaker
escalated to `triage` matched zero rows, returned False, and emitted no
`unblocked` event. Because check_respawn_guard uses the latest
`unblocked` event as the cutoff that clears the active_pr guard, a
triaged card carrying an open-PR comment stayed respawn-guarded forever:
the dispatcher refused to spawn the author every tick, and the standard
block->unblock recovery silently no-oped from triage.

Add `'triage'` to both `WHERE status IN (...)` clauses in unblock_task
(the stale-run-pointer SELECT and the status UPDATE), so a triaged card
transitions back to ready/todo (parent gate re-checked) and emits the
`unblocked` cutoff, clearing active_pr exactly like a normal
block->unblock. block_recurrences is deliberately still not reset, so
the loop breaker is preserved — a genuine same-finding loop still
escalates and the counter reset remains an explicit operator action.

Reproduces the wedge RED-first (a triage card with an inflated counter
and an open-PR comment stays active_pr-guarded), then GREEN. Adds
behavior-contract tests: unblock from triage emits the cutoff and clears
the guard; the parent gate is re-checked (undone parent -> todo); the
loop counter survives the unblock. Syncs the now-stale unblock CLI
comments/messages. Carried as a PATCHES.md row composing with the
block-loop-breaker + reset-recovery rows.

@cwest cwest left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No changes needed.

The fix is exactly the two-clause change it claims: 'triage' added to both the stale-run-pointer SELECT and the status UPDATE in unblock_task, so a card the loop breaker escalated to triage can emit the unblocked cutoff event that check_respawn_guard reads to clear active_pr. Everything downstream of those clauses already generalizes — the parent-gate recheck picks todo vs ready correctly from a triage source, and block_recurrences is left untouched so the loop breaker still escalates a genuine same-finding loop. The auto_route_review_bounce caller is unaffected: it filters on status = 'blocked' before it ever reaches unblock_task, so the only newly-reachable entry is the explicit hermes kanban unblock operator path, which is the intended recovery surface.

One thing I checked because the two triage kinds share a status column with no discriminator: an intake/specifier triage card can now also be moved out by unblock rather than only by specify_task. That's a deliberate consequence of the minimal option, it needs an explicit operator action to trigger, and specification stays the normal path — the specify tests still pass, so it doesn't regress that flow.

Verified against head d9a32b4: 14/14 in the new test file, 369/0 across the nine kanban files that touch the changed code (including the specify and loop-breaker suites). The one signal-handler timing failure I saw locally reproduces identically on the pristine base tip (08e4e5d) and is green in CI here, so it's a pre-existing flake, not this change. mergeable MERGEABLE, mergeStateStatus CLEAN, zero unresolved threads, all required checks green. PATCHES.md row is present and correctly scoped as permanent-local composing with the block-loop rows.

@cwest
cwest marked this pull request as ready for review July 11, 2026 20:50
@cwest
cwest merged commit 789e667 into cwest/integration Jul 11, 2026
31 checks passed
@cwest
cwest deleted the topic/kanban-unblock-from-triage branch July 11, 2026 20:57
cwest added a commit that referenced this pull request Jul 26, 2026
)

The outer feedback loop (feedback -> author on a card carrying an open
PR) had no automatic active_pr respawn-guard-clearing path — only the
inner review-bounce loop did. unblock_task matched only
`status IN ('blocked','scheduled')`, so a card the block-loop breaker
escalated to `triage` matched zero rows, returned False, and emitted no
`unblocked` event. Because check_respawn_guard uses the latest
`unblocked` event as the cutoff that clears the active_pr guard, a
triaged card carrying an open-PR comment stayed respawn-guarded forever:
the dispatcher refused to spawn the author every tick, and the standard
block->unblock recovery silently no-oped from triage.

Add `'triage'` to both `WHERE status IN (...)` clauses in unblock_task
(the stale-run-pointer SELECT and the status UPDATE), so a triaged card
transitions back to ready/todo (parent gate re-checked) and emits the
`unblocked` cutoff, clearing active_pr exactly like a normal
block->unblock. block_recurrences is deliberately still not reset, so
the loop breaker is preserved — a genuine same-finding loop still
escalates and the counter reset remains an explicit operator action.

Reproduces the wedge RED-first (a triage card with an inflated counter
and an open-PR comment stays active_pr-guarded), then GREEN. Adds
behavior-contract tests: unblock from triage emits the cutoff and clears
the guard; the parent gate is re-checked (undone parent -> todo); the
loop counter survives the unblock. Syncs the now-stale unblock CLI
comments/messages. Carried as a PATCHES.md row composing with the
block-loop-breaker + reset-recovery rows.

(cherry picked from commit 789e667)
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