mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
refactor(kanban): fold worker/orchestrator skills into injected guidance (#50473)
The kanban-worker and kanban-orchestrator bundled skills existed only to be force-loaded into dispatcher-spawned workers, gated by environments:[kanban] so they wouldn't leak into normal CLI listings. That gating was fragile (the leak that #50443 patched) and the --skills auto-load was already best-effort — most workers ran without it because the bundled skill isn't present in profile-scoped skills dirs. Remove the skills entirely and promote their load-bearing content (workspace kinds, deliverable artifacts, created-card integrity, profile discovery) into KANBAN_GUIDANCE, which is already injected into every kanban worker's system prompt. Net result: every worker reliably gets the guidance, nothing can leak into a CLI/blank-slate session, and the gating machinery is gone. - agent/prompt_builder.py: promote the 4 load-bearing rules into KANBAN_GUIDANCE - hermes_cli/kanban_db.py: drop --skills kanban-worker auto-injection + _kanban_worker_skill_available probe - hermes_cli/kanban_swarm.py: drop skills=[kanban-orchestrator] on the root card - hermes_cli/kanban.py: drop kanban-init skill seeding; fix help text - delete skills/devops/kanban-{worker,orchestrator} - docs: delete the two skill pages (EN+zh), fix sidebars/catalog/kanban.md/kanban-worker-lanes.md and the video-orchestrator + codex-lane references - tests: update spawn-argv expectations; re-bound the guidance-size guard Supersedes the skill-leak half of #50443 (credit @helix4u for flagging the area).
This commit is contained in:
parent
e5e2583635
commit
84e1d31e54
32 changed files with 160 additions and 1575 deletions
|
|
@ -26,7 +26,7 @@ from typing import Any, Optional
|
|||
|
||||
from hermes_cli import kanban_db as kb
|
||||
from hermes_cli import kanban_swarm as ks
|
||||
from hermes_cli.profiles import get_active_profile_name, get_profile_dir, seed_profile_skills
|
||||
from hermes_cli.profiles import get_active_profile_name
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -330,8 +330,8 @@ def build_parser(parent_subparsers: argparse._SubParsersAction) -> argparse.Argu
|
|||
help="Author name recorded on the task (default: user)")
|
||||
p_create.add_argument("--skill", action="append", default=[], dest="skills",
|
||||
help="Skill to force-load into the worker "
|
||||
"(repeatable). Appended to the built-in "
|
||||
"kanban-worker skill. Example: "
|
||||
"(repeatable). The kanban lifecycle is already "
|
||||
"injected automatically. Example: "
|
||||
"--skill translation --skill github-code-review")
|
||||
p_create.add_argument("--max-retries", type=int, default=None,
|
||||
metavar="N",
|
||||
|
|
@ -1223,21 +1223,6 @@ def _cmd_init(args: argparse.Namespace) -> int:
|
|||
path = kb.init_db()
|
||||
print(f"Kanban DB initialized at {path}")
|
||||
|
||||
# Seed bundled skills (e.g. kanban-worker) into the active profile so
|
||||
# the kanban dispatcher can use them without a separate `hermes profile
|
||||
# create` step. This is best-effort — a missing or broken profile is
|
||||
# not fatal to `kanban init`.
|
||||
try:
|
||||
profile_name = get_active_profile_name() or "default"
|
||||
profile_dir = get_profile_dir(profile_name)
|
||||
result = seed_profile_skills(profile_dir, quiet=True)
|
||||
if result:
|
||||
copied = result.get("copied", [])
|
||||
if copied:
|
||||
print(f"Seeded skill(s) into profile {profile_name}: {', '.join(copied)}")
|
||||
except Exception:
|
||||
pass # best-effort
|
||||
|
||||
print()
|
||||
# Enumerate profiles on disk so the user knows what assignees are
|
||||
# already addressable. Multica does this auto-detection on its
|
||||
|
|
@ -1461,8 +1446,7 @@ def _cmd_show(args: argparse.Namespace) -> int:
|
|||
parents = kb.parent_ids(conn, args.task_id)
|
||||
children = kb.child_ids(conn, args.task_id)
|
||||
runs = kb.list_runs(conn, args.task_id, **rsk)
|
||||
# Workers hand off via ``task_runs.summary`` (kanban-worker skill);
|
||||
# ``tasks.result`` is left NULL unless the caller explicitly passed
|
||||
# Workers hand off via ``task_runs.summary``; ``tasks.result`` is left NULL unless the caller explicitly passed
|
||||
# ``result=``. Surfacing the latest summary here keeps ``show`` from
|
||||
# looking like a no-op when the worker actually did real work.
|
||||
latest_summary = kb.latest_summary(conn, args.task_id)
|
||||
|
|
|
|||
|
|
@ -804,10 +804,9 @@ class Task:
|
|||
current_run_id: Optional[int] = None
|
||||
workflow_template_id: Optional[str] = None
|
||||
current_step_key: Optional[str] = None
|
||||
# Force-loaded skills for the worker on this task (appended to the
|
||||
# dispatcher's built-in `kanban-worker` via --skills). Stored as a
|
||||
# JSON array of skill names. None = use only the defaults; empty
|
||||
# list = explicitly no extra skills.
|
||||
# Force-loaded skills for the worker on this task (passed via
|
||||
# --skills). Stored as a JSON array of skill names. None = use only
|
||||
# the defaults; empty list = explicitly no extra skills.
|
||||
skills: Optional[list] = None
|
||||
model_override: Optional[str] = None
|
||||
# Per-task override for the consecutive-failure circuit breaker.
|
||||
|
|
@ -1045,8 +1044,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|||
workflow_template_id TEXT,
|
||||
current_step_key TEXT,
|
||||
-- Force-loaded skills for the worker on this task, stored as JSON.
|
||||
-- Appended to the dispatcher's built-in `--skills kanban-worker`.
|
||||
-- NULL or empty array = no extras.
|
||||
-- Passed to the worker via `--skills`. NULL or empty array = no extras.
|
||||
skills TEXT,
|
||||
-- Per-task model override. When set, the dispatcher passes -m <model>
|
||||
-- to the worker, overriding the profile's default model. NULL = use
|
||||
|
|
@ -1848,8 +1846,7 @@ def _migrate_add_optional_columns(conn: sqlite3.Connection) -> None:
|
|||
)
|
||||
if "skills" not in cols:
|
||||
# JSON array of skill names the dispatcher force-loads into the
|
||||
# worker (additive to the built-in `kanban-worker`). NULL is fine
|
||||
# for existing rows.
|
||||
# worker via --skills. NULL is fine for existing rows.
|
||||
_add_column_if_missing(conn, "tasks", "skills", "skills TEXT")
|
||||
|
||||
if "max_retries" not in cols:
|
||||
|
|
@ -2285,9 +2282,8 @@ def create_task(
|
|||
|
||||
``skills`` is an optional list of skill names to force-load into
|
||||
the worker when dispatched. Stored as JSON; the dispatcher passes
|
||||
each name to ``hermes --skills ...`` alongside the built-in
|
||||
``kanban-worker``. Use this to pin a task to a specialist skill
|
||||
(e.g. ``skills=["translation"]`` so the worker loads the
|
||||
each name to ``hermes --skills ...``. Use this to pin a task to a
|
||||
specialist skill (e.g. ``skills=["translation"]`` so the worker loads the
|
||||
translation skill regardless of the profile's default config).
|
||||
"""
|
||||
assignee = _canonical_assignee(assignee)
|
||||
|
|
@ -2348,7 +2344,7 @@ def create_task(
|
|||
f"{quoted} {noun}, not skill name(s). "
|
||||
"Put toolsets in the assignee profile's `toolsets:` config "
|
||||
"instead of per-task skills. Skills are named skill bundles "
|
||||
"(e.g. `kanban-worker`, `blogwatcher`); toolsets are runtime "
|
||||
"(e.g. `blogwatcher`, `github-code-review`); toolsets are runtime "
|
||||
"capabilities (e.g. `web`, `browser`, `terminal`)."
|
||||
)
|
||||
skills_list = cleaned
|
||||
|
|
@ -6994,11 +6990,11 @@ def _dispatch_once_locked(
|
|||
if claimed.workspace_kind == "worktree":
|
||||
set_branch_name(conn, claimed.id, resolved_branch_name or (claimed.branch_name or "").strip() or f"wt/{claimed.id}")
|
||||
_maybe_emit_scratch_tip(conn, claimed.id, claimed.workspace_kind)
|
||||
# Force-load sdlc-review skill for review agents. The
|
||||
# _default_spawn function already auto-loads kanban-worker, and
|
||||
# appends task.skills via --skills. Setting task.skills here
|
||||
# means the review agent gets both kanban-worker (lifecycle)
|
||||
# and sdlc-review (review logic: AC verification, merge, etc.).
|
||||
# Force-load the sdlc-review skill for review agents — it carries
|
||||
# the review logic (AC verification, merge, etc.). The mandatory
|
||||
# kanban lifecycle is already injected into every worker's system
|
||||
# prompt via KANBAN_GUIDANCE, so this is the only extra skill the
|
||||
# review agent needs.
|
||||
claimed.skills = ["sdlc-review"]
|
||||
_spawn = spawn_fn if spawn_fn is not None else _default_spawn
|
||||
try:
|
||||
|
|
@ -7223,41 +7219,6 @@ def _resolve_hermes_argv() -> list[str]:
|
|||
return _module_hermes_argv()
|
||||
|
||||
|
||||
def _kanban_worker_skill_available(hermes_home: Optional[str]) -> bool:
|
||||
"""True if the bundled ``kanban-worker`` skill resolves for the home the
|
||||
spawned worker will run under.
|
||||
|
||||
The dispatcher injects ``--skills kanban-worker`` into every worker. When
|
||||
the worker activates a profile (``hermes -p <name>``), its ``SKILLS_DIR``
|
||||
becomes ``<profile_home>/skills`` — which on many profiles does NOT contain
|
||||
the bundled skill (it ships in the *default* root home, not every
|
||||
profile-scoped skills dir). Preloading a missing skill is fatal at CLI
|
||||
startup (``ValueError: Unknown skill(s): kanban-worker``), aborting the
|
||||
worker before the agent loop runs. Gate the flag on actual resolvability;
|
||||
the kanban lifecycle contract is still injected via ``KANBAN_GUIDANCE``, so
|
||||
omitting the flag only drops the supplementary pattern library.
|
||||
"""
|
||||
from pathlib import Path as _Path
|
||||
|
||||
# An unset HERMES_HOME means the worker falls back to the default root
|
||||
# home (``~/.hermes``), which ships the bundled skill.
|
||||
base = _Path(hermes_home) if hermes_home else (_Path.home() / ".hermes")
|
||||
skills_root = base / "skills"
|
||||
if not skills_root.is_dir():
|
||||
return False
|
||||
# Canonical bundled location first (cheap), then a bounded scan for
|
||||
# profiles that have it nested elsewhere.
|
||||
if (skills_root / "devops" / "kanban-worker" / "SKILL.md").is_file():
|
||||
return True
|
||||
try:
|
||||
for skill_md in skills_root.rglob("kanban-worker/SKILL.md"):
|
||||
if skill_md.is_file():
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def _worker_terminal_timeout_env(
|
||||
max_runtime_seconds: Optional[int],
|
||||
current_timeout: Optional[str],
|
||||
|
|
@ -7440,32 +7401,14 @@ def _default_spawn(
|
|||
# profile-local worker sessions still register configured hooks.
|
||||
"--accept-hooks",
|
||||
]
|
||||
# Auto-load the kanban-worker skill so every dispatched worker
|
||||
# has the pattern library (good summary/metadata shapes, retry
|
||||
# diagnostics, block-reason examples) in its context, even if
|
||||
# the profile hasn't wired it into skills config. The MANDATORY
|
||||
# lifecycle is already in the system prompt via KANBAN_GUIDANCE;
|
||||
# this skill is the deeper reference. Users can point a profile
|
||||
# at a different/additional skill via config if they want —
|
||||
# --skills is additive to the profile's default skill set.
|
||||
#
|
||||
# Only add the flag when the skill actually resolves for the home
|
||||
# the worker runs under: the bundled skill is absent from many
|
||||
# profile-scoped skills dirs, and preloading a missing skill is
|
||||
# fatal at CLI startup. Omitting it is safe — the lifecycle
|
||||
# contract still ships via KANBAN_GUIDANCE.
|
||||
if _kanban_worker_skill_available(env.get("HERMES_HOME")):
|
||||
cmd.extend(["--skills", "kanban-worker"])
|
||||
# Per-task force-loaded skills. Each name goes in its own
|
||||
# `--skills X` pair rather than a single comma-joined arg: the CLI
|
||||
# accepts both forms (action='append' + comma-split), but
|
||||
# per-name pairs are easier to read in `ps` output and avoid any
|
||||
# quoting ambiguity if a skill name ever contains unusual chars.
|
||||
# Dedupe against the built-in so we don't double-load kanban-worker
|
||||
# if a task author asks for it explicitly.
|
||||
if task.skills:
|
||||
for sk in task.skills:
|
||||
if sk and sk != "kanban-worker":
|
||||
if sk:
|
||||
cmd.extend(["--skills", sk])
|
||||
if task.model_override:
|
||||
cmd.extend(["-m", task.model_override])
|
||||
|
|
@ -8322,7 +8265,7 @@ def latest_run(conn: sqlite3.Connection, task_id: str) -> Optional[Run]:
|
|||
def latest_summary(conn: sqlite3.Connection, task_id: str) -> Optional[str]:
|
||||
"""Return the latest non-null ``task_runs.summary`` for ``task_id``.
|
||||
|
||||
The kanban-worker skill writes its handoff to ``task_runs.summary``
|
||||
The worker writes its handoff to ``task_runs.summary``
|
||||
via ``complete_task(summary=...)``; ``tasks.result`` is left empty
|
||||
unless the caller passes ``result=`` explicitly. Dashboards and CLI
|
||||
"show" views need this value to surface what a worker actually did
|
||||
|
|
|
|||
|
|
@ -124,7 +124,6 @@ def create_swarm(
|
|||
idempotency_key=idempotency_key,
|
||||
workspace_kind=workspace_kind,
|
||||
workspace_path=workspace_path,
|
||||
skills=["kanban-orchestrator"],
|
||||
)
|
||||
|
||||
# If idempotency returned an existing non-archived root, do not duplicate the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue