{"repo":"NousResearch/hermes-agent","public_id":"41312","display_label":"Kanban worker TERMINAL_CWD path resolution bug and fixes","summary":"A bug where Kanban workers resolve relative file paths against the wrong working directory (gateway home instead of the assigned workspace) and several related fix attempts targeting TERMINAL_CWD environment variable handling.","subsystem":"environments","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":50348,"merged_at":"2026-06-21T19:43:37+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-21T19:43:37+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":34619,"pr_number":50348,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-21T19:43:37+00:00"},{"issue_number":41312,"pr_number":50348,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-21T19:43:37+00:00"}],"members":[{"kind":"issue","number":41312,"title":"[Bug]: Kanban worker resolves relative file paths against TERMINAL_CWD (gateway home), not the assigned HERMES_KANBAN_WORKSPACE","url":null,"state":"closed"},{"kind":"pr","number":41436,"title":"fix(kanban): pin TERMINAL_CWD to workspace so relative file paths resolve correctly","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"9630ec6c1","number":0},{"kind":"pr","value":"#50348","number":50348},{"kind":"pr","value":"#41312","number":41312}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":41450,"title":"fix(kanban): set TERMINAL_CWD in worker env for correct file path resolution (#41312)","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":50348,"title":"fix(kanban): pin worker TERMINAL_CWD to the task workspace","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/kanban_db.py","tests/hermes_cli/test_kanban_worker_terminal_cwd.py"],"fix_hunks":[{"file":"hermes_cli/kanban_db.py","hunks":["@@ -7246,6 +7246,20 @@ def _default_spawn(\n         env[\"HERMES_TENANT\"] = task.tenant\n     env[\"HERMES_KANBAN_TASK\"] = task.id\n     env[\"HERMES_KANBAN_WORKSPACE\"] = workspace\n+    # Pin TERMINAL_CWD to the task's workspace so the worker's file tools and\n+    # context-file loader anchor on the workspace, not whatever cwd the\n+    # dispatching gateway happened to export. The worker subprocess is already\n+    # launched with cwd=workspace, but TERMINAL_CWD takes precedence over the\n+    # process cwd in both file_tools._resolve_base_dir (#41312 — relative\n+    # write_file paths were landing in the gateway user's home) and\n+    # build_context_files_prompt (#34619 — workers loaded the dispatching\n+    # gateway's AGENTS.md instead of the task's). Setting it to the workspace\n+    # fixes both: the workspace is where the task's work actually happens.\n+    # Only pin a real, absolute directory — file_tools rejects relative /\n+    # sentinel TERMINAL_CWD values, so a non-dir workspace must NOT be set\n+    # here (leave the inherited value rather than write a meaningless one).\n+    if workspace and os.path.isabs(workspace) and os.path.isdir(workspace):\n+        env[\"TERMINAL_CWD\"] = workspace\n     if task.branch_name:\n         env[\"HERMES_KANBAN_BRANCH\"] = task.branch_name\n     if task.current_run_id is not None:"]},{"file":"tests/hermes_cli/test_kanban_worker_terminal_cwd.py","hunks":["@@ -0,0 +1,101 @@\n+\"\"\"Tests: kanban worker spawn pins TERMINAL_CWD to the task workspace.\n+\n+Regression coverage for #34619 and #41312 (same root cause): ``_default_spawn``\n+launched the worker subprocess with ``cwd=workspace`` and set\n+``HERMES_KANBAN_WORKSPACE``, but did NOT set ``TERMINAL_CWD``. Because\n+``TERMINAL_CWD`` takes precedence over the process cwd in both\n+``tools/file_tools.py::_resolve_base_dir`` (relative ``write_file`` paths) and\n+``agent_init``'s context-file loader (``AGENTS.md`` discovery), workers inherited\n+the dispatching gateway's cwd — relative writes landed in the gateway user's\n+home (#41312) and the wrong profile's ``AGENTS.md`` was loaded (#34619).\n+Pinning ``TERMINAL_CWD`` to the workspace fixes both.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import subprocess\n+\n+\n+def _make_task(kb, *, assignee: str = \"w\"):\n+    return kb.Task(\n+        id=\"t_cwd\",\n+        title=\"cwd pin\",\n+        body=None,\n+        assignee=assignee,\n+        status=\"running\",\n+        priority=0,\n+        created_by=\"test\",\n+        created_at=1,\n+        started_at=None,\n+        completed_at=None,\n+        workspace_kind=\"dir\",\n+        workspace_path=None,\n+        claim_lock=\"lock\",\n+        claim_expires=None,\n+        tenant=None,\n+        current_run_id=1,\n+    )\n+\n+\n+def _capture_spawn_env(kb, monkeypatch, workspace: str) -> dict:\n+    monkeypatch.setattr(kb, \"_resolve_hermes_argv\", lambda: [\"hermes\"])\n+\n+    captured: dict = {}\n+\n+    class FakeProc:\n+        pid = 4242\n+\n+    def fake_popen(cmd, *args, **kwargs):\n+        captured[\"cmd\"] = list(cmd)\n+        captured[\"env\"] = dict(kwargs.get(\"env\") or {})\n+        captured[\"cwd\"] = kwargs.get(\"cwd\")\n+        return FakeProc()\n+\n+    monkeypatch.setattr(subprocess, \"Popen\", fake_popen)\n+    kb._default_spawn(_make_task(kb), workspace)\n+    return captured\n+\n+\n+def test_terminal_cwd_pinned_to_workspace(monkeypatch, tmp_path):\n+    \"\"\"A real, absolute workspace dir is pinned as TERMINAL_CWD.\"\"\"\n+    root = tmp_path / \".hermes\"\n+    (root / \"profiles\" / \"w\").mkdir(parents=True)\n+    (root / \"profiles\" / \"w\" / \"config.yaml\").write_text(\"toolsets:\\n  - kanban\\n\", encoding=\"utf-8\")\n+    root.joinpath(\"config.yaml\").write_text(\"toolsets:\\n  - kanban\\n\", encoding=\"utf-8\")\n+    monkeypatch.setenv(\"HERMES_HOME\", str(root))\n+\n+    from hermes_cli import kanban_db as kb\n+\n+    workspace = tmp_path / \"ws\"\n+    workspace.mkdir()\n+\n+    captured = _capture_spawn_env(kb, monkeypatch, str(workspace))\n+\n+    assert captured[\"env\"][\"TERMINAL_CWD\"] == str(workspace)\n+    # The subprocess cwd and TERMINAL_CWD must agree — both anchor the workspace.\n+    assert captured[\"cwd\"] == str(workspace)\n+    assert captured[\"env\"][\"HERMES_KANBAN_WORKSPACE\"] == str(workspace)\n+\n+\n+def test_terminal_cwd_not_pinned_for_nonexistent_workspace(monkeypatch, tmp_path):\n+    \"\"\"A non-directory workspace must NOT clobber the inherited TERMINAL_CWD.\n+\n+    file_tools rejects relative / sentinel TERMINAL_CWD values, so writing a\n+    meaningless (nonexistent) path would be worse than leaving the inherited\n+    one. The guard requires an existing absolute dir.\n+    \"\"\"\n+    root = tmp_path / \".hermes\"\n+    (root / \"profiles\" / \"w\").mkdir(parents=True)\n+    (root / \"profiles\" / \"w\" / \"config.yaml\").write_text(\"toolsets:\\n  - kanban\\n\", encoding=\"utf-8\")\n+    root.joinpath(\"config.yaml\").write_text(\"toolsets:\\n  - kanban\\n\", encoding=\"utf-8\")\n+    monkeypatch.setenv(\"HERMES_HOME\", str(root))\n+    monkeypatch.setenv(\"TERMINAL_CWD\", \"/pre/existing/anchor\")\n+\n+    from hermes_cli import kanban_db as kb\n+\n+    missing = tmp_path / \"does-not-exist\"\n+\n+    captured = _capture_spawn_env(kb, monkeypatch, str(missing))\n+\n+    # Inherited value is preserved (not overwritten with a bogus path).\n+    assert captured[\"env\"][\"TERMINAL_CWD\"] == \"/pre/existing/anchor\"\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}