mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-06 07:51:53 +00:00
feat(kanban): configure worktree paths and branches
This commit is contained in:
parent
7fee1f61eb
commit
a7558cf27b
7 changed files with 163 additions and 22 deletions
|
|
@ -64,6 +64,7 @@ def _task_to_dict(t: kb.Task) -> dict[str, Any]:
|
|||
"tenant": t.tenant,
|
||||
"workspace_kind": t.workspace_kind,
|
||||
"workspace_path": t.workspace_path,
|
||||
"branch_name": t.branch_name,
|
||||
"created_by": t.created_by,
|
||||
"created_at": t.created_at,
|
||||
"started_at": t.started_at,
|
||||
|
|
@ -77,25 +78,42 @@ def _task_to_dict(t: kb.Task) -> dict[str, Any]:
|
|||
def _parse_workspace_flag(value: str) -> tuple[str, Optional[str]]:
|
||||
"""Parse ``--workspace`` into ``(kind, path|None)``.
|
||||
|
||||
Accepts: ``scratch``, ``worktree``, ``dir:<path>``.
|
||||
Accepts: ``scratch``, ``worktree``, ``worktree:<path>``, ``dir:<path>``.
|
||||
"""
|
||||
if not value:
|
||||
return ("scratch", None)
|
||||
v = value.strip()
|
||||
if v in {"scratch", "worktree"}:
|
||||
return (v, None)
|
||||
if v.startswith("dir:"):
|
||||
path = v[len("dir:"):].strip()
|
||||
for prefix, kind in (("dir:", "dir"), ("worktree:", "worktree")):
|
||||
if not v.startswith(prefix):
|
||||
continue
|
||||
path = v[len(prefix):].strip()
|
||||
if not path:
|
||||
raise argparse.ArgumentTypeError(
|
||||
"--workspace dir: requires a path after the colon"
|
||||
f"--workspace {prefix} requires a path after the colon"
|
||||
)
|
||||
return ("dir", os.path.expanduser(path))
|
||||
return (kind, os.path.expanduser(path))
|
||||
raise argparse.ArgumentTypeError(
|
||||
f"unknown --workspace value {value!r}: use scratch, worktree, or dir:<path>"
|
||||
f"unknown --workspace value {value!r}: use scratch, worktree, "
|
||||
"worktree:<path>, or dir:<path>"
|
||||
)
|
||||
|
||||
|
||||
def _parse_branch_flag(value: Optional[str]) -> Optional[str]:
|
||||
"""Normalize an optional branch name from ``kanban create --branch``."""
|
||||
if value is None:
|
||||
return None
|
||||
branch = value.strip()
|
||||
if not branch:
|
||||
raise argparse.ArgumentTypeError("--branch requires a non-empty name")
|
||||
if branch.startswith("-"):
|
||||
raise argparse.ArgumentTypeError("--branch must not start with '-'")
|
||||
if any(ch.isspace() for ch in branch):
|
||||
raise argparse.ArgumentTypeError("--branch must not contain whitespace")
|
||||
return branch
|
||||
|
||||
|
||||
def _check_dispatcher_presence() -> tuple[bool, str]:
|
||||
"""Return ``(running, message)``.
|
||||
|
||||
|
|
@ -265,7 +283,10 @@ def build_parser(parent_subparsers: argparse._SubParsersAction) -> argparse.Argu
|
|||
p_create.add_argument("--parent", action="append", default=[],
|
||||
help="Parent task id (repeatable)")
|
||||
p_create.add_argument("--workspace", default="scratch",
|
||||
help="scratch | worktree | dir:<path> (default: scratch)")
|
||||
help="scratch | worktree | worktree:<path> | dir:<path> "
|
||||
"(default: scratch)")
|
||||
p_create.add_argument("--branch", default=None,
|
||||
help="Branch name for worktree tasks, e.g. wt/t6-wire")
|
||||
p_create.add_argument("--tenant", default=None, help="Tenant namespace")
|
||||
p_create.add_argument("--priority", type=int, default=0, help="Priority tiebreaker")
|
||||
p_create.add_argument("--triage", action="store_true",
|
||||
|
|
@ -1046,7 +1067,15 @@ def _cmd_assignees(args: argparse.Namespace) -> int:
|
|||
|
||||
|
||||
def _cmd_create(args: argparse.Namespace) -> int:
|
||||
ws_kind, ws_path = _parse_workspace_flag(args.workspace)
|
||||
try:
|
||||
ws_kind, ws_path = _parse_workspace_flag(args.workspace)
|
||||
branch_name = _parse_branch_flag(getattr(args, "branch", None))
|
||||
except argparse.ArgumentTypeError as exc:
|
||||
print(f"kanban: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
if branch_name and ws_kind != "worktree":
|
||||
print("kanban: --branch is only valid with --workspace worktree", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
max_runtime = _parse_duration(getattr(args, "max_runtime", None))
|
||||
except ValueError as exc:
|
||||
|
|
@ -1069,6 +1098,7 @@ def _cmd_create(args: argparse.Namespace) -> int:
|
|||
created_by=args.created_by or _profile_author(),
|
||||
workspace_kind=ws_kind,
|
||||
workspace_path=ws_path,
|
||||
branch_name=branch_name,
|
||||
tenant=args.tenant,
|
||||
priority=args.priority,
|
||||
parents=tuple(args.parent or ()),
|
||||
|
|
@ -1202,6 +1232,8 @@ def _cmd_show(args: argparse.Namespace) -> int:
|
|||
print(f" tenant: {task.tenant}")
|
||||
print(f" workspace: {task.workspace_kind}" +
|
||||
(f" @ {task.workspace_path}" if task.workspace_path else ""))
|
||||
if task.branch_name:
|
||||
print(f" branch: {task.branch_name}")
|
||||
if task.skills:
|
||||
print(f" skills: {', '.join(task.skills)}")
|
||||
# Effective retry threshold. Show the per-task override if set,
|
||||
|
|
@ -2218,6 +2250,15 @@ def run_slash(rest: str) -> str:
|
|||
_choice.prog = f"/kanban {_name}"
|
||||
_choice.exit_on_error = False # type: ignore[attr-defined]
|
||||
|
||||
def _usage_for_error() -> str:
|
||||
if tokens:
|
||||
for _action in kanban_parser._actions:
|
||||
if isinstance(_action, argparse._SubParsersAction):
|
||||
subparser = _action.choices.get(tokens[0])
|
||||
if subparser is not None:
|
||||
return subparser.format_usage().rstrip()
|
||||
return kanban_parser.format_usage().rstrip()
|
||||
|
||||
buf_out = io.StringIO()
|
||||
buf_err = io.StringIO()
|
||||
# ``-h`` / ``--help`` makes argparse print to stdout and SystemExit(0).
|
||||
|
|
@ -2235,7 +2276,7 @@ def run_slash(rest: str) -> str:
|
|||
body = err or out
|
||||
return f"⚠ /kanban usage error\n{body}" if body else "⚠ /kanban usage error"
|
||||
except argparse.ArgumentError as exc:
|
||||
return f"⚠ /kanban usage error: {exc}"
|
||||
return f"⚠ /kanban usage error\n{_usage_for_error()}\n{exc}"
|
||||
|
||||
with contextlib.redirect_stdout(buf_out), contextlib.redirect_stderr(buf_err):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -574,6 +574,7 @@ class Task:
|
|||
claim_lock: Optional[str]
|
||||
claim_expires: Optional[int]
|
||||
tenant: Optional[str]
|
||||
branch_name: Optional[str] = None
|
||||
result: Optional[str] = None
|
||||
idempotency_key: Optional[str] = None
|
||||
# Unified non-success counter. Incremented on any of:
|
||||
|
|
@ -632,6 +633,7 @@ class Task:
|
|||
completed_at=row["completed_at"],
|
||||
workspace_kind=row["workspace_kind"],
|
||||
workspace_path=row["workspace_path"],
|
||||
branch_name=row["branch_name"] if "branch_name" in keys else None,
|
||||
claim_lock=row["claim_lock"],
|
||||
claim_expires=row["claim_expires"],
|
||||
tenant=row["tenant"] if "tenant" in keys else None,
|
||||
|
|
@ -764,6 +766,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|||
completed_at INTEGER,
|
||||
workspace_kind TEXT NOT NULL DEFAULT 'scratch',
|
||||
workspace_path TEXT,
|
||||
branch_name TEXT,
|
||||
claim_lock TEXT,
|
||||
claim_expires INTEGER,
|
||||
tenant TEXT,
|
||||
|
|
@ -996,6 +999,8 @@ def _migrate_add_optional_columns(conn: sqlite3.Connection) -> None:
|
|||
_add_column_if_missing(conn, "tasks", "tenant", "tenant TEXT")
|
||||
if "result" not in cols:
|
||||
_add_column_if_missing(conn, "tasks", "result", "result TEXT")
|
||||
if "branch_name" not in cols:
|
||||
_add_column_if_missing(conn, "tasks", "branch_name", "branch_name TEXT")
|
||||
if "idempotency_key" not in cols:
|
||||
_add_column_if_missing(
|
||||
conn, "tasks", "idempotency_key", "idempotency_key TEXT"
|
||||
|
|
@ -1236,6 +1241,7 @@ def create_task(
|
|||
created_by: Optional[str] = None,
|
||||
workspace_kind: str = "scratch",
|
||||
workspace_path: Optional[str] = None,
|
||||
branch_name: Optional[str] = None,
|
||||
tenant: Optional[str] = None,
|
||||
priority: int = 0,
|
||||
parents: Iterable[str] = (),
|
||||
|
|
@ -1277,6 +1283,10 @@ def create_task(
|
|||
f"workspace_kind must be one of {sorted(VALID_WORKSPACE_KINDS)}, "
|
||||
f"got {workspace_kind!r}"
|
||||
)
|
||||
if branch_name is not None:
|
||||
branch_name = str(branch_name).strip() or None
|
||||
if branch_name and workspace_kind != "worktree":
|
||||
raise ValueError("branch_name is only valid for worktree workspaces")
|
||||
parents = tuple(p for p in parents if p)
|
||||
|
||||
# Normalise + validate skills: strip whitespace, drop empties, dedupe
|
||||
|
|
@ -1376,9 +1386,9 @@ def create_task(
|
|||
INSERT INTO tasks (
|
||||
id, title, body, assignee, status, priority,
|
||||
created_by, created_at, workspace_kind, workspace_path,
|
||||
tenant, idempotency_key, max_runtime_seconds, skills,
|
||||
max_retries
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
branch_name, tenant, idempotency_key, max_runtime_seconds,
|
||||
skills, max_retries
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
task_id,
|
||||
|
|
@ -1391,6 +1401,7 @@ def create_task(
|
|||
now,
|
||||
workspace_kind,
|
||||
workspace_path,
|
||||
branch_name,
|
||||
tenant,
|
||||
idempotency_key,
|
||||
int(max_runtime_seconds) if max_runtime_seconds else None,
|
||||
|
|
@ -1412,6 +1423,7 @@ def create_task(
|
|||
"status": initial_status,
|
||||
"parents": list(parents),
|
||||
"tenant": tenant,
|
||||
"branch_name": branch_name,
|
||||
"skills": list(skills_list) if skills_list else None,
|
||||
},
|
||||
)
|
||||
|
|
@ -3953,6 +3965,8 @@ def _default_spawn(
|
|||
env["HERMES_TENANT"] = task.tenant
|
||||
env["HERMES_KANBAN_TASK"] = task.id
|
||||
env["HERMES_KANBAN_WORKSPACE"] = workspace
|
||||
if task.branch_name:
|
||||
env["HERMES_KANBAN_BRANCH"] = task.branch_name
|
||||
if task.current_run_id is not None:
|
||||
env["HERMES_KANBAN_RUN_ID"] = str(task.current_run_id)
|
||||
if task.claim_lock:
|
||||
|
|
@ -4146,6 +4160,8 @@ def build_worker_context(conn: sqlite3.Connection, task_id: str) -> str:
|
|||
if task.tenant:
|
||||
lines.append(f"Tenant: {task.tenant}")
|
||||
lines.append(f"Workspace: {task.workspace_kind} @ {task.workspace_path or '(unresolved)'}")
|
||||
if task.branch_name:
|
||||
lines.append(f"Branch: {task.branch_name}")
|
||||
lines.append("")
|
||||
|
||||
if task.body and task.body.strip():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue