feat(kanban): configure worktree paths and branches

Salvages #26496 by @aqilaziz. Adds branch_name column + CLI flag so
tasks with workspace_kind='worktree' can pin a target branch on
create. Schema migration added to _migrate_add_optional_columns.

- Task.branch_name field + DB column + migration
- create_task accepts branch_name kwarg
- hermes kanban create --branch <name> flag
- kanban show output includes 'Branch: <name>' when set

Cherry-picked the substantive commit (a7558cf27); the PR's tip was
an unrelated service-path-dirs commit. Resolved 2 INSERT-column-list
and show-output conflicts alongside main's session_id and
max_runtime_seconds additions; kept all three.
This commit is contained in:
aqilaziz 2026-05-18 21:33:02 -07:00 committed by Teknium
parent 53cf82a1ea
commit 1733cb3a13
6 changed files with 145 additions and 18 deletions

View file

@ -32,6 +32,7 @@ def kanban_home(tmp_path, monkeypatch):
[
("scratch", ("scratch", None)),
("worktree", ("worktree", None)),
("worktree:/tmp/wt", ("worktree", "/tmp/wt")),
("dir:/tmp/work", ("dir", "/tmp/work")),
],
)
@ -45,8 +46,12 @@ def test_parse_workspace_flag_expands_user():
assert path.endswith("/vault")
assert not path.startswith("~")
kind, path = kc._parse_workspace_flag("worktree:~/trees/t6-wire")
assert kind == "worktree"
assert path.endswith("/trees/t6-wire")
assert not path.startswith("~")
@pytest.mark.parametrize("bad", ["cloud", "dir:", "", "worktree:/x"])
@pytest.mark.parametrize("bad", ["cloud", "dir:", "worktree:", ""])
def test_parse_workspace_flag_rejects(bad):
if not bad:
# Empty -> defaults; not an error.
@ -56,6 +61,17 @@ def test_parse_workspace_flag_rejects(bad):
kc._parse_workspace_flag(bad)
def test_parse_branch_flag_rejects_empty_and_option_like():
assert kc._parse_branch_flag(None) is None
assert kc._parse_branch_flag(" wt/t6-wire ") == "wt/t6-wire"
with pytest.raises(argparse.ArgumentTypeError):
kc._parse_branch_flag(" ")
with pytest.raises(argparse.ArgumentTypeError):
kc._parse_branch_flag("-bad")
with pytest.raises(argparse.ArgumentTypeError):
kc._parse_branch_flag("bad branch")
# ---------------------------------------------------------------------------
# run_slash smoke tests (end-to-end via the same entry both CLI and gateway use)
# ---------------------------------------------------------------------------
@ -74,6 +90,27 @@ def test_run_slash_create_and_list(kanban_home):
assert "alice" in out
def test_run_slash_create_worktree_path_and_branch(kanban_home, tmp_path):
target = tmp_path / ".worktrees" / "t6-wire"
target_arg = target.as_posix()
out = kc.run_slash(
f"create 'ship worktree' --workspace worktree:{target_arg} --branch wt/t6-wire"
)
assert "Created" in out
with kb.connect() as conn:
tasks = kb.list_tasks(conn)
task = tasks[0]
assert task.workspace_kind == "worktree"
assert task.workspace_path == target_arg
assert task.branch_name == "wt/t6-wire"
def test_run_slash_rejects_branch_without_worktree(kanban_home):
out = kc.run_slash("create 'bad branch' --workspace scratch --branch wt/bad")
assert "--branch is only valid with --workspace worktree" in out
def test_run_slash_create_with_parent_and_cascade(kanban_home):
# Parent then child via --parent
out1 = kc.run_slash("create 'parent' --assignee alice")

View file

@ -81,6 +81,35 @@ def test_workspace_kind_validation(kanban_home):
kb.create_task(conn, title="bad ws", workspace_kind="cloud")
def test_create_task_persists_worktree_branch_name(kanban_home, tmp_path):
target = tmp_path / ".worktrees" / "t6-wire"
with kb.connect() as conn:
tid = kb.create_task(
conn,
title="ship worktree",
workspace_kind="worktree",
workspace_path=str(target),
branch_name=" wt/t6-wire ",
)
task = kb.get_task(conn, tid)
events = kb.list_events(conn, tid)
context = kb.build_worker_context(conn, tid)
assert task.branch_name == "wt/t6-wire"
assert events[0].payload["branch_name"] == "wt/t6-wire"
assert "Branch: wt/t6-wire" in context
def test_branch_name_requires_worktree_workspace(kanban_home):
with kb.connect() as conn, pytest.raises(ValueError, match="worktree"):
kb.create_task(
conn,
title="bad branch",
workspace_kind="scratch",
branch_name="wt/bad",
)
# ---------------------------------------------------------------------------
# Links + dependency resolution
# ---------------------------------------------------------------------------
@ -1654,11 +1683,12 @@ class TestSharedBoardPaths:
created_at=0,
started_at=None,
completed_at=None,
workspace_kind="scratch",
workspace_path=None,
workspace_kind="worktree",
workspace_path=str(tmp_path / "ws"),
claim_lock=None,
claim_expires=None,
tenant=None,
branch_name="wt/t_dispatch_env",
)
kb._default_spawn(task, str(tmp_path / "ws"))
@ -1668,6 +1698,7 @@ class TestSharedBoardPaths:
default_home / "kanban" / "workspaces"
)
assert env["HERMES_KANBAN_TASK"] == "t_dispatch_env"
assert env["HERMES_KANBAN_BRANCH"] == "wt/t_dispatch_env"
# ---------------------------------------------------------------------------
@ -1907,6 +1938,7 @@ def test_migrate_add_optional_columns_tolerates_concurrent_migration(kanban_home
tenant TEXT,
result TEXT,
idempotency_key TEXT,
branch_name TEXT,
consecutive_failures INTEGER NOT NULL DEFAULT 0,
worker_pid INTEGER,
last_failure_error TEXT,