Compare commits

...
Author SHA1 Message Date
Emeriko c99757a14a release: bump plugin.yaml (Hermes manifest) to 4.8.4 2026-06-29 07:19:46 +02:00
Emeriko 4b7a012626 release: bump .devin-plugin to 4.8.4 2026-06-29 07:15:31 +02:00
Emeriko 801d976c69 release: 4.8.4 2026-06-29 07:12:58 +02:00
DietrichGebert adad1e451a Merge pull request #447 from DietrichGebert/fix-skill-trigger-recall
Trigger ponytail skill on any coding task, not just keywords
2026-06-29 07:08:50 +02:00
Emeriko 0e3fd0cfee Trigger ponytail skill on any coding task, not just keywords
The skill description was written keyword-first ("use when the user
says lazy / complains about boilerplate"), so a model-invoked host
only loaded it on prompts that hit those words. Plain coding tasks
(add a date picker, write a dedupe function, build a cache, fix a bug)
were skipped, exactly the over-build cases ponytail is best at.

Tested via a skill-router proxy (3 runs, 12 labeled prompts): recall
on coding tasks was 2/6 before, 6/6 after, with precision unchanged at
6/6 (no false fires on non-coding prompts). Fix is one clause naming
coding tasks explicitly plus a negative clause to keep precision.

Trigger descriptions live in two source-of-truth spots, both updated:
skills/ponytail/SKILL.md and the DESCRIPTIONS map in
scripts/build-openclaw-skills.js (.openclaw regenerated). The store
blurbs in the plugin manifests are listing metadata, not triggers, so
they are left alone.
2026-06-29 07:06:59 +02:00
DietrichGebert c4d1925ae9 Merge pull request #323 from DietrichGebert/hermes-plugin
feat: add Hermes Agent plugin (rebuilt from #78)
2026-06-26 03:28:08 +02:00
Emeriko 6eb2b7829c Merge branch 'main' into salaamdev/main
# Conflicts:
#	README.md
2026-06-26 03:24:37 +02:00
Emeriko ac75159e5e Merge branch 'main' into salaamdev/main
# Conflicts:
#	README.md
2026-06-26 03:08:24 +02:00
Lakshya Sharma 8d154e6c2a fix(benchmarks): strip block comments before counting LOC (#232)
loc.js filtered comments line by line, so /* ... */ block comments whose
continuation lines are not *-aligned had their inner lines counted as code.
A plain indented block comment scored higher than the same code written
JSDoc-style. Strip /* ... */ before the line count so both are equal, and
add loc.test.js to lock the behavior.

Fixes #231
2026-06-26 03:02:55 +02:00
systemBlueandClaude Opus 4.8 e353a1a5d3 docs: add Swift/SwiftUI section to platform-native.md (#313)
Closes #299. The "does the platform already do this?" rung is especially
rich on Apple platforms, but the doc had no Swift coverage. Adds a
Swift/SwiftUI section in the existing two-table format: SwiftUI controls
and the Swift/framework layer, with library carve-outs kept inline to
match the `ms` / `requests` style already in the doc.


Claude-Session: https://claude.ai/code/session_01MtGgxEbiXPQjqMyXWbnixn

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 02:38:34 +02:00
DietrichGebert 7147937ae8 Sync ES and Korean READMEs with the Devin CLI addition (#322)
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:33:56 +02:00
Hamza Ali Shahjahan 33a4977a83 fix(benchmark): reject --ollama-url without a host (#315)
Follow-up to #274: the merged scheme validation accepts host-less URLs (http://, https://), which then fail opaquely inside urlopen(). Add a minimal netloc check after the existing scheme check, so a missing host is rejected up front with a clear argparse error. Additive; keeps the existing scheme message intact.
2026-06-26 02:28:51 +02:00
topherandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 7790c37b67 Add Devin CLI plugin manifest (#318)
Adds `.devin-plugin/plugin.json` so the plugin can be installed with
`devin plugins install DietrichGebert/ponytail`. The skills/ directory
is already at the repo root, which is exactly where Devin expects it.

Registers the new manifest in `scripts/check-versions.js` so it stays
in sync with every other host manifest on release.

Updates README: install/uninstall instructions, badge (14 → 15 agents),
and Devin added to the skill-capable hosts list in Commands.

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-26 02:26:13 +02:00
DietrichGebert 203f5fde58 Add a Sponsors section with GreenPT as the first sponsor (#321) 2026-06-26 00:23:00 +02:00
DietrichGebertandClaude Opus 4.8 64adbf9544 Use an announcement banner instead of the badge (#316)
Replace the flat orange badge with a clickable terminal-style banner
image: the "he's building" sticker, a bold headline, and an orange
JOIN THE WAITLIST call to action. Localized banners for EN/ES/KO.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:03:58 +02:00
DietrichGebertandClaude Opus 4.8 7086abecc2 Make the waitlist teaser pop and translate it (#314)
The blue NOTE callout was too quiet. Replace it with a centered bold
line plus a bright orange "join the waitlist" badge (matches the site),
and add the same banner to the Spanish and Korean READMEs.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:13:53 +02:00
DietrichGebertandClaude Opus 4.8 4e5f9ccc6c Add a waitlist teaser to the README (#312)
Promotes the pre-launch waitlist at ponytail.dev/soon with a NOTE
callout just under the header.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:05:42 +02:00
DietrichGebertandClaude Opus 4.8 a945778b4a docs: add Star History chart to end of READMEs (#294)
Appends a star-history.com chart (light/dark via <picture>) to the end of
the English, Spanish, and Korean READMEs. Heading follows each file's
convention: translated in Spanish ("Historial de estrellas"), English
elsewhere.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:37:06 +02:00
DietrichGebertandClaude Opus 4.8 6cd0c42e86 docs: add Trendshift daily and weekly badges to READMEs (#293)
Adds the daily and weekly Trendshift badges to the header badge area of
the English, Spanish, and Korean READMEs. Alt text decoded from %2F to a
plain slash for screen readers.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:30:26 +02:00
DietrichGebert 025da371cd release: 4.8.3 (#286) 2026-06-24 04:32:12 +02:00
salaamdev 731d3193e7 Merge branch 'main' into main 2026-06-19 05:58:49 +03:00
salaamdev b4f725f7ce Merge branch 'main' into main 2026-06-18 10:45:26 +03:00
salaamdev ce55fd460a Update author in plugin.yaml to Salaamdev 2026-06-15 23:13:09 +03:00
Abdisalam Hassan 4198fc30ac add Hermes plugin for Ponytail with command support and documentation 2026-06-15 19:57:45 +00:00
28 changed files with 769 additions and 28 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ponytail",
"version": "4.8.2",
"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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ponytail",
"version": "4.8.2",
"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",
+13
View File
@@ -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 -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.2",
"version": "4.8.4",
"author": {
"name": "Dietrich Gebert",
"url": "https://github.com/DietrichGebert"
+1 -1
View File
@@ -1,6 +1,6 @@
---
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
license: MIT
---
+40 -2
View File
@@ -15,10 +15,15 @@
<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/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">
</p>
<p align="center">
<a href="https://trendshift.io/repositories/50668" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/50668/daily" alt="DietrichGebert/ponytail | Trendshift" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/50668" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/50668/weekly" alt="DietrichGebert/ponytail | Trendshift" width="250" height="55"/></a>
</p>
<p align="center">
<strong>~54% menos código (hasta 94%) &middot; ~20% más barato &middot; ~27% más rápido &middot; 100% seguro</strong><br>
<sub>Medido en sesiones reales de Claude Code editando un repo open-source real (FastAPI + React), contra el mismo agente sin skill. ~54% es el promedio de 12 tareas de feature (Haiku 4.5, n=4); llega al 94% cuando un agente sobre-construye (un selector de fechas) y es casi cero cuando el código ya es mínimo. ponytail mantiene cada guarda de seguridad, mientras que un prompt pelado de "escribe one-liners" se salta una. (El benchmark anterior de un solo disparo reportaba 80-94% como cifra plana; contra un baseline agéntico justo, ese es el techo por tarea, no el promedio.) <a href="benchmarks/results/2026-06-18-agentic.md">Reporte completo</a> &middot; <a href="benchmarks/">reprodúcelo</a>.</sub>
@@ -30,6 +35,10 @@
---
<p align="center">
<a href="https://ponytail.dev/soon"><img src="assets/waitlist-banner-es.png" alt="Algo nuevo está por llegar, únete a la lista" width="760"></a>
</p>
Lo conoces. Cola de caballo larga. Lentes ovalados. Lleva más tiempo en la empresa que el control de versiones. Le muestras cincuenta líneas; las mira, no dice nada, y las reemplaza por una.
Ponytail lo pone dentro de tu agente de IA.
@@ -189,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.
### 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
```bash
@@ -223,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-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
@@ -252,6 +269,27 @@ El código que nunca escribiste escala infinitamente. Cero bugs, cero CVEs, 100%
**¿Por qué "ponytail"?**
Ya sabes exactamente por qué.
## Patrocinadores
<p align="center">
<a href="https://greenpt.com/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-greenpt-dark.svg">
<img src="assets/logo-greenpt.svg" width="260" alt="GreenPT">
</picture>
</a>
</p>
## Licencia
[MIT](LICENSE). La licencia más corta que funciona.
## Historial de estrellas
<a href="https://www.star-history.com/dietrichgebert/ponytail#history">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=DietrichGebert/ponytail&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=DietrichGebert/ponytail&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=DietrichGebert/ponytail&type=Date" />
</picture>
</a>
+40 -2
View File
@@ -15,10 +15,15 @@
<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-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">
</p>
<p align="center">
<a href="https://trendshift.io/repositories/50668" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/50668/daily" alt="DietrichGebert/ponytail | Trendshift" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/50668" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/50668/weekly" alt="DietrichGebert/ponytail | Trendshift" width="250" height="55"/></a>
</p>
<p align="center">
<strong>코드 약 54% 감소(최대 94%) &middot; 약 20% 저렴 &middot; 약 27% 빠름 &middot; 100% 안전</strong><br>
<sub>실제 오픈소스 저장소(FastAPI + React)를 고치는 실제 Claude Code 세션에서, 스킬을 끈 같은 에이전트와 견줘 측정했다. 약 54%는 기능 작업 12건의 평균이다(Haiku 4.5, n=4). 에이전트가 과하게 짤 여지가 있는 곳(날짜 선택기)에선 94%까지 오르고, 코드가 이미 최소한인 곳에선 0에 가깝다. ponytail은 안전 가드를 하나도 빼놓지 않지만, 그냥 "한 줄로 써"라고만 시킨 프롬프트는 그중 하나를 놓친다. (예전 단발성 벤치마크는 80-94%를 단일 수치로 내세웠는데, 공정한 에이전트 기준선에 견주면 그건 평균이 아니라 작업별 상한이다.) <a href="benchmarks/results/2026-06-18-agentic.md">전체 보고서</a> &middot; <a href="benchmarks/">직접 재현하기</a>.</sub>
@@ -30,6 +35,10 @@
---
<p align="center">
<a href="https://ponytail.dev/soon"><img src="assets/waitlist-banner-ko.png" alt="새로운 것이 다가오고 있습니다, 대기자 명단 신청" width="760"></a>
</p>
이런 사람, 다들 알 거다. 긴 포니테일에 타원형 안경. 버전 관리 시스템보다 회사에 오래 있었다. 코드 쉰 줄을 들이밀면 잠깐 보더니, 말없이 한 줄로 바꿔 놓는다.
Ponytail은 그를 당신의 AI 에이전트 안에 앉혀 둔다.
@@ -208,6 +217,14 @@ Swival도 프로젝트 루트의 `AGENTS.md`와 전역의 `~/.config/swival/AGEN
명령줄에서는 `$` 접두사로 스킬을 명시적으로 켠다. 예: `$ponytail-review`.
### Devin CLI
```bash
devin plugins install DietrichGebert/ponytail
```
ponytail을 Devin 플러그인으로 설치한다. 스킬은 `/ponytail:ponytail`, `/ponytail:ponytail-review` 등으로 쓸 수 있다.
### OpenClaw
```bash
@@ -243,7 +260,7 @@ Codex 확장을 쓰는 VS Code는 이 저장소가 함께 싣는 `AGENTS.md`를
| `/ponytail-gain` | 벤치마크로 잰 효과 스코어보드(코드 절감, 비용 절감, 속도 향상)를 보여 준다. |
| `/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
@@ -272,6 +289,27 @@ OpenClaw 스킬 패키지(`.openclaw/skills/`)는 `skills/`에서 생성된다.
**왜 하필 "ponytail"인가?**
당신은 이유를 정확히 안다.
## Sponsors
<p align="center">
<a href="https://greenpt.com/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-greenpt-dark.svg">
<img src="assets/logo-greenpt.svg" width="260" alt="GreenPT">
</picture>
</a>
</p>
## License
[MIT](LICENSE). 돌아가는 가장 짧은 라이선스.
## Star History
<a href="https://www.star-history.com/dietrichgebert/ponytail#history">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=DietrichGebert/ponytail&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=DietrichGebert/ponytail&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=DietrichGebert/ponytail&type=Date" />
</picture>
</a>
+49 -2
View File
@@ -15,10 +15,15 @@
<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-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">
</p>
<p align="center">
<a href="https://trendshift.io/repositories/50668" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/50668/daily" alt="DietrichGebert/ponytail | Trendshift" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/50668" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/50668/weekly" alt="DietrichGebert/ponytail | Trendshift" width="250" height="55"/></a>
</p>
<p align="center">
<strong>~54% less code (up to 94%) &middot; ~20% cheaper &middot; ~27% faster &middot; 100% safe</strong><br>
<sub>Measured on real Claude Code sessions editing a real open-source repo (FastAPI + React), against the same agent with no skill. ~54% is the mean across 12 feature tasks (Haiku 4.5, n=4); it reaches 94% where an agent over-builds (a date picker) and is near zero where the code is already minimal. ponytail keeps every safety guard while a bare "write one-liners" prompt drops one. (The earlier single-shot benchmark reported 80-94% as a flat figure; against a fair agentic baseline that is the per-task ceiling, not the average.) <a href="benchmarks/results/2026-06-18-agentic.md">Full writeup</a> &middot; <a href="benchmarks/">reproduce it</a>.</sub>
@@ -30,6 +35,10 @@
---
<p align="center">
<a href="https://ponytail.dev/soon"><img src="assets/waitlist-banner.png" alt="Something's coming, join the waitlist" width="760"></a>
</p>
You know him. Long ponytail. Oval glasses. Has been at the company longer than the version control. You show him fifty lines; he looks at them, says nothing, and replaces them with one.
Ponytail puts him inside your AI agent.
@@ -190,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/`.
### 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.
@@ -208,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`.
### 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
```bash
@@ -238,6 +263,7 @@ Which files map to which agent: [Agent portability](docs/agent-portability.md).
|------|---------|
| Claude Code | `/plugin remove ponytail` |
| Codex | `codex plugin remove ponytail` |
| Devin CLI | `devin plugins remove ponytail` |
| Pi agent | `pi uninstall ponytail` |
| Cursor / Windsurf / Cline / etc. | Delete the copied rule file |
@@ -254,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-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
@@ -283,6 +309,27 @@ The code you never wrote scales infinitely. Zero bugs, zero CVEs, 100% uptime si
**Why "ponytail"?**
You know exactly why.
## Sponsors
<p align="center">
<a href="https://greenpt.com/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-greenpt-dark.svg">
<img src="assets/logo-greenpt.svg" width="260" alt="GreenPT">
</picture>
</a>
</p>
## License
[MIT](LICENSE). The shortest license that works.
## Star History
<a href="https://www.star-history.com/dietrichgebert/ponytail#history">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=DietrichGebert/ponytail&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=DietrichGebert/ponytail&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=DietrichGebert/ponytail&type=Date" />
</picture>
</a>
+217
View File
@@ -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]",
)
+22
View File
@@ -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`.
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="394px" height="86px" viewBox="0 0 394 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>logo-greengpt-white</title>
<defs>
<linearGradient x1="46.5850926%" y1="13.9833492%" x2="59.2369668%" y2="85.5111279%" id="linearGradient-1">
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0.3" offset="100%"></stop>
</linearGradient>
</defs>
<g id="logo-greengpt-white" stroke="none" fill="none">
<g id="GreenPT" stroke-width="1" fill-rule="evenodd" transform="translate(108, 18)" fill="#FFFFFF">
<path d="M50,22.2527473 L25.7261411,22.2527473 L25.7261411,31.8681319 L38.6583679,31.8681319 C36.9294606,36.4697802 32.7109267,39.2857143 26.2793914,39.2857143 C16.8741355,39.2857143 11.0650069,33.3104396 11.0650069,25.1373626 C11.0650069,16.6895604 17.1507607,10.7142857 25.3112033,10.7142857 C30.7745505,10.7142857 35.131397,13.1868132 37.1369295,16.3461538 L46.5421853,10.989011 C42.461964,4.53296703 34.6473029,0 25.3803596,0 C11.1341632,0 0,11.1263736 0,25.0686813 C0,38.8736264 10.9266943,50 26.1410788,50 C39.6957123,50 50,41.0714286 50,26.3736264 L50,22.2527473 Z" id="Path" fill-rule="nonzero"></path>
<path d="M65.5,20.7941176 L65.5,14.6862745 L55,14.6862745 L55,49 L65.5,49 L65.5,33.4901961 C65.5,26.6960784 71.66,24.9803922 76,25.6666667 L76,14 C71.59,14 66.9,16.1960784 65.5,20.7941176 Z" id="Path" fill-rule="nonzero"></path>
<path d="M89.0076923,36.0909091 L113.584615,36.0909091 C113.861538,34.7954545 114,33.4318182 114,32 C114,21.7045455 106.523077,14 96.4153846,14 C85.4769231,14 78,21.8409091 78,32 C78,42.1590909 85.3384615,50 97.1769231,50 C103.753846,50 108.876923,47.6136364 112.269231,42.9772727 L103.961538,38.2727273 C102.576923,39.7727273 100.153846,40.8636364 97.3153846,40.8636364 C93.5076923,40.8636364 90.3230769,39.6363636 89.0076923,36.0909091 Z M88.8,28.4545455 C89.7692308,24.9772727 92.4,23.0681818 96.3461538,23.0681818 C99.4615385,23.0681818 102.576923,24.5 103.684615,28.4545455 L88.8,28.4545455 Z" id="Shape" fill-rule="nonzero"></path>
<path d="M128.007692,36.0909091 L152.584615,36.0909091 C152.861538,34.7954545 153,33.4318182 153,32 C153,21.7045455 145.523077,14 135.415385,14 C124.476923,14 117,21.8409091 117,32 C117,42.1590909 124.338462,50 136.176923,50 C142.753846,50 147.876923,47.6136364 151.269231,42.9772727 L142.961538,38.2727273 C141.576923,39.7727273 139.153846,40.8636364 136.315385,40.8636364 C132.507692,40.8636364 129.323077,39.6363636 128.007692,36.0909091 Z M127.8,28.4545455 C128.769231,24.9772727 131.4,23.0681818 135.346154,23.0681818 C138.461538,23.0681818 141.576923,24.5 142.684615,28.4545455 L127.8,28.4545455 Z" id="Shape" fill-rule="nonzero"></path>
<path d="M178.14375,14 C173.60625,14 170.16875,15.6342412 168.3125,18.1536965 L168.3125,14.9533074 L158,14.9533074 L158,49 L168.3125,49 L168.3125,30.4105058 C168.3125,25.5758755 170.925,23.3968872 174.70625,23.3968872 C178.00625,23.3968872 180.6875,25.3715953 180.6875,29.5933852 L180.6875,49 L191,49 L191,28.0953307 C191,18.9027237 185.0875,14 178.14375,14 Z" id="Path" fill-rule="nonzero"></path>
<path d="M216.186275,1 L198,1 L198,49 L208.980392,49 L208.980392,33.9142857 L216.186275,33.9142857 C225.656863,33.9142857 233,26.5771429 233,17.4571429 C233,8.33714286 225.656863,1 216.186275,1 Z M216.186275,23.6285714 L208.980392,23.6285714 L208.980392,11.2857143 L216.186275,11.2857143 C219.54902,11.2857143 222.019608,13.96 222.019608,17.4571429 C222.019608,20.9542857 219.54902,23.6285714 216.186275,23.6285714 Z" id="Shape" fill-rule="nonzero"></path>
<polygon id="Path" fill-rule="nonzero" points="270 1 234 1 234 11.56 246.461538 11.56 246.461538 49 257.538462 49 257.538462 11.56 270 11.56"></polygon>
</g>
<path d="M206.333333,218 C146.502603,218 98,169.198738 98,109 C98,48.8004798 146.502603,0 206.333333,0 C266.164063,0 314.666667,48.8004798 314.666667,109 C314.666667,169.198738 266.164063,218 206.333333,218 Z" id="Path"></path>
<g id="2993679_brand_brands_logo_logos_opera_icon" stroke-width="1" fill-rule="evenodd">
<path d="M43,0 C19.2516683,0 0,19.2516683 0,43 C0,66.7481131 19.2516683,86 43,86 C66.7483317,86 86,66.7481131 86,43 C86,19.2516683 66.7483317,0 43,0 Z M44.3616667,66.5066667 C31.4980597,66.5066667 21.07,56.0143953 21.07,43.0716667 C21.07,30.1287698 31.4980597,19.6366667 44.3616667,19.6366667 C57.2252736,19.6366667 67.6533333,30.1287698 67.6533333,43.0716667 C67.6533333,56.0143953 57.2252736,66.5066667 44.3616667,66.5066667 Z" id="Shape" fill="#9BE755" fill-rule="nonzero"></path>
<path d="M56.6038685,78.8333333 C37.0655323,78.8333333 21.2264507,62.7901304 21.2264507,43 C21.2264507,23.2096506 37.0655323,7.16666667 56.6038685,7.16666667 C60.972064,7.16666667 65.1372887,8.006729 69,9.4731752 C61.7278074,3.55796298 52.5057972,0 42.4529014,0 C19.006725,0 0,19.2516683 0,43 C0,66.7481131 19.006725,86 42.4529014,86 C52.5057972,86 61.7278074,82.4422556 69,76.5268248 C65.1372887,77.993271 60.972064,78.8333333 56.6038685,78.8333333 Z" id="Path" fill="#9BE755" fill-rule="nonzero"></path>
<path d="M56.6038685,78.8333333 C37.0655323,78.8333333 21.2264507,62.7901304 21.2264507,43 C21.2264507,23.2096506 37.0655323,7.16666667 56.6038685,7.16666667 C60.972064,7.16666667 65.1372887,8.006729 69,9.4731752 C61.7278074,3.55796298 52.5057972,0 42.4529014,0 C19.006725,0 0,19.2516683 0,43 C0,66.7481131 19.006725,86 42.4529014,86 C52.5057972,86 61.7278074,82.4422556 69,76.5268248 C65.1372887,77.993271 60.972064,78.8333333 56.6038685,78.8333333 Z" id="Path" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="394px" height="86px" viewBox="0 0 394 86" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>logo-greengpt-black</title>
<defs>
<linearGradient x1="46.5850926%" y1="13.9833492%" x2="59.2369668%" y2="85.5111279%" id="linearGradient-1">
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%"></stop>
<stop stop-color="#000000" stop-opacity="0.3" offset="100%"></stop>
</linearGradient>
</defs>
<g id="logo-greengpt-black" stroke="none" fill="none">
<g id="GreenPT" stroke-width="1" fill-rule="evenodd" transform="translate(108, 18)" fill="#000000">
<path d="M50,22.2527473 L25.7261411,22.2527473 L25.7261411,31.8681319 L38.6583679,31.8681319 C36.9294606,36.4697802 32.7109267,39.2857143 26.2793914,39.2857143 C16.8741355,39.2857143 11.0650069,33.3104396 11.0650069,25.1373626 C11.0650069,16.6895604 17.1507607,10.7142857 25.3112033,10.7142857 C30.7745505,10.7142857 35.131397,13.1868132 37.1369295,16.3461538 L46.5421853,10.989011 C42.461964,4.53296703 34.6473029,0 25.3803596,0 C11.1341632,0 0,11.1263736 0,25.0686813 C0,38.8736264 10.9266943,50 26.1410788,50 C39.6957123,50 50,41.0714286 50,26.3736264 L50,22.2527473 Z" id="Path" fill-rule="nonzero"></path>
<path d="M65.5,20.7941176 L65.5,14.6862745 L55,14.6862745 L55,49 L65.5,49 L65.5,33.4901961 C65.5,26.6960784 71.66,24.9803922 76,25.6666667 L76,14 C71.59,14 66.9,16.1960784 65.5,20.7941176 Z" id="Path" fill-rule="nonzero"></path>
<path d="M89.0076923,36.0909091 L113.584615,36.0909091 C113.861538,34.7954545 114,33.4318182 114,32 C114,21.7045455 106.523077,14 96.4153846,14 C85.4769231,14 78,21.8409091 78,32 C78,42.1590909 85.3384615,50 97.1769231,50 C103.753846,50 108.876923,47.6136364 112.269231,42.9772727 L103.961538,38.2727273 C102.576923,39.7727273 100.153846,40.8636364 97.3153846,40.8636364 C93.5076923,40.8636364 90.3230769,39.6363636 89.0076923,36.0909091 Z M88.8,28.4545455 C89.7692308,24.9772727 92.4,23.0681818 96.3461538,23.0681818 C99.4615385,23.0681818 102.576923,24.5 103.684615,28.4545455 L88.8,28.4545455 Z" id="Shape" fill-rule="nonzero"></path>
<path d="M128.007692,36.0909091 L152.584615,36.0909091 C152.861538,34.7954545 153,33.4318182 153,32 C153,21.7045455 145.523077,14 135.415385,14 C124.476923,14 117,21.8409091 117,32 C117,42.1590909 124.338462,50 136.176923,50 C142.753846,50 147.876923,47.6136364 151.269231,42.9772727 L142.961538,38.2727273 C141.576923,39.7727273 139.153846,40.8636364 136.315385,40.8636364 C132.507692,40.8636364 129.323077,39.6363636 128.007692,36.0909091 Z M127.8,28.4545455 C128.769231,24.9772727 131.4,23.0681818 135.346154,23.0681818 C138.461538,23.0681818 141.576923,24.5 142.684615,28.4545455 L127.8,28.4545455 Z" id="Shape" fill-rule="nonzero"></path>
<path d="M178.14375,14 C173.60625,14 170.16875,15.6342412 168.3125,18.1536965 L168.3125,14.9533074 L158,14.9533074 L158,49 L168.3125,49 L168.3125,30.4105058 C168.3125,25.5758755 170.925,23.3968872 174.70625,23.3968872 C178.00625,23.3968872 180.6875,25.3715953 180.6875,29.5933852 L180.6875,49 L191,49 L191,28.0953307 C191,18.9027237 185.0875,14 178.14375,14 Z" id="Path" fill-rule="nonzero"></path>
<path d="M216.186275,1 L198,1 L198,49 L208.980392,49 L208.980392,33.9142857 L216.186275,33.9142857 C225.656863,33.9142857 233,26.5771429 233,17.4571429 C233,8.33714286 225.656863,1 216.186275,1 Z M216.186275,23.6285714 L208.980392,23.6285714 L208.980392,11.2857143 L216.186275,11.2857143 C219.54902,11.2857143 222.019608,13.96 222.019608,17.4571429 C222.019608,20.9542857 219.54902,23.6285714 216.186275,23.6285714 Z" id="Shape" fill-rule="nonzero"></path>
<polygon id="Path" fill-rule="nonzero" points="270 1 234 1 234 11.56 246.461538 11.56 246.461538 49 257.538462 49 257.538462 11.56 270 11.56"></polygon>
</g>
<path d="M206.333333,218 C146.502603,218 98,169.198738 98,109 C98,48.8004798 146.502603,0 206.333333,0 C266.164063,0 314.666667,48.8004798 314.666667,109 C314.666667,169.198738 266.164063,218 206.333333,218 Z" id="Path"></path>
<g id="2993679_brand_brands_logo_logos_opera_icon" stroke-width="1" fill-rule="evenodd">
<path d="M43,0 C19.2516683,0 0,19.2516683 0,43 C0,66.7481131 19.2516683,86 43,86 C66.7483317,86 86,66.7481131 86,43 C86,19.2516683 66.7483317,0 43,0 Z M44.3616667,66.5066667 C31.4980597,66.5066667 21.07,56.0143953 21.07,43.0716667 C21.07,30.1287698 31.4980597,19.6366667 44.3616667,19.6366667 C57.2252736,19.6366667 67.6533333,30.1287698 67.6533333,43.0716667 C67.6533333,56.0143953 57.2252736,66.5066667 44.3616667,66.5066667 Z" id="Shape" fill="#9BE755" fill-rule="nonzero"></path>
<path d="M56.6038685,78.8333333 C37.0655323,78.8333333 21.2264507,62.7901304 21.2264507,43 C21.2264507,23.2096506 37.0655323,7.16666667 56.6038685,7.16666667 C60.972064,7.16666667 65.1372887,8.006729 69,9.4731752 C61.7278074,3.55796298 52.5057972,0 42.4529014,0 C19.006725,0 0,19.2516683 0,43 C0,66.7481131 19.006725,86 42.4529014,86 C52.5057972,86 61.7278074,82.4422556 69,76.5268248 C65.1372887,77.993271 60.972064,78.8333333 56.6038685,78.8333333 Z" id="Path" fill="#9BE755" fill-rule="nonzero"></path>
<path d="M56.6038685,78.8333333 C37.0655323,78.8333333 21.2264507,62.7901304 21.2264507,43 C21.2264507,23.2096506 37.0655323,7.16666667 56.6038685,7.16666667 C60.972064,7.16666667 65.1372887,8.006729 69,9.4731752 C61.7278074,3.55796298 52.5057972,0 42.4529014,0 C19.006725,0 0,19.2516683 0,43 C0,66.7481131 19.006725,86 42.4529014,86 C52.5057972,86 61.7278074,82.4422556 69,76.5268248 C65.1372887,77.993271 60.972064,78.8333333 56.6038685,78.8333333 Z" id="Path" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

+2
View File
@@ -154,6 +154,8 @@ 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)
+3 -1
View File
@@ -4,7 +4,9 @@
module.exports = (output) => {
const text = String(output || '');
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
.split('\n')
.map((l) => l.trim())
+22
View File
@@ -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`);
+1
View File
@@ -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. |
| 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,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
Packages that wrap Node built-ins.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ponytail",
"version": "4.8.2",
"version": "4.8.4",
"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.2",
"version": "4.8.4",
"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
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ponytail-mcp",
"version": "4.8.2",
"version": "4.8.4",
"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. 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-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.',
+3 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Version-consistency guard. Ponytail declares its version in six files across
// four host ecosystems, and every release bumps all of them by hand.
// Version-consistency guard. Ponytail declares its version in seven files across
// five host ecosystems, and every release bumps all of them by hand.
//
// 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:
@@ -21,6 +21,7 @@ const PINNED_SEMVER = /^\d+\.\d+\.\d+$/;
const VERSION_FILES = [
'.claude-plugin/plugin.json', // Claude Code plugin — what users install
'.codex-plugin/plugin.json', // Codex plugin
'.devin-plugin/plugin.json', // Devin CLI plugin
'.github/plugin/plugin.json', // Copilot plugin
'gemini-extension.json', // Gemini CLI extension
'package.json', // pi-package / repo root
+8 -5
View File
@@ -5,11 +5,14 @@ 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 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.
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).
argument-hint: "[lite|full|ultra]"
license: MIT
---
+222
View File
@@ -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);
});