feat: add ponytail skill, examples, README
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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 → 0–3 lines.** The fastest cache is the one you didn't have to debug.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user