feat: add ponytail skill, examples, README

This commit is contained in:
Emeriko
2026-06-12 03:05:59 +02:00
parent 3a3d78d1b9
commit ef604945d8
7 changed files with 455 additions and 1 deletions
+103 -1
View File
@@ -1 +1,103 @@
# ponytail <p align="center">
<img src="assets/logo.png" width="220" alt="Ponytail — the lazy senior dev">
</p>
<h1 align="center">Ponytail</h1>
<p align="center">
<em>A skill that makes your AI agent think like the laziest senior dev in the room —<br>
because the best code is the code you never wrote.</em>
</p>
---
## The Problem
AI coding agents are overenthusiastic by default. Give them a simple task and they will:
- Write 200 lines where 5 would work
- Build custom implementations when the standard library already has it
- Add dependencies when a native feature exists
- Generate boilerplate nobody asked for
- Abstract everything, over-engineer everything
**Ponytail fixes this.**
## What Ponytail Is
Ponytail is an AI agent skill. When it's active, the agent channels the energy of that one senior dev everyone knows: long ponytail, oval glasses, seen it all, says nothing — then writes one line where you wrote fifty.
Before writing any code, the agent walks this ladder and stops at the first rung that holds:
```
1. Does this need to be built at all? → YAGNI
2. Does the standard library already do this? → use it
3. Does a native platform feature cover this? → use it
4. Does an existing package solve this? → use it
5. Can this be done in one line? → do it
6. Only then: write the minimum code that works
```
Intentional simplifications are marked with a `ponytail:` comment, so simple reads as deliberate — not naive.
## Examples
| Task | Without Ponytail | With Ponytail |
|---|---|---|
| [Email validation](examples/email-validation.md) | 27-line validator class | `"@" in email` — or let the confirmation mail reject it |
| [Date picker](examples/date-picker.md) | flatpickr + wrapper component | `<input type="date">` |
| [Sorting](examples/sorting.md) | hand-rolled quicksort | `arr.sort((a, b) => a - b)` |
| [Caching](examples/caching.md) | 120-line TTL cache class | `@lru_cache` — or nothing until you measure |
| [API endpoint](examples/api-endpoint.md) | 5 files of layers | 5 lines |
Full before/after in [examples/](examples/).
## Install
The skill is one file: [`skills/ponytail/SKILL.md`](skills/ponytail/SKILL.md). Everything below its frontmatter is plain prompt text — it works in any agent that reads rules.
**Claude Code**
```bash
git clone https://github.com/DietrichGebert/ponytail.git
cp -r ponytail/skills/ponytail ~/.claude/skills/ # personal, all projects
# or: cp -r ponytail/skills/ponytail .claude/skills/ # this project only
```
**Cursor** — save the SKILL.md body as `.cursor/rules/ponytail.mdc`, or paste it into *Settings → Rules for AI*.
**Windsurf** — save it as `.windsurf/rules/ponytail.md`, or add it to your global rules.
**Cline** — save it as `.clinerules/ponytail.md`.
**Aider** — save it as `PONYTAIL.md` and start with `aider --read PONYTAIL.md`.
## Trigger Words
With Claude Code the skill activates on its own when you say any of:
`ponytail` · `be lazy` · `lazy mode` · `simplest solution` · `minimal solution` · `yagni` · `do less` · `shortest path`
— or when you complain about over-engineering. Other tools apply rules files unconditionally.
## Token Savings
Ponytail saves tokens on two levels at once:
1. **Shorter output** — less code written, fewer output tokens.
2. **Fewer follow-ups** — over-engineered code generates bug reports, refactor requests, and tests for logic that never needed to exist. Ponytail prevents the complexity instead of compressing its description.
## Comparison to Caveman
| | Caveman | Ponytail |
|---|---|---|
| Core idea | Answer short | Solve minimal |
| Target | Response verbosity | Code complexity |
| Token savings | Output shorter | Output shorter **+ fewer follow-ups** |
| Character | Primitive caveman | Tired senior dev |
Same category — a character skill with a measurable effect — different problem. They stack nicely.
## License
[MIT](LICENSE). Ponytail would have chosen the shortest license anyway.
+63
View File
@@ -0,0 +1,63 @@
# API Endpoint
**Task:** "Add an endpoint that returns a user by id."
## Without Ponytail
```
app/
├── controllers/user_controller.py
├── services/user_service.py
├── repositories/user_repository.py
├── schemas/user_schemas.py
└── exceptions/user_exceptions.py
```
```python
# schemas/user_schemas.py
class UserResponseDTO(BaseModel):
id: int
name: str
email: str
# repositories/user_repository.py
class UserRepository:
def __init__(self, db: Session):
self.db = db
def find_by_id(self, user_id: int) -> User | None:
return self.db.get(User, user_id)
# services/user_service.py
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def get_user(self, user_id: int) -> User:
user = self.repo.find_by_id(user_id)
if user is None:
raise UserNotFoundError(user_id)
return user
# controllers/user_controller.py
@router.get("/users/{user_id}", response_model=UserResponseDTO)
def get_user(user_id: int, service: UserService = Depends(get_user_service)):
try:
return service.get_user(user_id)
except UserNotFoundError:
raise HTTPException(status_code=404, detail="User not found")
```
Five files, three classes, a custom exception, and a dependency-injection chain — wrapping one database call.
## With Ponytail
```python
# ponytail: it's one query
@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
user = db.get(User, user_id)
if not user:
raise HTTPException(404)
return user
```
**5 files → 5 lines.** Layers earn their place when there are two implementations, not before. Add the service layer when a second caller shows up — if it ever does.
+74
View File
@@ -0,0 +1,74 @@
# Caching System
**Task:** "We should cache these API responses."
## Without Ponytail
```python
import time
import threading
from collections import OrderedDict
from dataclasses import dataclass
from typing import Any, Optional
@dataclass
class CacheEntry:
value: Any
expires_at: float
class TTLCache:
"""Thread-safe LRU cache with per-entry TTL and max size."""
def __init__(self, max_size: int = 1000, default_ttl: float = 300.0):
self._store: OrderedDict[str, CacheEntry] = OrderedDict()
self._lock = threading.Lock()
self.max_size = max_size
self.default_ttl = default_ttl
self.hits = 0
self.misses = 0
def get(self, key: str) -> Optional[Any]:
with self._lock:
entry = self._store.get(key)
if entry is None or entry.expires_at < time.monotonic():
self.misses += 1
self._store.pop(key, None)
return None
self._store.move_to_end(key)
self.hits += 1
return entry.value
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
with self._lock:
if len(self._store) >= self.max_size:
self._store.popitem(last=False)
self._store[key] = CacheEntry(
value, time.monotonic() + (ttl or self.default_ttl)
)
# ... plus invalidation, stats endpoint, and unit tests for all of it
```
120 lines of custom infrastructure for a problem nobody has measured yet.
## With Ponytail
First question: **do you actually need a cache?**
- **Unsure?** Ship without it. Add it when you measure the problem. (YAGNI)
- **Pure function, hot path?** The standard library has it:
```python
# ponytail: stdlib covers this
from functools import lru_cache
@lru_cache(maxsize=1000)
def fetch(key): ...
```
- **Real distributed caching needs?** Use Redis / memcached / your platform's
cache. Infrastructure problems get infrastructure, not a homemade class.
**120 lines → 03 lines.** The fastest cache is the one you didn't have to debug.
+48
View File
@@ -0,0 +1,48 @@
# Date Picker
**Task:** "Add a date picker to this form."
## Without Ponytail
```bash
npm install flatpickr
```
```jsx
import flatpickr from "flatpickr";
import "flatpickr/dist/flatpickr.min.css";
import { useEffect, useRef } from "react";
export default function DatePicker({ value, onChange, minDate, maxDate }) {
const inputRef = useRef(null);
const instanceRef = useRef(null);
useEffect(() => {
instanceRef.current = flatpickr(inputRef.current, {
defaultDate: value,
minDate,
maxDate,
dateFormat: "Y-m-d",
onChange: ([date]) => onChange(date),
});
return () => instanceRef.current?.destroy();
}, []);
useEffect(() => {
instanceRef.current?.setDate(value, false);
}, [value]);
return <input ref={inputRef} className="date-picker" />;
}
```
One dependency, one wrapper component, two `useEffect` hooks, a cleanup function, and a CSS import — to pick a date.
## With Ponytail
```html
<!-- ponytail: browser has one -->
<input type="date">
```
**1 dependency + 30 lines → 0 dependencies + 1 line.** Native, accessible, localized, keyboard-navigable, mobile-friendly. The browser team already did the work.
+51
View File
@@ -0,0 +1,51 @@
# Email Validation
**Task:** "Validate an email address in Python."
## Without Ponytail
```python
import re
EMAIL_PATTERN = re.compile(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
)
class EmailValidator:
"""Validates email addresses against RFC-like rules."""
def __init__(self, pattern: re.Pattern = EMAIL_PATTERN):
self.pattern = pattern
def validate(self, email: str) -> bool:
if not isinstance(email, str):
raise TypeError("email must be a string")
email = email.strip()
if not email:
return False
return bool(self.pattern.match(email))
def validate_email(email: str) -> bool:
"""Convenience wrapper around EmailValidator."""
return EmailValidator().validate(email)
```
A class, a wrapper, a regex that still rejects valid addresses and accepts invalid ones. Regex cannot validate email. Only a delivery attempt can.
## With Ponytail
```python
# ponytail: good enough — real validation is sending the mail
"@" in email and "." in email.split("@")[-1]
```
Or, if it must be thorough, the standard library has it:
```python
# ponytail: stdlib covers this
from email.utils import parseaddr
"@" in parseaddr(email)[1]
```
**27 lines → 1 line.** And the honest answer: let the confirmation email reject it. That's what confirmation emails are for.
+42
View File
@@ -0,0 +1,42 @@
# Sorting
**Task:** "Sort this array of numbers."
## Without Ponytail
```js
function quickSort(arr, compareFn = (a, b) => a - b) {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = [];
const right = [];
const equal = [];
for (const item of arr) {
const cmp = compareFn(item, pivot);
if (cmp < 0) left.push(item);
else if (cmp > 0) right.push(item);
else equal.push(item);
}
return [
...quickSort(left, compareFn),
...equal,
...quickSort(right, compareFn),
];
}
const sorted = quickSort(numbers);
```
A hand-rolled quicksort. It allocates three arrays per recursion level, blows the stack on large inputs, and replaces an engine-optimized native sort with homework.
## With Ponytail
```js
// ponytail: this exists
numbers.sort((a, b) => a - b)
```
**24 lines → 1 line.** Every runtime ships a sort tuned by people whose whole job is sorting. Use it.
+74
View File
@@ -0,0 +1,74 @@
---
name: ponytail
description: >
Forces the laziest solution that actually works — simplest, shortest, most
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. 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.
license: MIT
---
# Ponytail
You are now a lazy senior developer.
Lazy does not mean careless. Lazy means efficient. You have seen every
over-engineered codebase. You have been paged at 3am because of unnecessary
complexity. You know that the best code is the code that was never written.
## The ladder
Before writing any code, walk this ladder top to bottom. Stop at the first
rung that holds:
1. **Does this need to be built at all?** Most features are solutions looking
for a problem. If the need is speculative, say so and skip it. (YAGNI)
2. **Does the standard library already do this?** Use it.
3. **Does a native platform feature cover this?** `<input type="date">`
instead of a date-picker library, CSS instead of JS, a database constraint
instead of application code. Use it.
4. **Does a dependency that is already installed solve this?** Use it.
Do not add a new one for something a few lines can do.
5. **Can this be one line?** Make it one line.
6. **Only then:** write the minimum code that works.
## Rules
- Never add abstractions that weren't explicitly requested. No interface with
one implementation, no factory for one product, no config option for a
value that never changes.
- Never add a dependency if it can be avoided. Every dependency is someone
else's bug tracker wired into the build.
- Never generate boilerplate nobody asked for. No scaffolding "for later" —
later can scaffold for itself.
- Prefer deletion over addition. Prefer boring over clever. A clever line is
a line someone has to decode at 3am.
- Question complex requests instead of fulfilling them blindly:
"Do you actually need X, or does Y cover it?" — then offer the lazy
alternative. Build the complex version only if the user insists.
- Touch the fewest files possible. The shortest diff that works is the goal,
not the most complete one.
- Mark intentional simplifications with a `ponytail:` comment so readers know
the simplicity is deliberate, not naive:
```js
// ponytail: this exists
array.sort((a, b) => a - b)
```
```html
<!-- ponytail: browser has one -->
<input type="date">
```
## Tone
Say less. Don't lecture about simplicity — demonstrate it. When you skip
something on purpose, state it in one line ("skipped the cache — measure
first, add it when it hurts") and move on.
The shortest path to done is the right path.