From 2ec8fc6c9c7b3898e2cf9618608662174f8f4a04 Mon Sep 17 00:00:00 2001 From: JT Chin Date: Tue, 7 Jul 2026 16:34:31 +0800 Subject: [PATCH] fix: keep initially blocked kanban cards sticky --- hermes_cli/kanban_db.py | 16 ++++++++++ .../hermes_cli/test_kanban_blocked_sticky.py | 29 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index 6150b141537b9..22c5c0871e62d 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -2680,7 +2680,23 @@ def create_task( "goal_mode": bool(goal_mode) or None, }, ) + if task_status == "blocked": + # A task born blocked is an explicit human/ops gate, not a + # transient circuit-breaker block. Emit the same sticky + # signal used by block_task() so recompute_ready() cannot + # silently promote it on the next dispatcher tick. + _append_event( + conn, + task_id, + "blocked", + { + "reason": "initial_status=blocked", + "kind": None, + "recurrences": 1, + }, + ) return task_id + except sqlite3.IntegrityError: if attempt == 1: raise diff --git a/tests/hermes_cli/test_kanban_blocked_sticky.py b/tests/hermes_cli/test_kanban_blocked_sticky.py index 2d7cafef826f4..2aa840a6900fd 100644 --- a/tests/hermes_cli/test_kanban_blocked_sticky.py +++ b/tests/hermes_cli/test_kanban_blocked_sticky.py @@ -271,6 +271,35 @@ def test_protocol_violation_loop_is_broken(kanban_home: Path) -> None: assert kb.get_task(conn, tid).status == "blocked" +# --------------------------------------------------------------------------- +# Create-time blocked cards are also sticky +# --------------------------------------------------------------------------- + + +def test_initial_status_blocked_is_not_auto_promoted_by_recompute_ready(kanban_home: Path) -> None: + """A card created directly in ``blocked`` is an explicit human gate, + not a transient dependency/circuit-breaker block. + + Regression for JT's 2026-07-05 board drift: ``hermes kanban create + --initial-status blocked`` wrote only a ``created`` event with + ``status='blocked'``. Because no ``blocked`` event existed, + ``recompute_ready`` treated the task as non-sticky and silently + promoted it to ``ready`` on the next dispatcher tick. + """ + with kb.connect() as conn: + tid = kb.create_task( + conn, + title="approval-gated reminder", + initial_status="blocked", + ) + assert kb.get_task(conn, tid).status == "blocked" + + promoted = kb.recompute_ready(conn) + + assert promoted == 0 + assert kb.get_task(conn, tid).status == "blocked" + + # --------------------------------------------------------------------------- # Schema-init recovery on legacy DBs is covered by # tests/hermes_cli/test_kanban_db.py::test_connect_migrates_legacy_db_before_optional_column_indexes