Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions hermes_cli/kanban.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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}.")
Expand Down Expand Up @@ -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


# ---------------------------------------------------------------------------


Expand Down
16 changes: 16 additions & 0 deletions hermes_cli/kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ def read_board_metadata(board: Optional[str] = None) -> dict:
"description": "",
"icon": "",
"color": "",
"default_workdir": None,
"created_at": None,
"archived": False,
}
Expand All @@ -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``.

Expand All @@ -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)
Expand All @@ -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.

Expand All @@ -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)
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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()
Expand Down
41 changes: 41 additions & 0 deletions tests/hermes_cli/test_kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading