mirror of
https://github.com/obra/superpowers.git
synced 2026-07-25 11:44:01 +08:00
feat(hermes): Hermes Agent harness support, rebased to a Hermes-only 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>
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -11,3 +11,9 @@ triage/
|
|||||||
# development (see CLAUDE.md / README.md). It is not part of the published
|
# development (see CLAUDE.md / README.md). It is not part of the published
|
||||||
# plugin, so the whole directory is ignored here.
|
# plugin, so the whole directory is ignored here.
|
||||||
evals/
|
evals/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.pytest_cache/
|
||||||
|
|||||||
30
.hermes-plugin/INSTALL.md
Normal file
30
.hermes-plugin/INSTALL.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Hermes Agent — Superpowers Plugin
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes plugins install obra/superpowers --enable
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart any active Hermes sessions after installing.
|
||||||
|
|
||||||
|
## Smoke check
|
||||||
|
|
||||||
|
Start a new session and send:
|
||||||
|
> What are your superpowers?
|
||||||
|
|
||||||
|
The model should describe brainstorming, TDD, debugging, and planning skills.
|
||||||
|
If it doesn't, the bootstrap isn't loading — reinstall and restart.
|
||||||
|
|
||||||
|
## Acceptance test
|
||||||
|
|
||||||
|
Send in a fresh session:
|
||||||
|
> Let's make a react todo list
|
||||||
|
|
||||||
|
The `brainstorming` skill must trigger and run its flow before any code is written.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes plugins remove superpowers
|
||||||
|
```
|
||||||
101
.hermes-plugin/__init__.py
Normal file
101
.hermes-plugin/__init__.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for hermes"
|
||||||
|
|
||||||
|
# Resolved once at import — avoids repeated path work on every session-start.
|
||||||
|
# hermes plugins install does a full git clone, so .hermes-plugin/__init__.py
|
||||||
|
# and skills/ end up at the same level in ~/.hermes/plugins/superpowers/.
|
||||||
|
_SKILLS_DIR: str = os.path.realpath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "skills")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Module-level cache:
|
||||||
|
# None = not yet assembled
|
||||||
|
# False = SKILL.md missing (skip injection silently)
|
||||||
|
# str = assembled bootstrap content
|
||||||
|
_bootstrap_cache = None
|
||||||
|
|
||||||
|
_last_session_id = None
|
||||||
|
|
||||||
|
|
||||||
|
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 _hermes_tool_mapping() -> str:
|
||||||
|
# Tool names confirmed empirically in Task 1.
|
||||||
|
return """\
|
||||||
|
## Hermes tool mapping
|
||||||
|
|
||||||
|
When skills request actions, use these Hermes equivalents:
|
||||||
|
|
||||||
|
| Action | Hermes tool |
|
||||||
|
|--------|-------------|
|
||||||
|
| Read a file | `read_file` |
|
||||||
|
| Create a new file | `write_file` |
|
||||||
|
| Edit a file (targeted patch) | `patch` |
|
||||||
|
| Run a shell command | `terminal` |
|
||||||
|
| Search file contents | `search_files` |
|
||||||
|
| Find files by name | `terminal` with `find` |
|
||||||
|
| Fetch a URL / read a webpage | `web_extract(urls=[...])` |
|
||||||
|
| Search the web | `web_search(query=...)` |
|
||||||
|
| Dispatch a subagent | `delegate_task(goal=..., context=..., toolsets=[...], role="leaf")` |
|
||||||
|
| Task tracking | `todo` tool |
|
||||||
|
| Invoke a skill | `skill_view("skill-name")` — this is the native skill-loading mechanism on Hermes |
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_bootstrap() -> Optional[str]:
|
||||||
|
global _bootstrap_cache
|
||||||
|
if _bootstrap_cache is not None:
|
||||||
|
return None if _bootstrap_cache is False else _bootstrap_cache
|
||||||
|
|
||||||
|
skill_path = os.path.join(_SKILLS_DIR, "using-superpowers", "SKILL.md")
|
||||||
|
if not os.path.exists(skill_path):
|
||||||
|
_bootstrap_cache = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(skill_path, encoding="utf-8") as f:
|
||||||
|
raw = f.read()
|
||||||
|
|
||||||
|
body = _strip_frontmatter(raw)
|
||||||
|
_bootstrap_cache = (
|
||||||
|
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 NOT in Hermes' built-in skill registry — "
|
||||||
|
f"`skill_view` will return 'not found' for them. "
|
||||||
|
f"When using-superpowers tells you to invoke a skill, use this two-step:\n\n"
|
||||||
|
f"1. Try `skill_view(\"skill-name\")` first (it works for Hermes' own skills).\n"
|
||||||
|
f"2. If that returns 'not found', immediately use `read_file` instead:\n"
|
||||||
|
f" `read_file(\"{_SKILLS_DIR}/skill-name/SKILL.md\")`\n\n"
|
||||||
|
f"Example — to load the brainstorming skill:\n"
|
||||||
|
f" `read_file(\"{_SKILLS_DIR}/brainstorming/SKILL.md\")`\n\n"
|
||||||
|
f"The superpowers skills directory is: `{_SKILLS_DIR}`\n\n"
|
||||||
|
f"{_hermes_tool_mapping()}\n"
|
||||||
|
f"</EXTREMELY_IMPORTANT>"
|
||||||
|
)
|
||||||
|
return _bootstrap_cache
|
||||||
|
|
||||||
|
|
||||||
|
def register(ctx):
|
||||||
|
def on_session_start(**kwargs):
|
||||||
|
global _last_session_id
|
||||||
|
session_id = kwargs.get("session_id")
|
||||||
|
if session_id is not None and session_id == _last_session_id:
|
||||||
|
return
|
||||||
|
bootstrap = _get_bootstrap()
|
||||||
|
if bootstrap is None:
|
||||||
|
return
|
||||||
|
ctx.inject_message(bootstrap, role="user")
|
||||||
|
_last_session_id = session_id
|
||||||
|
|
||||||
|
ctx.register_hook("on_session_start", on_session_start)
|
||||||
6
.hermes-plugin/plugin.yaml
Normal file
6
.hermes-plugin/plugin.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
name: superpowers
|
||||||
|
version: 6.0.3
|
||||||
|
description: Superpowers skills and workflow bootstrap for Hermes Agent
|
||||||
|
author: obra
|
||||||
|
provides_hooks:
|
||||||
|
- on_session_start
|
||||||
14
README.md
14
README.md
@@ -11,7 +11,7 @@ If this sounds like someone you know, definitely send them our way.
|
|||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [Kimi Code](#kimi-code), [OpenCode](#opencode), [Pi](#pi).
|
Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [Hermes Agent](#hermes-agent), [Kimi Code](#kimi-code), [OpenCode](#opencode), [Pi](#pi).
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
@@ -199,6 +199,18 @@ pi -e /path/to/superpowers
|
|||||||
|
|
||||||
The Pi package loads the Superpowers skills and a small extension that injects the `using-superpowers` bootstrap at session startup and again after compaction. Pi has native skills, so no compatibility `Skill` tool is required. Subagent and task-list tools remain optional Pi companion packages.
|
The Pi package loads the Superpowers skills and a small extension that injects the `using-superpowers` bootstrap at session startup and again after compaction. Pi has native skills, so no compatibility `Skill` tool is required. Subagent and task-list tools remain optional Pi companion packages.
|
||||||
|
|
||||||
|
### Hermes Agent
|
||||||
|
|
||||||
|
Install Superpowers as a Hermes plugin from this repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes plugins install obra/superpowers --enable
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart any active Hermes sessions after installing.
|
||||||
|
|
||||||
|
Detailed docs: [docs/README.hermes.md](docs/README.hermes.md)
|
||||||
|
|
||||||
## The Basic Workflow
|
## The Basic Workflow
|
||||||
|
|
||||||
1. **brainstorming** - Activates before writing code. Refines rough ideas through questions, explores alternatives, presents design in sections for validation. Saves design document.
|
1. **brainstorming** - Activates before writing code. Refines rough ideas through questions, explores alternatives, presents design in sections for validation. Saves design document.
|
||||||
|
|||||||
29
docs/README.hermes.md
Normal file
29
docs/README.hermes.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Hermes Agent
|
||||||
|
|
||||||
|
Superpowers supports Hermes Agent via an in-process Python plugin (Shape B).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes plugins install obra/superpowers --enable
|
||||||
|
```
|
||||||
|
|
||||||
|
## What you get
|
||||||
|
|
||||||
|
All Superpowers skills auto-trigger in Hermes sessions:
|
||||||
|
brainstorming before feature work, systematic-debugging on bugs,
|
||||||
|
test-driven-development for implementation, writing-plans before
|
||||||
|
touching code, and all other skills in `skills/`.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
The plugin registers an `on_session_start` hook with the Hermes plugin API.
|
||||||
|
At the start of each session, the hook injects the `using-superpowers` bootstrap
|
||||||
|
as a user-role message via `ctx.inject_message(role="user")`. A session-id guard
|
||||||
|
prevents double-injection if the hook fires more than once per session.
|
||||||
|
|
||||||
|
Skills are loaded on demand during the session using `skill_view("skill-name")`.
|
||||||
|
|
||||||
|
## Verifying
|
||||||
|
|
||||||
|
See `.hermes-plugin/INSTALL.md` for the smoke check and acceptance test.
|
||||||
@@ -56,6 +56,7 @@ If your harness appears here, read its reference file for special instructions:
|
|||||||
- Codex: `references/codex-tools.md`
|
- Codex: `references/codex-tools.md`
|
||||||
- Pi: `references/pi-tools.md`
|
- Pi: `references/pi-tools.md`
|
||||||
- Antigravity: `references/antigravity-tools.md`
|
- Antigravity: `references/antigravity-tools.md`
|
||||||
|
- Hermes Agent: `references/hermes-tools.md`
|
||||||
|
|
||||||
## User Instructions
|
## User Instructions
|
||||||
|
|
||||||
|
|||||||
56
skills/using-superpowers/references/hermes-tools.md
Normal file
56
skills/using-superpowers/references/hermes-tools.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Hermes Agent Tool Mapping
|
||||||
|
|
||||||
|
Skills speak in actions ("dispatch a subagent", "create a todo", "read a file"). On Hermes Agent these resolve to the tools below.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
| Action skills request | Hermes tool |
|
||||||
|
|---|---|
|
||||||
|
| Read a file | `read_file` |
|
||||||
|
| Create a new file | `write_file` |
|
||||||
|
| Edit a file (targeted patch) | `patch` |
|
||||||
|
| Run a shell command | `terminal` |
|
||||||
|
| Search file contents | `search_files` |
|
||||||
|
| Find files by name | `terminal` with `find` |
|
||||||
|
| Fetch a URL / read a webpage | `web_extract(urls=[...])` |
|
||||||
|
| Search the web | `web_search(query=...)` |
|
||||||
|
| Dispatch a subagent | `delegate_task(goal=..., context=..., toolsets=[...], role="leaf")` |
|
||||||
|
| Task tracking | `todo` tool |
|
||||||
|
| Invoke a skill | `skill_view("skill-name")` |
|
||||||
|
|
||||||
|
## Instructions file
|
||||||
|
|
||||||
|
When a skill mentions "your instructions file," on Hermes Agent this is **`AGENTS.md`** in the project directory, or **`SOUL.md`** globally at `~/.hermes/SOUL.md`.
|
||||||
|
|
||||||
|
## Invoking a skill
|
||||||
|
|
||||||
|
Hermes Agent has a `skills` toolset with `skill_view` and `skills_list` tools.
|
||||||
|
To invoke a superpowers skill, use:
|
||||||
|
|
||||||
|
```
|
||||||
|
skill_view("brainstorming")
|
||||||
|
skill_view("test-driven-development")
|
||||||
|
```
|
||||||
|
|
||||||
|
If `skill_view` cannot find a superpowers skill (it may not appear in the catalog
|
||||||
|
until the plugin fully registers it), fall back to reading the SKILL.md directly:
|
||||||
|
|
||||||
|
```
|
||||||
|
read_file(path="~/.hermes/plugins/superpowers/skills/<skill-name>/SKILL.md")
|
||||||
|
```
|
||||||
|
|
||||||
|
This fallback is the same mechanism used by other harnesses without native skill loading.
|
||||||
|
|
||||||
|
## Subagent dispatch
|
||||||
|
|
||||||
|
Use `delegate_task` to spawn isolated subagents for parallel or sequential workstreams:
|
||||||
|
|
||||||
|
```
|
||||||
|
delegate_task(goal="...", context="...", toolsets=[...], role="leaf")
|
||||||
|
```
|
||||||
|
|
||||||
|
If `delegate_task` is unavailable, do the work inline rather than inventing tool calls.
|
||||||
|
|
||||||
|
## Task tracking
|
||||||
|
|
||||||
|
Use the `todo` tool for task tracking within a session. For multi-agent task boards, use `hermes kanban` CLI if available. Treat older `TodoWrite` references as the task-tracking action.
|
||||||
0
tests/hermes/__init__.py
Normal file
0
tests/hermes/__init__.py
Normal file
19
tests/hermes/conftest.py
Normal file
19
tests/hermes/conftest.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_ctx():
|
||||||
|
ctx = MagicMock()
|
||||||
|
ctx._hooks = {}
|
||||||
|
ctx._injected = []
|
||||||
|
|
||||||
|
def register_hook(event, fn):
|
||||||
|
ctx._hooks[event] = fn
|
||||||
|
|
||||||
|
def inject_message(content, role="user"):
|
||||||
|
ctx._injected.append({"content": content, "role": role})
|
||||||
|
|
||||||
|
ctx.register_hook.side_effect = register_hook
|
||||||
|
ctx.inject_message.side_effect = inject_message
|
||||||
|
return ctx
|
||||||
81
tests/hermes/test_bootstrap.py
Normal file
81
tests/hermes/test_bootstrap.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import importlib
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "../../.hermes-plugin")
|
||||||
|
))
|
||||||
|
|
||||||
|
BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for hermes"
|
||||||
|
|
||||||
|
|
||||||
|
def _load():
|
||||||
|
if "__init__" in sys.modules:
|
||||||
|
del sys.modules["__init__"]
|
||||||
|
return importlib.import_module("__init__")
|
||||||
|
|
||||||
|
|
||||||
|
class TestStripFrontmatter:
|
||||||
|
def test_strips_yaml_block(self):
|
||||||
|
m = _load()
|
||||||
|
content = "---\nname: foo\ndescription: bar\n---\n# Body\nContent here"
|
||||||
|
assert m._strip_frontmatter(content) == "# Body\nContent here"
|
||||||
|
|
||||||
|
def test_no_frontmatter_returns_trimmed_content(self):
|
||||||
|
m = _load()
|
||||||
|
content = "# No frontmatter\nJust content"
|
||||||
|
assert m._strip_frontmatter(content) == "# No frontmatter\nJust content"
|
||||||
|
|
||||||
|
def test_strips_surrounding_whitespace_from_body(self):
|
||||||
|
m = _load()
|
||||||
|
content = "---\nname: foo\n---\n\n\n# Body\n\n"
|
||||||
|
assert m._strip_frontmatter(content) == "# Body"
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetBootstrap:
|
||||||
|
def test_returns_none_when_skill_file_missing(self, tmp_path):
|
||||||
|
m = _load()
|
||||||
|
m._bootstrap_cache = None
|
||||||
|
m._SKILLS_DIR = str(tmp_path / "nonexistent")
|
||||||
|
assert m._get_bootstrap() is None
|
||||||
|
|
||||||
|
def test_caches_false_on_missing_file(self, tmp_path):
|
||||||
|
m = _load()
|
||||||
|
m._bootstrap_cache = None
|
||||||
|
m._SKILLS_DIR = str(tmp_path / "nonexistent")
|
||||||
|
m._get_bootstrap()
|
||||||
|
assert m._bootstrap_cache is False
|
||||||
|
|
||||||
|
def test_returns_string_with_real_skill(self):
|
||||||
|
m = _load()
|
||||||
|
m._bootstrap_cache = None
|
||||||
|
result = m._get_bootstrap()
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, str)
|
||||||
|
|
||||||
|
def test_same_object_returned_on_second_call(self):
|
||||||
|
m = _load()
|
||||||
|
m._bootstrap_cache = None
|
||||||
|
r1 = m._get_bootstrap()
|
||||||
|
r2 = m._get_bootstrap()
|
||||||
|
assert r1 is r2
|
||||||
|
|
||||||
|
def test_contains_marker(self):
|
||||||
|
m = _load()
|
||||||
|
m._bootstrap_cache = None
|
||||||
|
result = m._get_bootstrap()
|
||||||
|
assert BOOTSTRAP_MARKER in result
|
||||||
|
|
||||||
|
def test_contains_extremely_important_wrapper(self):
|
||||||
|
m = _load()
|
||||||
|
m._bootstrap_cache = None
|
||||||
|
result = m._get_bootstrap()
|
||||||
|
assert result.startswith("<EXTREMELY_IMPORTANT>")
|
||||||
|
assert result.rstrip().endswith("</EXTREMELY_IMPORTANT>")
|
||||||
|
|
||||||
|
def test_frontmatter_absent_from_output(self):
|
||||||
|
m = _load()
|
||||||
|
m._bootstrap_cache = None
|
||||||
|
result = m._get_bootstrap()
|
||||||
|
assert "---\nname:" not in result
|
||||||
105
tests/hermes/test_plugin.py
Normal file
105
tests/hermes/test_plugin.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import importlib
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Point at the plugin directory
|
||||||
|
_PLUGIN_DIR = os.path.join(os.path.dirname(__file__), "../../.hermes-plugin")
|
||||||
|
sys.path.insert(0, os.path.abspath(_PLUGIN_DIR))
|
||||||
|
|
||||||
|
BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for hermes"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_plugin():
|
||||||
|
"""Re-import plugin module fresh (clears module-level cache)."""
|
||||||
|
if "__init__" in sys.modules:
|
||||||
|
del sys.modules["__init__"]
|
||||||
|
return importlib.import_module("__init__")
|
||||||
|
|
||||||
|
|
||||||
|
class TestPluginRegistration:
|
||||||
|
def test_register_attaches_session_start_hook(self, mock_ctx):
|
||||||
|
plugin = _load_plugin()
|
||||||
|
plugin.register(mock_ctx)
|
||||||
|
mock_ctx.register_hook.assert_called_once()
|
||||||
|
event_name = mock_ctx.register_hook.call_args[0][0]
|
||||||
|
assert event_name == "on_session_start"
|
||||||
|
|
||||||
|
|
||||||
|
class TestBootstrapInjection:
|
||||||
|
def test_first_session_start_injects_bootstrap(self, mock_ctx):
|
||||||
|
plugin = _load_plugin()
|
||||||
|
plugin.register(mock_ctx)
|
||||||
|
handler = mock_ctx._hooks["on_session_start"]
|
||||||
|
handler(session_id="sess-1", model="test-model", platform="test")
|
||||||
|
assert len(mock_ctx._injected) == 1
|
||||||
|
assert BOOTSTRAP_MARKER in mock_ctx._injected[0]["content"]
|
||||||
|
|
||||||
|
def test_injection_uses_user_role(self, mock_ctx):
|
||||||
|
plugin = _load_plugin()
|
||||||
|
plugin.register(mock_ctx)
|
||||||
|
handler = mock_ctx._hooks["on_session_start"]
|
||||||
|
handler(session_id="sess-1", model="test-model", platform="test")
|
||||||
|
assert mock_ctx._injected[0]["role"] == "user"
|
||||||
|
|
||||||
|
def test_dedup_skips_on_same_session_id(self, mock_ctx):
|
||||||
|
plugin = _load_plugin()
|
||||||
|
plugin.register(mock_ctx)
|
||||||
|
handler = mock_ctx._hooks["on_session_start"]
|
||||||
|
handler(session_id="sess-1", model="test-model", platform="test")
|
||||||
|
handler(session_id="sess-1", model="test-model", platform="test")
|
||||||
|
assert len(mock_ctx._injected) == 1
|
||||||
|
|
||||||
|
def test_reinjects_on_new_session_id(self, mock_ctx):
|
||||||
|
plugin = _load_plugin()
|
||||||
|
plugin.register(mock_ctx)
|
||||||
|
handler = mock_ctx._hooks["on_session_start"]
|
||||||
|
handler(session_id="sess-1", model="test-model", platform="test")
|
||||||
|
handler(session_id="sess-2", model="test-model", platform="test")
|
||||||
|
assert len(mock_ctx._injected) == 2
|
||||||
|
|
||||||
|
def test_missing_skill_file_skips_silently(self, mock_ctx, tmp_path):
|
||||||
|
plugin = _load_plugin()
|
||||||
|
plugin._bootstrap_cache = None
|
||||||
|
plugin._SKILLS_DIR = str(tmp_path / "nonexistent")
|
||||||
|
plugin.register(mock_ctx)
|
||||||
|
handler = mock_ctx._hooks["on_session_start"]
|
||||||
|
handler(session_id="sess-1", model="test-model", platform="test")
|
||||||
|
assert len(mock_ctx._injected) == 0
|
||||||
|
|
||||||
|
def test_cache_populated_after_first_call(self, mock_ctx):
|
||||||
|
plugin = _load_plugin()
|
||||||
|
plugin._bootstrap_cache = None
|
||||||
|
plugin.register(mock_ctx)
|
||||||
|
handler = mock_ctx._hooks["on_session_start"]
|
||||||
|
handler(session_id="sess-1", model="test-model", platform="test")
|
||||||
|
assert plugin._bootstrap_cache is not None
|
||||||
|
assert plugin._bootstrap_cache is not False
|
||||||
|
|
||||||
|
|
||||||
|
class TestBootstrapContent:
|
||||||
|
def test_contains_extremely_important_tags(self, mock_ctx):
|
||||||
|
plugin = _load_plugin()
|
||||||
|
plugin.register(mock_ctx)
|
||||||
|
handler = mock_ctx._hooks["on_session_start"]
|
||||||
|
handler(session_id="sess-1", model="test-model", platform="test")
|
||||||
|
content = mock_ctx._injected[0]["content"]
|
||||||
|
assert "<EXTREMELY_IMPORTANT>" in content
|
||||||
|
assert "</EXTREMELY_IMPORTANT>" in content
|
||||||
|
|
||||||
|
def test_frontmatter_stripped(self, mock_ctx):
|
||||||
|
plugin = _load_plugin()
|
||||||
|
plugin.register(mock_ctx)
|
||||||
|
handler = mock_ctx._hooks["on_session_start"]
|
||||||
|
handler(session_id="sess-1", model="test-model", platform="test")
|
||||||
|
content = mock_ctx._injected[0]["content"]
|
||||||
|
assert "---\nname:" not in content
|
||||||
|
|
||||||
|
def test_tool_mapping_present(self, mock_ctx):
|
||||||
|
plugin = _load_plugin()
|
||||||
|
plugin.register(mock_ctx)
|
||||||
|
handler = mock_ctx._hooks["on_session_start"]
|
||||||
|
handler(session_id="sess-1", model="test-model", platform="test")
|
||||||
|
content = mock_ctx._injected[0]["content"]
|
||||||
|
assert "Hermes tool mapping" in content
|
||||||
|
assert "read_file" in content
|
||||||
Reference in New Issue
Block a user