diff --git a/tests/tools/test_kanban_tools.py b/tests/tools/test_kanban_tools.py index 24fa09d8b..2bf894499 100644 --- a/tests/tools/test_kanban_tools.py +++ b/tests/tools/test_kanban_tools.py @@ -768,6 +768,83 @@ def test_create_happy_path(worker_env): conn.close() +def test_create_inherits_worker_dir_workspace(monkeypatch, worker_env): + """A worker scoped to a dir: task that spawns a child without a + workspace arg inherits the dir, not scratch (so follow-up code-gen + lands in the same project).""" + from tools import kanban_tools as kt + from hermes_cli import kanban_db as kb + + proj = "/home/teknium/myproject" + conn = kb.connect() + try: + self_tid = kb.create_task( + conn, title="dir worker", assignee="test-worker", + workspace_kind="dir", workspace_path=proj, + ) + kb.claim_task(conn, self_tid) + finally: + conn.close() + monkeypatch.setenv("HERMES_KANBAN_TASK", self_tid) + + d = json.loads(kt._handle_create({"title": "follow-up", "assignee": "peer"})) + assert d["ok"] is True + conn = kb.connect() + try: + child = kb.get_task(conn, d["task_id"]) + assert child.workspace_kind == "dir" + assert child.workspace_path == proj + finally: + conn.close() + + +def test_create_explicit_workspace_beats_inheritance(monkeypatch, worker_env): + """An explicit workspace arg overrides worker-task inheritance.""" + from tools import kanban_tools as kt + from hermes_cli import kanban_db as kb + + conn = kb.connect() + try: + self_tid = kb.create_task( + conn, title="dir worker", assignee="test-worker", + workspace_kind="dir", workspace_path="/home/teknium/proj", + ) + kb.claim_task(conn, self_tid) + finally: + conn.close() + monkeypatch.setenv("HERMES_KANBAN_TASK", self_tid) + + d = json.loads(kt._handle_create({ + "title": "scratch child", "assignee": "peer", + "workspace_kind": "scratch", + })) + assert d["ok"] is True + conn = kb.connect() + try: + child = kb.get_task(conn, d["task_id"]) + assert child.workspace_kind == "scratch" + finally: + conn.close() + + +def test_create_no_worker_task_stays_scratch(monkeypatch, worker_env): + """Orchestrator/CLI callers (no HERMES_KANBAN_TASK) still default to + scratch — inheritance only applies to task-scoped workers.""" + from tools import kanban_tools as kt + from hermes_cli import kanban_db as kb + + monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False) + d = json.loads(kt._handle_create({"title": "orch child", "assignee": "peer"})) + assert d["ok"] is True + conn = kb.connect() + try: + child = kb.get_task(conn, d["task_id"]) + assert child.workspace_kind == "scratch" + assert child.workspace_path is None + finally: + conn.close() + + def test_create_stamps_session_id_from_env(monkeypatch, worker_env): """When the agent loop runs under ACP, the server propagates the originating chat session id via HERMES_SESSION_ID. ``kanban_create`` diff --git a/tools/kanban_tools.py b/tools/kanban_tools.py index 3b4ede304..67157dfc1 100644 --- a/tools/kanban_tools.py +++ b/tools/kanban_tools.py @@ -743,8 +743,18 @@ def _handle_create(args: dict, **kw) -> str: # CLI / dashboard paths and on legacy hosts that don't set the env. session_id = args.get("session_id") or os.environ.get("HERMES_SESSION_ID") priority = args.get("priority") - workspace_kind = args.get("workspace_kind") or "scratch" + # Resolve workspace. If the caller passed one explicitly, honor it. + # Otherwise, a dispatcher-spawned worker (HERMES_KANBAN_TASK set) + # inherits its own running task's workspace, so a worker editing a + # dir:/worktree project that spawns a follow-up child keeps the child + # in that project instead of a throwaway scratch dir. Orchestrators + # (kanban toolset, no HERMES_KANBAN_TASK) and CLI/dashboard callers + # fall back to scratch as before. Explicit None path stays None. + workspace_kind = args.get("workspace_kind") workspace_path = args.get("workspace_path") + _inherit_workspace = workspace_kind is None and workspace_path is None + if workspace_kind is None: + workspace_kind = "scratch" triage, bool_error = _parse_bool_arg(args, "triage") if bool_error: return tool_error(bool_error) @@ -773,6 +783,15 @@ def _handle_create(args: dict, **kw) -> str: try: kb, conn = _connect(board=board) try: + # Inherit the spawning worker's own task workspace when the + # caller didn't specify one (see resolution note above). + if _inherit_workspace: + _self_tid = os.environ.get("HERMES_KANBAN_TASK") + if _self_tid: + _self_task = kb.get_task(conn, _self_tid) + if _self_task is not None and _self_task.workspace_kind: + workspace_kind = _self_task.workspace_kind + workspace_path = _self_task.workspace_path new_tid = kb.create_task( conn, title=str(title).strip(),