From b22b3f506a34cf848d68bb2ab17b68b0bc8ec152 Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Tue, 5 May 2026 12:23:43 +0530 Subject: [PATCH] fix(cli): pin HERMES_KANBAN_BOARD at chat boot to stop subprocess board drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without an explicit pin, in-process kanban tools and shelled-out `hermes kanban …` subprocesses resolve the active board on different paths: the env var when set, otherwise the global `/kanban/current` file. When a concurrent session toggles the current-board pointer mid-turn, the same chat ends up routing tool calls to board A while its shell calls hit board B, surfacing as phantom "no such task" errors. Pin the resolved board into env once at `cmd_chat` boot when HERMES_KANBAN_BOARD isn't already set. Mirrors what the dispatcher does for spawned workers (kanban_db.py:2622-2623). Idempotent and a no-op when the env is already pinned by the caller. Closes #20074 --- hermes_cli/main.py | 22 ++++++++ tests/hermes_cli/test_pin_kanban_board_env.py | 54 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tests/hermes_cli/test_pin_kanban_board_env.py diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 4e709a8f83..112d839db2 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1216,6 +1216,26 @@ def _launch_tui( sys.exit(code) +def _pin_kanban_board_env() -> None: + """Pin the active kanban board into ``HERMES_KANBAN_BOARD`` for the chat session. + + Without this, in-process tools (``kanban_*``) and shelled-out CLI calls + (``hermes kanban …``) resolve the board on different paths: the env-pin if + set, otherwise the global ``/kanban/current`` file. A concurrent + ``hermes kanban boards switch`` from another session can flip the file + mid-turn, so the same chat sees its tool calls hit board A while its shell + calls hit board B (#20074). Pinning at chat boot mirrors what the + dispatcher already does for spawned workers. + """ + if os.environ.get("HERMES_KANBAN_BOARD"): + return + try: + from hermes_cli.kanban_db import get_current_board + os.environ["HERMES_KANBAN_BOARD"] = get_current_board() + except Exception: + pass + + def cmd_chat(args): """Run interactive chat CLI.""" use_tui = getattr(args, "tui", False) or os.environ.get("HERMES_TUI") == "1" @@ -1324,6 +1344,8 @@ def cmd_chat(args): if getattr(args, "source", None): os.environ["HERMES_SESSION_SOURCE"] = args.source + _pin_kanban_board_env() + if use_tui: _launch_tui( getattr(args, "resume", None), diff --git a/tests/hermes_cli/test_pin_kanban_board_env.py b/tests/hermes_cli/test_pin_kanban_board_env.py new file mode 100644 index 0000000000..c4965ecf4d --- /dev/null +++ b/tests/hermes_cli/test_pin_kanban_board_env.py @@ -0,0 +1,54 @@ +"""Tests for `_pin_kanban_board_env` helper invoked by `cmd_chat`. + +Regression coverage for #20074: a chat session must export the active kanban +board into `HERMES_KANBAN_BOARD` at boot so subprocess shell-outs (e.g. +`hermes kanban …`) inherit the same board the in-process kanban tools resolve. +Without this, a concurrent `hermes kanban boards switch` from another session +can flip the global current-board file mid-turn and silently divert the +shell calls to a different DB. +""" +import importlib + + +def test_pin_writes_resolved_board_when_env_unset(monkeypatch): + monkeypatch.delenv("HERMES_KANBAN_BOARD", raising=False) + main_mod = importlib.import_module("hermes_cli.main") + + import hermes_cli.kanban_db as kdb + monkeypatch.setattr(kdb, "get_current_board", lambda: "space") + + main_mod._pin_kanban_board_env() + + assert main_mod.os.environ.get("HERMES_KANBAN_BOARD") == "space" + + +def test_pin_does_not_overwrite_existing_env(monkeypatch): + monkeypatch.setenv("HERMES_KANBAN_BOARD", "preset") + main_mod = importlib.import_module("hermes_cli.main") + + import hermes_cli.kanban_db as kdb + + def _explode(): + raise AssertionError("get_current_board must not be called when env is set") + + monkeypatch.setattr(kdb, "get_current_board", _explode) + + main_mod._pin_kanban_board_env() + + assert main_mod.os.environ.get("HERMES_KANBAN_BOARD") == "preset" + + +def test_pin_swallows_resolution_failures(monkeypatch): + monkeypatch.delenv("HERMES_KANBAN_BOARD", raising=False) + main_mod = importlib.import_module("hermes_cli.main") + + import hermes_cli.kanban_db as kdb + + def _boom(): + raise RuntimeError("disk gone") + + monkeypatch.setattr(kdb, "get_current_board", _boom) + + main_mod._pin_kanban_board_env() + + assert "HERMES_KANBAN_BOARD" not in main_mod.os.environ