feat(kanban): typed block reasons + unblock-loop breaker - #52848
Merged
Conversation
Contributor
🔎 Lint report:
|
| 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.
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
force-pushed
the
kanban/block-loop-breaker
branch
from
June 26, 2026 04:36
b2c090a to
37c376d
Compare
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>
13 tasks
1 task
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.
11 tasks
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.
This was referenced Aug 3, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_tasknow 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_taskresetconsecutive_failures = 0every 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:block_kind(dependency | needs_input | capability | transient, NULL = legacy) andblock_recurrencescolumns ontasks(SCHEMA_SQL + legacy-DB migration + dataclass +from_row).block_task(reason, kind=None)routes by kind:dependency→todo(parent-gated, auto-resumed, never the human bucket);needs_input/capability/transient/untyped →blocked, incrementingblock_recurrenceson a same-cause re-block; atBLOCK_RECURRENCE_LIMIT(default 2) →triagefor a human. Emitsdependency_wait/block_loop_detectedevents.unblock_taskdeliberately no longer resetsblock_recurrences(the amnesia that let the loop run);complete_taskclears it on success.tools/kanban_tools.py:kanban_blockgains akindarg + 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):triage, recurrences=2 ✓todo, no human ✓ready✓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
kindis an optional kwarg; every existingblock_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