diff --git a/README.md b/README.md index 70a0f09..47d925e 100644 --- a/README.md +++ b/README.md @@ -1 +1,103 @@ -# ponytail \ No newline at end of file +

+ Ponytail — the lazy senior dev +

+ +

Ponytail

+ +

+ A skill that makes your AI agent think like the laziest senior dev in the room —
+ because the best code is the code you never wrote.
+

+ +--- + +## 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 | `` | +| [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. diff --git a/examples/api-endpoint.md b/examples/api-endpoint.md new file mode 100644 index 0000000..8773ba6 --- /dev/null +++ b/examples/api-endpoint.md @@ -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. diff --git a/examples/caching.md b/examples/caching.md new file mode 100644 index 0000000..7dbe205 --- /dev/null +++ b/examples/caching.md @@ -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. diff --git a/examples/date-picker.md b/examples/date-picker.md new file mode 100644 index 0000000..f0a6ca9 --- /dev/null +++ b/examples/date-picker.md @@ -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 ; +} +``` + +One dependency, one wrapper component, two `useEffect` hooks, a cleanup function, and a CSS import — to pick a date. + +## With Ponytail + +```html + + +``` + +**1 dependency + 30 lines → 0 dependencies + 1 line.** Native, accessible, localized, keyboard-navigable, mobile-friendly. The browser team already did the work. diff --git a/examples/email-validation.md b/examples/email-validation.md new file mode 100644 index 0000000..ea7a86d --- /dev/null +++ b/examples/email-validation.md @@ -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. diff --git a/examples/sorting.md b/examples/sorting.md new file mode 100644 index 0000000..aa31f3b --- /dev/null +++ b/examples/sorting.md @@ -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. diff --git a/skills/ponytail/SKILL.md b/skills/ponytail/SKILL.md new file mode 100644 index 0000000..47f8148 --- /dev/null +++ b/skills/ponytail/SKILL.md @@ -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?** `` + 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 + + + ``` + +## 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.