Skip to content

🐛 fix(kanban): clear the active_pr respawn guard on an outer-loop feedback bounce - #72

Merged
cwest merged 1 commit into
cwest/integrationfrom
topic/kanban-outer-loop-route-feedback
Jul 19, 2026
Merged

🐛 fix(kanban): clear the active_pr respawn guard on an outer-loop feedback bounce#72
cwest merged 1 commit into
cwest/integrationfrom
topic/kanban-outer-loop-route-feedback

Conversation

@cwest

@cwest cwest commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Why

The OUTER feedback loop — a human's feedback on an accepted card, routed back to the author for a revision — had no code path that cleared the active_pr respawn guard, so it wedged every time and required a hand-run block→unblock→reassign dance.

Root cause (hermes_cli/kanban_db.py):

  • The INNER review-bounce loop self-heals via auto_route_review_bounce, but it fires ONLY for the review-changes-requested reason. A human-feedback bounce is a different reason, so it never fires.
  • A raw move_card blocked/acceptance → ready emits only status_changed, never the unblocked cutoff event check_respawn_guard honors. So the guard stays active_pr (the PR is genuinely open) and the dispatcher refuses to spawn the author every tick (respawn_guarded {active_pr}).
  • Repeated churn inflates block_recurrences past BLOCK_RECURRENCE_LIMIT → the loop breaker escalates the card to triage, where a naive block→unblock cutoff silently no-ops.

What

Adds route_feedback_to_author, a sanctioned caller-driven primitive that performs the whole outer-loop bounce atomically, composed from existing sanctioned building blocks so the INNER loop, the reviewer PASS → acceptance path (accept_task), and the merge → done path are all left unchanged:

  1. reset_block_recurrences — frees a triage card and prevents immediate re-escalation on the next same-cause block;
  2. reassign to the author (emits assigned), fenced on the card still being in a transitionable lane (blocked / scheduled / triage);
  3. unblock_task — emits the unblocked cutoff event NOW, causally AFTER the PR-URL handoff comment, which becomes the pr_cutoff check_respawn_guard honors so the active_pr (and recent_success) guard clears and the next dispatch spawns the author;
  4. recompute_ready + an [audit] comment naming the PR + feedback.

Recovers a card from blocked, acceptance (blocked + acceptance owner), AND triage. Idempotent: once routed off the parked lane, a second call is a clean no-op (returns False, no duplicate audit comment). A blank author raises rather than guessing; a missing card returns False.

The triage-transition half of unblock_task is already live (it accepts blocked / scheduled / triage), so this change is scoped to the outer-loop auto-clear primitive only — it does not re-implement the triage transition.

Testing

New behavior-contract tests (tests/hermes_cli/test_kanban_route_feedback_to_author.py, 9 tests):

  • guard cleared from the acceptance lane (check_respawn_guard(...) is None after the route);
  • the routed card is genuinely dispatchable via the real dispatch_once spawn path (not a live spawn);
  • recovery from triage with an inflated block_recurrences (counter reset to 0);
  • recovery from a plain blocked lane;
  • an audit comment records the route and names the PR;
  • idempotency (one route, one audit comment);
  • a blank author raises; a missing card returns False;
  • a regression guard that the INNER auto_route_review_bounce path still works.

RED check: reverting the unblock_task cutoff step leaves the guard active_pr and the guard-clear tests fail — proving the cutoff emission is the load-bearing behavior.

Full tests/hermes_cli/test_kanban_db.py (280) and the surrounding kanban lifecycle suites pass with zero regressions. (One pre-existing macOS-only test, test_signal_handler_kanban_worker.py, depends on Linux /proc for PID liveness and cannot pass on Darwin; it is byte-identical to base and unrelated to this change.)

…dback bounce

Routing an accepted card back to its author on outer-loop feedback had no
code path that cleared the active_pr respawn guard. A raw move_card
blocked/acceptance -> ready emits only status_changed, never the unblocked
cutoff event check_respawn_guard honors, so the guard stayed active_pr (the
PR is genuinely open) and the dispatcher refused to spawn the author every
tick. Repeated churn inflated block_recurrences past BLOCK_RECURRENCE_LIMIT
and escalated the card to triage, where a naive block->unblock cutoff
silently no-ops.

Add route_feedback_to_author, a sanctioned caller-driven primitive that
performs the whole outer-loop bounce atomically, composed from existing
building blocks so the inner review-bounce loop, the reviewer PASS ->
acceptance path (accept_task), and the merge -> done path are all unchanged:

  1. reset_block_recurrences (frees a triage card, prevents re-escalation),
  2. reassign to the author (assigned event), fenced on a transitionable lane,
  3. unblock_task (emits the unblocked cutoff AFTER the PR-URL comment ts,
     which is what clears the active_pr / recent_success guard),
  4. recompute_ready + an audit comment naming the PR + feedback.

Recovers a card from blocked, acceptance (blocked + acceptance owner), and
triage. Idempotent: a second call on an already-routed card is a clean no-op.
Behavior-contract tests assert the guard clears (check_respawn_guard is None),
the card is spawnable via the real dispatch_once path, the triage counter
resets, and a reverted guard-clear leaves active_pr and fails.
@cwest
cwest marked this pull request as ready for review July 19, 2026 20:10

@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.

The primitive is built entirely from existing sanctioned pieces, and each one behaves the way this code relies on. reset_block_recurrences frees a triage card and zeroes the loop counter; the fenced reassign emits the assigned event; unblock_task accepts blocked/scheduled/triage and writes the unblocked event now, causally after the PR-URL handoff comment. That ordering is the whole mechanism: check_respawn_guard sets its pr_cutoff to max(24h window, latest unblocked ts), so an unblock stamped after the PR comment pushes the comment out of the scan window and the active_pr guard clears. Same story for recent_success. Read the guard against integration HEAD and it lines up exactly.

Scope is right. The triage transition in unblock_task is already live, and this change doesn't re-touch it. Nothing wires the primitive into a live dispatch path yet, which matches the plan to add the caller separately. The inner auto_route_review_bounce path, accept_task, and the merge-to-done path are all left alone, and the surrounding kanban suite stays green.

Tests assert behavior, not snapshots: guard cleared from the acceptance lane, the routed card actually spawning through dispatch_once, triage recovery with the counter reset, idempotency, the blank-author raise, the missing-card no-op. I reproduced the RED check independently by removing the unblock step from a scratch copy of the file: five tests fail with the guard still active, which is the load-bearing behavior. Restored, then ran the new file (9 pass) and test_kanban_db.py (271 pass). All CI checks pass on this SHA.

No changes needed.

@cwest
cwest merged commit eb026be into cwest/integration Jul 19, 2026
31 checks passed
@cwest
cwest deleted the topic/kanban-outer-loop-route-feedback branch July 19, 2026 20:16
cwest added a commit that referenced this pull request Jul 26, 2026
…dback bounce (#72)

Routing an accepted card back to its author on outer-loop feedback had no
code path that cleared the active_pr respawn guard. A raw move_card
blocked/acceptance -> ready emits only status_changed, never the unblocked
cutoff event check_respawn_guard honors, so the guard stayed active_pr (the
PR is genuinely open) and the dispatcher refused to spawn the author every
tick. Repeated churn inflated block_recurrences past BLOCK_RECURRENCE_LIMIT
and escalated the card to triage, where a naive block->unblock cutoff
silently no-ops.

Add route_feedback_to_author, a sanctioned caller-driven primitive that
performs the whole outer-loop bounce atomically, composed from existing
building blocks so the inner review-bounce loop, the reviewer PASS ->
acceptance path (accept_task), and the merge -> done path are all unchanged:

  1. reset_block_recurrences (frees a triage card, prevents re-escalation),
  2. reassign to the author (assigned event), fenced on a transitionable lane,
  3. unblock_task (emits the unblocked cutoff AFTER the PR-URL comment ts,
     which is what clears the active_pr / recent_success guard),
  4. recompute_ready + an audit comment naming the PR + feedback.

Recovers a card from blocked, acceptance (blocked + acceptance owner), and
triage. Idempotent: a second call on an already-routed card is a clean no-op.
Behavior-contract tests assert the guard clears (check_respawn_guard is None),
the card is spawnable via the real dispatch_once path, the triage counter
resets, and a reverted guard-clear leaves active_pr and fails.

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