From 4198fc30ac6d5090b9c16dba9bac572fab59075e Mon Sep 17 00:00:00 2001 From: Abdisalam Hassan Date: Mon, 15 Jun 2026 19:57:45 +0000 Subject: [PATCH 1/2] add Hermes plugin for Ponytail with command support and documentation --- README.md | 12 +- __init__.py | 216 +++++++++++++++++++++++++++++++++++ after-install.md | 21 ++++ docs/agent-portability.md | 1 + plugin.yaml | 19 ++++ tests/hermes-plugin.test.js | 221 ++++++++++++++++++++++++++++++++++++ 6 files changed, 488 insertions(+), 2 deletions(-) create mode 100644 __init__.py create mode 100644 after-install.md create mode 100644 plugin.yaml create mode 100644 tests/hermes-plugin.test.js diff --git a/README.md b/README.md index beec279..509a311 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

Stars Release - Works with 13 agents + Works with 14 agents MIT license

@@ -149,6 +149,14 @@ agy plugin install https://github.com/DietrichGebert/ponytail It reuses this repo's `gemini-extension.json`. One difference: Antigravity converts the `/ponytail` commands into skills, so you type them into the chat (e.g. `/ponytail-review` as a message) instead of picking them from a slash menu. Until the migration completes (around June 18, 2026), `gemini extensions install` still works too. To run it as an always-on rule instead, drop the ruleset into `.agents/rules/`. +### Hermes Agent + +```bash +hermes plugins install DietrichGebert/ponytail --enable +``` + +Restart Hermes after installing. The plugin injects the active Ponytail mode before each LLM turn, registers the bundled skills as `ponytail:`, and adds `/ponytail`, `/ponytail-review`, `/ponytail-audit`, `/ponytail-debt`, and `/ponytail-help`. In shared gateways, restrict `/ponytail` to trusted users with Hermes slash-command access controls; runtime mode is process-local. + That was it. He'd be proud. He won't say it. Active every session, with a handful of commands (see [Commands](#commands)). `/ponytail ultra` exists for when the codebase has wronged you personally. Startup and mode-change text shows the current mode. @@ -175,7 +183,7 @@ Which files map to which agent: [Agent portability](docs/agent-portability.md). | `/ponytail-debt` | Harvest the `ponytail:` shortcuts you've deferred into a ledger, so "later" doesn't become "never". | | `/ponytail-help` | Quick reference for the commands above. | -Commands need a skill-capable host (Claude Code, Codex, OpenCode, Gemini, pi). In Codex they're skills, invoke with `@` (`@ponytail-review`). The instruction-only adapters (Cursor, Windsurf, Cline, Copilot, Kiro, Antigravity) load the always-on ruleset without the commands. +Commands need a command/skill-capable host (Claude Code, Codex, OpenCode, Gemini, pi, Hermes Agent). In Codex they're skills, invoke with `@` (`@ponytail-review`). The instruction-only adapters (Cursor, Windsurf, Cline, Copilot, Kiro, Antigravity) load the always-on ruleset without the commands. ## Development diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0ba03ab --- /dev/null +++ b/__init__.py @@ -0,0 +1,216 @@ +"""Hermes plugin for Ponytail.""" + +from __future__ import annotations + +import json +import os +import re +from pathlib import Path +from typing import Any, Callable + +DEFAULT_MODE = "full" +RUNTIME_MODES = {"off", "lite", "full", "ultra"} +CONFIG_MODES = RUNTIME_MODES | {"review"} +SKILL_COMMANDS = { + "ponytail-review": "Review the current diff or provided target for over-engineering.", + "ponytail-audit": "Audit the repo for over-engineering and deletion opportunities.", + "ponytail-debt": "List every deliberate `ponytail:` shortcut and its upgrade path.", + "ponytail-help": "Show the Ponytail command reference.", +} + +ROOT = Path(__file__).resolve().parent +SKILLS_DIR = ROOT / "skills" +PONYTAIL_SKILL = SKILLS_DIR / "ponytail" / "SKILL.md" +REVIEW_SKILL = SKILLS_DIR / "ponytail-review" / "SKILL.md" + +_current_mode = None + + +def _normalize_runtime_mode(mode: str | None) -> str | None: + if not isinstance(mode, str): + return None + mode = mode.strip().lower() + return mode if mode in RUNTIME_MODES else None + + +def _normalize_config_mode(mode: str | None) -> str | None: + if not isinstance(mode, str): + return None + mode = mode.strip().lower() + return mode if mode in CONFIG_MODES else None + + +def _config_dir() -> Path: + if os.environ.get("XDG_CONFIG_HOME"): + return Path(os.environ["XDG_CONFIG_HOME"]) / "ponytail" + if os.name == "nt": + return Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming")) / "ponytail" + return Path.home() / ".config" / "ponytail" + + +def _default_mode() -> str: + env_mode = _normalize_config_mode(os.environ.get("PONYTAIL_DEFAULT_MODE")) + if env_mode: + return env_mode + try: + data = json.loads((_config_dir() / "config.json").read_text(encoding="utf-8")) + file_mode = _normalize_config_mode(data.get("defaultMode")) + if file_mode: + return file_mode + except Exception: + pass + return DEFAULT_MODE + + +def _strip_frontmatter(text: str) -> str: + return re.sub(r"^---[\s\S]*?---\s*", "", text or "", count=1) + + +def _filter_skill_body_for_mode(body: str, mode: str) -> str: + effective = _normalize_runtime_mode(mode) or DEFAULT_MODE + lines = [] + for line in _strip_frontmatter(body).splitlines(): + table_label = re.match(r"^\|\s*\*\*(.+?)\*\*\s*\|", line) + if table_label: + label_mode = _normalize_runtime_mode(table_label.group(1)) + if label_mode and label_mode != effective: + continue + + example_label = re.match(r"^-\s*([^:]+):\s*", line) + if example_label: + label_mode = _normalize_runtime_mode(example_label.group(1)) + if label_mode and label_mode != effective: + continue + + lines.append(line) + return "\n".join(lines) + + +def _fallback_instructions(mode: str) -> str: + return ( + f"PONYTAIL MODE ACTIVE — level: {mode}\n\n" + "You are a lazy senior developer. Lazy means efficient, not careless. " + "The best code is the code never written.\n\n" + "Before any code, stop at the first rung that holds: YAGNI, stdlib, " + "native platform, installed dependency, one line, then minimum code. " + "No unrequested abstractions, avoidable dependencies, boilerplate, or " + "speculative scaffolding. Deletion over addition. Boring over clever. " + "Do not simplify away trust-boundary validation, data-loss handling, " + "security, accessibility, explicitly requested behavior, or one small " + "runnable check for non-trivial logic." + ) + + +def build_injected_context(mode: str | None = None) -> str: + """Return the mode-filtered Ponytail context injected before LLM turns.""" + configured = _normalize_config_mode(mode) or _default_mode() + if configured == "off": + return "" + if configured == "review": + try: + body = REVIEW_SKILL.read_text(encoding="utf-8") + return f"PONYTAIL MODE ACTIVE — level: review\n\n{_strip_frontmatter(body)}" + except OSError: + return "PONYTAIL MODE ACTIVE — level: review. Review diffs for unnecessary complexity." + + effective = _normalize_runtime_mode(configured) or DEFAULT_MODE + try: + body = PONYTAIL_SKILL.read_text(encoding="utf-8") + return f"PONYTAIL MODE ACTIVE — level: {effective}\n\n{_filter_skill_body_for_mode(body, effective)}" + except OSError: + return _fallback_instructions(effective) + + +def _pre_llm_call(session_id: str = "", **_: Any) -> dict[str, str] | None: + mode = _current_mode or _default_mode() + context = build_injected_context(mode) + return {"context": context} if context else None + + +def _skill_prompt(command: str, args: str = "") -> str: + tail = args.strip() + target = f"\n\nUser arguments: {tail}" if tail else "" + return ( + f"Load and follow the Hermes plugin skill `ponytail:{command}`. " + f"{SKILL_COMMANDS[command]}{target}" + ) + + +def _slash_access_denied(event: Any, gateway: Any, command: str) -> bool: + if gateway is None or event is None: + return False + checker = getattr(gateway, "_check_slash_access", None) + source = getattr(event, "source", None) + if checker is None or source is None: + return False + try: + return checker(source, command) is not None + except Exception: + return True + + +def rewrite_gateway_command(event: Any = None, gateway: Any = None, **_: Any) -> dict[str, str] | None: + """Rewrite authorized gateway /ponytail-* commands into normal agent prompts.""" + text = str(getattr(event, "text", "") or "").strip() + if not text.startswith("/"): + return None + head, _, rest = text[1:].partition(" ") + command = head.replace("_", "-").lower() + if command not in SKILL_COMMANDS: + return None + if _slash_access_denied(event, gateway, command): + return None + return {"action": "rewrite", "text": _skill_prompt(command, rest)} + + +def _handle_mode_command(raw_args: str) -> str: + global _current_mode + arg = (raw_args or "").strip().lower() + if not arg: + mode = _current_mode or _default_mode() + return f"Ponytail mode: {mode}. Use `/ponytail lite|full|ultra|off`." + mode = _normalize_runtime_mode(arg) + if not mode: + return "Usage: /ponytail [lite|full|ultra|off]" + _current_mode = mode + return f"Ponytail mode set to {mode}." + + +def _make_skill_command_handler(ctx: Any, command: str) -> Callable[[str], str]: + def handler(raw_args: str) -> str: + prompt = _skill_prompt(command, raw_args or "") + injected = False + try: + injected = bool(ctx.inject_message(prompt)) + except Exception: + injected = False + if injected: + return f"Queued `{command}` for the agent." + return prompt + + return handler + + +def register(ctx: Any) -> None: + """Register Ponytail hooks, skills, and slash commands with Hermes.""" + for child in sorted(SKILLS_DIR.iterdir() if SKILLS_DIR.exists() else []): + skill_md = child / "SKILL.md" + if child.is_dir() and skill_md.exists(): + ctx.register_skill(child.name, skill_md) + + ctx.register_hook("pre_llm_call", _pre_llm_call) + ctx.register_hook("pre_gateway_dispatch", rewrite_gateway_command) + + ctx.register_command( + "ponytail", + _handle_mode_command, + description="Set Ponytail lazy senior dev mode: lite, full, ultra, or off.", + args_hint="[lite|full|ultra|off]", + ) + for command, description in SKILL_COMMANDS.items(): + ctx.register_command( + command, + _make_skill_command_handler(ctx, command), + description=description, + args_hint="[target or notes]", + ) diff --git a/after-install.md b/after-install.md new file mode 100644 index 0000000..245aa51 --- /dev/null +++ b/after-install.md @@ -0,0 +1,21 @@ +# Ponytail for Hermes installed + +Enable it if you did not install with `--enable`: + +```bash +hermes plugins enable ponytail +``` + +Restart Hermes or the gateway after enabling. + +In shared gateways, restrict `/ponytail` to trusted users with Hermes slash-command access controls; runtime mode is process-local. + +Commands: + +- `/ponytail [lite|full|ultra|off]` +- `/ponytail-review [target]` +- `/ponytail-audit [target]` +- `/ponytail-debt` +- `/ponytail-help` + +Bundled skills are available as `ponytail:ponytail`, `ponytail:ponytail-review`, `ponytail:ponytail-audit`, `ponytail:ponytail-debt`, and `ponytail:ponytail-help`. diff --git a/docs/agent-portability.md b/docs/agent-portability.md index 40b4af8..002be1c 100644 --- a/docs/agent-portability.md +++ b/docs/agent-portability.md @@ -12,6 +12,7 @@ to load in a given agent. | Codex | `.codex-plugin/plugin.json`, `hooks/hooks.json`, `hooks/`, `skills/` | Plugin install with the same skills plus lifecycle hooks for activation and mode tracking. | | OpenCode | `.opencode/plugins/ponytail.mjs`, `.opencode/command/`, `hooks/`, `skills/` | Server plugin injects the ruleset each turn via `experimental.chat.system.transform` and persists `/ponytail` switches; reuses the shared instruction builder. | | pi | `pi-extension/`, `skills/`, `hooks/` | Package extension: injects the ruleset each turn through the shared instruction builder and registers the `/ponytail` commands. | +| Hermes Agent | `plugin.yaml`, `__init__.py`, `skills/` | Native Hermes plugin: injects active mode through `pre_llm_call`, rewrites gateway `/ponytail-*` skill commands into agent prompts, registers `/ponytail` mode switching, and exposes bundled skills as `ponytail:`. | | Gemini CLI | `gemini-extension.json`, `AGENTS.md`, `commands/`, `skills/` | Extension manifest points `contextFileName` at `AGENTS.md` for always-on rules, and reuses the existing `commands/*.toml` and `skills/`, which Gemini CLI auto-discovers. | | Cursor | `.cursor/rules/ponytail.mdc` | Always-on project rule. | | Windsurf | `.windsurf/rules/ponytail.md` | Project rule. | diff --git a/plugin.yaml b/plugin.yaml new file mode 100644 index 0000000..f0fb021 --- /dev/null +++ b/plugin.yaml @@ -0,0 +1,19 @@ +name: ponytail +version: 0.1.0 +description: Lazy senior dev mode for Hermes Agent — always-on context, bundled skills, and slash commands. +author: Dietrich Gebert and contributors +provides_hooks: + - pre_llm_call + - pre_gateway_dispatch +provides_commands: + - ponytail + - ponytail-review + - ponytail-audit + - ponytail-debt + - ponytail-help +provides_skills: + - ponytail + - ponytail-review + - ponytail-audit + - ponytail-debt + - ponytail-help diff --git a/tests/hermes-plugin.test.js b/tests/hermes-plugin.test.js new file mode 100644 index 0000000..f8e90dd --- /dev/null +++ b/tests/hermes-plugin.test.js @@ -0,0 +1,221 @@ +#!/usr/bin/env node +// Hermes support is a real plugin, not just copied rules: the repo root must be +// installable with `hermes plugins install owner/repo`, register bundled skills, +// inject active mode context, and expose slash commands. + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const commands = ['ponytail', 'ponytail-review', 'ponytail-audit', 'ponytail-debt', 'ponytail-help']; +const skillCommands = commands.filter((name) => name !== 'ponytail'); + +const root = path.join(__dirname, '..'); + +function python(script, env = {}) { + const result = spawnSync('python3', ['-c', script], { + cwd: root, + env: { ...process.env, ...env }, + encoding: 'utf8', + }); + if (result.status !== 0) { + throw new Error(`python failed\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}`); + } + return result.stdout.trim(); +} + +test('Hermes plugin manifest matches runtime skills, hooks, commands, and package version', () => { + const manifestPath = path.join(root, 'plugin.yaml'); + assert.ok(fs.existsSync(manifestPath), 'missing root plugin.yaml'); + const manifest = fs.readFileSync(manifestPath, 'utf8'); + const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); + const skillDirs = fs.readdirSync(path.join(root, 'skills')) + .filter((name) => fs.existsSync(path.join(root, 'skills', name, 'SKILL.md'))) + .sort(); + + assert.match(manifest, /^name:\s*ponytail$/m); + assert.match(manifest, new RegExp(`^version:\\s*${packageJson.version}$`, 'm')); + assert.deepEqual(commands.filter((name) => manifest.includes(` - ${name}`)), commands); + assert.deepEqual(skillDirs.filter((name) => manifest.includes(` - ${name}`)), skillDirs); + assert.match(manifest, /pre_llm_call/); + assert.match(manifest, /pre_gateway_dispatch/); +}); + +test('Hermes plugin registers every shipped skill under the ponytail namespace', () => { + const output = python(String.raw` +import importlib.util, json, pathlib +spec = importlib.util.spec_from_file_location('ponytail_hermes_plugin', '__init__.py') +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) +class Ctx: + def __init__(self): + self.skills = [] + self.hooks = [] + self.commands = [] + def register_skill(self, name, path): + self.skills.append((name, pathlib.Path(path).as_posix())) + def register_hook(self, name, handler): + self.hooks.append(name) + def register_command(self, name, handler, description='', args_hint=''): + self.commands.append(name) +ctx = Ctx() +mod.register(ctx) +print(json.dumps({'skills': ctx.skills, 'hooks': ctx.hooks, 'commands': ctx.commands}, sort_keys=True)) +`); + const data = JSON.parse(output); + assert.deepEqual(data.skills.map(([name]) => name).sort(), [ + 'ponytail', + 'ponytail-audit', + 'ponytail-debt', + 'ponytail-help', + 'ponytail-review', + ]); + assert.ok(data.skills.every(([, skillPath]) => skillPath.endsWith('/SKILL.md'))); + assert.ok(data.hooks.includes('pre_llm_call')); + assert.ok(data.commands.includes('ponytail')); + assert.ok(data.commands.includes('ponytail-review')); +}); + +test('Hermes plugin builds mode-aware injected context from the canonical skill', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ponytail-config-')); + const output = python(String.raw` +import importlib.util, json +spec = importlib.util.spec_from_file_location('ponytail_hermes_plugin', '__init__.py') +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) +ctx = mod.build_injected_context('ultra') +print(json.dumps({'ctx': ctx})) +`, { XDG_CONFIG_HOME: tmp }); + const { ctx } = JSON.parse(output); + + assert.match(ctx, /PONYTAIL MODE ACTIVE — level: ultra/); + assert.match(ctx, /The best\s+code is the code never written/); + assert.match(ctx, /ultra/i); + assert.doesNotMatch(ctx, /^---/); + assert.doesNotMatch(ctx, /\|\s*\*\*Lite\*\*/i); +}); + +test('Hermes mode config respects env, config file, off, and invalid command behavior', () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ponytail-config-')); + fs.mkdirSync(path.join(tmp, 'ponytail'), { recursive: true }); + fs.writeFileSync(path.join(tmp, 'ponytail', 'config.json'), JSON.stringify({ defaultMode: 'lite' })); + const output = python(String.raw` +import importlib.util, json +spec = importlib.util.spec_from_file_location('ponytail_hermes_plugin', '__init__.py') +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) +class Ctx: + def __init__(self): self.commands = {} + def register_skill(self, name, path): pass + def register_hook(self, name, handler): pass + def register_command(self, name, handler, description='', args_hint=''): + self.commands[name] = handler +ctx = Ctx() +mod.register(ctx) +status_before = ctx.commands['ponytail']('') +invalid = ctx.commands['ponytail']('maximum') +status_after = ctx.commands['ponytail']('') +print(json.dumps({ + 'default': mod.build_injected_context(None), + 'off': mod.build_injected_context('off'), + 'status_before': status_before, + 'invalid': invalid, + 'status_after': status_after, +})) +`, { XDG_CONFIG_HOME: tmp, PONYTAIL_DEFAULT_MODE: 'ultra' }); + const data = JSON.parse(output); + assert.match(data.default, /level: ultra/); + assert.equal(data.off, ''); + assert.match(data.status_before, /Ponytail mode: ultra/); + assert.match(data.invalid, /Usage:/); + assert.match(data.status_after, /Ponytail mode: ultra/); +}); + +test('Hermes plugin review mode injects the real review skill body', () => { + const output = python(String.raw` +import importlib.util, json +spec = importlib.util.spec_from_file_location('ponytail_hermes_plugin', '__init__.py') +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) +ctx = mod.build_injected_context('review') +print(json.dumps({'ctx': ctx})) +`); + const { ctx } = JSON.parse(output); + assert.match(ctx, /PONYTAIL MODE ACTIVE — level: review/); + assert.match(ctx, /Review diffs for unnecessary complexity/); + assert.match(ctx, /net: - lines possible/); + assert.doesNotMatch(ctx, /^---/); +}); + +test('Hermes /ponytail command changes mode and pre_llm_call injects current context', () => { + const output = python(String.raw` +import importlib.util, json +spec = importlib.util.spec_from_file_location('ponytail_hermes_plugin', '__init__.py') +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) +class Ctx: + def __init__(self): + self.hooks = {} + self.commands = {} + def register_skill(self, name, path): pass + def register_hook(self, name, handler): self.hooks[name] = handler + def register_command(self, name, handler, description='', args_hint=''): + self.commands[name] = handler +ctx = Ctx() +mod.register(ctx) +message = ctx.commands['ponytail']('ultra') +injected = ctx.hooks['pre_llm_call'](session_id='s1', user_message='build it', conversation_history=[], is_first_turn=False, model='m', platform='cli') +print(json.dumps({'message': message, 'context': injected['context']})) +`); + const data = JSON.parse(output); + assert.match(data.message, /ultra/); + assert.match(data.context, /PONYTAIL MODE ACTIVE — level: ultra/); +}); + +test('Hermes gateway rewrite respects slash access denial', () => { + const output = python(String.raw` +import importlib.util, json +spec = importlib.util.spec_from_file_location('ponytail_hermes_plugin', '__init__.py') +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) +class Source: + platform = None + chat_id = 'c1' + user_id = 'u1' +class Event: + text = '/ponytail-review src/app.js' + source = Source() +class Gateway: + def _check_slash_access(self, source, command): + return 'denied' +result = mod.rewrite_gateway_command(event=Event(), gateway=Gateway()) +print(json.dumps(result)) +`); + assert.equal(output, 'null'); +}); + +test('Hermes gateway rewrite preserves every skill command and ignores unrelated text', () => { + const output = python(String.raw` +import importlib.util, json +spec = importlib.util.spec_from_file_location('ponytail_hermes_plugin', '__init__.py') +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) +class Event: + def __init__(self, text): self.text = text +cases = {} +for text in ['/ponytail-review x', '/ponytail_audit repo', '/ponytail-debt', '/ponytail-help', '/status', 'hello']: + cases[text] = mod.rewrite_gateway_command(event=Event(text)) +print(json.dumps(cases, sort_keys=True)) +`); + const data = JSON.parse(output); + assert.match(data['/ponytail-review x'].text, /ponytail-review/); + assert.match(data['/ponytail_audit repo'].text, /ponytail-audit/); + assert.match(data['/ponytail_audit repo'].text, /repo/); + assert.match(data['/ponytail-debt'].text, /ponytail-debt/); + assert.match(data['/ponytail-help'].text, /ponytail-help/); + assert.equal(data['/status'], null); + assert.equal(data.hello, null); +}); From ce55fd460a1bad7cbe95d18dca5f28e633c80b86 Mon Sep 17 00:00:00 2001 From: salaamdev <38502084+salaamdev@users.noreply.github.com> Date: Mon, 15 Jun 2026 23:13:09 +0300 Subject: [PATCH 2/2] Update author in plugin.yaml to Salaamdev --- plugin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.yaml b/plugin.yaml index f0fb021..1713538 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -1,7 +1,7 @@ name: ponytail version: 0.1.0 description: Lazy senior dev mode for Hermes Agent — always-on context, bundled skills, and slash commands. -author: Dietrich Gebert and contributors +author: Salaamdev provides_hooks: - pre_llm_call - pre_gateway_dispatch