NousResearch/hermes-agent

Pin worker TERMINAL_CWD in kanban spawn

Cluster 34619 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #50348 on 21 Jun 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #50348. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #50348. That is separate from the fix event.
Still open
Nothing open on GitHub.

These items identify and address a bug where kanban workers inherit the wrong working directory context during multi-profile dispatch. The fix pins TERMINAL_CWD to the appropriate task workspace or profile directory at spawn time.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #50348 fix(kanban): pin worker TERMINAL_CWD to the task workspace

    merged to main

    files: hermes_cli/kanban_db.py, tests/hermes_cli/test_kanban_worker_terminal_cwd.py

    Show the diff
    • hermes_cli/kanban_db.py
      @@ -7246,6 +7246,20 @@ def _default_spawn(
               env["HERMES_TENANT"] = task.tenant
           env["HERMES_KANBAN_TASK"] = task.id
           env["HERMES_KANBAN_WORKSPACE"] = workspace
      +    # Pin TERMINAL_CWD to the task's workspace so the worker's file tools and
      +    # context-file loader anchor on the workspace, not whatever cwd the
      +    # dispatching gateway happened to export. The worker subprocess is already
      +    # launched with cwd=workspace, but TERMINAL_CWD takes precedence over the
      +    # process cwd in both file_tools._resolve_base_dir (#41312 — relative
      +    # write_file paths were landing in the gateway user's home) and
      +    # build_context_files_prompt (#34619 — workers loaded the dispatching
      +    # gateway's AGENTS.md instead of the task's). Setting it to the workspace
      +    # fixes both: the workspace is where the task's work actually happens.
      +    # Only pin a real, absolute directory — file_tools rejects relative /
      +    # sentinel TERMINAL_CWD values, so a non-dir workspace must NOT be set
      +    # here (leave the inherited value rather than write a meaningless one).
      +    if workspace and os.path.isabs(workspace) and os.path.isdir(workspace):
      +        env["TERMINAL_CWD"] = workspace
           if task.branch_name:
               env["HERMES_KANBAN_BRANCH"] = task.branch_name
           if task.current_run_id is not None:
    • tests/hermes_cli/test_kanban_worker_terminal_cwd.py
      @@ -0,0 +1,101 @@
      +"""Tests: kanban worker spawn pins TERMINAL_CWD to the task workspace.
      +
      +Regression coverage for #34619 and #41312 (same root cause): ``_default_spawn``
      +launched the worker subprocess with ``cwd=workspace`` and set
      +``HERMES_KANBAN_WORKSPACE``, but did NOT set ``TERMINAL_CWD``. Because
      +``TERMINAL_CWD`` takes precedence over the process cwd in both
      +``tools/file_tools.py::_resolve_base_dir`` (relative ``write_file`` paths) and
      +``agent_init``'s context-file loader (``AGENTS.md`` discovery), workers inherited
      +the dispatching gateway's cwd — relative writes landed in the gateway user's
      +home (#41312) and the wrong profile's ``AGENTS.md`` was loaded (#34619).
      +Pinning ``TERMINAL_CWD`` to the workspace fixes both.
      +"""
      +
      +from __future__ import annotations
      +
      +import subprocess
      +
      +
      +def _make_task(kb, *, assignee: str = "w"):
      +    return kb.Task(
      +        id="t_cwd",
      +        title="cwd pin",
      +        body=None,
      +        assignee=assignee,
      +        status="running",
      +        priority=0,
      +        created_by="test",
      +        created_at=1,
      +        started_at=None,
      +        completed_at=None,
      +        workspace_kind="dir",
      +        workspace_path=None,
      +        claim_lock="lock",
      +        claim_expires=None,
      +        tenant=None,
      +        current_run_id=1,
      +    )
      +
      +
      +def _capture_spawn_env(kb, monkeypatch, workspace: str) -> dict:
      +    monkeypatch.setattr(kb, "_resolve_hermes_argv", lambda: ["hermes"])
      +
      +    captured: dict = {}
      +
      +    class FakeProc:
      +        pid = 4242
      +
      +    def fake_popen(cmd, *args, **kwargs):
      +        captured["cmd"] = list(cmd)
      +        captured["env"] = dict(kwargs.get("env") or {})
      +        captured["cwd"] = kwargs.get("cwd")
      +        return FakeProc()
      +
      +    monkeypatch.setattr(subprocess, "Popen", fake_popen)
      +    kb._default_spawn(_make_task(kb), workspace)
      +    return captured
      +
      +
      +def test_terminal_cwd_pinned_to_workspace(monkeypatch, tmp_path):
      +    """A real, absolute workspace dir is pinned as TERMINAL_CWD."""
      +    root = tmp_path / ".hermes"
      +    (root / "profiles" / "w").mkdir(parents=True)
      +    (root / "profiles" / "w" / "config.yaml").write_text("toolsets:\n  - kanban\n", encoding="utf-8")
      +    root.joinpath("config.yaml").write_text("toolsets:\n  - kanban\n", encoding="utf-8")
      +    monkeypatch.setenv("HERMES_HOME", str(root))
      +
      +    from hermes_cli import kanban_db as kb
      +
      +    workspace = tmp_path / "ws"
      +    workspace.mkdir()
      +
      +    captured = _capture_spawn_env(kb, monkeypatch, str(workspace))
      +
      +    assert captured["env"]["TERMINAL_CWD"] == str(workspace)
      +    # The subprocess cwd and TERMINAL_CWD must agree — both anchor the workspace.
      +    assert captured["cwd"] == str(workspace)
      +    assert captured["env"]["HERMES_KANBAN_WORKSPACE"] == str(workspace)
      +
      +
      +def test_terminal_cwd_not_pinned_for_nonexistent_workspace(monkeypatch, tmp_path):
      +    """A non-directory workspace must NOT clobber the inherited TERMINAL_CWD.
      +
      +    file_tools rejects relative / sentinel TERMINAL_CWD values, so writing a
      +    meaningless (nonexistent) path would be worse than leaving the inherited
      +    one. The guard requires an existing absolute dir.
      +    """
      +    root = tmp_path / ".hermes"
      +    (root / "profiles" / "w").mkdir(parents=True)
      +    (root / "profiles" / "w" / "config.yaml").write_text("toolsets:\n  - kanban\n", encoding="utf-8")
      +    root.joinpath("config.yaml").write_text("toolsets:\n  - kanban\n", encoding="utf-8")
      +    monkeypatch.setenv("HERMES_HOME", str(root))
      +    monkeypatch.setenv("TERMINAL_CWD", "/pre/existing/anchor")
      +
      +    from hermes_cli import kanban_db as kb
      +
      +    missing = tmp_path / "does-not-exist"
      +
      +    captured = _capture_spawn_env(kb, monkeypatch, str(missing))
      +
      +    # Inherited value is preserved (not overwritten with a bogus path).
      +    assert captured["env"]["TERMINAL_CWD"] == "/pre/existing/anchor"

1 PR closed without merging

From the close comments: done elsewhere 1

  • #34626 fix(cli): pin worker TERMINAL_CWD to profile dir in kanban spawn · done elsewhere → 9630ec6, #34619
Closed issues · 1
  • Closed#34619`_default_spawn` injects `HERMES_HOME` but not `TERMINAL_CWD` — kanban workers load the dispatching gateway's `AGENTS.md` under multi-profile dispatch
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
hermes cli
Merged PRs
#50348
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
21 Jun 2026

For your agent

The same facts, typed.