fix(kanban): add per-path env overrides + dispatcher env injection

Layers defense-in-depth on top of the shared-root anchoring (base commit).

Changes in hermes_cli/kanban_db.py:
- kanban_db_path() now honours HERMES_KANBAN_DB first, then falls through
  to kanban_home()/kanban.db.
- workspaces_root() now honours HERMES_KANBAN_WORKSPACES_ROOT first, then
  falls through to kanban_home()/kanban/workspaces.
- All three overrides (HERMES_KANBAN_HOME, HERMES_KANBAN_DB,
  HERMES_KANBAN_WORKSPACES_ROOT) now call .expanduser() for consistency.
- _default_spawn() injects HERMES_KANBAN_DB and
  HERMES_KANBAN_WORKSPACES_ROOT into the worker subprocess env. Even
  when the worker's get_default_hermes_root() resolution somehow
  disagrees with the dispatcher's (symlinks, unusual Docker layouts),
  the two processes still open the same SQLite file.

Module docstring updated to describe all three overrides and the
dispatcher env-injection contract.

Tests (tests/hermes_cli/test_kanban_db.py, TestSharedBoardPaths):
- test_hermes_kanban_db_pin_beats_kanban_home
- test_hermes_kanban_workspaces_root_pin_beats_kanban_home
- test_empty_per_path_overrides_fall_through
- test_dispatcher_spawn_injects_kanban_db_and_workspaces_root
  (monkeypatches subprocess.Popen, asserts both env vars reach the
  child even after HERMES_HOME is rewritten by `hermes -p <profile>`.)

Docs: website/docs/reference/environment-variables.md gets entries
for the three kanban env vars.

This fusion is built on the cleanest of the seven competing PRs that
targeted issue #18442:

* Base commit (from PR #19350 by @GodsBoy): add `kanban_home()` helper
  anchored at `get_default_hermes_root()`, reroute all 5 kanban path
  sites through it (including the 3 sibling log-dir sites that the
  other six PRs missed), 8-test regression class.
* Dispatcher env-var injection approach drawn from PRs #18300
  (@quocanh261997) and #19100 (@cg2aigc).
* Per-path env overrides drawn from PR #19100 (@cg2aigc).
* get_default_hermes_root() resolution direction first proposed in
  PR #18503 (@beibi9966) and PR #18985 (@Gosuj).

Closes the duplicate/competing PRs: #18300, #18503, #18670, #18985,
#19037, #19056, #19100. Fixes #18442 and #19348.

Co-authored-by: quocanh261997 <17986614+quocanh261997@users.noreply.github.com>
Co-authored-by: cg2aigc <232694053+cg2aigc@users.noreply.github.com>
Co-authored-by: beibi9966 <beibei1988@proton.me>
Co-authored-by: Gosuj <123411271+Gosuj@users.noreply.github.com>
Co-authored-by: LeonSGP43 <154585401+LeonSGP43@users.noreply.github.com>
This commit is contained in:
teknium1
2026-05-03 15:05:28 -07:00
committed by Teknium
parent f5bd77b3e1
commit 2658494e81
3 changed files with 143 additions and 4 deletions

View File

@@ -9,8 +9,20 @@ board as the dispatcher that claimed the task. The same applies to
In standard installs ``<root>`` is ``~/.hermes``. In Docker / custom
deployments where ``HERMES_HOME`` points outside ``~/.hermes`` (e.g.
``/opt/hermes``), ``<root>`` is ``HERMES_HOME``. Set ``HERMES_KANBAN_HOME``
to override the resolution explicitly (tests, unusual deployments).
``/opt/hermes``), ``<root>`` is ``HERMES_HOME``. Three env-var overrides
are available (highest precedence first, all optional):
* ``HERMES_KANBAN_DB`` — pin the database file path directly.
* ``HERMES_KANBAN_WORKSPACES_ROOT`` — pin the workspaces root directly.
* ``HERMES_KANBAN_HOME`` — pin the umbrella root that anchors all three
kanban paths (db + workspaces + logs). Useful for tests and unusual
deployments where a single override is enough.
The dispatcher injects ``HERMES_KANBAN_DB`` and
``HERMES_KANBAN_WORKSPACES_ROOT`` into the worker subprocess env as a
defense-in-depth measure: even if the worker's ``get_default_hermes_root()``
resolution somehow disagrees with the dispatcher's (unusual symlink or
Docker layout), the two processes still converge on the same files.
Schema is intentionally small: tasks, task_links, task_comments,
task_events. The ``workspace_kind`` field decouples coordination from git
@@ -87,7 +99,7 @@ def kanban_home() -> Path:
"""
override = os.environ.get("HERMES_KANBAN_HOME", "").strip()
if override:
return Path(override)
return Path(override).expanduser()
from hermes_constants import get_default_hermes_root
return get_default_hermes_root()
@@ -97,8 +109,13 @@ def kanban_db_path() -> Path:
Anchored at :func:`kanban_home`, not the active profile's
``HERMES_HOME``, so profile workers and the dispatcher converge on
the same board.
the same board. ``HERMES_KANBAN_DB`` pins the path directly (highest
precedence) — the dispatcher injects this into worker subprocess env
as defense-in-depth.
"""
override = os.environ.get("HERMES_KANBAN_DB", "").strip()
if override:
return Path(override).expanduser()
return kanban_home() / "kanban.db"
@@ -107,7 +124,13 @@ def workspaces_root() -> Path:
Anchored at :func:`kanban_home` so workspace paths are stable across
profile workers spawned by the dispatcher.
``HERMES_KANBAN_WORKSPACES_ROOT`` pins the path directly (highest
precedence) — the dispatcher injects this into worker subprocess env
as defense-in-depth.
"""
override = os.environ.get("HERMES_KANBAN_WORKSPACES_ROOT", "").strip()
if override:
return Path(override).expanduser()
return kanban_home() / "kanban" / "workspaces"
@@ -2111,6 +2134,14 @@ def _default_spawn(task: Task, workspace: str) -> Optional[int]:
env["HERMES_TENANT"] = task.tenant
env["HERMES_KANBAN_TASK"] = task.id
env["HERMES_KANBAN_WORKSPACE"] = workspace
# Pin the shared board + workspaces root the dispatcher resolved, so
# that even when the worker activates a profile (`hermes -p <name>`
# rewrites HERMES_HOME), its kanban paths still match the
# dispatcher's. Belt-and-braces with the `get_default_hermes_root()`
# resolution in `kanban_home()` — symmetric resolution is the norm,
# but unusual symlink / Docker layouts are caught here too.
env["HERMES_KANBAN_DB"] = str(kanban_db_path())
env["HERMES_KANBAN_WORKSPACES_ROOT"] = str(workspaces_root())
# HERMES_PROFILE is the author the kanban_comment tool defaults to.
# `hermes -p <assignee>` activates the profile, but the env var is
# what the tool reads — set it explicitly here so comments are