mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
feat(kanban): add board-level default workdir (#25430)
This commit is contained in:
parent
8bfb456948
commit
fe5e0bf5a3
3 changed files with 89 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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…
Add table
Add a link
Reference in a new issue