Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16f6cbf4b8 | ||
|
|
85cc1d9935 | ||
|
|
bc9ee949d5 | ||
|
|
c99757a14a | ||
|
|
4b7a012626 | ||
|
|
801d976c69 | ||
|
|
adad1e451a | ||
|
|
0e3fd0cfee | ||
|
|
c4d1925ae9 | ||
|
|
6eb2b7829c | ||
|
|
ac75159e5e | ||
|
|
8d154e6c2a | ||
|
|
e353a1a5d3 | ||
|
|
7147937ae8 | ||
|
|
33a4977a83 | ||
|
|
7790c37b67 | ||
|
|
203f5fde58 | ||
|
|
731d3193e7 | ||
|
|
b4f725f7ce | ||
|
|
ce55fd460a | ||
|
|
4198fc30ac |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail",
|
"name": "ponytail",
|
||||||
"version": "4.8.3",
|
"version": "4.8.4",
|
||||||
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Dietrich Gebert",
|
"name": "Dietrich Gebert",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail",
|
"name": "ponytail",
|
||||||
"version": "4.8.3",
|
"version": "4.8.4",
|
||||||
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Dietrich Gebert",
|
"name": "Dietrich Gebert",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "ponytail",
|
||||||
|
"version": "4.8.4",
|
||||||
|
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
||||||
|
"author": {
|
||||||
|
"name": "Dietrich Gebert",
|
||||||
|
"url": "https://github.com/DietrichGebert"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/DietrichGebert/ponytail",
|
||||||
|
"repository": "https://github.com/DietrichGebert/ponytail",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": ["yagni", "minimalism", "code-review", "productivity"]
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail",
|
"name": "ponytail",
|
||||||
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
||||||
"version": "4.8.3",
|
"version": "4.8.4",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Dietrich Gebert",
|
"name": "Dietrich Gebert",
|
||||||
"url": "https://github.com/DietrichGebert"
|
"url": "https://github.com/DietrichGebert"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: ponytail
|
name: ponytail
|
||||||
description: "Lazy senior dev mode. Forces the simplest, shortest solution that works: YAGNI, stdlib first, no unrequested abstractions."
|
description: "Lazy senior dev mode for any coding task (write, refactor, fix, review): YAGNI, stdlib first, no unrequested abstractions. Not for non-coding requests."
|
||||||
homepage: https://github.com/DietrichGebert/ponytail
|
homepage: https://github.com/DietrichGebert/ponytail
|
||||||
license: MIT
|
license: MIT
|
||||||
---
|
---
|
||||||
|
|||||||
+10
-2
@@ -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/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/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/npm/v/@dietrichgebert/ponytail?style=flat-square&color=111111&label=npm" alt="npm">
|
||||||
<img src="https://img.shields.io/badge/funciona%20con-14%20agentes-111111?style=flat-square" alt="Works with 14 agents">
|
<img src="https://img.shields.io/badge/funciona%20con-15%20agentes-111111?style=flat-square" alt="Works with 15 agents">
|
||||||
<img src="https://img.shields.io/badge/licencia-MIT-111111?style=flat-square" alt="MIT license">
|
<img src="https://img.shields.io/badge/licencia-MIT-111111?style=flat-square" alt="MIT license">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -198,6 +198,14 @@ Reutiliza el `gemini-extension.json` de este repo. Una diferencia: Antigravity c
|
|||||||
|
|
||||||
Lee `AGENTS.md` desde la raíz del proyecto, sin configuración. Copia [`AGENTS.md`](AGENTS.md) a tu proyecto, o ejecuta `codewhale` desde un checkout de este repo. Eso es todo.
|
Lee `AGENTS.md` desde la raíz del proyecto, sin configuración. Copia [`AGENTS.md`](AGENTS.md) a tu proyecto, o ejecuta `codewhale` desde un checkout de este repo. Eso es todo.
|
||||||
|
|
||||||
|
### Devin CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
devin plugins install DietrichGebert/ponytail
|
||||||
|
```
|
||||||
|
|
||||||
|
Instala ponytail como plugin de Devin; los skills quedan disponibles como `/ponytail:ponytail`, `/ponytail:ponytail-review`, etc.
|
||||||
|
|
||||||
### OpenClaw
|
### OpenClaw
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -232,7 +240,7 @@ Qué archivos corresponden a qué agente: [Portabilidad de agentes](docs/agent-p
|
|||||||
| `/ponytail-debt` | Recolecta los atajos marcados con `ponytail:` que dejaste pendientes en un registro, para que "después" no se convierta en "nunca". |
|
| `/ponytail-debt` | Recolecta los atajos marcados con `ponytail:` que dejaste pendientes en un registro, para que "después" no se convierta en "nunca". |
|
||||||
| `/ponytail-help` | Referencia rápida de los comandos anteriores. |
|
| `/ponytail-help` | Referencia rápida de los comandos anteriores. |
|
||||||
|
|
||||||
Los comandos requieren un host compatible con skills (Claude Code, Codex, OpenCode, Gemini, pi). En Codex son skills; se invocan con `@` (`@ponytail-review`). Los adaptadores de solo instrucciones (Cursor, Windsurf, Cline, Copilot, Kiro, Antigravity) cargan el ruleset permanente sin los comandos.
|
Los comandos requieren un host compatible con skills (Claude Code, Codex, Devin CLI, OpenCode, Gemini, pi, Swival). En Codex son skills; se invocan con `@` (`@ponytail-review`). Los adaptadores de solo instrucciones (Cursor, Windsurf, Cline, Copilot, Kiro, Antigravity) cargan el ruleset permanente sin los comandos.
|
||||||
|
|
||||||
## Desarrollo
|
## Desarrollo
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -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/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/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/npm/v/@dietrichgebert/ponytail?style=flat-square&color=111111&label=npm" alt="npm">
|
||||||
<img src="https://img.shields.io/badge/works%20with-14%20agents-111111?style=flat-square" alt="Works with 14 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">
|
<img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -217,6 +217,14 @@ Swival도 프로젝트 루트의 `AGENTS.md`와 전역의 `~/.config/swival/AGEN
|
|||||||
|
|
||||||
명령줄에서는 `$` 접두사로 스킬을 명시적으로 켠다. 예: `$ponytail-review`.
|
명령줄에서는 `$` 접두사로 스킬을 명시적으로 켠다. 예: `$ponytail-review`.
|
||||||
|
|
||||||
|
### Devin CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
devin plugins install DietrichGebert/ponytail
|
||||||
|
```
|
||||||
|
|
||||||
|
ponytail을 Devin 플러그인으로 설치한다. 스킬은 `/ponytail:ponytail`, `/ponytail:ponytail-review` 등으로 쓸 수 있다.
|
||||||
|
|
||||||
### OpenClaw
|
### OpenClaw
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -252,7 +260,7 @@ Codex 확장을 쓰는 VS Code는 이 저장소가 함께 싣는 `AGENTS.md`를
|
|||||||
| `/ponytail-gain` | 벤치마크로 잰 효과 스코어보드(코드 절감, 비용 절감, 속도 향상)를 보여 준다. |
|
| `/ponytail-gain` | 벤치마크로 잰 효과 스코어보드(코드 절감, 비용 절감, 속도 향상)를 보여 준다. |
|
||||||
| `/ponytail-help` | 위 명령들의 빠른 참조. |
|
| `/ponytail-help` | 위 명령들의 빠른 참조. |
|
||||||
|
|
||||||
명령들은 스킬을 지원하는 호스트가 있어야 돈다(Claude Code, Codex, OpenCode, Gemini, pi). Codex에선 스킬이라 `@`로 부른다(`@ponytail-review`). 지시문 전용 어댑터(Cursor, Windsurf, Cline, Copilot, Kiro, Antigravity)는 명령 없이 늘 켜진 룰셋만 불러온다.
|
명령들은 스킬을 지원하는 호스트가 있어야 돈다(Claude Code, Codex, Devin CLI, OpenCode, Gemini, pi, Swival). Codex에선 스킬이라 `@`로 부른다(`@ponytail-review`). 지시문 전용 어댑터(Cursor, Windsurf, Cline, Copilot, Kiro, Antigravity)는 명령 없이 늘 켜진 룰셋만 불러온다.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -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/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/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/npm/v/@dietrichgebert/ponytail?style=flat-square&color=111111&label=npm" alt="npm">
|
||||||
<img src="https://img.shields.io/badge/works%20with-14%20agents-111111?style=flat-square" alt="Works with 14 agents">
|
<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/license-MIT-111111?style=flat-square" alt="MIT license">
|
<img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -199,6 +199,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/`.
|
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
|
### 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.
|
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.
|
||||||
@@ -217,6 +225,14 @@ Swival also reads `AGENTS.md` from the project root and `~/.config/swival/AGENTS
|
|||||||
|
|
||||||
On the command line, use a `$` prefix to explicitly activate a skill. For example: `$ponytail-review`.
|
On the command line, use a `$` prefix to explicitly activate a skill. For example: `$ponytail-review`.
|
||||||
|
|
||||||
|
### Devin CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
devin plugins install DietrichGebert/ponytail
|
||||||
|
```
|
||||||
|
|
||||||
|
Installs ponytail as a Devin plugin; skills are available as `/ponytail:ponytail`, `/ponytail:ponytail-review`, and so on.
|
||||||
|
|
||||||
### OpenClaw
|
### OpenClaw
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -247,6 +263,7 @@ Which files map to which agent: [Agent portability](docs/agent-portability.md).
|
|||||||
|------|---------|
|
|------|---------|
|
||||||
| Claude Code | `/plugin remove ponytail` |
|
| Claude Code | `/plugin remove ponytail` |
|
||||||
| Codex | `codex plugin remove ponytail` |
|
| Codex | `codex plugin remove ponytail` |
|
||||||
|
| Devin CLI | `devin plugins remove ponytail` |
|
||||||
| Pi agent | `pi uninstall ponytail` |
|
| Pi agent | `pi uninstall ponytail` |
|
||||||
| Cursor / Windsurf / Cline / etc. | Delete the copied rule file |
|
| Cursor / Windsurf / Cline / etc. | Delete the copied rule file |
|
||||||
|
|
||||||
@@ -263,7 +280,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-gain` | Show the measured impact scoreboard (less code, less cost, more speed) from the benchmark. |
|
||||||
| `/ponytail-help` | Quick reference for the commands above. |
|
| `/ponytail-help` | Quick reference for the commands above. |
|
||||||
|
|
||||||
Commands need a skill-capable host (Claude Code, Codex, 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.
|
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.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+217
@@ -0,0 +1,217 @@
|
|||||||
|
"""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]",
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# 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`.
|
||||||
@@ -70,6 +70,22 @@ Versus baseline, ponytail writes **80-94% less code**, costs **42-75% less**, an
|
|||||||
> while the bare "one-liner" prompt drops a guard. That is the honest, defensible number. See
|
> while the bare "one-liner" prompt drops a guard. That is the honest, defensible number. See
|
||||||
> [results/2026-06-18-agentic.md](results/2026-06-18-agentic.md).
|
> [results/2026-06-18-agentic.md](results/2026-06-18-agentic.md).
|
||||||
|
|
||||||
|
## Independent benchmarks
|
||||||
|
|
||||||
|
Run by other people, not by us, on their own harnesses and machines. Linked for
|
||||||
|
transparency: the numbers are theirs, may shift between runs, and are corroboration
|
||||||
|
rather than official figures. Only plugin-installed runs are listed, since pasting
|
||||||
|
`SKILL.md` into a prompt is a rough approximation of `full` and skews the result.
|
||||||
|
|
||||||
|
| Source | Method | Headline | Date |
|
||||||
|
|---|---|---|---|
|
||||||
|
| [KuldeepB19](https://kuldeepb19.github.io/ponytail-benchmark/) | Installed plugin, 24 tasks, no-skill vs Lite/Full/Ultra, 5 runs each (480 builds), Opus 4.8, graded by executing the code | ~44% less code (53% fewer statements), no correctness or security regression; trims everyday bad-input handling on 5/24 tasks | 2026-06-24 |
|
||||||
|
| [RicardoCostaGit](https://github.com/RicardoCostaGit/ponytail-benchmark-from-cursor) | Multi-turn agentic runs via the Cursor SDK, isolated git worktrees, rule file toggled per run | Leaner output but higher process cost (more tool calls/tokens) on large completion-forced tasks; savings land on blocked/snowball-prone tasks | 2026-06-16 |
|
||||||
|
|
||||||
|
Both land on the same split as the honesty note above: ponytail reliably writes less
|
||||||
|
code, and whether that *saves money* depends on the workload (big win on
|
||||||
|
over-build and blocked tasks, can cost more on large completion-forced agentic runs).
|
||||||
|
|
||||||
## Metrics
|
## Metrics
|
||||||
|
|
||||||
| File | Metric | Behavior |
|
| File | Metric | Behavior |
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ def main():
|
|||||||
parsed_url = urllib.parse.urlparse(args.ollama_url)
|
parsed_url = urllib.parse.urlparse(args.ollama_url)
|
||||||
if parsed_url.scheme not in ("http", "https"):
|
if parsed_url.scheme not in ("http", "https"):
|
||||||
parser.error(f"Invalid --ollama-url scheme: '{parsed_url.scheme}'. Only 'http' and 'https' are supported.")
|
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)
|
run(args.model, args.repeat, args.ollama_url)
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -4,7 +4,9 @@
|
|||||||
module.exports = (output) => {
|
module.exports = (output) => {
|
||||||
const text = String(output || '');
|
const text = String(output || '');
|
||||||
const blocks = [...text.matchAll(/```[a-zA-Z0-9_+-]*\n([\s\S]*?)```/g)].map((m) => m[1]);
|
const blocks = [...text.matchAll(/```[a-zA-Z0-9_+-]*\n([\s\S]*?)```/g)].map((m) => m[1]);
|
||||||
const code = blocks.length ? blocks.join('\n') : text;
|
// 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 loc = code
|
const loc = code
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((l) => l.trim())
|
.map((l) => l.trim())
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// 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`);
|
||||||
@@ -12,6 +12,7 @@ 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| Cursor | `.cursor/rules/ponytail.mdc` | Always-on project rule. |
|
||||||
| Windsurf | `.windsurf/rules/ponytail.md` | Project rule. |
|
| Windsurf | `.windsurf/rules/ponytail.md` | Project rule. |
|
||||||
|
|||||||
@@ -86,6 +86,44 @@ 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
|
## Node.js Standard Library
|
||||||
|
|
||||||
Packages that wrap Node built-ins.
|
Packages that wrap Node built-ins.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail",
|
"name": "ponytail",
|
||||||
"version": "4.8.3",
|
"version": "4.8.4",
|
||||||
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
||||||
"contextFileName": "AGENTS.md"
|
"contextFileName": "AGENTS.md"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dietrichgebert/ponytail",
|
"name": "@dietrichgebert/ponytail",
|
||||||
"version": "4.8.3",
|
"version": "4.8.4",
|
||||||
"description": "Lazy senior dev mode for AI agents. The best code is the code you never wrote.",
|
"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"],
|
"keywords": ["opencode-plugin", "opencode", "ponytail", "pi-package", "pi", "skills"],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail-mcp",
|
"name": "ponytail-mcp",
|
||||||
"version": "4.8.3",
|
"version": "4.8.4",
|
||||||
"description": "MCP server that serves Ponytail's lazy-senior-dev instructions as a prompt and a tool.",
|
"description": "MCP server that serves Ponytail's lazy-senior-dev instructions as a prompt and a tool.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const ROOT = path.join(__dirname, '..');
|
|||||||
const HOMEPAGE = 'https://github.com/DietrichGebert/ponytail';
|
const HOMEPAGE = 'https://github.com/DietrichGebert/ponytail';
|
||||||
|
|
||||||
const DESCRIPTIONS = {
|
const DESCRIPTIONS = {
|
||||||
'ponytail': 'Lazy senior dev mode. Forces the simplest, shortest solution that works: YAGNI, stdlib first, no unrequested abstractions.',
|
'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-review': 'Review a diff for over-engineering. Finds what to delete: reinvented stdlib, needless deps, speculative abstractions. One line per finding.',
|
'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-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.',
|
'ponytail-debt': 'Harvest every ponytail: shortcut comment into one debt ledger, so deferrals get tracked instead of forgotten. One-shot report.',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// Version-consistency guard. Ponytail declares its version in six files across
|
// Version-consistency guard. Ponytail declares its version in seven files across
|
||||||
// four host ecosystems, and every release bumps all of them by hand.
|
// five host ecosystems, and every release bumps all of them by hand.
|
||||||
//
|
//
|
||||||
// tests/gemini-extension.test.js already checks the four plugin manifests agree
|
// tests/gemini-extension.test.js already checks the four plugin manifests agree
|
||||||
// with each other, but that can't catch the failure mode that shipped in v4.8.0:
|
// with each other, but that can't catch the failure mode that shipped in v4.8.0:
|
||||||
@@ -21,6 +21,7 @@ const PINNED_SEMVER = /^\d+\.\d+\.\d+$/;
|
|||||||
const VERSION_FILES = [
|
const VERSION_FILES = [
|
||||||
'.claude-plugin/plugin.json', // Claude Code plugin — what users install
|
'.claude-plugin/plugin.json', // Claude Code plugin — what users install
|
||||||
'.codex-plugin/plugin.json', // Codex plugin
|
'.codex-plugin/plugin.json', // Codex plugin
|
||||||
|
'.devin-plugin/plugin.json', // Devin CLI plugin
|
||||||
'.github/plugin/plugin.json', // Copilot plugin
|
'.github/plugin/plugin.json', // Copilot plugin
|
||||||
'gemini-extension.json', // Gemini CLI extension
|
'gemini-extension.json', // Gemini CLI extension
|
||||||
'package.json', // pi-package / repo root
|
'package.json', // pi-package / repo root
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ description: >
|
|||||||
minimal. Channels a senior dev who has seen everything: question whether the
|
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
|
task needs to exist at all (YAGNI), reach for the standard library before
|
||||||
custom code, native platform features before dependencies, one line before
|
custom code, native platform features before dependencies, one line before
|
||||||
fifty. Supports intensity levels: lite, full (default), ultra. Use whenever
|
fifty. Supports intensity levels: lite, full (default), ultra. Use on ANY
|
||||||
the user says "ponytail", "be lazy", "lazy mode", "simplest solution",
|
coding task: writing, adding, refactoring, fixing, reviewing, or designing
|
||||||
"minimal solution", "yagni", "do less", or "shortest path", and whenever
|
code, and choosing libraries or dependencies. Also use whenever the user
|
||||||
they complain about over-engineering, bloat, boilerplate, or unnecessary
|
says "ponytail", "be lazy", "lazy mode", "simplest solution", "minimal
|
||||||
dependencies.
|
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).
|
||||||
argument-hint: "[lite|full|ultra]"
|
argument-hint: "[lite|full|ultra]"
|
||||||
license: MIT
|
license: MIT
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const REQUIRED_COMMAND_FILES = [
|
|||||||
'ponytail-review.toml',
|
'ponytail-review.toml',
|
||||||
'ponytail-audit.toml',
|
'ponytail-audit.toml',
|
||||||
'ponytail-debt.toml',
|
'ponytail-debt.toml',
|
||||||
|
'ponytail-gain.toml',
|
||||||
|
'ponytail-help.toml',
|
||||||
];
|
];
|
||||||
|
|
||||||
function readJSON(relPath) {
|
function readJSON(relPath) {
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
#!/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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user