Skip to content

feat(kanban): typed block reasons + unblock-loop breaker - #52848

Merged
teknium1 merged 2 commits into
mainfrom
kanban/block-loop-breaker
Jun 26, 2026
Merged

feat(kanban): typed block reasons + unblock-loop breaker#52848
teknium1 merged 2 commits into
mainfrom
kanban/block-loop-breaker

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Kanban tasks no longer get stuck in an unblock↔re-block loop — a worker blocks a task, a cron unblocks it, the worker re-blocks for the same reason, repeat forever. block_task now carries a typed reason and a persistent recurrence counter, so the two fundamentally different meanings of "blocked" are routed differently and a repeated same-cause block escalates to a human instead of spinning.

Root cause: unblock_task reset consecutive_failures = 0 every time, so the dispatcher's circuit breaker never tripped on a manual unblock loop — and there was no other counter watching it. "Blocked" was also one undifferentiated bucket: a dependency-wait and a hard human-only wall looked identical, so a cron treated both as "unblock and retry."

Changes

  • hermes_cli/kanban_db.py:
    • New block_kind (dependency | needs_input | capability | transient, NULL = legacy) and block_recurrences columns on tasks (SCHEMA_SQL + legacy-DB migration + dataclass + from_row).
    • block_task(reason, kind=None) routes by kind: dependencytodo (parent-gated, auto-resumed, never the human bucket); needs_input/capability/transient/untyped → blocked, incrementing block_recurrences on a same-cause re-block; at BLOCK_RECURRENCE_LIMIT (default 2) → triage for a human. Emits dependency_wait / block_loop_detected events.
    • unblock_task deliberately no longer resets block_recurrences (the amnesia that let the loop run); complete_task clears it on success.
  • tools/kanban_tools.py: kanban_block gains a kind arg + validation, and returns where the task actually landed.
  • hermes_cli/kanban.py: hermes kanban block --kind, reporting the routed status (→ todo / → triage).
  • website/docs/user-guide/features/kanban.md: event reference + tool/CLI docs.

Validation

Live E2E against a real SQLite board (real block_task/unblock_task/complete_task, the worker tool handler, and the CLI cron-unblock path):

Scenario Result
needs_input → unblock → re-block (same kind) routed to triage, recurrences=2 ✓
unblock preserves recurrence counter counter stays at 1 (not reset) ✓
dependency block parked in todo, no human ✓
dependency parent completes child auto-promotes to ready
successful completion recurrences→0, kind→NULL ✓
different kinds don't compound counter resets to 1, stays blocked ✓
untyped (legacy) block loop still trips to triage ✓
invalid kind rejected ✓

Tests: 11 new in tests/hermes_cli/test_kanban_block_kinds.py; 536 existing kanban tests green (kanban_db, kanban_tools, blocked_sticky, core, diagnostics, dispatch). Ruff clean.

Backward compatibility

kind is an optional kwarg; every existing block_task(reason=...) caller keeps the old single-block behaviour (kind=NULL). Legacy DBs get the two columns via the additive migration (existing rows start at recurrences=0).

Infographic

kanban-block-loop-breaker

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: kanban/block-loop-breaker vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11396 on HEAD, 11371 on base (🆕 +25)

🆕 New issues (5):

Rule Count
unresolved-attribute 4
unresolved-import 1
First entries
tests/hermes_cli/test_kanban_block_kinds.py:201: [unresolved-attribute] unresolved-attribute: Attribute `status` is not defined on `None` in union `Task | None`
tests/hermes_cli/test_kanban_block_kinds.py:22: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_kanban_block_kinds.py:179: [unresolved-attribute] unresolved-attribute: Attribute `block_recurrences` is not defined on `None` in union `Task | None`
tests/hermes_cli/test_kanban_block_kinds.py:202: [unresolved-attribute] unresolved-attribute: Attribute `block_kind` is not defined on `None` in union `Task | None`
tests/tools/test_kanban_tools.py:1820: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str` in union `str | dict[str, str | dict[str, dict[str, str]] | list[Unknown]] | dict[str, str | dict[str, dict[str, str] | dict[str, str | list[str]]] | list[Unknown]] | ... omitted 4 union elements`

✅ Fixed issues (1):

Rule Count
unresolved-attribute 1
First entries
tests/tools/test_kanban_tools.py:1820: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str` in union `str | dict[str, str | dict[str, dict[str, str]] | list[Unknown]] | dict[str, str | dict[str, dict[str, str] | dict[str, str | list[str]]] | list[Unknown]] | ... omitted 3 union elements`

Unchanged: 5996 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

teknium1 added 2 commits June 25, 2026 21:36
Stops the kanban blocked-task loop: a worker blocks a task, a cron
unblocks it, the worker re-blocks for the same reason, repeat forever.

block_task now takes a typed kind and a persistent block_recurrences
counter on the tasks table:

- kind=dependency routes to todo (parent-gated, auto-resumed), never
  the human 'blocked' bucket a cron would keep unblocking.
- needs_input/capability/transient/untyped land in blocked; each
  same-cause re-block after an unblock increments block_recurrences,
  and at BLOCK_RECURRENCE_LIMIT (default 2) the task routes to triage
  for a human instead of blocked.
- unblock_task no longer resets block_recurrences (the amnesia that
  let the loop run unbounded); complete_task clears it on success.

Wired through the worker kanban_block tool (new kind arg) and the
hermes kanban block --kind CLI flag, both reporting where the task
actually landed. Docs + 11 new tests; 536 existing kanban tests green.
test_notifier_second_blocked_delivers blocked the same task twice with
the same (untyped) reason, which now trips the new unblock-loop breaker
and routes the second block to triage instead of blocked — so only one
'blocked' notification fired. The test's actual intent is that TWO
distinct block cycles each notify; give the two cycles different kinds
(needs_input then capability) so they're genuinely separate blocks. The
same-cause loop→triage path is covered by test_kanban_block_kinds.py.
@teknium1
teknium1 force-pushed the kanban/block-loop-breaker branch from b2c090a to 37c376d Compare June 26, 2026 04:36
@alt-glitch alt-glitch added type/feature New feature or request comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have labels Jun 26, 2026
@teknium1
teknium1 merged commit 5b5c79a into main Jun 26, 2026
27 checks passed
@teknium1
teknium1 deleted the kanban/block-loop-breaker branch June 26, 2026 04:47
pai-scaffolde pushed a commit to pai-scaffolde/hermes-agent that referenced this pull request Jun 28, 2026
…h#52848)

* feat(kanban): typed block reasons + unblock-loop breaker

Stops the kanban blocked-task loop: a worker blocks a task, a cron
unblocks it, the worker re-blocks for the same reason, repeat forever.

block_task now takes a typed kind and a persistent block_recurrences
counter on the tasks table:

- kind=dependency routes to todo (parent-gated, auto-resumed), never
  the human 'blocked' bucket a cron would keep unblocking.
- needs_input/capability/transient/untyped land in blocked; each
  same-cause re-block after an unblock increments block_recurrences,
  and at BLOCK_RECURRENCE_LIMIT (default 2) the task routes to triage
  for a human instead of blocked.
- unblock_task no longer resets block_recurrences (the amnesia that
  let the loop run unbounded); complete_task clears it on success.

Wired through the worker kanban_block tool (new kind arg) and the
hermes kanban block --kind CLI flag, both reporting where the task
actually landed. Docs + 11 new tests; 536 existing kanban tests green.

* test(kanban): make second-block notify test use a distinct block cause

test_notifier_second_blocked_delivers blocked the same task twice with
the same (untyped) reason, which now trips the new unblock-loop breaker
and routes the second block to triage instead of blocked — so only one
'blocked' notification fired. The test's actual intent is that TWO
distinct block cycles each notify; give the two cycles different kinds
(needs_input then capability) so they're genuinely separate blocks. The
same-cause loop→triage path is covered by test_kanban_block_kinds.py.
obelisk-complex added a commit to obelisk-complex/hermes-agent that referenced this pull request Jun 29, 2026
…stream-sync coexistence

The nightly upstream sync has failed since upstream PR NousResearch#50349 (e217fd4,
2026-06-21) added a NATIVE kanban_task_blocked lifecycle observer. The fork's
quality-gate plugin already registered a hook of the SAME name and fired it via
_invoke_kanban_hook; upstream fires its own via _fire_kanban_lifecycle_hook.
Both dispatch through hermes_cli.plugins.invoke_hook, so the shared name made
the rebase collide and, worse, would have delivered upstream's lean kwargs
(board/assignee/profile_name) to the fork's quality-gate consumer, which reads
the fork's rich escalation kwargs (trigger/consecutive_failures/...).

Resolution (keep BOTH, rename the fork's): the rebase resolves the four
kanban-hook conflicts as a UNION that preserves upstream's structure and
re-applies the fork additions verbatim under the OLD name (so downstream fork
commits, incl. the em-dash style commit, still replay); THIS commit then does
the single uniform rename kanban_task_blocked -> fork_kanban_task_blocked across
every fork site:
  - plugins.py VALID_HOOKS: fork entry renamed; upstream's own entry kept.
  - kanban_db.py: the two fork _invoke_kanban_hook fire sites (block_task manual
    + _record_task_failure auto). Upstream's _fire_kanban_lifecycle_hook calls
    are untouched.
  - quality-gate plugin (register + handler), plugin.yaml manifest, and all
    fork hook-name tests.

Harden the upstream-sync gate and make the PR NousResearch#52848 (BLOCK_RECURRENCE_LIMIT)
interaction safe:
  - blocked_hook now checks requeue_blocked_task's bool: on a no-op (card routed
    to 'triage' by the recurrence breaker, or already moved) it logs loud and
    skips the escalation notify instead of claiming a phantom escalation.
  - test_b2 gains a collision guard (fork dispatcher never fires the bare name),
    a coexistence guard (upstream's fire survives), and a triage-breaker/ladder
    regression test. The new upstream-presence/breaker assertions are GATED on
    upstream markers so the guard is a no-op on the fork's pre-sync main (push
    CI) and full-strength in the post-rebase sync gate.
  - sync workflow py_compile loop now also covers kanban_db.py and the
    quality-gate plugin (the files that carry the conflict resolutions + rename).

Phase 2 (deferred): raise BLOCK_RECURRENCE_LIMIT to scale with the quality-gate
ladder depth (needs config plumbing; block_task is config-agnostic core today).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
obelisk-complex added a commit to obelisk-complex/hermes-agent that referenced this pull request Jun 30, 2026
…stream-sync coexistence

The nightly upstream sync has failed since upstream PR NousResearch#50349 (e217fd4,
2026-06-21) added a NATIVE kanban_task_blocked lifecycle observer. The fork's
quality-gate plugin already registered a hook of the SAME name and fired it via
_invoke_kanban_hook; upstream fires its own via _fire_kanban_lifecycle_hook.
Both dispatch through hermes_cli.plugins.invoke_hook, so the shared name made
the rebase collide and, worse, would have delivered upstream's lean kwargs
(board/assignee/profile_name) to the fork's quality-gate consumer, which reads
the fork's rich escalation kwargs (trigger/consecutive_failures/...).

Resolution (keep BOTH, rename the fork's): the rebase resolves the four
kanban-hook conflicts as a UNION that preserves upstream's structure and
re-applies the fork additions verbatim under the OLD name (so downstream fork
commits, incl. the em-dash style commit, still replay); THIS commit then does
the single uniform rename kanban_task_blocked -> fork_kanban_task_blocked across
every fork site:
  - plugins.py VALID_HOOKS: fork entry renamed; upstream's own entry kept.
  - kanban_db.py: the two fork _invoke_kanban_hook fire sites (block_task manual
    + _record_task_failure auto). Upstream's _fire_kanban_lifecycle_hook calls
    are untouched.
  - quality-gate plugin (register + handler), plugin.yaml manifest, and all
    fork hook-name tests.

Harden the upstream-sync gate and make the PR NousResearch#52848 (BLOCK_RECURRENCE_LIMIT)
interaction safe:
  - blocked_hook now checks requeue_blocked_task's bool: on a no-op (card routed
    to 'triage' by the recurrence breaker, or already moved) it logs loud and
    skips the escalation notify instead of claiming a phantom escalation.
  - test_b2 gains a collision guard (fork dispatcher never fires the bare name),
    a coexistence guard (upstream's fire survives), and a triage-breaker/ladder
    regression test. The new upstream-presence/breaker assertions are GATED on
    upstream markers so the guard is a no-op on the fork's pre-sync main (push
    CI) and full-strength in the post-rebase sync gate.
  - sync workflow py_compile loop now also covers kanban_db.py and the
    quality-gate plugin (the files that carry the conflict resolutions + rename).

Phase 2 (deferred): raise BLOCK_RECURRENCE_LIMIT to scale with the quality-gate
ladder depth (needs config plumbing; block_task is config-agnostic core today).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…h#52848)

* feat(kanban): typed block reasons + unblock-loop breaker

Stops the kanban blocked-task loop: a worker blocks a task, a cron
unblocks it, the worker re-blocks for the same reason, repeat forever.

block_task now takes a typed kind and a persistent block_recurrences
counter on the tasks table:

- kind=dependency routes to todo (parent-gated, auto-resumed), never
  the human 'blocked' bucket a cron would keep unblocking.
- needs_input/capability/transient/untyped land in blocked; each
  same-cause re-block after an unblock increments block_recurrences,
  and at BLOCK_RECURRENCE_LIMIT (default 2) the task routes to triage
  for a human instead of blocked.
- unblock_task no longer resets block_recurrences (the amnesia that
  let the loop run unbounded); complete_task clears it on success.

Wired through the worker kanban_block tool (new kind arg) and the
hermes kanban block --kind CLI flag, both reporting where the task
actually landed. Docs + 11 new tests; 536 existing kanban tests green.

* test(kanban): make second-block notify test use a distinct block cause

test_notifier_second_blocked_delivers blocked the same task twice with
the same (untyped) reason, which now trips the new unblock-loop breaker
and routes the second block to triage instead of blocked — so only one
'blocked' notification fired. The test's actual intent is that TWO
distinct block cycles each notify; give the two cycles different kinds
(needs_input then capability) so they're genuinely separate blocks. The
same-cause loop→triage path is covered by test_kanban_block_kinds.py.
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…h#52848)

* feat(kanban): typed block reasons + unblock-loop breaker

Stops the kanban blocked-task loop: a worker blocks a task, a cron
unblocks it, the worker re-blocks for the same reason, repeat forever.

block_task now takes a typed kind and a persistent block_recurrences
counter on the tasks table:

- kind=dependency routes to todo (parent-gated, auto-resumed), never
  the human 'blocked' bucket a cron would keep unblocking.
- needs_input/capability/transient/untyped land in blocked; each
  same-cause re-block after an unblock increments block_recurrences,
  and at BLOCK_RECURRENCE_LIMIT (default 2) the task routes to triage
  for a human instead of blocked.
- unblock_task no longer resets block_recurrences (the amnesia that
  let the loop run unbounded); complete_task clears it on success.

Wired through the worker kanban_block tool (new kind arg) and the
hermes kanban block --kind CLI flag, both reporting where the task
actually landed. Docs + 11 new tests; 536 existing kanban tests green.

* test(kanban): make second-block notify test use a distinct block cause

test_notifier_second_blocked_delivers blocked the same task twice with
the same (untyped) reason, which now trips the new unblock-loop breaker
and routes the second block to triage instead of blocked — so only one
'blocked' notification fired. The test's actual intent is that TWO
distinct block cycles each notify; give the two cycles different kinds
(needs_input then capability) so they're genuinely separate blocks. The
same-cause loop→triage path is covered by test_kanban_block_kinds.py.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…h#52848)

* feat(kanban): typed block reasons + unblock-loop breaker

Stops the kanban blocked-task loop: a worker blocks a task, a cron
unblocks it, the worker re-blocks for the same reason, repeat forever.

block_task now takes a typed kind and a persistent block_recurrences
counter on the tasks table:

- kind=dependency routes to todo (parent-gated, auto-resumed), never
  the human 'blocked' bucket a cron would keep unblocking.
- needs_input/capability/transient/untyped land in blocked; each
  same-cause re-block after an unblock increments block_recurrences,
  and at BLOCK_RECURRENCE_LIMIT (default 2) the task routes to triage
  for a human instead of blocked.
- unblock_task no longer resets block_recurrences (the amnesia that
  let the loop run unbounded); complete_task clears it on success.

Wired through the worker kanban_block tool (new kind arg) and the
hermes kanban block --kind CLI flag, both reporting where the task
actually landed. Docs + 11 new tests; 536 existing kanban tests green.

* test(kanban): make second-block notify test use a distinct block cause

test_notifier_second_blocked_delivers blocked the same task twice with
the same (untyped) reason, which now trips the new unblock-loop breaker
and routes the second block to triage instead of blocked — so only one
'blocked' notification fired. The test's actual intent is that TWO
distinct block cycles each notify; give the two cycles different kinds
(needs_input then capability) so they're genuinely separate blocks. The
same-cause loop→triage path is covered by test_kanban_block_kinds.py.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…h#52848)

* feat(kanban): typed block reasons + unblock-loop breaker

Stops the kanban blocked-task loop: a worker blocks a task, a cron
unblocks it, the worker re-blocks for the same reason, repeat forever.

block_task now takes a typed kind and a persistent block_recurrences
counter on the tasks table:

- kind=dependency routes to todo (parent-gated, auto-resumed), never
  the human 'blocked' bucket a cron would keep unblocking.
- needs_input/capability/transient/untyped land in blocked; each
  same-cause re-block after an unblock increments block_recurrences,
  and at BLOCK_RECURRENCE_LIMIT (default 2) the task routes to triage
  for a human instead of blocked.
- unblock_task no longer resets block_recurrences (the amnesia that
  let the loop run unbounded); complete_task clears it on success.

Wired through the worker kanban_block tool (new kind arg) and the
hermes kanban block --kind CLI flag, both reporting where the task
actually landed. Docs + 11 new tests; 536 existing kanban tests green.

* test(kanban): make second-block notify test use a distinct block cause

test_notifier_second_blocked_delivers blocked the same task twice with
the same (untyped) reason, which now trips the new unblock-loop breaker
and routes the second block to triage instead of blocked — so only one
'blocked' notification fired. The test's actual intent is that TWO
distinct block cycles each notify; give the two cycles different kinds
(needs_input then capability) so they're genuinely separate blocks. The
same-cause loop→triage path is covered by test_kanban_block_kinds.py.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…h#52848)

* feat(kanban): typed block reasons + unblock-loop breaker

Stops the kanban blocked-task loop: a worker blocks a task, a cron
unblocks it, the worker re-blocks for the same reason, repeat forever.

block_task now takes a typed kind and a persistent block_recurrences
counter on the tasks table:

- kind=dependency routes to todo (parent-gated, auto-resumed), never
  the human 'blocked' bucket a cron would keep unblocking.
- needs_input/capability/transient/untyped land in blocked; each
  same-cause re-block after an unblock increments block_recurrences,
  and at BLOCK_RECURRENCE_LIMIT (default 2) the task routes to triage
  for a human instead of blocked.
- unblock_task no longer resets block_recurrences (the amnesia that
  let the loop run unbounded); complete_task clears it on success.

Wired through the worker kanban_block tool (new kind arg) and the
hermes kanban block --kind CLI flag, both reporting where the task
actually landed. Docs + 11 new tests; 536 existing kanban tests green.

* test(kanban): make second-block notify test use a distinct block cause

test_notifier_second_blocked_delivers blocked the same task twice with
the same (untyped) reason, which now trips the new unblock-loop breaker
and routes the second block to triage instead of blocked — so only one
'blocked' notification fired. The test's actual intent is that TWO
distinct block cycles each notify; give the two cycles different kinds
(needs_input then capability) so they're genuinely separate blocks. The
same-cause loop→triage path is covered by test_kanban_block_kinds.py.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants