# NousResearch/hermes-agent · Kanban column label undefined aria-label bug fixes

## Open now

- Nothing open on GitHub.

Multiple reports and fix attempts address an undefined COLUMN_LABEL variable causing rendering errors and broken aria-labels on kanban column select-all checkboxes. One unrelated member covers SPA auto-reload on stale-token 401 responses.

## TL;DR

- 6 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 2 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #23791. Verify it addresses your case.
- **Recommended:** read PR #23791 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.29`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #23620: \[Bug\]: Kanban dashboard: COLUMN_LABEL undefined in aria-label at column select-all checkbox
- #24188: Kanban dashboard tab — rendering error: COLUMN_LABEL is not defined

## Prior & competing PRs

- **#23791: fix(kanban): batch-salvage 7 community kanban bug fixes**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/run.py`, `hermes_cli/kanban.py`, `hermes_cli/kanban_db.py`, `plugins/kanban/dashboard/dist/index.js`, `run_agent.py`, `scripts/release.py`, `tests/hermes_cli/test_kanban_cli.py`, `tests/hermes_cli/test_kanban_notify.py`, `tests/run_agent/test_run_agent.py`, `tests/tools/test_send_message_tool.py`, `tools/send_message_tool.py`
  - fix hunks:
  - `gateway/run.py`

    ```diff
    @@ -8329,6 +8329,7 @@ async def _handle_kanban_command(self, event: MessageEvent) -> str:
             """
             import asyncio
             import re
    +        import shlex
             from hermes_cli.kanban import run_slash
     
             text = (event.text or "").strip()
    ```

    ```diff
    @@ -8338,7 +8339,26 @@ async def _handle_kanban_command(self, event: MessageEvent) -> str:
             if text.startswith("kanban"):
                 text = text[len("kanban"):].lstrip()
     
    -        is_create = text.split(None, 1)[:1] == ["create"]
    +        tokens = [redacted:secret-assignment] if text else []
    +        requested_board = None
    +        action = None
    +        i = 0
    +        while i < len(tokens):
    +            tok = tokens[i]
    +            if tok == "--board":
    +                if i + 1 >= len(tokens):
    +                    break
    +                requested_board = tokens[i + 1]
    +                i += 2
    +                continue
    +            if tok.startswith("--board="):
    +                requested_board = tok.split("=", 1)[1]
    +                i += 1
    +                continue
    +            action = tok
    +            break
    +
    +        is_create = action == "create"
     
             try:
                 output = await asyncio.to_thread(run_slash, text)
    ```

    ```diff
    @@ -8365,7 +8385,7 @@ async def _handle_kanban_command(self, event: MessageEvent) -> str:
                         if platform_str and chat_id:
                             def _sub():
                                 from hermes_cli import kanban_db as _kb
    -                            conn = _kb.connect()
    +                            conn = _kb.connect(board=requested_board)
                                 try:
                                     _kb.add_notify_sub(
                                         conn, task_id=task_id,
    ```
  - `hermes_cli/kanban.py`

    ```diff
    @@ -652,6 +652,16 @@ def kanban_command(args: argparse.Namespace) -> int:
         # keeps the patch small and inherits the exact same resolution the
         # dispatcher uses for workers — consistency is a feature here.
         board_override = getattr(args, "board", None)
    +    prev_board_env = os.environ.get("HERMES_KANBAN_BOARD")
    +    restore_board_env = False
    +
    +    def _restore_board_env() -> None:
    +        if not restore_board_env:
    +            return
    +        if prev_board_env is None:
    +            os.environ.pop("HERMES_KANBAN_BOARD", None)
    +        else:
    +            os.environ["HERMES_KANBAN_BOARD"] = prev_board_env
         if board_override:
             try:
                 normed = kb._normalize_board_slug(board_override)
    ```

    ```diff
    @@ -671,12 +681,16 @@ def kanban_command(args: argparse.Namespace) -> int:
                 )
                 return 1
             os.environ["HERMES_KANBAN_BOARD"] = normed
    +        restore_board_env = True
     
         # Boards management doesn't touch the DB at all — dispatch early so
         # fresh installs that haven't initialized any DB can still use
         # `hermes kanban boards create …`.
         if action == "boards":
    -        return _dispatch_boards(args)
    +        try:
    +            return _dispatch_boards(args)
    +        finally:
    +            _restore_board_env()
     
         # Auto-initialize the DB before dispatching any subcommand. init_db
         # is idempotent, so running it every invocation is cheap (one
    ```

    ```diff
    @@ -689,6 +703,7 @@ def kanban_command(args: argparse.Namespace) -> int:
             kb.init_db()
         except Exception as exc:
             print(f"kanban: could not initialize database: {exc}", file=sys.stderr)
    +        _restore_board_env()
             return 1
     
         handlers = {
    ```

    ```diff
    @@ -730,12 +745,16 @@ def kanban_command(args: argparse.Namespace) -> int:
         handler = handlers.get(action)
         if not handler:
             print(f"kanban: unknown action {action!r}", file=sys.stderr)
    +        _restore_board_env()
             return 2
         try:
             return int(handler(args) or 0)
         except (ValueError, RuntimeError) as exc:
             print(f"kanban: {exc}", file=sys.stderr)
    +        _restore_board_env()
             return 1
    +    finally:
    +        _restore_board_env()
     
     
     # ---------------------------------------------------------------------------
    ```
  - `hermes_cli/kanban_db.py`

    ```diff
    @@ -1826,7 +1826,7 @@ def _synthesize_ended_run(
     # ---------------------------------------------------------------------------
     
     def recompute_ready(conn: sqlite3.Connection) -> int:
    -    """Promote ``todo`` tasks to ``ready`` when all parents are ``done``.
    +    """Promote ``todo`` tasks to ``ready`` when all parents are ``done`` or ``archived``.
     
         Returns the number of tasks promoted.  Safe to call inside or outside
         an existing transaction; it opens its own IMMEDIATE txn.
    ```

    ```diff
    @@ -1844,7 +1844,7 @@ def recompute_ready(conn: sqlite3.Connection) -> int:
                     "WHERE l.child_id = ?",
                     (task_id,),
                 ).fetchall()
    -            if all(p["status"] == "done" for p in parents):
    +            if all(p["status"] in ("done", "archived") for p in parents):
                     conn.execute(
                         "UPDATE tasks SET status = 'ready' WHERE id = ? AND status = 'todo'",
                         (task_id,),
    ```

    ```diff
    @@ -1885,7 +1885,7 @@ def claim_task(
             undone = conn.execute(
                 "SELECT 1 FROM task_links l "
                 "JOIN tasks p ON p.id = l.parent_id "
    -            "WHERE l.child_id = ? AND p.status != 'done' LIMIT 1",
    +            "WHERE l.child_id = ? AND p.status NOT IN ('done', 'archived') LIMIT 1",
                 (task_id,),
             ).fetchone()
             if undone:
    ```

    ```diff
    @@ -3930,6 +3930,25 @@ def _default_spawn(
     
         prompt = f"work kanban task {task.id}"
         env = dict(os.environ)
    +
    +    # Inject HERMES_HOME so the worker reads the profile-scoped config.yaml
    +    # (fallback_providers, toolsets, agent settings, etc.) instead of the root
    +    # config.  Without this, `env = dict(os.environ)` copies only the parent's
    +    # env, and when the child process starts `hermes -p <name>` the
    +    # _apply_profile_override() runs *before* hermes_constants is imported.
    +    # If HERMES_HOME is absent from the child's env, get_hermes_home() falls
    +    # back to Path.home() / ".hermes" (the DEFAULT profile root), ignoring the
    +    # profile-specific config entirely.  Fixes profile-scoped fallback_providers
    +    # being invisible to kanban workers.
    +    from hermes_cli.profiles import resolve_profile_env
    +    try:
    +        env["HERMES_HOME"] = resolve_profile_env(profile_arg)
    +    except FileNotFoundError:
    +        # Profile dir doesn't exist — defer resolution to the CLI's
    +        # _apply_profile_override() via HERMES_PROFILE (set below).
    +        # This only happens in test fixtures where the isolated
    +        # HERMES_HOME never had profiles created.
    +        pass
         if task.tenant:
             env["HERMES_TENANT"] = task.tenant
         env["HERMES_KANBAN_TASK"] = task.id
    ```
  - `plugins/kanban/dashboard/dist/index.js`

    ```diff
    @@ -1898,7 +1898,7 @@
               type: "checkbox",
               className: "hermes-kanban-col-check",
               title: "Select all tasks in this column",
    -          "aria-label": `Select all tasks in ${COLUMN_LABEL[props.column.name] || props.column.name}`,
    +          "aria-label": `Select all tasks in ${colLabel || props.column.name}`,
               checked: props.column.tasks.length > 0 && props.column.tasks.every(function (t) { return props.selectedIds.has(t.id); }),
               onChange: function (e) {
                 e.stopPropagation();
    ```
  - `run_agent.py`

    ```diff
    @@ -14987,7 +14987,41 @@ def _stop_spinner():
                         "— requesting summary..."
                     )
                 final_response = self._handle_max_iterations(messages, api_call_count)
    -        
    +
    +            # If running as a kanban worker, block the task so the dispatcher
    +            # knows the worker could not complete (rather than treating it as a
    +            # protocol violation).  The agent loop strips tools before calling
    +            # _handle_max_iterations, so the model cannot call kanban_block
    +            # itself — we must do it on its behalf.
    +            _kanban_task = os.environ.get("HERMES_KANBAN_TASK")
    +            if _kanban_task:
    +                try:
    +                    handle_function_call(
    +                        "kanban_block",
    +                        {
    +                            "task_id": _kanban_task,
    +                            "reason": (
    +                                f"Iteration budget exhausted "
    +                                f"({api_call_count}/{self.max_iterations}) — "
    +                                "task could not complete within the allowed "
    +                                "iterations"
    +                            ),
    +                        },
    +                        task_id=effective_task_id,
    +                    )
    +                    logger.info(
    +                        "kanban_block called for task %s after iteration "
    +                        "exhaustion (%d/%d)",
    +                        _kanban_task, api_call_count, self.max_iterations,
    +                    )
    +                except Exception:
    +                    logger.warning(
    +                        "Failed to call kanban_block after iteration "
    +                        "exhaustion for task %s",
    +                        _kanban_task,
    +                        exc_info=True,
    +                    )
    +
             # Determine if conversation completed successfully
             completed = final_response is not None and api_call_count < self.max_iterations
     
    ```
  - `scripts/release.py`

    ```diff
    @@ -971,6 +971,12 @@
         "[redacted:email]": "leehack",  # PR #22053 salvage (telegram DM topic reply fallback)
         # pander: empty email, salvaged via PR #19665 from #16126 by @ms-alan
         "[redacted:email]": "A-kamal",  # PR #18678 (xAI image resolution fix)
    +    # Kanban bug-fix batch salvage (May 2026)
    +    "[redacted:email]": "Frowtek",  # salvage of #23206 (gateway --board auto-subscribe)
    +    "[redacted:email]": "Sylw3ster",  # salvage of #23252 (HERMES_KANBAN_BOARD restore)
    +    "[redacted:email]": "dmnkhorvath",  # salvage of #23358 (kanban worker send_message)
    +    "[redacted:email]": "smwbev",  # salvage of #23659 (aria-label colLabel)
    +    "[redacted:email]": "TurgutKural",  # salvage of #23356 (HERMES_HOME inject)
     }
     
     
    ```
  - `tests/hermes_cli/test_kanban_cli.py`

    ```diff
    @@ -392,3 +392,13 @@ def test_run_slash_missing_required_arg_friendly_error(kanban_home):
         out = kc.run_slash("show")
         assert "/kanban show" in out
         assert "task_id" in out
    +
    +
    +def test_run_slash_board_override_restores_prior_env(kanban_home, monkeypatch):
    +    kb.create_board("alpha")
    +    kb.create_board("beta")
    +    monkeypatch.setenv("HERMES_KANBAN_BOARD", "beta")
    +
    +    kc.run_slash("--board alpha list")
    +
    +    assert os.environ.get("HERMES_KANBAN_BOARD") == "beta"
    ```
  - `tests/hermes_cli/test_kanban_notify.py`

    ```diff
    @@ -2,6 +2,7 @@
     import pytest
     
     from pathlib import Path
    +from types import SimpleNamespace
     from hermes_cli import kanban_db as kb
     from unittest.mock import AsyncMock, MagicMock, patch
     
    ```

    ```diff
    @@ -429,3 +430,52 @@ async def _fast_sleep(_):
         finally:
             conn.close()
         assert subs == []
    +
    +
    [redacted:email]
    +async def test_gateway_create_autosubscribes_on_explicit_board(kanban_home):
    +    """`/kanban --board <slug> create ...` must subscribe on that board.
    +
    +    The gateway handler currently auto-subscribes after `/kanban create`,
    +    but the create detection must still work when the shared `--board`
    +    flag appears before the subcommand, and the subscription must land in
    +    that board's DB rather than the ambient/default board.
    +    """
    +    from gateway.run import GatewayRunner
    +    from gateway.config import Platform
    +
    +    kb.create_board("projx")
    +
    +    runner = object.__new__(GatewayRunner)
    +    source = SimpleNamespace(
    +        platform=Platform.TELEGRAM,
    +        chat_id="chat1",
    +        thread_id="th1",
    +        user_id="u1",
    +    )
    +    event = SimpleNamespace(
    +        text='/kanban --board projx create "hello" --assignee alice',
    +        source=source,
    +    )
    +
    +    out = await GatewayRunner._handle_kanban_command(runner, event)
    +
    +    assert "subscribed" in out.lower()
    +
    +    conn = kb.connect(board="projx")
    +    try:
    +        subs = kb.list_notify_subs(conn)
    +        tasks = kb.list_tasks(conn)
    +    finally:
    +        conn.close()
    +
    +    assert [t.title for t in tasks] == ["hello"]
    +    assert len(subs) == 1
    +    assert subs[0]["chat_id"] == "chat1"
    +    assert subs[0]["thread_id"] == "th1"
    +
    +    conn = kb.connect(board="default")
    +    try:
    +        assert kb.list_notify_subs(conn) == []
    +    finally:
    +        conn.close()
    ```
  - `tests/run_agent/test_run_agent.py`

    ```diff
    @@ -3344,6 +3344,88 @@ def test_truncated_tool_args_detected_when_finish_reason_not_length(self, agent)
             assert "truncated due to output length limit" in result["error"]
             mock_handle_function_call.assert_not_called()
     
    +    def test_kanban_block_called_on_iteration_exhaustion(self, agent, monkeypatch):
    +        """Regression: kanban worker must call kanban_block when iteration
    +        budget is exhausted, otherwise the dispatcher sees a protocol
    +        violation and gives up after 1 failure (issue #23216)."""
    +        self._setup_agent(agent)
    +        agent.max_iterations = 2
    +
    +        monkeypatch.setenv("HERMES_KANBAN_TASK", "t_test_task_123")
    +
    +        # Return a tool call for every iteration to exhaust the budget.
    +        tc = _mock_tool_call(name="web_search", arguments="{}", call_id="c1")
    +        tool_resp = _mock_response(
    +            content="", finish_reason="tool_calls", tool_calls=[tc],
    +        )
    +        # Final summary response from _handle_max_iterations.
    +        summary_resp = _mock_response(
    +            content="Could not finish — budget exhausted.", finish_reason="stop",
    +        )
    +        agent.client.chat.completions.create.side_effect = [
    +            tool_resp, tool_resp, summary_resp,
    +        ]
    +
    +        with (
    +            patch("run_agent.handle_function_call", return_value="ok") as mock_hfc,
    +            patch.object(agent, "_persist_session"),
    +            patch.object(agent, "_save_trajectory"),
    +            patch.object(agent, "_cleanup_task_resources"),
    +        ):
    +            result = agent.run_conversation("do the kanban work")
    +
    +        # The agent should have reported the task as not completed.
    +        assert result["completed"] is False
    +
    +        # Among all handle_function_call invocations, one must be
    +        # kanban_block with the correct task_id and a reason mentioning
    +        # iteration exhaustion.
    +        kanban_block_calls = [
    +            c for c in mock_hfc.call_args_list
    +            if c[0][0] == "kanban_block"
    +        ]
    +        assert len(kanban_block_calls) == 1, (
    +            f"Expected exactly 1 kanban_block call, got {len(kanban_block_calls)}. "
    +            f"All calls: {mock_hfc.call_args_list}"
    +        )
    +        call = kanban_block_calls[0]
    +        assert call[0][1]["task_id"] == "t_test_task_123"
    +        assert "Iteration budget exhausted" in call[0][1]["reason"]
    +
    +    def test_no_kanban_block_when_not_in_kanban_mode(self, agent, monkeypatch):
    +        """kanban_block must NOT be called when HERMES_KANBAN_TASK is unset."""
    +        self._setup_agent(agent)
    +        agent.max_iterations = 2
    +
    +        monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False)
    +
    +        tc = _mock_tool_call(name="web_search", arguments="{}", call_id="c1")
    +        tool_resp = _mock_response(
    +            content="", finish_reason="tool_calls", tool_calls=[tc],
    +        )
    +        summary_resp = _mock_response(
    +            content="Summary.", finish_reason="stop",
    +        )
    +        agent.client.chat.completions.create.side_effect = [
    +            tool_resp, tool_resp, summary_resp,
    +        ]
    +
    +        with (
    +            patch("run_agent.handle_function_call", return_value="ok") as mock_hfc,
    +            patch.object(agent, "_persist_session"),
    +            patch.object(agent, "_save_trajectory"),
    +            patch.object(agent, "_cleanup_task_resources"),
    +        ):
    +            agent.run_conversation("do stuff")
    +
    +        kanban_block_calls = [
    +            c for c in mock_hfc.call_args_list
    +            if c[0][0] == "kanban_block"
    +        ]
    +        assert len(kanban_block_calls) == 0, (
    +            "kanban_block should not be called outside kanban mode"
    +        )
    +
     
     class TestRetryExhaustion:
         """Regression: retry_count > max_retries was dead code (off-by-one).
    ```
  - `tests/tools/test_send_message_tool.py`

    ```diff
    @@ -2229,3 +2229,106 @@ async def fake_send(pconfig, chat_id, message, **kwargs):
             assert result["success"] is True
             assert result["message_id"] == "abc-123"
             assert result["extra_field"] == "preserved"
    +
    +
    +# ---------------------------------------------------------------------------
    +# _check_send_message — availability gating
    +# ---------------------------------------------------------------------------
    +
    +class TestCheckSendMessage:
    +    """The tool's check_fn governs whether the model sees ``send_message`` as
    +    callable for a given session. The four passing conditions are:
    +
    +    1. ``HERMES_KANBAN_TASK`` is set (worker spawned by the kanban dispatcher
    +       — parent gateway is by definition running, but the worker's
    +       ``HERMES_HOME`` may be a profile dir without a ``gateway.pid``).
    +    2. ``HERMES_SESSION_PLATFORM`` resolves to a non-empty, non-``local`` value
    +       (the session is wired to a messaging platform like Telegram).
    +    3. ``is_gateway_running()`` returns True (CLI / orchestrator profile with
    +       a live gateway colocated under the same ``HERMES_HOME``).
    +    4. None of the above → False, tool is hidden.
    +    """
    +
    +    def test_kanban_task_env_grants_access(self, monkeypatch):
    +        """Workers spawned by the dispatcher (HERMES_KANBAN_TASK set) must be
    +        allowed regardless of session_platform / gateway-pid state."""
    +        from tools.send_message_tool import _check_send_message
    +
    +        monkeypatch.setenv("HERMES_KANBAN_TASK", "t_abc12345")
    +        monkeypatch.delenv("HERMES_SESSION_PLATFORM", raising=False)
    +
    +        with patch("gateway.session_context.get_session_env", return_value=""), \
    +             patch("gateway.status.is_gateway_running", return_value=False):
    +            assert _check_send_message() is True
    +
    +    def test_kanban_task_env_short_circuits_before_gateway_check(self, monkeypatch):
    +        """Honoring HERMES_KANBAN_TASK must not depend on importing or calling
    +        gateway.status — the worker may run with a HERMES_HOME that has no
    +        gateway.pid, and we don't want that import path to be load-bearing."""
    +        from tools.send_message_tool import _check_send_message
    +
    +        monkeypatch.setenv("HERMES_KANBAN_TASK", "t_abc12345")
    +
    +        with patch("gateway.session_context.get_session_env",
    +                   side_effect=AssertionError("session_context not consulted "
    +                                              "when HERMES_KANBAN_TASK is set")), \
    +             patch("gateway.status.is_gateway_running",
    +                   side_effect=AssertionError("gateway.status not consulted "
    +                                              "when HERMES_KANBAN_TASK is set")):
    +            assert _check_send_message() is True
    +
    +    def test_messaging_platform_session_grants_access(self, monkeypatch):
    +        """Telegram/Discord/etc. sessions pass via the platform branch even
    +        without HERMES_KANBAN_TASK."""
    +        from tools.send_message_tool import _check_send_message
    +
    +        monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False)
    +
    +        with patch("gateway.session_context.get_session_env", return_value="telegram"), \
    +             patch("gateway.status.is_gateway_running", return_value=False):
    +            assert _check_send_message() is True
    +
    +    def test_local_platform_falls_through_to_gateway_check(self, monkeypatch):
    +        """``HERMES_SESSION_PLATFORM=local`` means CLI-style — must defer to
    +        is_gateway_running() rather than auto-grant."""
    +        from tools.send_message_tool import _check_send_message
    +
    +        monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False)
    +
    +        with patch("gateway.session_context.get_session_env", return_value="local"), \
    +             patch("gateway.status.is_gateway_running", return_value=True) as gw_mock:
    +            assert _check_send_message() is True
    +            gw_mock.assert_called_once()
    +
    +    def test_running_gateway_grants_access(self, monkeypatch):
    +        """Plain CLI session (no kanban task, empty platform) with a live
    +        gateway: tool is callable."""
    +        from tools.send_message_tool import _check_send_message
    +
    +        monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False)
    +
    +        with patch("gateway.session_context.get_session_env", return_value=""), \
    +             patch("gateway.status.is_gateway_running", return_value=True):
    +            assert _check_send_message() is True
    +
    +    def test_no_signals_means_unavailable(self, monkeypatch):
    +        """No kanban task, no platform, no gateway: tool is hidden."""
    +        from tools.send_message_tool import _check_send_message
    +
    +        monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False)
    +
    +        with patch("gateway.session_context.get_session_env", return_value=""), \
    +             patch("gateway.status.is_gateway_running", return_value=False):
    +            assert _check_send_message() is False
    +
    +    def test_gateway_status_import_error_is_swallowed(self, monkeypatch):
    +        """If gateway.status can't be imported (unusual deployment / partial
    +        install), the check returns False rather than raising."""
    +        from tools.send_message_tool import _check_send_message
    +
    +        monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False)
    +
    +        with patch("gateway.session_context.get_session_env", return_value=""), \
    +             patch("gateway.status.is_gateway_running",
    +                   side_effect=ImportError("simulated")):
    +            assert _check_send_message() is False
    ```
  - `tools/send_message_tool.py`

    ```diff
    @@ -1757,7 +1757,20 @@ async def _send_feishu(pconfig, chat_id, message, media_files=None, thread_id=No
     
     
     def _check_send_message():
    -    """Gate send_message on gateway running (always available on messaging platforms)."""
    +    """Gate send_message on gateway running (always available on messaging platforms).
    +
    +    Also passes for kanban workers — the dispatcher sets ``HERMES_KANBAN_TASK``
    +    on every spawned worker, but those workers run with the assignee profile's
    +    ``HERMES_HOME`` which has no ``gateway.pid``, so the gateway-running check
    +    would fail even though the parent gateway is alive. Honoring the env var
    +    lets workers call ``send_message`` to deliver rich content directly to the
    +    originating chat (paired with ``kanban_complete`` for the short notifier
    +    summary), which is the canonical pattern for any worker that needs to
    +    reply with more than the ~200-char first-line truncation the kanban
    +    notifier applies.
    +    """
    +    if os.environ.get("HERMES_KANBAN_TASK"):
    +        return True
         from gateway.session_context import get_session_env
         platform = get_session_env("HERMES_SESSION_PLATFORM", "")
         if platform and platform != "local":
    ```
- **#33861: fix(dashboard): auto-reload SPA on stale-token 401 in loopback mode**. merged to main · fix tier `suggested`
  - maintainer direction: category `merged`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#23637: fix(kanban): use FALLBACK_COLUMN_LABEL in column select-all aria-label**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#23791`, `#23659`, `#23659`
- **#23659: fix(kanban): use localized column label in select-all aria label**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#23791`

## Fix status

- Merged to main in PR #23791 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-11`.
- Subsystem: `hermes_cli`
