feat(kanban): add board-level default workdir (#25430)

This commit is contained in:
zccyman 2026-05-18 20:23:58 -07:00 committed by Teknium
parent 8bfb456948
commit fe5e0bf5a3
3 changed files with 89 additions and 0 deletions

View file

@ -230,6 +230,8 @@ def build_parser(parent_subparsers: argparse._SubParsersAction) -> argparse.Argu
help="Optional hex color (e.g. '#8b5cf6') for the dashboard")
b_create.add_argument("--switch", action="store_true",
help="Switch to the new board after creating it")
b_create.add_argument("--default-workdir", default=None,
help="Default workspace path for tasks created on this board")
b_rm = boards_sub.add_parser(
"rm", aliases=["remove", "delete"],
@ -258,6 +260,14 @@ def build_parser(parent_subparsers: argparse._SubParsersAction) -> argparse.Argu
b_rename.add_argument("slug")
b_rename.add_argument("name", help="New display name")
b_set_wd = boards_sub.add_parser(
"set-default-workdir",
help="Set the default workspace path for tasks on a board",
)
b_set_wd.add_argument("slug")
b_set_wd.add_argument("path", nargs="?", default=None,
help="Absolute path to use as default workdir. Omit to clear.")
# --- create ---
p_create = sub.add_parser("create", help="Create a new task")
p_create.add_argument("title", help="Task title")
@ -846,6 +856,8 @@ def _dispatch_boards(args: argparse.Namespace) -> int:
return _cmd_boards_show(args)
if sub == "rename":
return _cmd_boards_rename(args)
if sub == "set-default-workdir":
return _cmd_boards_set_default_workdir(args)
print(f"kanban boards: unknown action {sub!r}", file=sys.stderr)
return 2
@ -916,6 +928,7 @@ def _cmd_boards_create(args: argparse.Namespace) -> int:
description=args.description,
icon=args.icon,
color=args.color,
default_workdir=args.default_workdir,
)
verb = "already exists" if already else "created"
print(f"Board {meta['slug']!r} {verb}.")
@ -996,6 +1009,25 @@ def _cmd_boards_rename(args: argparse.Namespace) -> int:
return 0
def _cmd_boards_set_default_workdir(args: argparse.Namespace) -> int:
try:
normed = kb._normalize_board_slug(args.slug)
except ValueError as exc:
print(f"kanban boards set-default-workdir: {exc}", file=sys.stderr)
return 2
if not normed or not kb.board_exists(normed):
print(f"kanban boards set-default-workdir: board {args.slug!r} does not exist",
file=sys.stderr)
return 1
meta = kb.write_board_metadata(normed, default_workdir=args.path)
new_val = meta.get("default_workdir")
if new_val:
print(f"Board {normed!r} default workdir set to {new_val!r}.")
else:
print(f"Board {normed!r} default workdir cleared.")
return 0
# ---------------------------------------------------------------------------

View file

@ -404,6 +404,7 @@ def read_board_metadata(board: Optional[str] = None) -> dict:
"description": "",
"icon": "",
"color": "",
"default_workdir": None,
"created_at": None,
"archived": False,
}
@ -430,6 +431,7 @@ def write_board_metadata(
icon: Optional[str] = None,
color: Optional[str] = None,
archived: Optional[bool] = None,
default_workdir: Optional[str] = None,
) -> dict:
"""Create / update ``board.json`` for ``board``.
@ -451,6 +453,8 @@ def write_board_metadata(
meta["color"] = str(color)
if archived is not None:
meta["archived"] = bool(archived)
if default_workdir is not None:
meta["default_workdir"] = str(default_workdir) if default_workdir else None
if not meta.get("created_at"):
meta["created_at"] = int(time.time())
path = board_metadata_path(slug)
@ -470,6 +474,7 @@ def create_board(
description: Optional[str] = None,
icon: Optional[str] = None,
color: Optional[str] = None,
default_workdir: Optional[str] = None,
) -> dict:
"""Create a new board directory + DB + metadata. Idempotent.
@ -486,6 +491,7 @@ def create_board(
description=description,
icon=icon,
color=color,
default_workdir=default_workdir,
)
# Touch the DB so list_boards() sees it immediately.
init_db(board=normed)
@ -1294,6 +1300,7 @@ def create_task(
max_runtime_seconds: Optional[int] = None,
skills: Optional[Iterable[str]] = None,
max_retries: Optional[int] = None,
board: Optional[str] = None,
) -> str:
"""Create a new task and optionally link it under parent tasks.
@ -1391,6 +1398,15 @@ def create_task(
now = int(time.time())
# Resolve workspace_path from board-level default_workdir when the
# caller did not specify one explicitly.
if workspace_path is None:
board_slug = board if board else get_current_board()
board_meta = read_board_metadata(board_slug)
board_default = board_meta.get("default_workdir")
if board_default:
workspace_path = str(board_default)
# Retry once on the extremely unlikely id collision.
for attempt in range(2):
task_id = _new_task_id()

View file

@ -1744,3 +1744,44 @@ def test_task_dict_survives_corrupt_created_at(tmp_path, monkeypatch):
conn.close()
age = kb.task_age(task)
assert age["created_age_seconds"] is None
# ---------------------------------------------------------------------------
# Board-level default_workdir
# ---------------------------------------------------------------------------
def test_create_task_without_workspace_inherits_board_default_workdir(kanban_home, monkeypatch):
"""Board with default_workdir → create_task without workspace_path → inherits default."""
default_wd = "/home/user/project"
kb.create_board("work-proj", default_workdir=default_wd)
with kb.connect(board="work-proj") as conn:
tid = kb.create_task(conn, title="inherited", board="work-proj")
t = kb.get_task(conn, tid)
assert t is not None
assert t.workspace_path == default_wd
def test_create_task_without_workspace_no_default_stays_none(kanban_home):
"""Board without default_workdir → create_task without workspace_path → stays None."""
kb.create_board("empty-board")
with kb.connect(board="empty-board") as conn:
tid = kb.create_task(conn, title="none", board="empty-board")
t = kb.get_task(conn, tid)
assert t is not None
assert t.workspace_path is None
def test_create_task_with_explicit_workspace_ignores_board_default(kanban_home):
"""create_task with explicit workspace_path → ignores board default."""
kb.create_board("custom-ws-board", default_workdir="/board/default")
explicit = "/my/explicit/path"
with kb.connect(board="custom-ws-board") as conn:
tid = kb.create_task(conn, title="explicit", workspace_path=explicit, board="custom-ws-board")
t = kb.get_task(conn, tid)
assert t is not None
assert t.workspace_path == explicit
assert t.workspace_path != "/board/default"