# NousResearch/hermes-agent · Kanban dispatcher spawn path fix

## Open now

- Nothing open on GitHub.

These issues and PRs address a bug where the Kanban dispatcher fails to spawn correctly due to PATH or executable resolution problems. The fixes involve changing how the dispatcher process is launched, targeting compatibility with environments like NixOS.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #23222. Verify it addresses your case.
- **Recommended:** read PR #23222 (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_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: 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

- #23184: \[Bug\]: Unable to use Kanban with nixos module

## Prior & competing PRs

- **#23222: fix(kanban): PATH-independent dispatcher spawn (salvage #23198)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/kanban_db.py`, `tests/hermes_cli/test_kanban_db.py`
  - fix hunks:
  - `hermes_cli/kanban_db.py`

    ```diff
    @@ -3749,6 +3749,35 @@ def _rotate_worker_log(log_path: Path, max_bytes: int) -> None:
             pass
     
     
    +def _resolve_hermes_argv() -> list[str]:
    +    """Resolve the ``hermes`` invocation as argv parts for ``Popen``.
    +
    +    Tries in order:
    +
    +    1. ``shutil.which("hermes")`` — the console-script shim, the same form
    +       that shows up in ``ps`` output and existing logs. Preferred so live
    +       systems' diagnostics stay familiar.
    +    2. ``sys.executable -m hermes_cli.main`` — fallback for setups where
    +       Hermes is launched from a venv and the ``hermes`` shim is not on
    +       the dispatcher's ``$PATH`` (cron, systemd ``User=`` services,
    +       launchd jobs, detached processes, etc.). Goes through the running
    +       interpreter so the result is independent of ``$PATH``.
    +
    +    Mirrors ``gateway.run._resolve_hermes_bin`` for the same reason. Kept
    +    local (not imported from gateway) because ``hermes_cli`` sits below
    +    ``gateway`` in the dependency order.
    +    """
    +    import shutil
    +
    +    hermes_bin = shutil.which("hermes")
    +    if hermes_bin:
    +        return [hermes_bin]
    +    # Fallback to the module form. ``hermes_cli.main`` is the actual
    +    # console-script target declared in pyproject.toml, NOT a top-level
    +    # ``hermes`` package — there is no ``hermes`` package to import.
    +    return [sys.executable, "-m", "hermes_cli.main"]
    +
    +
     def _default_spawn(
         task: Task,
         workspace: str,
    ```

    ```diff
    @@ -3805,7 +3834,7 @@ def _default_spawn(
         env["HERMES_PROFILE"] = profile_arg
     
         cmd = [
    -        "hermes",
    +        *_resolve_hermes_argv(),
             "-p", profile_arg,
             # Auto-load the kanban-worker skill so every dispatched worker
             # has the pattern library (good summary/metadata shapes, retry
    ```
  - `tests/hermes_cli/test_kanban_db.py`

    ```diff
    @@ -1199,3 +1199,68 @@ def test_migrate_add_optional_columns_tolerates_concurrent_migration(kanban_home
         # Running migration on an already-migrated schema must not raise.
         kb._migrate_add_optional_columns(conn)
         conn.close()
    +
    +
    +# ---------------------------------------------------------------------------
    +# Dispatcher spawn invocation — _resolve_hermes_argv()
    +#
    +# Workers spawned by the dispatcher must use a `hermes` invocation that does
    +# not depend on PATH being set up correctly. cron jobs, systemd User= services,
    +# launchd jobs, and other detached processes routinely run with a stripped
    +# $PATH that doesn't include the venv's bin/, so a bare `["hermes", ...]`
    +# spawn fails with FileNotFoundError and the task gets stuck. The resolver
    +# prefers the PATH shim (familiar `ps` output) but falls back to the module
    +# form so the spawn keeps working when PATH is missing the shim.
    +# ---------------------------------------------------------------------------
    +
    +
    +def test_resolve_hermes_argv_prefers_path_shim(monkeypatch):
    +    """When `hermes` is on PATH, use the shim — preserves familiar ps output."""
    +    import shutil
    +    import hermes_cli.kanban_db as kb
    +
    +    monkeypatch.setattr(shutil, "which", lambda name: "/usr/local/bin/hermes")
    +    argv = kb._resolve_hermes_argv()
    +    assert argv == ["/usr/local/bin/hermes"]
    +
    +
    +def test_resolve_hermes_argv_falls_back_to_module_form_when_no_path_shim(monkeypatch):
    +    """When the shim is not on PATH, fall back to `python -m hermes_cli.main`.
    +
    +    Pins the correct module name (NOT `hermes` — there is no top-level
    +    `hermes` package). Regression for #23198: the original PR shipped
    +    `python -m hermes` which fails with `No module named hermes` on every
    +    invocation.
    +    """
    +    import shutil
    +    import sys
    +    import hermes_cli.kanban_db as kb
    +
    +    monkeypatch.setattr(shutil, "which", lambda name: None)
    +    argv = kb._resolve_hermes_argv()
    +    assert argv == [sys.executable, "-m", "hermes_cli.main"]
    +
    +
    +def test_resolve_hermes_argv_module_actually_runs():
    +    """The fallback module name must be importable + runnable.
    +
    +    A unit test that pins the literal string is necessary but not
    +    sufficient — if `hermes_cli.main` ever loses `if __name__ == "__main__"`
    +    handling or its argparse setup, `python -m hermes_cli.main --version`
    +    would fail and so would every dispatcher spawn that hits the fallback.
    +    Run it as a real subprocess to catch that regression.
    +    """
    +    import subprocess
    +    import sys
    +    import hermes_cli.kanban_db as kb
    +    import shutil
    +    import unittest.mock as mock
    +
    +    with mock.patch.object(shutil, "which", return_value=None):
    +        argv = kb._resolve_hermes_argv()
    +    r = subprocess.run(argv + ["--version"], capture_output=True, text=True, timeout=30)
    +    assert r.returncode == 0, (
    +        f"`{' '.join(argv)} --version` failed (rc={r.returncode}); "
    +        f"stderr={r.stderr[:200]!r}"
    +    )
    +    assert "Hermes Agent" in r.stdout, f"unexpected output: {r.stdout[:200]!r}"
    ```
- **#23198: fix(kanban): use sys.executable -m hermes for dispatcher spawn**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#23222`, `#1049`

## Fix status

- Merged to main in PR #23222 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-10`.
