Skip to content

fix(kanban): emit blocked event when initial_status='blocked' so sticky-block guard fires - #34735

Open
begjb wants to merge 2 commits into
NousResearch:mainfrom
begjb:pr/fix-kanban-initial-status-blocked-emits-event
Open

fix(kanban): emit blocked event when initial_status='blocked' so sticky-block guard fires#34735
begjb wants to merge 2 commits into
NousResearch:mainfrom
begjb:pr/fix-kanban-initial-status-blocked-emits-event

Conversation

@begjb

@begjb begjb commented May 29, 2026

Copy link
Copy Markdown

Bug

create_task(initial_status="blocked") in hermes_cli/kanban_db.py writes tasks.status='blocked' but does NOT emit a blocked event row. The sticky-block guard (_has_sticky_block, L2412) decides whether recompute_ready (L2450) respects the block by querying 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 silently promotes the task blocked → ready on the next dispatcher tick. The dispatcher then claims and runs.

The _has_sticky_block docstring already acknowledges the gap (L2436-2439, "preserves pre-#28712 auto-recover semantics") — written before --initial-status blocked existed.

Impact

The --initial-status blocked primitive 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 created event is appended, if task_status == "blocked" append a blocked event with payload {"reason": "initial-status: created-blocked", "source": "create_task"}. Keyed off the resolved task_status rather than the input initial_status arg 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 task
  • test_initial_status_blocked_survives_recompute_ready — 5 ticks of recompute_ready leave the task blocked
  • test_initial_status_blocked_unblock_clears_state — lifecycle park-blocked → unblock → ready
  • test_initial_status_blocked_with_done_parents_still_sticky — the most dangerous false-positive: parent-done child-blocked must stay blocked (this is the exact path recompute_ready was designed for, which is why the sticky guard must dominate)

10/10 test_kanban_blocked_sticky.py pass; 211/211 across the sticky + full test_kanban_db.py suites.

What it doesn't do

  • Doesn't touch _has_sticky_block or recompute_ready — the predicate and the promotion loop were correct; only create_task was missing the event emission.
  • Doesn't change behaviour for the triage / running / ready paths — task_status == "blocked" is the only branch that fires.
  • Doesn't touch the CLI / tool surface — wire format is unchanged.

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

Copy link
Copy Markdown
Contributor

Thanks for the focused regression fix. The premise remains valid on current main: create_task() sets task_status = "blocked" for initial_status="blocked" at hermes_cli/kanban_db.py:2585, then emits only the created event at hermes_cli/kanban_db.py:2670-2683. _has_sticky_block() consequently returns false without a lifecycle event (hermes_cli/kanban_db.py:3268-3279), and recompute_ready() can promote that blocked task (hermes_cli/kanban_db.py:3324-3357).

The proposed blocked event makes the existing guard enforce the intended parked-task behavior, and the four tests cover event creation, repeated recomputation, unblocking, and done-parent behavior. The surrounding create-task code moved after the PR base, but transplanting the narrow block after the current created event is mechanical.

Automated hermes-sweeper review.

@teknium1 teknium1 added the sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users label Jul 13, 2026
@begjb

begjb commented Jul 14, 2026

Copy link
Copy Markdown
Author

Refreshed this branch onto current main.

What changed: merged the latest main in — no change to the fix itself. The branch had drifted ~10 commits behind and the target area in create_task had moved (main also added a goal_mode field to the created event right next to it), so I wanted the fix re-anchored to today's code rather than merged blind.

Placement confirmed: the blocked-event emit still sits directly after the created event inside create_task — exactly where it needs to be for _has_sticky_block to see it.

Re-verified against the merged tree: tests/hermes_cli/test_kanban_blocked_sticky.py + tests/hermes_cli/test_kanban_db.py240 passed, 0 failed. (Count grew from the 211 in the original description because main has added tests since I branched; all green with this change in place.)

Caveat: the branch still has no CI running against it (no checks reported), so the only test evidence is what I ran locally. Happy to adjust if there's a preferred way to get a CI signal on this.

Merges cleanly now. Thanks for the review.

@Kayhusk Kayhusk left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Independent validation against Nous main confirms this is the lifecycle-consistent fix for #39609.

Evidence:

  • Reproduced on e361c5e20402375c74a65ca52810c6a380461226 with a temp HERMES_HOME.
  • RED regression: a task created with initial_status="blocked" ended its event stream at created, not blocked; recompute_ready() could then promote it.
  • The minimal fix is the shape used here: append blocked immediately after created, inside the existing create_task() transaction, when resolved task_status == "blocked".
  • This preserves _has_sticky_block() as the single lifecycle predicate, keeps gateway/diagnostic consumers on the established blocked event, 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-program execution-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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants