fix(kanban): emit blocked event when initial_status='blocked' so sticky-block guard fires - #34735
Conversation
…ky-block guard fires
create_task(initial_status='blocked') wrote tasks.status='blocked' but did
NOT emit a 'blocked' event row. _has_sticky_block queries task_events
WHERE kind IN ('blocked', 'unblocked'); for a freshly created
--initial-status blocked task that query returns nothing, the predicate
returns False, and recompute_ready promotes blocked -> ready on the next
dispatcher tick.
This nullified the entire --initial-status blocked primitive. Every task
ever filed with it was silently promoted on the first tick. Downstream
impact at JetMinds: the autonomy-boundary primitive (intended to gate
agent-governing edits behind a human unblock) was a no-op.
Fix: append a 'blocked' event after the 'created' event whenever
task_status == 'blocked'. Keyed off the resolved task_status (not the
input arg) so the invariant 'task row is blocked => blocked event exists'
holds independent of the call shape.
Adds four regression tests pinning the contract:
* blocked event is emitted alongside created event
* survives 5 rounds of recompute_ready without promotion
* unblock_task cleanly transitions blocked -> ready
* child with done parents still sticky (the most dangerous false-positive
path that recompute_ready was designed for)
420/420 tests pass across the kanban_db / sticky / core / specify /
decompose suites; 113/113 across kanban_tools / specify / decompose /
swarm.
|
Thanks for the focused regression fix. The premise remains valid on current The proposed Automated hermes-sweeper review. |
|
Refreshed this branch onto current What changed: merged the latest Placement confirmed: the Re-verified against the merged tree: Caveat: the branch still has no CI running against it ( Merges cleanly now. Thanks for the review. |
Kayhusk
left a comment
There was a problem hiding this comment.
Independent validation against Nous main confirms this is the lifecycle-consistent fix for #39609.
Evidence:
- Reproduced on
e361c5e20402375c74a65ca52810c6a380461226with a tempHERMES_HOME. - RED regression: a task created with
initial_status="blocked"ended its event stream atcreated, notblocked;recompute_ready()could then promote it. - The minimal fix is the shape used here: append
blockedimmediately aftercreated, inside the existingcreate_task()transaction, when resolvedtask_status == "blocked". - This preserves
_has_sticky_block()as the single lifecycle predicate, keeps gateway/diagnostic consumers on the establishedblockedevent, and leaves circuit-breaker/direct-DB blocks without that event eligible for recovery. - Affected Kanban DB/core/tool suites: 524 passed.
- Gateway blocked-event and Kanban diagnostics suites: 55 passed.
- Sticky-block module: 7 passed.
- Ruff,
py_compile,git diff --check, and added-line security scan passed. - Canonical full runner completed with no candidate-specific regression: its four failures were reproduced unchanged on an untouched latest-main worktree and are unrelated environment failures in TUI/web hardlink tests and
sort --compress-programexecution-flag detection.
I did not open a competing PR because CONTRIBUTING.md recommends improving/reviewing the existing one. The event-emission approach here is narrower and more architecture-aligned than teaching _has_sticky_block() to reinterpret created payloads.
Bug
create_task(initial_status="blocked")inhermes_cli/kanban_db.pywritestasks.status='blocked'but does NOT emit ablockedevent row. The sticky-block guard (_has_sticky_block, L2412) decides whetherrecompute_ready(L2450) respects the block by queryingtask_events WHERE kind IN ('blocked', 'unblocked'). For a freshly created--initial-status blockedtask that query returns nothing, the predicate returns False, andrecompute_readysilently promotes the taskblocked → readyon the next dispatcher tick. The dispatcher then claims and runs.The
_has_sticky_blockdocstring already acknowledges the gap (L2436-2439, "preserves pre-#28712 auto-recover semantics") — written before--initial-status blockedexisted.Impact
The
--initial-status blockedprimitive is meant to park tasks at creation for explicit human / operator review (R3 gates, autonomy-boundary primitives, decision-required handoffs filed by automated pipelines). Without this fix it's a silent no-op: every task ever filed with the flag is promoted on the first dispatcher tick.Fix
After the
createdevent is appended, iftask_status == "blocked"append ablockedevent with payload{"reason": "initial-status: created-blocked", "source": "create_task"}. Keyed off the resolvedtask_statusrather than the inputinitial_statusarg so the invariant task row is blocked ⇒ blocked event exists holds independent of the call shape.No semantic change for callers that don't use
initial_status="blocked".Tests
Four regression tests in
tests/hermes_cli/test_kanban_blocked_sticky.py:test_initial_status_blocked_emits_blocked_event— created + blocked event sequence on a freshly parked tasktest_initial_status_blocked_survives_recompute_ready— 5 ticks ofrecompute_readyleave the task blockedtest_initial_status_blocked_unblock_clears_state— lifecycle park-blocked → unblock → readytest_initial_status_blocked_with_done_parents_still_sticky— the most dangerous false-positive: parent-done child-blocked must stay blocked (this is the exact pathrecompute_readywas designed for, which is why the sticky guard must dominate)10/10
test_kanban_blocked_sticky.pypass; 211/211 across the sticky + fulltest_kanban_db.pysuites.What it doesn't do
_has_sticky_blockorrecompute_ready— the predicate and the promotion loop were correct; onlycreate_taskwas missing the event emission.triage/running/readypaths —task_status == "blocked"is the only branch that fires.