mirror of
https://github.com/obra/superpowers.git
synced 2026-07-24 11:14:03 +08:00
Empirical findings from the quorum eval bring-up (superpowers-evals
docs/experiments/2026-07-23-hermes-target-bringup.md):
- ctx.inject_message exists but returns False when called from
on_session_start — nothing reaches the model. The documented path,
a pre_llm_call hook returning {"context": ...} on is_first_turn,
verifiably delivers (probe model echoed an injected codeword).
- ctx.register_skill requires a pathlib.Path; passing a str raises
AttributeError inside hermes, which silently disables the entire
plugin (no log line anywhere). This also means any exception in
register() is invisible — keep register() failure-proof.
- Registered skills are namespaced by plugin name: models invoke
skill_view("superpowers:brainstorming") and receive the stock
SKILL.md — verified live on GLM 5.2, both install layouts.
The plugin now: resolves skills/ for both the git-clone layout
(.hermes-plugin/ and skills/ as siblings) and a flattened install,
raising loudly when neither matches; registers every stock skill with
Hermes' native loader (no per-harness skill copies); injects the
using-superpowers bootstrap via pre_llm_call on the first turn; and
sources the tool mapping from references/hermes-tools.md instead of
duplicating it. Injected context is transient (API-call time only, never
persisted in the session export) — verification of injection must be
behavioral.
105 lines
3.8 KiB
Python
105 lines
3.8 KiB
Python
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for hermes"
|
|
|
|
|
|
def _skills_dir() -> str:
|
|
"""Locate the stock skills/ tree for either supported install layout.
|
|
|
|
- git-clone install (`hermes plugins install obra/superpowers`): the plugin
|
|
dir is the repo root, so `.hermes-plugin/` and `skills/` are siblings and
|
|
this module resolves `../skills`.
|
|
- flattened install (plugin files copied to the plugin dir root): `skills/`
|
|
sits next to this module.
|
|
|
|
Raises loudly when neither matches — a bootstrap that silently skips is how
|
|
a broken install masquerades as a working one.
|
|
"""
|
|
here = os.path.dirname(os.path.realpath(__file__))
|
|
candidates = (
|
|
os.path.realpath(os.path.join(here, "..", "skills")),
|
|
os.path.realpath(os.path.join(here, "skills")),
|
|
)
|
|
for cand in candidates:
|
|
if os.path.isfile(os.path.join(cand, "using-superpowers", "SKILL.md")):
|
|
return cand
|
|
raise RuntimeError(
|
|
"superpowers plugin: cannot find the skills/ tree "
|
|
f"(looked at {candidates}). Reinstall with "
|
|
"`hermes plugins install obra/superpowers`."
|
|
)
|
|
|
|
|
|
def _strip_frontmatter(content: str) -> str:
|
|
match = re.match(r"^---\n[\s\S]*?\n---\n([\s\S]*)$", content)
|
|
return (match.group(1) if match else content).strip()
|
|
|
|
|
|
def _build_bootstrap(skills_dir: str) -> str:
|
|
with open(
|
|
os.path.join(skills_dir, "using-superpowers", "SKILL.md"),
|
|
encoding="utf-8",
|
|
) as f:
|
|
body = _strip_frontmatter(f.read())
|
|
|
|
tools_path = os.path.join(
|
|
skills_dir, "using-superpowers", "references", "hermes-tools.md"
|
|
)
|
|
with open(tools_path, encoding="utf-8") as f:
|
|
tool_mapping = f.read().strip()
|
|
|
|
return (
|
|
f"<EXTREMELY_IMPORTANT>\n"
|
|
f"{BOOTSTRAP_MARKER}\n\n"
|
|
f"You have superpowers.\n\n"
|
|
f"The using-superpowers skill content is included below and is already "
|
|
f"loaded for this Hermes session. Follow it now. "
|
|
f"Do not try to load using-superpowers again.\n\n"
|
|
f"{body}\n\n"
|
|
f"## Loading Superpowers Skills on Hermes\n\n"
|
|
f"Superpowers skills are registered with Hermes' native skill loader: "
|
|
f'invoke one with `skill_view("superpowers:skill-name")` '
|
|
f'(for example `skill_view("superpowers:brainstorming")`). '
|
|
f"If a namespaced lookup returns 'not found', read the skill file "
|
|
f"directly instead:\n"
|
|
f'`read_file("{skills_dir}/skill-name/SKILL.md")`\n\n'
|
|
f"The superpowers skills directory is: `{skills_dir}`\n\n"
|
|
f"{tool_mapping}\n"
|
|
f"</EXTREMELY_IMPORTANT>"
|
|
)
|
|
|
|
|
|
def register(ctx):
|
|
skills_dir = _skills_dir()
|
|
bootstrap = _build_bootstrap(skills_dir)
|
|
|
|
# Register every stock skill with Hermes' native loader so skill_view can
|
|
# load them on demand. Standard markdown; no conversion (plugin guide).
|
|
# register_skill requires a pathlib.Path — a str raises AttributeError and
|
|
# hermes silently disables the whole plugin (verified 2026-07-23).
|
|
for name in sorted(os.listdir(skills_dir)):
|
|
skill_md = os.path.join(skills_dir, name, "SKILL.md")
|
|
if os.path.isfile(skill_md):
|
|
ctx.register_skill(name, Path(skill_md))
|
|
|
|
# pre_llm_call returning {"context": ...} is the documented injection path
|
|
# (on_session_start return values are ignored, and ctx.inject_message
|
|
# refuses from that hook — verified empirically 2026-07-23). The context is
|
|
# appended to the first turn's user message.
|
|
def pre_llm_call(
|
|
session_id=None,
|
|
user_message=None,
|
|
conversation_history=None,
|
|
is_first_turn=None,
|
|
model=None,
|
|
platform=None,
|
|
**kwargs,
|
|
):
|
|
if is_first_turn:
|
|
return {"context": bootstrap}
|
|
return None
|
|
|
|
ctx.register_hook("pre_llm_call", pre_llm_call)
|