Compare commits

..
Author SHA1 Message Date
Emeriko d8f3ac1afd Sync ES and Korean READMEs with the Devin CLI addition
Follow-up to #318, which updated only the English README. Bumps the agent badge 14->15, adds a Devin CLI install section before OpenClaw, and lists Devin CLI in the skill-capable host line. Also adds Swival to both host lines, which the translations were already missing.
2026-06-26 02:32:19 +02:00
20 changed files with 17 additions and 575 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ponytail",
"version": "4.8.4",
"version": "4.8.3",
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
"author": {
"name": "Dietrich Gebert",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ponytail",
"version": "4.8.4",
"version": "4.8.3",
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
"author": {
"name": "Dietrich Gebert",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ponytail",
"version": "4.8.4",
"version": "4.8.3",
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
"author": {
"name": "Dietrich Gebert",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "ponytail",
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
"version": "4.8.4",
"version": "4.8.3",
"author": {
"name": "Dietrich Gebert",
"url": "https://github.com/DietrichGebert"
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: ponytail
description: "Lazy senior dev mode for any coding task (write, refactor, fix, review): YAGNI, stdlib first, no unrequested abstractions. Not for non-coding requests."
description: "Lazy senior dev mode. Forces the simplest, shortest solution that works: YAGNI, stdlib first, no unrequested abstractions."
homepage: https://github.com/DietrichGebert/ponytail
license: MIT
---
+2 -10
View File
@@ -15,7 +15,7 @@
<img src="https://img.shields.io/github/stars/DietrichGebert/ponytail?style=flat-square&color=111111&label=stars" alt="Stars">
<img src="https://img.shields.io/github/v/release/DietrichGebert/ponytail?style=flat-square&color=111111&label=release" alt="Release">
<img src="https://img.shields.io/npm/v/@dietrichgebert/ponytail?style=flat-square&color=111111&label=npm" alt="npm">
<img src="https://img.shields.io/badge/works%20with-16%20agents-111111?style=flat-square" alt="Works with 16 agents">
<img src="https://img.shields.io/badge/works%20with-15%20agents-111111?style=flat-square" alt="Works with 15 agents">
<img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
</p>
@@ -199,14 +199,6 @@ 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:<skill>`, and adds `/ponytail`, `/ponytail-review`, `/ponytail-audit`, `/ponytail-debt`, `/ponytail-gain`, and `/ponytail-help`. In shared gateways, restrict `/ponytail` to trusted users with Hermes slash-command access controls; runtime mode is process-local.
### CodeWhale
Reads `AGENTS.md` from the project root, zero setup. Copy [`AGENTS.md`](AGENTS.md) to your project, or run `codewhale` from a checkout of this repo. That's it.
@@ -280,7 +272,7 @@ These remove the plugin's own files. They leave behind a small amount of state p
| `/ponytail-gain` | Show the measured impact scoreboard (less code, less cost, more speed) from the benchmark. |
| `/ponytail-help` | Quick reference for the commands above. |
Commands need a skill-capable host (Claude Code, Codex, Devin CLI, OpenCode, Gemini, pi, Swival, 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.
Commands need a skill-capable host (Claude Code, Codex, Devin CLI, OpenCode, Gemini, pi, Swival). 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
-217
View File
@@ -1,217 +0,0 @@
"""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-gain": "Show the measured-impact scoreboard (less code, less cost, more speed).",
"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]",
)
-22
View File
@@ -1,22 +0,0 @@
# 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-gain`
- `/ponytail-help`
Bundled skills are available as `ponytail:ponytail`, `ponytail:ponytail-review`, `ponytail:ponytail-audit`, `ponytail:ponytail-debt`, `ponytail:ponytail-gain`, and `ponytail:ponytail-help`.
-2
View File
@@ -154,8 +154,6 @@ def main():
parsed_url = urllib.parse.urlparse(args.ollama_url)
if parsed_url.scheme not in ("http", "https"):
parser.error(f"Invalid --ollama-url scheme: '{parsed_url.scheme}'. Only 'http' and 'https' are supported.")
if not parsed_url.netloc:
parser.error(f"--ollama-url must include a host, e.g. http://localhost:11434 (got '{args.ollama_url}').")
run(args.model, args.repeat, args.ollama_url)
+1 -3
View File
@@ -4,9 +4,7 @@
module.exports = (output) => {
const text = String(output || '');
const blocks = [...text.matchAll(/```[a-zA-Z0-9_+-]*\n([\s\S]*?)```/g)].map((m) => m[1]);
// Drop /* ... */ block comments before counting; the line filter below only
// caught `*`-aligned JSDoc, so plain block comments were miscounted as code.
const code = (blocks.length ? blocks.join('\n') : text).replace(/\/\*[\s\S]*?\*\//g, '');
const code = blocks.length ? blocks.join('\n') : text;
const loc = code
.split('\n')
.map((l) => l.trim())
-22
View File
@@ -1,22 +0,0 @@
// Regression guard for loc.js comment handling. Run: node loc.test.js
const assert = require('assert');
const loc = require('./loc.js');
const score = (src) => loc(src).score;
let pass = 0;
const cases = [
// /* ... */ block comments must not count as code, whether or not the
// continuation lines are *-aligned (the old filter only caught JSDoc style).
['plain block comment not counted', score('```js\nfunction f() {\n /* explain\n the rest */\n return 1;\n}\n```'), 3],
['jsdoc block comment not counted', score('```js\nfunction g() {\n /*\n * explain\n */\n return 2;\n}\n```'), 3],
['inline block comment keeps its code line', score('```js\nconst x = 1; /* note */\nconst y = 2;\n```'), 2],
['line comments still stripped', score('```js\n// header\nconst x = 1;\n```'), 1],
['plain code unchanged', score('```js\nconst a = 1;\nconst b = 2;\n```'), 2],
];
for (const [name, got, want] of cases) {
assert.strictEqual(got, want, `FAILED: ${name} (got ${got}, want ${want})`);
console.log(`ok - ${name}`);
pass++;
}
console.log(`\n${pass}/${cases.length} passed`);
-1
View File
@@ -12,7 +12,6 @@ to load in a given agent.
| Codex | `.codex-plugin/plugin.json`, `hooks/claude-codex-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:<skill>`. |
| 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. The Claude/Codex hook map is not placed at Gemini's auto-discovered `hooks/hooks.json` path. |
| Cursor | `.cursor/rules/ponytail.mdc` | Always-on project rule. |
| Windsurf | `.windsurf/rules/ponytail.md` | Project rule. |
-38
View File
@@ -86,44 +86,6 @@ const debounce = (fn, ms) => (...args) => { clearTimeout(t); t = setTimeout(() =
---
## Swift / SwiftUI
UI components people reach for a library or a custom view for.
| You think you need | What the platform has |
|---|---|
| Date/time picker library | `DatePicker` |
| Color picker library | `ColorPicker` |
| Search bar + filtering | `.searchable(text:)` |
| Pull-to-refresh library | `.refreshable { }` |
| Swipe-to-delete / row actions | `.swipeActions { }` |
| Async image loading + cache | `AsyncImage` |
| Charting library | Swift Charts (`import Charts`) |
| Markdown rendering | `Text(...)` markdown / `AttributedString(markdown:)` |
| Share sheet wrapper | `ShareLink` |
| Loading spinner | `ProgressView()` |
| Photo picker | `PhotosPicker` |
| Map SDK (basic) | `Map` (MapKit for SwiftUI) |
| Grid layout library | `Grid` / `LazyVGrid` |
Frameworks and stdlib that wrappers wrap.
| You think you need | What the platform has |
|---|---|
| JSON library (SwiftyJSON) | `Codable` + `JSONDecoder` / `JSONEncoder` |
| HTTP client (Alamofire, simple use) | `URLSession` async/await; Alamofire earns it for complex retry/multipart at scale |
| Date/number/currency formatting | `.formatted()` / `FormatStyle` |
| Regex library | Swift regex literals + `Regex` |
| Crypto library (CryptoSwift) | `CryptoKit` |
| Keychain wrapper | Security `SecItem`; a few lines, not a dependency |
| Persistence / ORM | `SwiftData`, or `@AppStorage` for small key-values |
| Logging library | `Logger` (`os.log`) |
| UUID / Base64 helpers | `UUID()`, `Data(...).base64EncodedString()` |
| Image downsampling | ImageIO `CGImageSourceCreateThumbnailAtIndex` |
| Combine wrappers for async | async/await + `AsyncSequence` |
---
## Node.js Standard Library
Packages that wrap Node built-ins.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ponytail",
"version": "4.8.4",
"version": "4.8.3",
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
"contextFileName": "AGENTS.md"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@dietrichgebert/ponytail",
"version": "4.8.4",
"version": "4.8.3",
"description": "Lazy senior dev mode for AI agents. The best code is the code you never wrote.",
"keywords": ["opencode-plugin", "opencode", "ponytail", "pi-package", "pi", "skills"],
"license": "MIT",
-21
View File
@@ -1,21 +0,0 @@
name: ponytail
version: 4.8.4
description: Lazy senior dev mode for Hermes Agent, always-on context, bundled skills, and slash commands.
author: Salaamdev
provides_hooks:
- pre_llm_call
- pre_gateway_dispatch
provides_commands:
- ponytail
- ponytail-review
- ponytail-audit
- ponytail-debt
- ponytail-gain
- ponytail-help
provides_skills:
- ponytail
- ponytail-review
- ponytail-audit
- ponytail-debt
- ponytail-gain
- ponytail-help
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ponytail-mcp",
"version": "4.8.4",
"version": "4.8.3",
"description": "MCP server that serves Ponytail's lazy-senior-dev instructions as a prompt and a tool.",
"private": true,
"type": "module",
+1 -1
View File
@@ -17,7 +17,7 @@ const ROOT = path.join(__dirname, '..');
const HOMEPAGE = 'https://github.com/DietrichGebert/ponytail';
const DESCRIPTIONS = {
'ponytail': 'Lazy senior dev mode for any coding task (write, refactor, fix, review): YAGNI, stdlib first, no unrequested abstractions. Not for non-coding requests.',
'ponytail': 'Lazy senior dev mode. Forces the simplest, shortest solution that works: YAGNI, stdlib first, no unrequested abstractions.',
'ponytail-review': 'Review a diff for over-engineering. Finds what to delete: reinvented stdlib, needless deps, speculative abstractions. One line per finding.',
'ponytail-audit': 'Audit the whole repo for over-engineering. A ranked list of what to delete, simplify, or replace with stdlib or native features.',
'ponytail-debt': 'Harvest every ponytail: shortcut comment into one debt ledger, so deferrals get tracked instead of forgotten. One-shot report.',
+5 -8
View File
@@ -5,14 +5,11 @@ description: >
minimal. Channels a senior dev who has seen everything: question whether the
task needs to exist at all (YAGNI), reach for the standard library before
custom code, native platform features before dependencies, one line before
fifty. Supports intensity levels: lite, full (default), ultra. Use on ANY
coding task: writing, adding, refactoring, fixing, reviewing, or designing
code, and choosing libraries or dependencies. Also use whenever the user
says "ponytail", "be lazy", "lazy mode", "simplest solution", "minimal
solution", "yagni", "do less", or "shortest path", or complains about
over-engineering, bloat, boilerplate, or unnecessary dependencies. Do NOT
use for non-coding requests (general knowledge, prose, translation,
summaries, recipes).
fifty. Supports intensity levels: lite, full (default), ultra. Use whenever
the user says "ponytail", "be lazy", "lazy mode", "simplest solution",
"minimal solution", "yagni", "do less", or "shortest path", and whenever
they complain about over-engineering, bloat, boilerplate, or unnecessary
dependencies.
argument-hint: "[lite|full|ultra]"
license: MIT
---
-222
View File
@@ -1,222 +0,0 @@
#!/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-gain', '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-gain',
'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: -<N> 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);
});