fix(kanban): kanban_create inherits the spawning worker's task workspace (#37182)
When a dispatcher-spawned worker (HERMES_KANBAN_TASK set) calls kanban_create without an explicit workspace, the new child now inherits the worker's own running-task workspace_kind/workspace_path instead of defaulting to scratch. A worker editing a dir:/worktree project that spawns a follow-up child keeps it in that project. Orchestrators (kanban toolset, no HERMES_KANBAN_TASK) and CLI/dashboard callers still default to scratch. An explicit workspace arg always wins.
This commit is contained in:
@ -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``
|
||||
|
||||
@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user