fix(hermes): accept slug alias + derive divisions from divisions.json (#666)

Two correctness fixes to the Hermes plugin generator:

1. slug alias (#665): agency_agents_search returns results keyed by `slug`,
   but load/inspect/delegate only accepted a param named `agent`, so the
   natural chain search -> load(slug=...) failed with "agent not found".
   Add `slug` as an optional alias across the READ/PROMPT/DELEGATE schemas
   and resolve either key in the handlers (via _identifier), with a clear
   "agent or slug is required" error when neither is passed. Backward
   compatible; `required` relaxed to [] (task-only for delegate).

2. division drift: AGENT_DIRS was a hardcoded copy of the division list that
   the bash check-divisions.sh guard can't see (it's a Python list), so it
   silently dropped healthcare (#655) — the two healthcare agents were
   missing from the Hermes roster. Derive the division dirs from
   divisions.json instead (mirrors the #659 fix to check-agent-originality.sh),
   so the roster stays in sync with the catalog by construction.

Verified on the regenerated plugin: roster is 235 agents (healthcare now
indexed); search "clinical evidence healthcare" -> inspect(slug=...) resolves
to Clinical Evidence Agent — exercising both fixes together. agent= still works.

Fixes #665


Claude-Session: https://claude.ai/code/session_01WKnDRWM4izsB8WAXKszhsq

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Michael Sitarzewski
2026-07-05 17:25:08 -05:00
committed by GitHub
parent 217a63b8b6
commit 6f8d5e50ea
+41 -29
View File
@@ -15,28 +15,19 @@ import shutil
import textwrap
from pathlib import Path
AGENT_DIRS = [
"academic",
"design",
"engineering",
"finance",
"game-development",
"gis",
"marketing",
"paid-media",
"product",
"project-management",
"sales",
"security",
"spatial-computing",
"specialized",
"support",
"testing",
]
PLUGIN_NAME = "agency-agents-router"
def division_dirs(repo_root: Path) -> list[str]:
# divisions.json (repo root) is the single source of truth for the division
# set. Read it rather than hardcoding a copy here: a hardcoded list silently
# drops new divisions from the Hermes roster (e.g. healthcare) the moment the
# catalog grows. check-divisions.sh guards divisions.json against the tracked
# dirs, so deriving from it keeps this plugin in sync by construction.
data = json.loads((repo_root / "divisions.json").read_text(encoding="utf-8"))
return sorted(data["divisions"].keys())
def slugify(value: str) -> str:
value = value.lower()
value = re.sub(r"[^a-z0-9]+", "-", value)
@@ -78,7 +69,7 @@ def parse_agent(path: Path, repo_root: Path) -> dict[str, str] | None:
def collect_agents(repo_root: Path) -> list[dict[str, str]]:
agents: list[dict[str, str]] = []
for dirname in AGENT_DIRS:
for dirname in division_dirs(repo_root):
base = repo_root / dirname
if not base.is_dir():
continue
@@ -153,6 +144,21 @@ def _agent_lookup(identifier: str) -> dict[str, Any] | None:
return None
def _identifier(args: dict[str, Any]) -> str:
# Accept either "agent" or "slug": agency_agents_search returns results keyed
# by "slug", so callers naturally chain search -> load/inspect/delegate with
# slug=. Both name the same thing (a slug or exact display name).
return str(args.get("agent") or args.get("slug") or "").strip()
def _not_found(identifier: str) -> dict[str, Any]:
return {
"success": False,
"error": "agent not found" if identifier else "agent or slug is required",
"agent": identifier or None,
}
def _score(agent: dict[str, Any], query_tokens: set[str], query_text: str) -> float:
haystack_fields = [
agent.get("name", ""),
@@ -236,9 +242,10 @@ READ_SCHEMA = {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"include_body": {"type": "boolean", "description": "Include full specialist instructions."},
},
"required": ["agent"],
"required": [],
}
PROMPT_DESCRIPTION = (
@@ -249,9 +256,10 @@ PROMPT_SCHEMA = {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"task": {"type": "string", "description": "The user's task to pair with the specialist context."},
},
"required": ["agent"],
"required": [],
}
DELEGATE_DESCRIPTION = (
@@ -263,6 +271,7 @@ DELEGATE_SCHEMA = {
"type": "object",
"properties": {
"agent": {"type": "string", "description": "Agent slug or exact display name."},
"slug": {"type": "string", "description": "Alias for agent. Pass the slug from agency_agents_search results."},
"task": {"type": "string", "description": "Concrete task for the specialist."},
"toolsets": {
"type": "array",
@@ -270,7 +279,7 @@ DELEGATE_SCHEMA = {
"description": "Optional Hermes toolsets for the delegated worker, e.g. ['terminal','file'].",
},
},
"required": ["agent", "task"],
"required": ["task"],
}
@@ -304,9 +313,10 @@ def register(ctx):
def read(args: dict[str, Any], **kwargs) -> str:
del kwargs
agent = _agent_lookup(str(args.get("agent", "")))
identifier = _identifier(args)
agent = _agent_lookup(identifier)
if not agent:
return _json({"success": False, "error": "agent not found", "agent": args.get("agent")})
return _json(_not_found(identifier))
payload = {"success": True, "agent": _summary(agent)}
if bool(args.get("include_body", False)):
payload["body"] = agent.get("body", "")
@@ -314,9 +324,10 @@ def register(ctx):
def prompt(args: dict[str, Any], **kwargs) -> str:
del kwargs
agent = _agent_lookup(str(args.get("agent", "")))
identifier = _identifier(args)
agent = _agent_lookup(identifier)
if not agent:
return _json({"success": False, "error": "agent not found", "agent": args.get("agent")})
return _json(_not_found(identifier))
return _json({
"success": True,
"agent": _summary(agent),
@@ -325,10 +336,11 @@ def register(ctx):
def delegate(args: dict[str, Any], **kwargs) -> str:
del kwargs
agent = _agent_lookup(str(args.get("agent", "")))
identifier = _identifier(args)
agent = _agent_lookup(identifier)
task = str(args.get("task", "")).strip()
if not agent:
return _json({"success": False, "error": "agent not found", "agent": args.get("agent")})
return _json(_not_found(identifier))
if not task:
return _json({"success": False, "error": "task is required"})
composed = _specialist_prompt(agent, task)