Skip to content

feat(hermes): Hermes Agent harness support — eval-verified pre_llm_call bootstrap#2025

Open
obra wants to merge 3 commits into
devfrom
hermes-harness-rebase
Open

feat(hermes): Hermes Agent harness support — eval-verified pre_llm_call bootstrap#2025
obra wants to merge 3 commits into
devfrom
hermes-harness-rebase

Conversation

@obra

@obra obra commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Credit: This integration originates with @kumarabd's #1922 — the plugin structure, tests, docs, README/porting entries, and the hermes-tools.md adaptation are their work, carried here via Co-authored-by (commit 7b17761). This PR is the maintainer-directed landing vehicle: #1922's branch bundled unrelated release drift, and its injection mechanism turned out to be non-functional (empirical details below), so the maintainers rebased it clean and repaired the mechanism with live-eval evidence. @kumarabd — please review; your report started this.

Who is submitting this PR? (required)

Field Value
Your model + version Claude Fable 5 (claude-fable-5)
Harness + version Claude Code 2.1.218 (macOS)
All plugins installed superpowers (dev), superpowers-chrome, superpowers-lab, elements-of-style, episodic-memory, frontend-design, plugin-dev, agent-sdk-dev, mcp-server-dev, code-simplifier, claude-code-setup, context7, linear, github-triage, claude-session-driver, release-radar, summarize-meetings, worldview-synthesis, primeradiant-ops, plus personal skills
Human partner who reviewed this diff Jesse Vincent (@obra) — maintainer; directed the triage, eval bring-up, and mechanism fix interactively; reviews the final diff on this PR

What problem are you trying to solve?

Hermes Agent (NousResearch) users have no Superpowers support. Three community PRs attempted it (#1922, #1892, #1861 — plus closed slop #1972/#2021); #1922 was the best but its own acceptance transcript showed brainstorming being skipped, and live testing in the superpowers-evals quorum harness proved why: its ctx.inject_message-from-on_session_start mechanism silently delivers nothing (the call returns False). The bootstrap never reached the model, making the skills dead weight.

What does this PR change?

Adds Hermes Agent harness support with a working, eval-verified bootstrap: .hermes-plugin/ (a Hermes plugin that injects the using-superpowers bootstrap via the documented pre_llm_call{"context": …} path on the first turn, and registers every stock skill with Hermes' native loader so skill_view("superpowers:<name>") works — no per-harness skill copies), references/hermes-tools.md, tests/hermes/ (19 tests against the real plugin contract, including install-layout resolution, Path-type fidelity for register_skill, and a bootstrap-size guard under hermes' 10k-char context spill threshold), Platform Adaptation table row, and a two-line install in the main README (hermes plugins install obra/superpowers --enable + restart note + compaction caveat) — no separate per-harness doc files, per maintainer direction.

Hard-won implementation facts encoded in the plugin (all verified against the real CLI in a container):

  • on_session_start return values are ignored and ctx.inject_message refuses from it (returns False) — pre_llm_call is the working injection path.
  • ctx.register_skill requires a pathlib.Path; a str raises inside hermes and silently disables the whole plugin — so register() is kept failure-proof and path handling is explicit.
  • Skills resolve for both install layouts (git-clone: .hermes-plugin/ and skills/ as siblings; flattened) and fail loudly when absent instead of silently skipping.
  • Injection semantics (verified in hermes source, agent/conversation_loop.py + agent/turn_context.py): the hook fires before every LLM call; we return context only on is_first_turn. Hermes stamps the composed content onto that user message as a persisted api_content sidecar and replays it byte-identically on every later API call (its prompt-cache invariant), so one first-turn injection stays in model context for the whole session. Session exports show only the clean content, so verification must be behavioral — which the eval below is.
  • Known limitation: hermes' compressor exposes no plugin hook, and drop_stale_api_content() discards the sidecar if compression rewrites the first user row — a long session that compacts over the first turn loses the bootstrap with no re-injection seam. Upstream feature request (post_compression hook) is the clean fix; ECA (postCompact) and oh-my-pi (session_compact) both provide the equivalent.

Is this change appropriate for the core library?

Yes — new harness support, the one category where new integration surface belongs in core. Stock skills untouched; one adaptation reference file per the established pattern (codex/pi/antigravity).

What alternatives did you consider?

  • feat(hermes): Add Hermes Agent harness — on_session_start bootstrap injection #1922 as-is (on_session_start + inject_message): mechanism empirically dead; branch bundled ~14 files of v6.1.0 release drift.
  • feat: add Hermes Agent harness support (dist/hermes/) #1892's approach (transformed per-skill copies under dist/hermes/, manual per-skill install, per-session opt-in): rejected — Superpowers ships one canonical skills tree, and the repo's harness rules require session-start bootstrap with no per-session opt-in.
  • Add Hermes Agent plugin support #1861's approach (pre_llm_call — the right hook! — but targeting main, plugin files at repo root, and an extra behavioral "startup guard" injected into the tuned bootstrap): the hook choice is vindicated here; the bootstrap tampering is not carried.
  • hermes skills tap instead of a plugin: taps register skills but provide no session-start injection; the bootstrap is what makes skills auto-trigger, so a plugin is required regardless.

Does this PR contain multiple unrelated changes?

No. Every file serves Hermes harness support.

Existing PRs

Environment tested

Harness (e.g. Claude Code, Cursor) Harness version Model Model version/ID
Hermes Agent CLI (NousResearch install.sh, Linux container) current (hermes version git install) GLM 5.2 via OpenRouter z-ai/glm-5.2

New harness support (required if this PR adds a new harness)

The integration loads the bootstrap at session start via the plugin's pre_llm_call hook — no manual copying, no runtime shim, no per-session opt-in. Acceptance test run as a live quorum eval (superpowers-evals hermes target, real hermes REPL driven over tmux in a container, isolated throwaway $HOME): deterministic checks passskill-called superpowers:brainstorming ✓, skill-before-Write ✓, skill-before-Edit ✓. The same scenario against #1922's original mechanism fails all three (recorded RED baseline). Full campaign log: superpowers-evals docs/experiments/2026-07-23-hermes-target-bringup.md.

Clean-session transcript for "Let's make a react todo list"
Session 20260723_222400_614edd — model z-ai/glm-5.2 — 18 messages

[user]
Let's make a react todo list

[assistant]
I'll follow the superpowers workflow here. The instruction is clear: for "let's build X" requests, I should invoke the brainstorming skill first, then implementation skills. Let me also check the current project state since the workspace mentions a `package.json`.

Let me load the brainstorming skill and inspect the project in parallel.
  -> tool_call skill_view({"name": "superpowers:brainstorming"})
  -> tool_call read_file({"path": "package.json"})
  -> tool_call terminal({"command": "ls -la && git status && git log --oneline -5"})

[tool]
{"success": true, "name": "superpowers:brainstorming", "content": "[Bundle context: This skill is part of the 'superpowers' plugin.\nSibling skills: dispatching-parallel-agents, executing-plans, finishing-a-development-branch, receiving-code-review, requesting-code-review, subagent-driven-development, systematic-debugging, test-driven-development, using-git-worktrees, using-superpowers, verification-before-completion, writing-plans, writing-skills.\nUse qualified form to invoke siblings (e.g. superpowers:dispatching-parallel-agents).]\n\n---\nname: brainstorming\ndescription: \"You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.\"\n---\n\n# Brai …[truncated]

[tool]
{"content": "1|{\n2|  \"name\": \"drill-test-project\",\n3|  \"version\": \"1.0.0\",\n4|  \"description\": \"Test project for Drill scenarios\",\n5|  \"main\": \"src/index.js\"\n6|}\n7|", "total_lines": 6, "file_size": 136, "truncated": false, "is_binary": false, "is_image": false}

[tool]
{"output": "total 8\ndrwxr-xr-x+      1 501 20 192 Jul 23 22:23 .\ndrwxr-xr-x+      1 501 20 224 Jul 23 22:23 ..\ndrwxr-xr-x+      1 501 20 384 Jul 23 22:24 .git\n-rw-r--r--+      1 501 20  60 Jul 23 22:23 README.md\n-rw-r--r--+      1 501 20 136 Jul 23 22:23 package.json\ndrwxr-xr-x+      1 501 20 128 Jul 23 22:23 src\nOn branch main\nnothing to commit, working tree clean\n313a9f9 add entry point\n39fcaf7 add utils module\n3e2f9fc initial commit", "exit_code": 0, "error": null}

[assistant]
Let me explore the existing project structure before we brainstorm the design.
  -> tool_call search_files({"pattern": "*", "target": "files", "path": "src"})
  -> tool_call read_file({"path": "README.md"})

[tool]
{"total_count": 2, "files": ["src/index.js", "src/utils.js"]}

[tool]
{"content": "1|# Test Project\n2|\n3|A minimal project for Drill test scenarios.\n4|", "total_lines": 3, "file_size": 60, "truncated": false, "is_binary": false, "is_image": false}

[assistant]
  -> tool_call read_file({"path": "src/index.js"})
  -> tool_call read_file({"path": "src/utils.js"})

[tool]
{"content": "1|const { greet } = require('./utils');\n2|\n3|function main() {\n4|  console.log(greet('world'));\n5|}\n6|\n7|main();\n8|", "total_lines": 7, "file_size": 99, "truncated": false, "is_binary": false, "is_image": false}

[tool]
{"content": "1|function greet(name) {\n2|  return `Hello, ${name}!`;\n3|}\n4|\n5|module.exports = { greet };\n6|", "total_lines": 5, "file_size": 82, "truncated": false, "is_binary": false, "is_image": false}

[assistant]
Using the brainstorming skill to design this before building.

Here's what I'm working with: the current repo is a tiny Node project (`drill-test-project`) with a couple of CommonJS files (`src/index.js`, `src/utils.js`) — no React, no build tooling, no dependencies beyond the bare `package.json`. So building a React todo list here means introducing React + a build/dev setup from scratch.

Before I propose a design, I have one clarifying question (the skill says one at a time):
  -> tool_call clarify({"question": "How should this React todo list live alongside the existing Node project?", "choices": ["Replace it entirely — turn this into a React project", "Coexist — add the todo app in a subfolder…)

[tool]
{"question": "How should this React todo list live alongside the existing Node project?", "choices_offered": ["Replace it entirely — turn this into a React project", "Coexist — add the todo app in a subfolder, keep the Node files", "Fresh separate directory — start clean, ignore the existing files"], "user_response": "Coexist — add the todo app in a subfolder, keep the Node files"}

[assistant]
Good. Now the next clarifying question — the tech stack choice matters for how much tooling we pull in:
  -> tool_call clarify({"choices": ["Vite + React (fast dev server, minimal config — my recommendation)", "Create React App (classic, heavier, more opinionated)", "No build tool — single HTML file with React via CDN"], "que…)

[tool]
{"question": "Which build setup do you want for the React app?", "choices_offered": ["Vite + React (fast dev server, minimal config — my recommendation)", "Create React App (classic, heavier, more opinionated)", "No build tool — single HTML file with React via CDN"], "user_response": "Vite + React (fast dev server, minimal config — my recommendation)"}

[assistant]
Good. Last clarifying question — the feature scope determines the design:
  -> tool_call clarify({"choices": ["Just the essentials — add, mark complete, delete", "Essentials + edit-in-place and clear-all-completed", "Full features — edit, filters (all/active/done), local persistence"], "question"…)

[tool]
{"question": "How much feature scope do you want in the todo list?", "choices_offered": ["Just the essentials — add, mark complete, delete", "Essentials + edit-in-place and clear-all-completed", "Full features — edit, filters (all/active/done), local persistence"], "user_response": "Just the essentials — add, mark complete, delete"}

[assistant]
Here's my proposed design, scaled to this simple app:

DESIGN — React Todo List

Location: new `todo-app/` subfolder at the repo root. Existing Node files (`src/`, `package.json`) stay untouched; the React app gets its own `package.json` and `vite.config.js` inside `todo-app/`.

Stack: Vite + React 18. Single `TodoApp` component holding a list of todos in state. No router, no state library, no CSS framework — plain CSS in a single stylesheet.

Data model:
  type Todo = { id: string, text: string, done: boolean }
  id generated with crypto.randomUUID()

Component structure (one file, one component — YAGNI for more):
  - App renders: an input + Add button, the list, each item with a checkbox and a delete button
  - State: `todos` array via useState
  - Handlers: addTodo, toggleTodo, deleteTo …[truncated]

Evaluation

  • Initial prompt: maintainer-directed triage of the open Hermes PRs, then "can you look at what it would take to spin up a hermes agent in a container to test the skills? … let's add hermes" — the eval harness was built first, then used to diagnose and fix this integration.
  • Eval sessions after the change: live quorum runs of scenarios/superpowers-bootstrap (the acceptance test) against this branch in the eval container, plus targeted mechanism probes (hook firing, injection delivery, skill_view resolution, both install layouts).
  • Before/after: original mechanism (7b17761): RED — brainstorming never invoked, Write fired unguarded; bootstrap absent from model context. Fixed mechanism (178528c): GREEN — all three deterministic checks pass; the agent's trace explicitly cites the bootstrap's "Let's build X → superpowers:brainstorming first" rule before acting; skill_view("superpowers:brainstorming") returns the stock SKILL.md in both install layouts.

Validation coverage and known gaps

  • Verified: GLM 5.2 via OpenRouter, Linux container, real interactive REPL driven over tmux with isolated $HOME (the quorum eval), both install layouts, unit suite 19/19.
  • Hermes-family models: structurally blocked via OpenRouter — neither nousresearch/hermes-4-405b nor -70b exposes the tools parameter there, and the hermes CLI is tool-driven; every call 404s ("No endpoints found that support tool use"). Native Hermes-4 validation requires the Nous Portal API and a key; recorded as a documented negative result in the evals experiment log, to be run when a Nous credential exists.
  • Post-merge check: the true hermes plugins install obra/superpowers GitHub-clone flow can only be exercised once this lands on the default branch (the eval stages the plugin from a checkout); one manual smoke after merge closes that.

Rigor

  • If this is a skills change: I used superpowers:writing-skills and completed adversarial pressure testing — N/A as such: no tuned skill content is modified; the only skills/ change is adding references/hermes-tools.md and one Platform Adaptation table row (the established per-harness pattern). The bootstrap is carried verbatim.
  • This change was tested adversarially, not just on the happy path — the RED baseline, silent-failure probes, and both-layout tests are exactly that.
  • I did not modify carefully-tuned content (Red Flags table, rationalizations, "human partner" language) without extensive evals showing the change is an improvement

Human review

  • A human has reviewed the COMPLETE proposed diff before submission — the human partner is the repo maintainer, who commissioned the work and performs that review on this PR (opened as draft for exactly that)

obra and others added 3 commits July 23, 2026 12:18
…diff

Rebase of PR #1922 onto current dev: the ~14 files of v6.1.0-era
codex/release drift are dropped, the porting-guide edits (stale against
the post-prune rewrite, no Hermes content) are dropped, and the Hermes
surface is kept intact: .hermes-plugin/ (on_session_start bootstrap
injection), tests/hermes/ (20 tests, passing), docs/README.hermes.md,
references/hermes-tools.md, the Platform Adaptation row, README section,
and Python ignores.

Known open items from review, unchanged by this rebase: the injection
mechanism uses ctx.inject_message from on_session_start, which the
official plugin guide does not document (pre_llm_call returning
{"context": ...} is the sanctioned path), skills are not registered via
ctx.register_skill, and the acceptance transcript predates the fix.

Co-authored-by: kumarabd <kumarabd@users.noreply.github.com>
…ill registration

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.
…s to the README section

The 20-test suite still exercised the dead on_session_start/inject_message
mechanism (17 failures against the rewritten plugin). Rewritten for the
real contract: pre_llm_call registration + first-turn-only context return,
register_skill receiving pathlib.Path (the conftest mock now raises on str,
mirroring hermes' AttributeError that silently disables a plugin), both
install layouts resolving skills, loud failure when skills are missing,
tool mapping sourced verbatim from hermes-tools.md, and a bootstrap-size
guard against hermes' 10k-char context spill threshold. 19 tests, passing.

Install docs collapse into the README section per maintainer direction:
docs/README.hermes.md and .hermes-plugin/INSTALL.md are gone; the README
carries the two-line install plus the compaction caveat. plugin.yaml
version aligned to 6.1.1.
@obra
obra marked this pull request as ready for review July 24, 2026 00:44
@MRDGH2821

MRDGH2821 commented Jul 24, 2026

Copy link
Copy Markdown

Nice to see this being worked on!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants