fix(kanban): anchor no-path worktree tasks on board default_workdir

Follow-up to the salvaged worktree-materialization fix. When a worktree
task has no explicit workspace_path, resolve the anchor from the board's
default_workdir (a git repo) and materialize <repo>/.worktrees/<id> per
task, instead of silently rooting under the dispatcher's CWD (whatever
directory launched the gateway, e.g. the Hermes checkout). If no
default_workdir is configured, raise with a clear message rather than
guessing from CWD.

Adds AUTHOR_MAP entry for the salvaged commit.
This commit is contained in:
teknium1 2026-06-20 15:13:37 -07:00 committed by Teknium
parent d79f67fda6
commit 15cfc2836f
3 changed files with 86 additions and 10 deletions

View file

@ -4850,15 +4850,44 @@ def _ensure_git_worktree(repo_root: Path, target: Path, branch_name: str) -> Non
)
def _resolve_worktree_workspace(task: Task) -> tuple[Path, str]:
"""Resolve + materialize a linked git worktree for ``task``."""
def _resolve_worktree_workspace(
task: Task, *, board: Optional[str] = None
) -> tuple[Path, str]:
"""Resolve + materialize a linked git worktree for ``task``.
When ``task.workspace_path`` is unset, the anchor is the board's
``default_workdir`` (a persistent project checkout). This keeps every
worktree task under a meaningful, board-owned repo ``<repo>/.worktrees/
<task-id>`` instead of silently landing under the dispatcher's current
working directory (which is whatever directory the gateway happened to be
launched from, e.g. the Hermes checkout). If no anchor is configured
anywhere, we fail loudly rather than guess.
"""
branch_name = (task.branch_name or "").strip() or f"wt/{task.id}"
if not task.workspace_path:
repo_root = _git_toplevel(Path.cwd())
if repo_root is None:
# Anchor on the board's configured default_workdir, not Path.cwd().
# The dispatcher's CWD is incidental (gateway launch dir) and using it
# scatters worktrees under whatever repo the gateway started in.
board_slug = board if board else get_current_board()
board_default = (read_board_metadata(board_slug).get("default_workdir") or "").strip()
if not board_default:
raise ValueError(
f"task {task.id} has workspace_kind=worktree but no workspace_path, "
"and the dispatcher's current working directory is not inside a git repo"
f"and board {board_slug!r} has no default_workdir set. Set a board "
"default workdir (a git repo) or create the task with "
"--workspace worktree:<absolute-repo-path>."
)
anchor = Path(board_default).expanduser()
if not anchor.is_absolute():
raise ValueError(
f"board {board_slug!r} default_workdir {board_default!r} is not "
"absolute; use an absolute path to a git repo"
)
repo_root = _git_toplevel(anchor)
if repo_root is None:
raise ValueError(
f"task {task.id} has workspace_kind=worktree but board "
f"{board_slug!r} default_workdir {board_default!r} is not inside a git repo"
)
target = repo_root / ".worktrees" / task.id
_ensure_git_worktree(repo_root, target, branch_name)
@ -4908,8 +4937,12 @@ def resolve_workspace(task: Task, *, board: Optional[str] = None) -> Path:
- ``worktree``: a real linked git worktree. If ``workspace_path`` names
a repo root, Hermes treats it as an anchor and materializes a linked
worktree at ``<repo>/.worktrees/<task-id>``. If ``workspace_path`` names
a concrete target path, Hermes creates/reuses that linked worktree. When
``branch_name`` is empty, Hermes uses ``wt/<task-id>``.
a concrete target path, Hermes creates/reuses that linked worktree. With
no ``workspace_path``, Hermes anchors on the board's ``default_workdir``
and materializes ``<repo>/.worktrees/<task-id>`` per task; if no
``default_workdir`` is configured it raises rather than guessing from the
dispatcher's CWD. When ``branch_name`` is empty, Hermes uses
``wt/<task-id>``.
Persist the resolved path back to the task row via ``set_workspace_path``
so subsequent runs reuse the same directory.
@ -4945,7 +4978,7 @@ def resolve_workspace(task: Task, *, board: Optional[str] = None) -> Path:
p.mkdir(parents=True, exist_ok=True)
return p
if kind == "worktree":
p, _branch_name = _resolve_worktree_workspace(task)
p, _branch_name = _resolve_worktree_workspace(task, board=board)
return p
raise ValueError(f"unknown workspace_kind: {kind}")
@ -6569,7 +6602,7 @@ def dispatch_once(
try:
resolved_branch_name = None
if claimed.workspace_kind == "worktree":
workspace, resolved_branch_name = _resolve_worktree_workspace(claimed)
workspace, resolved_branch_name = _resolve_worktree_workspace(claimed, board=board)
else:
workspace = resolve_workspace(claimed, board=board)
except Exception as exc:
@ -6661,7 +6694,7 @@ def dispatch_once(
try:
resolved_branch_name = None
if claimed.workspace_kind == "worktree":
workspace, resolved_branch_name = _resolve_worktree_workspace(claimed)
workspace, resolved_branch_name = _resolve_worktree_workspace(claimed, board=board)
else:
workspace = resolve_workspace(claimed, board=board)
except Exception as exc:

View file

@ -47,6 +47,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
AUTHOR_MAP = {
"w31rdm4ch1n3z@protonmail.com": "w31rdm4ch1nZ",
"xtpeeps@gmail.com": "x7peeps",
"ahmad@madsgency.com": "ahmadashfq",
"rratmansky@gmail.com": "rratmansky",
"lkz-de@users.noreply.github.com": "lkz-de",
"charles@salesondemand.io": "salesondemandio",

View file

@ -2132,6 +2132,48 @@ def test_worktree_workspace_repo_root_anchor_materializes_linked_worktree(kanban
assert f"branch refs/heads/wt/{t}" in listed
def test_worktree_no_path_anchors_on_board_default_workdir(kanban_home, tmp_path):
"""A worktree task created with no explicit path inherits the board's
default_workdir as its anchor and materializes a per-task linked worktree
at ``<repo>/.worktrees/<id>`` NOT the dispatcher's CWD, and NOT the
shared default_workdir verbatim (which would collapse every task into one
directory)."""
repo = tmp_path / "repo"
_init_git_repo(repo)
kb.create_board("wt-default-board", default_workdir=str(repo))
with kb.connect(board="wt-default-board") as conn:
t = kb.create_task(
conn, title="ship", workspace_kind="worktree", board="wt-default-board"
)
task = kb.get_task(conn, t)
assert task is not None
ws = kb.resolve_workspace(task, board="wt-default-board")
expected = repo / ".worktrees" / t
assert ws == expected
assert ws.exists()
assert ws != repo # not the shared default verbatim
def test_worktree_no_path_no_board_default_raises(kanban_home, tmp_path, monkeypatch):
"""With neither an explicit workspace_path nor a board default_workdir,
resolution fails loudly pointing at default_workdir / worktree:<path>
rather than silently materializing under the dispatcher's CWD (the old
behavior that scattered worktrees under whatever dir launched the
gateway)."""
# Park the dispatcher CWD inside a real git repo so the OLD cwd-anchored
# code would have "succeeded" — proving the new code does NOT use cwd.
decoy_repo = tmp_path / "decoy"
_init_git_repo(decoy_repo)
monkeypatch.chdir(decoy_repo)
with kb.connect() as conn:
t = kb.create_task(conn, title="ship", workspace_kind="worktree")
task = kb.get_task(conn, t)
assert task is not None
with pytest.raises(ValueError, match="default_workdir"):
kb.resolve_workspace(task)
def test_worktree_workspace_explicit_target_materializes_linked_worktree(kanban_home, tmp_path):
repo = tmp_path / "repo"
_init_git_repo(repo)