mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-21 10:22:18 +00:00
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:
parent
d79f67fda6
commit
15cfc2836f
3 changed files with 86 additions and 10 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue