From 6f8d5e50ea6343debf22d1fc38d65d007eaeaa30 Mon Sep 17 00:00:00 2001 From: Michael Sitarzewski Date: Sun, 5 Jul 2026 17:25:08 -0500 Subject: [PATCH] fix(hermes): accept `slug` alias + derive divisions from divisions.json (#666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/build-hermes-plugin.py | 70 ++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/scripts/build-hermes-plugin.py b/scripts/build-hermes-plugin.py index 0f001cbc..14e4bc4e 100644 --- a/scripts/build-hermes-plugin.py +++ b/scripts/build-hermes-plugin.py @@ -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)