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
17 changes: 17 additions & 0 deletions agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,23 @@ def _strip_yaml_frontmatter(content: str) -> str:
"of the decomposition. Do NOT execute the work yourself; your job is "
"routing, not implementation.\n"
"\n"
"## Reference details that change outcomes\n"
"\n"
"- **Workspace.** `cd $HERMES_KANBAN_WORKSPACE` first. For a `worktree` kind "
"with no `.git`, `git worktree add <path> "
"${HERMES_KANBAN_BRANCH:-wt/$HERMES_KANBAN_TASK}` from the main repo, then "
"cd there.\n"
"- **Deliverables.** Files a human wants go in "
"`kanban_complete(artifacts=[<absolute paths>])` (top-level param; paths in "
"`metadata` are NOT uploaded). Files must exist at completion.\n"
"- **Created cards.** List ids in `kanban_complete(created_cards=[...])` "
"ONLY when captured from a successful `kanban_create` return — never invent "
"or paste ids; the kernel rejects the completion on any phantom id.\n"
"- **Orchestrating: discover profiles first.** The dispatcher SILENTLY "
"drops a card with an unknown assignee (it sits in `ready` forever). Ground "
"every assignee in a real profile (`hermes profile list`, or ask the user), "
"and express dependencies via `parents=[...]` on `kanban_create`, not prose.\n"
"\n"
"## Do NOT\n"
"\n"
"- Do not shell out to `hermes kanban <verb>` for board operations. Use "
Expand Down
6 changes: 3 additions & 3 deletions agent/skill_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,9 @@ def skill_matches_environment(frontmatter: Dict[str, Any]) -> bool:
This is an OFFER-time filter: it controls whether a skill shows up in the
skills index / autocomplete / slash-command list. It is intentionally NOT
enforced by ``skill_view`` or ``--skills`` preloading — an explicit load is
explicit consent, and load-bearing force-loads (e.g. the kanban dispatcher
injecting ``--skills kanban-worker``) must always succeed regardless of how
the offer surfaces filter the skill.
explicit consent, and load-bearing force-loads (e.g. a dispatcher pinning
a task to a specialist skill via ``--skills``) must always succeed
regardless of how the offer surfaces filter the skill.

A skill matches when ANY of its declared environments is currently active
(OR semantics, mirroring ``platforms``). Unknown env tags fail open.
Expand Down
24 changes: 4 additions & 20 deletions hermes_cli/kanban.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

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


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
87 changes: 15 additions & 72 deletions hermes_cli/kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -1045,8 +1044,7 @@ class Event:
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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion hermes_cli/kanban_swarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions optional-skills/creative/kanban-video-orchestrator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ platforms: [linux, macos, windows]
metadata:
hermes:
tags: [video, kanban, multi-agent, orchestration, production-pipeline]
related_skills: [kanban-orchestrator, kanban-worker, ascii-video, manim-video, p5js, comfyui, touchdesigner-mcp, blender-mcp, pixel-art, ascii-art, songwriting-and-ai-music, heartmula, songsee, spotify, youtube-content, claude-design, excalidraw, architecture-diagram, concept-diagrams, baoyu-comic, baoyu-infographic, humanizer, gif-search, meme-generation]
related_skills: [ascii-video, manim-video, p5js, comfyui, touchdesigner-mcp, blender-mcp, pixel-art, ascii-art, songwriting-and-ai-music, heartmula, songsee, spotify, youtube-content, claude-design, excalidraw, architecture-diagram, concept-diagrams, baoyu-comic, baoyu-infographic, humanizer, gif-search, meme-generation]
credits: |
The single-project workspace layout, profile-config patching pattern,
SOUL.md-per-profile model, TEAM.md task-graph convention, and
Expand Down Expand Up @@ -174,8 +174,9 @@ task graphs. See **[references/examples.md](references/examples.md)**.
6. **The director never executes.** Even with the full `kanban + terminal +
file` toolset, the director's `SOUL.md` rules forbid it from executing
work itself. It decomposes and routes only — every concrete task becomes
a `hermes kanban create` call to a specialist profile. The
`kanban-orchestrator` skill spells this out further.
a `hermes kanban create` call to a specialist profile. The kanban
orchestration guidance auto-injected into every kanban worker's system
prompt spells this out further.

7. **Don't over-decompose.** A 30-second product video does NOT need 20 tasks.
Aim for the smallest task graph that still parallelizes well and exposes the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ echo "═══ Configuring profiles ═══"
configure_profile() {
local profile="$1"
local toolsets_json="$2" # JSON array string, e.g. '["kanban","terminal","file"]'
local skills_json="$3" # JSON array string, e.g. '["kanban-worker","ascii-video"]'
local skills_json="$3" # JSON array string, e.g. '["ascii-video"]'
python3 - "$profile" "$toolsets_json" "$skills_json" "$WORKSPACE" <<'PY'
"""Patch a Hermes profile config.yaml using PyYAML so we don't depend on the
exact default-config string format. Validates the patch took effect and exits
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ T8 reviewer final QA (parent: T7)
**Key choices:**
- Local ComfyUI via `comfyui` skill is preferred over external API for
cost/control — but external APIs are fine if ComfyUI isn't installed
- `editor` profile is ffmpeg-only, no Hermes skill required beyond
`kanban-worker`
- `editor` profile is ffmpeg-only, no Hermes skill required (kanban guidance
is auto-injected into every kanban worker)
- Storyboarder produces `storyboard.excalidraw` alongside the markdown

## Example 2 — Product / marketing teaser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ default-config schema drift:
configure_profile() {
local profile="$1"
local toolsets_json="$2" # JSON array, e.g. '["kanban","terminal","file"]'
local skills_json="$3" # JSON array, e.g. '["kanban-worker","ascii-video"]'
local skills_json="$3" # JSON array, e.g. '["ascii-video"]'
python3 - "$profile" "$toolsets_json" "$skills_json" <<'PY'
import json, os, sys, yaml
profile, ts_json, sk_json = sys.argv[1:4]
Expand Down Expand Up @@ -133,16 +133,16 @@ the entire production. **Critical content for the director's SOUL.md:**

- **Anti-temptation rules:** "Do not execute the work yourself. For every
concrete task, create a kanban task and assign it. Decompose, route, comment,
approve — that's the whole job." (The `kanban-orchestrator` skill provides
the deeper playbook; load it.)
approve — that's the whole job." (The kanban orchestration guidance is
auto-injected into every kanban worker's system prompt — no skill to load.)
- **Decomposition steps:** Read `brief.md`, `TEAM.md`, `taste/`. Use the team
graph in `TEAM.md` to fan out tasks.
- **The workspace_path rule** (see below).

Other profiles' SOUL.md is briefer; mostly mechanical: who you are, what you
read, what you produce, what skills/tools to use, where to write outputs.
Most non-director profiles should `always_load: kanban-worker` for the
deeper-than-baseline kanban guidance.
The kanban lifecycle guidance is auto-injected into every kanban worker's
system prompt, so no profile needs to load a kanban skill.

### Initial kanban task

Expand Down
Loading
Loading