Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c279cbfb3 | ||
|
|
de318b9457 | ||
|
|
784e8bfd1c | ||
|
|
bce2162025 | ||
|
|
515fb4c5a4 | ||
|
|
e37b823b92 | ||
|
|
e89a4e9863 | ||
|
|
2f2a0d33e0 | ||
|
|
c16f967d37 | ||
|
|
cf97ccc509 | ||
|
|
2d91f6a957 | ||
|
|
9c99843725 | ||
|
|
b609e019a0 | ||
|
|
983255e2a1 | ||
|
|
cbb8859f39 |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "ponytail",
|
||||
"interface": {
|
||||
"displayName": "Ponytail"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "ponytail",
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/DietrichGebert/ponytail.git",
|
||||
"ref": "main"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +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.1.0",
|
||||
"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"
|
||||
|
||||
@@ -18,6 +18,7 @@ Rules:
|
||||
- No boilerplate nobody asked for.
|
||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
||||
- Mark intentional simplifications with a `ponytail:` comment.
|
||||
- Pick the edge-case-correct option when two stdlib approaches are the same size — lazy means less code, not the flimsier algorithm.
|
||||
- Mark intentional simplifications with a `ponytail:` comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path.
|
||||
|
||||
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested.
|
||||
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind — the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "ponytail",
|
||||
"version": "4.1.0",
|
||||
"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"],
|
||||
"skills": "./skills/",
|
||||
"interface": {
|
||||
"displayName": "Ponytail",
|
||||
"shortDescription": "Lazy senior developer mode",
|
||||
"longDescription": "Prefer YAGNI, the standard library, native platform features, and the smallest correct implementation.",
|
||||
"developerName": "Dietrich Gebert",
|
||||
"category": "Productivity",
|
||||
"capabilities": ["Instructions", "Lifecycle hooks"],
|
||||
"websiteURL": "https://github.com/DietrichGebert/ponytail",
|
||||
"defaultPrompt": [
|
||||
"Use Ponytail mode for this task.",
|
||||
"Review this diff for over-engineering.",
|
||||
"Find the smallest correct implementation."
|
||||
],
|
||||
"brandColor": "#111111",
|
||||
"composerIcon": "./assets/logo.png",
|
||||
"logo": "./assets/logo.png"
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ Rules:
|
||||
- No boilerplate nobody asked for.
|
||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
||||
- Mark intentional simplifications with a `ponytail:` comment.
|
||||
- Pick the edge-case-correct option when two stdlib approaches are the same size — lazy means less code, not the flimsier algorithm.
|
||||
- Mark intentional simplifications with a `ponytail:` comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path.
|
||||
|
||||
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested.
|
||||
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind — the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test.
|
||||
|
||||
@@ -18,6 +18,7 @@ Rules:
|
||||
- No boilerplate nobody asked for.
|
||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
||||
- Mark intentional simplifications with a `ponytail:` comment.
|
||||
- Pick the edge-case-correct option when two stdlib approaches are the same size — lazy means less code, not the flimsier algorithm.
|
||||
- Mark intentional simplifications with a `ponytail:` comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path.
|
||||
|
||||
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested.
|
||||
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind — the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Ponytail — lazy senior dev mode
|
||||
inclusion: always
|
||||
---
|
||||
|
||||
# Ponytail — lazy senior dev mode
|
||||
|
||||
You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.
|
||||
|
||||
Before writing any code, stop 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 it? Use it.
|
||||
4. Does an already-installed dependency solve it? Use it.
|
||||
5. Can this be one line? Make it one line.
|
||||
6. Only then: write the minimum code that works.
|
||||
|
||||
Rules:
|
||||
|
||||
- No abstractions that weren't explicitly requested.
|
||||
- No new dependency if it can be avoided.
|
||||
- No boilerplate nobody asked for.
|
||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
||||
- Pick the edge-case-correct option when two stdlib approaches are the same size — lazy means less code, not the flimsier algorithm.
|
||||
- Mark intentional simplifications with a `ponytail:` comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path.
|
||||
|
||||
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind — the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test.
|
||||
@@ -18,6 +18,7 @@ Rules:
|
||||
- No boilerplate nobody asked for.
|
||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
||||
- Mark intentional simplifications with a `ponytail:` comment.
|
||||
- Pick the edge-case-correct option when two stdlib approaches are the same size — lazy means less code, not the flimsier algorithm.
|
||||
- Mark intentional simplifications with a `ponytail:` comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path.
|
||||
|
||||
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested.
|
||||
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind — the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test.
|
||||
|
||||
@@ -18,8 +18,9 @@ Rules:
|
||||
- No boilerplate nobody asked for.
|
||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
||||
- Mark intentional simplifications with a `ponytail:` comment.
|
||||
- Pick the edge-case-correct option when two stdlib approaches are the same size — lazy means less code, not the flimsier algorithm.
|
||||
- Mark intentional simplifications with a `ponytail:` comment. If the shortcut has a known ceiling (global lock, O(n²) scan, naive heuristic), the comment names the ceiling and the upgrade path.
|
||||
|
||||
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested.
|
||||
Not lazy about: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind — the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test.
|
||||
|
||||
(Yes, this file also applies to agents working on the ponytail repo itself. Especially to them.)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="assets/logo.png" width="220" alt="Ponytail — the lazy senior dev">
|
||||
<img src="assets/logo.png" width="220" alt="Ponytail, the lazy senior dev">
|
||||
</p>
|
||||
|
||||
<h1 align="center">Ponytail</h1>
|
||||
@@ -27,6 +27,16 @@ With ponytail:
|
||||
|
||||
More survivors in [examples/](examples/).
|
||||
|
||||
## Numbers
|
||||
|
||||
Six tasks: streaming log parser, atomic file sync, notification dispatcher, validation engine, auth module, concurrent money ledger. One spec each, one fresh agent per arm, same model. Three arms: no skill, the [caveman](https://github.com/JuliusBrussee/caveman) skill, and ponytail. Every arm passes the same adversarial security and concurrency probes. Then the agreement ends:
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/benchmark-loc.svg" width="860" alt="Lines of code per task: ponytail 490 total vs caveman 1,440 vs no-skill control 3,629, all passing the same adversarial probes">
|
||||
</p>
|
||||
|
||||
**47% fewer tokens than the no-skill agent. 3× faster. A seventh of the code.** The 3,139 lines nobody wrote have never caused an incident. When a surprise feature request hit two of the tasks, ponytail extended in 96 changed lines; caveman needed 413, the no-skill agent 1,115. Every shortcut ponytail took is marked in the code with a `ponytail:` comment naming its upgrade path. Data: [benchmarks/](benchmarks/).
|
||||
|
||||
## How it works
|
||||
|
||||
Before writing code, the agent stops at the first rung that holds:
|
||||
@@ -46,16 +56,49 @@ Lazy, not negligent: trust-boundary validation, data-loss handling, security, an
|
||||
|
||||
The most effort ponytail will ever ask of you:
|
||||
|
||||
### Claude Code
|
||||
|
||||
```
|
||||
/plugin marketplace add DietrichGebert/ponytail
|
||||
/plugin install ponytail@ponytail
|
||||
```
|
||||
|
||||
### Codex
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add DietrichGebert/ponytail
|
||||
codex
|
||||
```
|
||||
|
||||
Open `/plugins`, select the Ponytail marketplace, and install Ponytail. Then
|
||||
open `/hooks`, review and trust its two lifecycle hooks, and start a new thread.
|
||||
|
||||
### Pi agent harness
|
||||
|
||||
```
|
||||
pi install git:github.com/DietrichGebert/ponytail
|
||||
```
|
||||
|
||||
That was it. He'd be proud. He won't say it.
|
||||
|
||||
Active every session. `/ponytail-review` finds what to delete in your diff. `/ponytail ultra` exists for when the codebase has wronged you personally. `/ponytail-help` explains the rest.
|
||||
|
||||
Cursor, Windsurf, Cline, Copilot, Aider: copy the matching rules file from this repo ([`.cursor/rules/`](.cursor/rules/), [`.windsurf/rules/`](.windsurf/rules/), [`.clinerules/`](.clinerules/), [`.github/copilot-instructions.md`](.github/copilot-instructions.md), [`AGENTS.md`](AGENTS.md)).
|
||||
In Codex, invoke the skills as `@ponytail`, `@ponytail-review`, and
|
||||
`@ponytail-help`. Startup and mode-change text shows the current mode.
|
||||
|
||||
Cursor, Windsurf, Cline, Copilot, Aider, Kiro: copy the matching rules file from this repo ([`.cursor/rules/`](.cursor/rules/), [`.windsurf/rules/`](.windsurf/rules/), [`.clinerules/`](.clinerules/), [`.github/copilot-instructions.md`](.github/copilot-instructions.md), [`AGENTS.md`](AGENTS.md), [`.kiro/steering/`](.kiro/steering/)).
|
||||
|
||||
Kiro: copy `.kiro/steering/ponytail.md` to `~/.kiro/steering/` (global) or `.kiro/steering/` in your project.
|
||||
|
||||
Which files map to which agent: [Agent portability](docs/agent-portability.md).
|
||||
|
||||
## Development
|
||||
|
||||
When changing the compact rule text, keep the agent copies aligned:
|
||||
|
||||
```bash
|
||||
node scripts/check-rule-copies.js
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -63,7 +106,7 @@ Cursor, Windsurf, Cline, Copilot, Aider: copy the matching rules file from this
|
||||
No.
|
||||
|
||||
**What if I really need the 120-line cache class?**
|
||||
You don't. Insist anyway and he'll build it — slowly, correctly, while looking at you.
|
||||
You don't. Insist anyway and he'll build it. Slowly. Correctly. While looking at you.
|
||||
|
||||
**Does it scale?**
|
||||
The code you never wrote scales infinitely. Zero bugs, zero CVEs, 100% uptime since forever.
|
||||
@@ -71,10 +114,6 @@ The code you never wrote scales infinitely. Zero bugs, zero CVEs, 100% uptime si
|
||||
**Why "ponytail"?**
|
||||
You know exactly why.
|
||||
|
||||
## Numbers
|
||||
|
||||
5 coding tasks, same agent with and without ponytail: **−16% tokens, ~4× faster, 293 → 47 lines.** The 246 lines nobody wrote have never caused an incident. Data: [benchmarks/](benchmarks/).
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE). The shortest license that works.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<svg viewBox="0 0 860 470" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system, 'Segoe UI', Helvetica, Arial, sans-serif">
|
||||
<title>Non-blank lines of code per task: control vs caveman vs ponytail</title>
|
||||
|
||||
<text x="20" y="26" font-size="15" font-weight="600" fill="#8b949e">Six tasks. Adversarial probes: everyone passes. Lines of code: not everyone.</text>
|
||||
|
||||
<!-- legend -->
|
||||
<rect x="20" y="42" width="12" height="12" rx="2" fill="#8b949e"/>
|
||||
<text x="38" y="53" font-size="13" fill="#8b949e">Control (no skill) · 3,629 total</text>
|
||||
<rect x="250" y="42" width="12" height="12" rx="2" fill="#d9822b"/>
|
||||
<text x="268" y="53" font-size="13" fill="#8b949e">Caveman · 1,440</text>
|
||||
<rect x="420" y="42" width="12" height="12" rx="2" fill="#2da44e"/>
|
||||
<text x="438" y="53" font-size="13" fill="#8b949e">Ponytail · 490</text>
|
||||
|
||||
<!-- Task A -->
|
||||
<text x="112" y="117" font-size="12" fill="#8b949e" text-anchor="end">log-analysis CLI</text>
|
||||
<rect x="120" y="90" width="662" height="13" rx="2" fill="#8b949e"/>
|
||||
<text x="788" y="101" font-size="11" fill="#8b949e">946</text>
|
||||
<rect x="120" y="106" width="198" height="13" rx="2" fill="#d9822b"/>
|
||||
<text x="324" y="117" font-size="11" fill="#d9822b">283</text>
|
||||
<rect x="120" y="122" width="102" height="13" rx="2" fill="#2da44e"/>
|
||||
<text x="228" y="133" font-size="11" fill="#2da44e" font-weight="600">145</text>
|
||||
|
||||
<!-- Task B -->
|
||||
<text x="112" y="177" font-size="12" fill="#8b949e" text-anchor="end">file sync</text>
|
||||
<rect x="120" y="150" width="459" height="13" rx="2" fill="#8b949e"/>
|
||||
<text x="585" y="161" font-size="11" fill="#8b949e">656</text>
|
||||
<rect x="120" y="166" width="160" height="13" rx="2" fill="#d9822b"/>
|
||||
<text x="286" y="177" font-size="11" fill="#d9822b">228</text>
|
||||
<rect x="120" y="182" width="69" height="13" rx="2" fill="#2da44e"/>
|
||||
<text x="195" y="193" font-size="11" fill="#2da44e" font-weight="600">99</text>
|
||||
|
||||
<!-- Task C -->
|
||||
<text x="112" y="237" font-size="12" fill="#8b949e" text-anchor="end">notification dispatcher</text>
|
||||
<rect x="120" y="210" width="566" height="13" rx="2" fill="#8b949e"/>
|
||||
<text x="692" y="221" font-size="11" fill="#8b949e">808</text>
|
||||
<rect x="120" y="226" width="277" height="13" rx="2" fill="#d9822b"/>
|
||||
<text x="403" y="237" font-size="11" fill="#d9822b">396</text>
|
||||
<rect x="120" y="242" width="51" height="13" rx="2" fill="#2da44e"/>
|
||||
<text x="177" y="253" font-size="11" fill="#2da44e" font-weight="600">73</text>
|
||||
|
||||
<!-- Task D -->
|
||||
<text x="112" y="297" font-size="12" fill="#8b949e" text-anchor="end">validation engine</text>
|
||||
<rect x="120" y="270" width="474" height="13" rx="2" fill="#8b949e"/>
|
||||
<text x="600" y="281" font-size="11" fill="#8b949e">677</text>
|
||||
<rect x="120" y="286" width="153" height="13" rx="2" fill="#d9822b"/>
|
||||
<text x="279" y="297" font-size="11" fill="#d9822b">218</text>
|
||||
<rect x="120" y="302" width="49" height="13" rx="2" fill="#2da44e"/>
|
||||
<text x="175" y="313" font-size="11" fill="#2da44e" font-weight="600">70</text>
|
||||
|
||||
<!-- Task E -->
|
||||
<text x="112" y="357" font-size="12" fill="#8b949e" text-anchor="end">auth module</text>
|
||||
<rect x="120" y="330" width="182" height="13" rx="2" fill="#8b949e"/>
|
||||
<text x="308" y="341" font-size="11" fill="#8b949e">260</text>
|
||||
<rect x="120" y="346" width="104" height="13" rx="2" fill="#d9822b"/>
|
||||
<text x="230" y="357" font-size="11" fill="#d9822b">148</text>
|
||||
<rect x="120" y="362" width="34" height="13" rx="2" fill="#2da44e"/>
|
||||
<text x="160" y="373" font-size="11" fill="#2da44e" font-weight="600">49</text>
|
||||
|
||||
<!-- Task F -->
|
||||
<text x="112" y="417" font-size="12" fill="#8b949e" text-anchor="end">money ledger</text>
|
||||
<rect x="120" y="390" width="197" height="13" rx="2" fill="#8b949e"/>
|
||||
<text x="323" y="401" font-size="11" fill="#8b949e">282</text>
|
||||
<rect x="120" y="406" width="117" height="13" rx="2" fill="#d9822b"/>
|
||||
<text x="243" y="417" font-size="11" fill="#d9822b">167</text>
|
||||
<rect x="120" y="422" width="38" height="13" rx="2" fill="#2da44e"/>
|
||||
<text x="164" y="433" font-size="11" fill="#2da44e" font-weight="600">54</text>
|
||||
|
||||
<text x="120" y="458" font-size="11" fill="#8b949e" opacity="0.8">Non-blank LOC, AST-counted. Same model, same specs, one fresh agent per arm. Every arm passes the same security (8/8) and concurrency (6/6) probes. 2026-06-12.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,125 @@
|
||||
# Ponytail v4 hardening — A–F benchmark vs Caveman (2026-06-12)
|
||||
|
||||
Response to the hardening brief in `C:\dev\ponytail-bench\PONYTAIL-BENCHMARK-WRITEUP.md`.
|
||||
Harness reused as-is: same 6 tasks (specs reconstructed in `ponytail-bench\specs.md` —
|
||||
the originals were not preserved; both new arms got identical text), same scorer
|
||||
(`score.py`, arms now auto-discovered), same adversarial probes (`probe_e.py`,
|
||||
`probe_f.py`), same extension protocol (phase-1 git commit, cost = `git diff
|
||||
--numstat` insertions + new-file LOC). Caveman = `JuliusBrussee/caveman` SKILL.md
|
||||
verbatim (full level), saved at `ponytail-bench\caveman-SKILL.md`. One fresh
|
||||
subagent per task × arm, same model for all 16 runs. Caveat: this model/harness
|
||||
differs from the original Cursor runs, so comparisons to the old treatment
|
||||
numbers are directional; the ponytail4-vs-caveman head-to-head is same-model.
|
||||
|
||||
## v4 changes (the hardening, ~10 lines of prompt total)
|
||||
|
||||
1. **Test reflex** (brief 5.1): non-trivial logic leaves ONE runnable check —
|
||||
assert-based `demo()`/`__main__` self-check or one small `test_*.py`. No
|
||||
frameworks. One-liners need no test.
|
||||
2. **Ceiling comments** (5.2): a `ponytail:` shortcut with a known ceiling must
|
||||
name the ceiling and the upgrade path in the comment.
|
||||
3. **Robust variant rule** (5.3): between two same-size stdlib options, take the
|
||||
edge-case-correct one.
|
||||
|
||||
Applied to SKILL.md, all five cross-agent rule copies, the hook fallback, and a
|
||||
guard line in ponytail-review (never flag the minimal check as bloat).
|
||||
|
||||
## Build phase — non-blank LOC / .py files (scorer-verified)
|
||||
|
||||
| Task | Control (orig) | Treatment v3 (orig) | **Ponytail v4** | **Caveman** |
|
||||
|---|--:|--:|--:|--:|
|
||||
| A log CLI | 970 / 13 | 150 / 1 | **145 / 2** | 283 / 1 |
|
||||
| B file sync | 587 / 9 | 175 / 2 | **99 / 1** | 228 / 2 |
|
||||
| C dispatcher | 726 / 13 | 85 / 1 | **73 / 1** | 396 / 10 |
|
||||
| D validation | 343 / 8 | 93 / 1 | **70 / 1** | 218 / 3 |
|
||||
| E auth | 155 / 1 | 74 / 1 | **49 / 1** | 148 / 1 |
|
||||
| F ledger | 162 / 1 | 86 / 1 | **54 / 1** | 167 / 1 |
|
||||
| **Total** | 2943 | 663 | **490** | 1440 |
|
||||
|
||||
v4 is at or below v3 on every task (−3% to −43%) **despite now shipping a
|
||||
runnable check in all six arms** — the test reflex did not cause bloat creep.
|
||||
v4 is 34% of Caveman's size. Task A is the one place Caveman has fewer .py
|
||||
files (1 vs 2): v4's second file is the 24-line regression check Caveman
|
||||
doesn't ship — deleting it to win file count would sacrifice the safety clause
|
||||
to win on size, which the brief forbids.
|
||||
|
||||
## Extension phase (tasks C, D — surprise requests, git-measured)
|
||||
|
||||
| Metric | C v4 | C caveman | D v4 | D caveman |
|
||||
|---|--:|--:|--:|--:|
|
||||
| Lines changed (insertions + new-file LOC) | **41** | 156 | **55** | 257 |
|
||||
| Files touched | 1 | 7 | 1 | 3 |
|
||||
| Still correct after | yes | yes | yes | yes |
|
||||
|
||||
v4 honored the requested seams (duck-typed registry in C, `@rule` registry in
|
||||
D) and extended 74–79% cheaper. Both arms' extended demos re-run exit 0.
|
||||
|
||||
## Safety — adversarial probes (independently executed)
|
||||
|
||||
| Probe | v4 | caveman |
|
||||
|---|--:|--:|
|
||||
| Security, task E (8 checks) | **8/8** | 8/8 |
|
||||
| Concurrency, task F (6 checks) | **6/6** | 6/6 |
|
||||
|
||||
No regression from the added rules. v4's E chose PBKDF2-HMAC-SHA256 (600k
|
||||
iters) + 16-byte `secrets` salt + `hmac.compare_digest` + `token_urlsafe(32)`;
|
||||
F kept integer cents + a global lock with the ceiling comment naming the
|
||||
per-account-lock upgrade (5.2 working as designed; Caveman built per-account
|
||||
locks at 3× the LOC).
|
||||
|
||||
## Correctness
|
||||
|
||||
19/19 independent re-runs exit 0 (14 build demos/tests + 5 post-extension).
|
||||
|
||||
## Acceptance criteria (brief §5.6)
|
||||
|
||||
1. Probes 100% — **pass** (8/8 + 6/6).
|
||||
2. Every treatment arm ships a runnable check — **pass** (A: `test_loganalyze.py`;
|
||||
B–F: assert-based `__main__` checks; all executed). This was the #1 gap (was 1/4).
|
||||
3. LOC within ~20% of v3 treatment numbers — **pass on intent**: every arm at or
|
||||
below v3 (A −3%, C −14%; B/D/E/F 25–43% *below* — leaner, not bloated).
|
||||
4. Ceiling-bearing `ponytail:` comments name upgrade paths — **pass**, verified
|
||||
per arm: global lock→per-account locks (F), no token TTL→add TTL (E),
|
||||
sequential sends→async/threaded + hardcoded route→routing table (C),
|
||||
special-cased `unique`→DATASET_RULES registry (D), observed-hours stats→
|
||||
impute full range (A), no empty-dir handling→dir pass (B).
|
||||
5. Head-to-head vs Caveman — **pass**: ≥ on every axis, strictly better on three.
|
||||
- Safety: tie at 100% (≥, never regressed to win on size).
|
||||
- Size: LOC strictly better 6/6; files ≤ on 5/6 (A caveat above).
|
||||
- Extension cost: strictly better on both tasks.
|
||||
- Reviewability: strictly better — every v4 simplification is `ponytail:`-marked
|
||||
with its ceiling; Caveman's code marks only spec-allowed simulated transports,
|
||||
and its design trade-offs live in the chat report, invisible to a later reviewer.
|
||||
|
||||
## Addendum: same-model control arm (control2, added same day)
|
||||
|
||||
The control numbers above were inherited from the original Cursor harness,
|
||||
which could not expose token counts. Six fresh `task*-control2` arms were run
|
||||
through this harness (no skill, "build production-normal", same model, same
|
||||
specs), making all three arms same-model. Control2 passes both probes (8/8,
|
||||
6/6) and all 10 demo/test runs exit 0; extensions on C and D re-verified.
|
||||
|
||||
| Whole benchmark (6 builds + C/D extensions) | Control2 | Caveman | Ponytail v4 |
|
||||
|---|--:|--:|--:|
|
||||
| Build LOC | 3,629 | 1,440 | **490** |
|
||||
| Build LOC per task (A-F) | 946/656/808/677/260/282 | 283/228/396/218/148/167 | **145/99/73/70/49/54** |
|
||||
| Extension lines changed (C, D) | 378, 737 | 156, 257 | **41, 55** |
|
||||
| Agent tokens, total | 430,697 | 290,546 | **229,370 (-47% vs control2)** |
|
||||
| Agent wall time, total | 2,749s | 1,596s | **821s (3.3x)** |
|
||||
| Probes | 8/8 + 6/6 | 8/8 + 6/6 | 8/8 + 6/6 |
|
||||
|
||||
Wall times carry parallel-scheduling noise (arms ran concurrently, n=1 per
|
||||
cell); token counts are exact from agent telemetry. The README "Numbers"
|
||||
section now cites this same-model dataset and retires the older 5-task v3
|
||||
figures (still recorded in `2026-06-12-caveman-vs-ponytail.md`).
|
||||
|
||||
## Residual (honest notes)
|
||||
|
||||
- A's spike stats still use observed-hours-only mean+3σ rather than a
|
||||
leave-one-out/imputed baseline (Caveman zero-filled the hour range). The 5.3
|
||||
rule softened but did not eliminate the naive-algorithm tendency; the choice
|
||||
is now at least documented with its upgrade path (5.2). Candidate for a
|
||||
future eval if it bites in practice.
|
||||
- Caveman is a prose-compression skill that explicitly writes code "normal" —
|
||||
it loses on code size by design. The meaningful result is that adding the
|
||||
test reflex did not erode ponytail's size advantage or its 100% probe record.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Portability
|
||||
|
||||
Ponytail is an agent-portable skill distribution. The skills in `skills/` hold
|
||||
the core behavior; host-specific files are adapters that make that behavior easy
|
||||
to load in a given agent.
|
||||
|
||||
## Supported Adapters
|
||||
|
||||
| Host | Files | Notes |
|
||||
|------|-------|-------|
|
||||
| Claude Code | `.claude-plugin/`, `commands/`, `hooks/` | Full plugin install with session activation, mode tracking, commands, and statusline support. |
|
||||
| Codex | `.codex-plugin/plugin.json`, `hooks/hooks.json`, `hooks/`, `skills/` | Plugin install with the same skills plus lifecycle hooks for activation and mode tracking. |
|
||||
| Cursor | `.cursor/rules/ponytail.mdc` | Always-on project rule. |
|
||||
| Windsurf | `.windsurf/rules/ponytail.md` | Project rule. |
|
||||
| Cline | `.clinerules/ponytail.md` | Project rule. |
|
||||
| GitHub Copilot | `.github/copilot-instructions.md` | Repository instruction file. |
|
||||
| Kiro | `.kiro/steering/ponytail.md` | Steering rule; copy globally or into a project. |
|
||||
| Generic agents | `AGENTS.md` or `skills/*/SKILL.md` | Copy the compact rule file or load the skill files directly. |
|
||||
|
||||
## Adapter Rule
|
||||
|
||||
Keep adapters thin. When a host supports skills or hooks, point it at the
|
||||
existing `skills/` and `hooks/` files. When a host only supports project
|
||||
instructions, keep its copied rule text aligned with `AGENTS.md`.
|
||||
|
||||
## Portable Behavior
|
||||
|
||||
- `skills/ponytail/SKILL.md`: lazy senior dev mode
|
||||
- `skills/ponytail-review/SKILL.md`: over-engineering review
|
||||
- `skills/ponytail-help/SKILL.md`: quick reference
|
||||
- `AGENTS.md`: compact always-on instruction set for agents without skill support
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|clear|compact",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${PLUGIN_ROOT}/hooks/ponytail-activate.js\"",
|
||||
"commandWindows": "node \"%PLUGIN_ROOT%\\hooks\\ponytail-activate.js\"",
|
||||
"timeout": 5,
|
||||
"statusMessage": "Loading ponytail mode..."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${PLUGIN_ROOT}/hooks/ponytail-mode-tracker.js\"",
|
||||
"commandWindows": "node \"%PLUGIN_ROOT%\\hooks\\ponytail-mode-tracker.js\"",
|
||||
"timeout": 5,
|
||||
"statusMessage": "Tracking ponytail mode..."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+13
-94
@@ -10,119 +10,38 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { getDefaultMode } = require('./ponytail-config');
|
||||
const { getPonytailInstructions } = require('./ponytail-instructions');
|
||||
const {
|
||||
clearMode,
|
||||
isCodex,
|
||||
setMode,
|
||||
writeHookOutput,
|
||||
} = require('./ponytail-runtime');
|
||||
|
||||
const claudeDir = path.join(os.homedir(), '.claude');
|
||||
const flagPath = path.join(claudeDir, '.ponytail-active');
|
||||
const settingsPath = path.join(claudeDir, 'settings.json');
|
||||
|
||||
const mode = getDefaultMode();
|
||||
|
||||
// "off" mode — skip activation entirely, don't write flag or emit rules
|
||||
if (mode === 'off') {
|
||||
try { fs.unlinkSync(flagPath); } catch (e) {}
|
||||
process.stdout.write('OK');
|
||||
clearMode();
|
||||
writeHookOutput('SessionStart', 'off', isCodex ? '' : 'OK');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 1. Write flag file
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(flagPath), { recursive: true });
|
||||
fs.writeFileSync(flagPath, mode);
|
||||
setMode(mode);
|
||||
} catch (e) {
|
||||
// Silent fail -- flag is best-effort, don't block the hook
|
||||
}
|
||||
|
||||
// 2. Emit the ponytail ruleset, filtered to the active intensity level.
|
||||
// A short summary is too weak — models drift back to over-building
|
||||
// mid-conversation, especially after context compression prunes it.
|
||||
// Full rules with examples anchor behavior much more reliably.
|
||||
//
|
||||
// Reads SKILL.md at runtime so edits to the source of truth propagate
|
||||
// automatically — no hardcoded duplication to go stale.
|
||||
|
||||
// Modes that have their own independent skill files — not intensity levels.
|
||||
// For these, emit a short activation line; the skill itself handles behavior.
|
||||
const INDEPENDENT_MODES = new Set(['review']);
|
||||
|
||||
if (INDEPENDENT_MODES.has(mode)) {
|
||||
process.stdout.write('PONYTAIL MODE ACTIVE — level: ' + mode + '. Behavior defined by /ponytail-' + mode + ' skill.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Read SKILL.md — the single source of truth for ponytail behavior.
|
||||
// Plugin installs: __dirname = <plugin_root>/hooks/, SKILL.md at <plugin_root>/skills/ponytail/SKILL.md
|
||||
let skillContent = '';
|
||||
try {
|
||||
skillContent = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'skills', 'ponytail', 'SKILL.md'), 'utf8'
|
||||
);
|
||||
} catch (e) { /* standalone install — will use fallback below */ }
|
||||
|
||||
let output;
|
||||
|
||||
if (skillContent) {
|
||||
// Strip YAML frontmatter
|
||||
const body = skillContent.replace(/^---[\s\S]*?---\s*/, '');
|
||||
|
||||
// Filter intensity table: keep header rows + only the active level's row
|
||||
const filtered = body.split('\n').reduce((acc, line) => {
|
||||
// Intensity table rows start with | **level** |
|
||||
const tableRowMatch = line.match(/^\|\s*\*\*(\S+?)\*\*\s*\|/);
|
||||
if (tableRowMatch) {
|
||||
if (tableRowMatch[1] === mode) {
|
||||
acc.push(line);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Example lines start with "- level:" — keep only lines matching active level
|
||||
const exampleMatch = line.match(/^- (\S+?):\s/);
|
||||
if (exampleMatch) {
|
||||
if (exampleMatch[1] === mode) {
|
||||
acc.push(line);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push(line);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
output = 'PONYTAIL MODE ACTIVE — level: ' + mode + '\n\n' + filtered.join('\n');
|
||||
} else {
|
||||
// Fallback when SKILL.md is not found (hook installed without skills dir).
|
||||
// Minimum viable ruleset — better than nothing.
|
||||
output =
|
||||
'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' +
|
||||
'## Persistence\n\n' +
|
||||
'ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure. Off only: "stop ponytail" / "normal mode".\n\n' +
|
||||
'Current level: **' + mode + '**. Switch: `/ponytail lite|full|ultra`.\n\n' +
|
||||
'## The ladder\n\n' +
|
||||
'Before any code, stop at the first rung that holds:\n' +
|
||||
'1. Does this need to be built at all? (YAGNI)\n' +
|
||||
'2. Does the standard library do this? Use it.\n' +
|
||||
'3. Does a native platform feature cover it? Use it.\n' +
|
||||
'4. Does an already-installed dependency solve it? Use it.\n' +
|
||||
'5. Can this be one line? Make it one line.\n' +
|
||||
'6. Only then: write the minimum code that works.\n\n' +
|
||||
'## Rules\n\n' +
|
||||
'No abstractions that were not requested. No avoidable dependencies. No boilerplate nobody asked for. ' +
|
||||
'Deletion over addition. Boring over clever. Fewest files possible. ' +
|
||||
'Ship the lazy version and question the complex request in the same response — never stall. ' +
|
||||
'Mark intentional simplifications with a `ponytail:` comment.\n\n' +
|
||||
'## Output\n\n' +
|
||||
'Code first. Then at most three short lines: what was skipped, when to add it. ' +
|
||||
'If the explanation is longer than the code, delete the explanation.\n\n' +
|
||||
'## When NOT to be lazy\n\n' +
|
||||
'Never simplify away: input validation at trust boundaries, error handling that prevents data loss, ' +
|
||||
'security measures, accessibility basics, anything the user explicitly asked to keep.\n\n' +
|
||||
'## Boundaries\n\n' +
|
||||
'Ponytail governs what you build, not how you talk. "stop ponytail" or "normal mode": revert. Level persists until changed or session end.';
|
||||
}
|
||||
let output = getPonytailInstructions(mode);
|
||||
|
||||
// 3. Detect missing statusline config — nudge Claude to help set it up
|
||||
try {
|
||||
if (!isCodex) try {
|
||||
let hasStatusline = false;
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
@@ -151,4 +70,4 @@ try {
|
||||
// Silent fail — don't block session start over statusline detection
|
||||
}
|
||||
|
||||
process.stdout.write(output);
|
||||
writeHookOutput('SessionStart', mode, output);
|
||||
|
||||
@@ -13,7 +13,25 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const DEFAULT_MODE = 'full';
|
||||
const VALID_MODES = ['off', 'lite', 'full', 'ultra', 'review'];
|
||||
const RUNTIME_MODES = ['off', 'lite', 'full', 'ultra'];
|
||||
|
||||
function normalizeMode(mode) {
|
||||
if (typeof mode !== 'string') return null;
|
||||
const normalized = mode.trim().toLowerCase();
|
||||
return RUNTIME_MODES.includes(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
function normalizeConfigMode(mode) {
|
||||
if (typeof mode !== 'string') return null;
|
||||
const normalized = mode.trim().toLowerCase();
|
||||
return VALID_MODES.includes(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
function normalizePersistedMode(mode) {
|
||||
return normalizeMode(mode) || normalizeConfigMode(mode);
|
||||
}
|
||||
|
||||
function getConfigDir() {
|
||||
if (process.env.XDG_CONFIG_HOME) {
|
||||
@@ -51,7 +69,28 @@ function getDefaultMode() {
|
||||
}
|
||||
|
||||
// 3. Default
|
||||
return 'full';
|
||||
return DEFAULT_MODE;
|
||||
}
|
||||
|
||||
module.exports = { getDefaultMode, getConfigDir, getConfigPath, VALID_MODES };
|
||||
function writeDefaultMode(mode) {
|
||||
const normalized = normalizeConfigMode(mode);
|
||||
if (!normalized) return null;
|
||||
|
||||
const configPath = getConfigPath();
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, JSON.stringify({ defaultMode: normalized }, null, 2), 'utf8');
|
||||
return normalized;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_MODE,
|
||||
VALID_MODES,
|
||||
RUNTIME_MODES,
|
||||
getDefaultMode,
|
||||
getConfigDir,
|
||||
getConfigPath,
|
||||
normalizeMode,
|
||||
normalizeConfigMode,
|
||||
normalizePersistedMode,
|
||||
writeDefaultMode,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env node
|
||||
// Shared Ponytail instruction builder for Claude hooks and Pi extension.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { DEFAULT_MODE, normalizeMode, normalizePersistedMode } = require('./ponytail-config');
|
||||
|
||||
const INDEPENDENT_MODES = new Set(['review']);
|
||||
const SKILL_PATH = path.join(__dirname, '..', 'skills', 'ponytail', 'SKILL.md');
|
||||
|
||||
function filterSkillBodyForMode(body, mode) {
|
||||
const effectiveMode = normalizeMode(mode) || DEFAULT_MODE;
|
||||
const withoutFrontmatter = String(body || '').replace(/^---[\s\S]*?---\s*/, '');
|
||||
|
||||
return withoutFrontmatter
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => {
|
||||
const tableMatch = line.match(/^\|\s*\*\*(.+?)\*\*\s*\|/);
|
||||
if (tableMatch) return tableMatch[1].trim() === effectiveMode;
|
||||
|
||||
const exampleMatch = line.match(/^-\s*([^:]+):\s*/);
|
||||
if (exampleMatch) return exampleMatch[1].trim() === effectiveMode;
|
||||
|
||||
return true;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function getFallbackInstructions(mode) {
|
||||
return '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' +
|
||||
'## Persistence\n\n' +
|
||||
'ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure. Off only: "stop ponytail" / "normal mode".\n\n' +
|
||||
'Current level: **' + mode + '**. Switch: `/ponytail lite|full|ultra`.\n\n' +
|
||||
'## The ladder\n\n' +
|
||||
'Before any code, stop at the first rung that holds:\n' +
|
||||
'1. Does this need to be built at all? (YAGNI)\n' +
|
||||
'2. Does the standard library do this? Use it.\n' +
|
||||
'3. Does a native platform feature cover it? Use it.\n' +
|
||||
'4. Does an already-installed dependency solve it? Use it.\n' +
|
||||
'5. Can this be one line? Make it one line.\n' +
|
||||
'6. Only then: write the minimum code that works.\n\n' +
|
||||
'## Rules\n\n' +
|
||||
'No abstractions that were not requested. No avoidable dependencies. No boilerplate nobody asked for. ' +
|
||||
'Deletion over addition. Boring over clever. Fewest files possible. ' +
|
||||
'Ship the lazy version and question the complex request in the same response — never stall. ' +
|
||||
'Between two same-size stdlib options, pick the one correct on edge cases. ' +
|
||||
'Mark intentional simplifications with a `ponytail:` comment — a shortcut with a known ceiling names the ceiling and the upgrade path in the comment.\n\n' +
|
||||
'## Output\n\n' +
|
||||
'Code first. Then at most three short lines: what was skipped, when to add it. ' +
|
||||
'If the explanation is longer than the code, delete the explanation.\n\n' +
|
||||
'## When NOT to be lazy\n\n' +
|
||||
'Never simplify away: input validation at trust boundaries, error handling that prevents data loss, ' +
|
||||
'security measures, accessibility basics, anything the user explicitly asked to keep. ' +
|
||||
'Non-trivial logic leaves ONE runnable check behind (assert-based demo/self-check or one small test file; no frameworks). Trivial one-liners need no test.\n\n' +
|
||||
'## Boundaries\n\n' +
|
||||
'Ponytail governs what you build, not how you talk. "stop ponytail" or "normal mode": revert. Level persists until changed or session end.';
|
||||
}
|
||||
|
||||
function getPonytailInstructions(mode) {
|
||||
const configuredMode = normalizePersistedMode(mode) || DEFAULT_MODE;
|
||||
|
||||
if (INDEPENDENT_MODES.has(configuredMode)) {
|
||||
return 'PONYTAIL MODE ACTIVE — level: ' + configuredMode + '. Behavior defined by /ponytail-' + configuredMode + ' skill.';
|
||||
}
|
||||
|
||||
const effectiveMode = normalizeMode(configuredMode) || DEFAULT_MODE;
|
||||
|
||||
try {
|
||||
return 'PONYTAIL MODE ACTIVE — level: ' + effectiveMode + '\n\n' +
|
||||
filterSkillBodyForMode(fs.readFileSync(SKILL_PATH, 'utf8'), effectiveMode);
|
||||
} catch (e) {
|
||||
return getFallbackInstructions(effectiveMode);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
filterSkillBodyForMode,
|
||||
getFallbackInstructions,
|
||||
getPonytailInstructions,
|
||||
};
|
||||
@@ -2,12 +2,8 @@
|
||||
// ponytail — UserPromptSubmit hook to track which ponytail mode is active
|
||||
// Inspects user input for /ponytail commands and writes mode to flag file
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { getDefaultMode } = require('./ponytail-config');
|
||||
|
||||
const flagPath = path.join(os.homedir(), '.claude', '.ponytail-active');
|
||||
const { clearMode, setMode, writeHookOutput } = require('./ponytail-runtime');
|
||||
|
||||
let input = '';
|
||||
process.stdin.on('data', chunk => { input += chunk; });
|
||||
@@ -18,9 +14,9 @@ process.stdin.on('end', () => {
|
||||
const prompt = (data.prompt || '').trim().toLowerCase();
|
||||
|
||||
// Match /ponytail commands
|
||||
if (prompt.startsWith('/ponytail')) {
|
||||
if (/^[/@$]ponytail/.test(prompt)) {
|
||||
const parts = prompt.split(/\s+/);
|
||||
const cmd = parts[0]; // /ponytail, /ponytail-review, /ponytail:ponytail, etc.
|
||||
const cmd = parts[0].replace(/^[@$]/, '/');
|
||||
const arg = parts[1] || '';
|
||||
|
||||
let mode = null;
|
||||
@@ -36,16 +32,22 @@ process.stdin.on('end', () => {
|
||||
}
|
||||
|
||||
if (mode && mode !== 'off') {
|
||||
fs.mkdirSync(path.dirname(flagPath), { recursive: true });
|
||||
fs.writeFileSync(flagPath, mode);
|
||||
setMode(mode);
|
||||
writeHookOutput(
|
||||
'UserPromptSubmit',
|
||||
mode,
|
||||
'PONYTAIL MODE CHANGED — level: ' + mode,
|
||||
);
|
||||
} else if (mode === 'off') {
|
||||
try { fs.unlinkSync(flagPath); } catch (e) {}
|
||||
clearMode();
|
||||
writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF');
|
||||
}
|
||||
}
|
||||
|
||||
// Detect deactivation
|
||||
if (/\b(stop ponytail|normal mode)\b/i.test(prompt)) {
|
||||
try { fs.unlinkSync(flagPath); } catch (e) {}
|
||||
clearMode();
|
||||
writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF');
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent fail
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const isCodex = Boolean(process.env.PLUGIN_DATA);
|
||||
const statePath = isCodex
|
||||
? path.join(process.env.PLUGIN_DATA, '.ponytail-active')
|
||||
: path.join(os.homedir(), '.claude', '.ponytail-active');
|
||||
|
||||
function setMode(mode) {
|
||||
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
||||
fs.writeFileSync(statePath, mode);
|
||||
}
|
||||
|
||||
function clearMode() {
|
||||
try { fs.unlinkSync(statePath); } catch (e) {}
|
||||
}
|
||||
|
||||
function writeHookOutput(event, mode, context = '') {
|
||||
if (!isCodex) {
|
||||
process.stdout.write(context);
|
||||
return;
|
||||
}
|
||||
const output = { systemMessage: `PONYTAIL:${mode.toUpperCase()}` };
|
||||
if (context) {
|
||||
output.hookSpecificOutput = {
|
||||
hookEventName: event,
|
||||
additionalContext: context,
|
||||
};
|
||||
}
|
||||
process.stdout.write(JSON.stringify(output));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
clearMode,
|
||||
isCodex,
|
||||
setMode,
|
||||
writeHookOutput,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "ponytail",
|
||||
"version": "0.1.0",
|
||||
"description": "Lazy senior dev mode for AI agents. The best code is the code you never wrote.",
|
||||
"keywords": ["pi-package", "pi", "skills", "ponytail"],
|
||||
"license": "MIT",
|
||||
"pi": {
|
||||
"extensions": ["./pi-extension/index.js"],
|
||||
"skills": ["./skills"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const {
|
||||
DEFAULT_MODE,
|
||||
getDefaultMode,
|
||||
normalizeMode,
|
||||
normalizeConfigMode,
|
||||
normalizePersistedMode,
|
||||
writeDefaultMode,
|
||||
} = require("../hooks/ponytail-config.js");
|
||||
const { getPonytailInstructions, filterSkillBodyForMode } = require("../hooks/ponytail-instructions.js");
|
||||
|
||||
export { filterSkillBodyForMode };
|
||||
export const readDefaultMode = getDefaultMode;
|
||||
|
||||
export function resolveSessionMode(entries, fallbackMode = DEFAULT_MODE) {
|
||||
const fallback = normalizePersistedMode(fallbackMode) || DEFAULT_MODE;
|
||||
if (!Array.isArray(entries)) return fallback;
|
||||
|
||||
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
||||
const entry = entries[i];
|
||||
if (entry?.type !== "custom" || entry?.customType !== "ponytail-mode") continue;
|
||||
|
||||
const mode = normalizePersistedMode(entry?.data?.mode);
|
||||
if (mode) return mode;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function parsePonytailCommand(text, defaultMode = DEFAULT_MODE) {
|
||||
const fallback = normalizePersistedMode(defaultMode) || DEFAULT_MODE;
|
||||
const normalizedText = String(text || "").trim().toLowerCase();
|
||||
|
||||
if (!normalizedText) {
|
||||
return { type: "set-mode", mode: fallback === "off" ? "full" : fallback };
|
||||
}
|
||||
|
||||
const [primary, secondary] = normalizedText.split(/\s+/);
|
||||
|
||||
if (primary === "status") return { type: "status" };
|
||||
|
||||
if (primary === "default") {
|
||||
const mode = normalizeConfigMode(secondary);
|
||||
return mode ? { type: "set-default", mode } : { type: "invalid", reason: "invalid-default-mode" };
|
||||
}
|
||||
|
||||
const mode = normalizeMode(primary);
|
||||
return mode ? { type: "set-mode", mode } : { type: "invalid", reason: "invalid-mode", mode: primary };
|
||||
}
|
||||
|
||||
export { writeDefaultMode };
|
||||
|
||||
export default function ponytailExtension(pi) {
|
||||
let currentMode = DEFAULT_MODE;
|
||||
let configuredDefaultMode = getDefaultMode();
|
||||
|
||||
const setMode = (mode, ctx) => {
|
||||
const normalized = normalizePersistedMode(mode);
|
||||
if (!normalized) return;
|
||||
|
||||
currentMode = normalized;
|
||||
pi.appendEntry("ponytail-mode", { mode: normalized });
|
||||
ctx?.ui?.notify?.(`Ponytail mode set to ${normalized}.`, "info");
|
||||
};
|
||||
|
||||
const sendAlias = (skillName, args, ctx) => {
|
||||
const normalized = String(args || "").trim();
|
||||
const message = normalized ? `${skillName} ${normalized}` : skillName;
|
||||
|
||||
if (ctx?.isIdle?.() === false) {
|
||||
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
||||
ctx?.ui?.notify?.(`${skillName} queued as follow-up.`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
pi.sendUserMessage(message);
|
||||
};
|
||||
|
||||
pi.registerCommand("ponytail", {
|
||||
description: "Set or report Ponytail mode",
|
||||
handler: async (args, ctx) => {
|
||||
const parsed = parsePonytailCommand(args, configuredDefaultMode);
|
||||
|
||||
if (parsed.type === "status") {
|
||||
ctx?.ui?.notify?.(`Ponytail: current ${currentMode} • default ${configuredDefaultMode}`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.type === "set-default") {
|
||||
const written = writeDefaultMode(parsed.mode);
|
||||
if (written) {
|
||||
configuredDefaultMode = getDefaultMode();
|
||||
const message = configuredDefaultMode === written
|
||||
? `Default Ponytail mode set to ${written}.`
|
||||
: `Saved default ${written}, but env override keeps default at ${configuredDefaultMode}.`;
|
||||
ctx?.ui?.notify?.(message, "info");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.type === "set-mode") {
|
||||
setMode(parsed.mode, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx?.ui?.notify?.("Unknown or unsupported /ponytail mode.", "warning");
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("ponytail-review", {
|
||||
description: "Run /skill:ponytail-review",
|
||||
handler: (_args, ctx) => sendAlias("/skill:ponytail-review", "", ctx),
|
||||
});
|
||||
|
||||
pi.registerCommand("ponytail-help", {
|
||||
description: "Run /skill:ponytail-help",
|
||||
handler: (_args, ctx) => sendAlias("/skill:ponytail-help", "", ctx),
|
||||
});
|
||||
|
||||
pi.on("input", async (event) => {
|
||||
if (event?.source === "extension") return;
|
||||
|
||||
const text = String(event?.text || "");
|
||||
if (currentMode !== "off" && /\b(stop ponytail|normal mode)\b/i.test(text)) {
|
||||
setMode("off");
|
||||
}
|
||||
});
|
||||
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
const entries = ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || [];
|
||||
configuredDefaultMode = getDefaultMode();
|
||||
currentMode = resolveSessionMode(entries, configuredDefaultMode);
|
||||
});
|
||||
|
||||
pi.on("before_agent_start", async (event) => {
|
||||
if (!currentMode || currentMode === "off") return;
|
||||
return { systemPrompt: `${event.systemPrompt}\n\n${getPonytailInstructions(currentMode)}` };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "ponytail-pi-extension-dev",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "node --test ./test/*.test.js"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import ponytailExtension from "../index.js";
|
||||
|
||||
function createPiHarness() {
|
||||
const events = new Map();
|
||||
const commands = new Map();
|
||||
const appendedEntries = [];
|
||||
const sentUserMessages = [];
|
||||
|
||||
const pi = {
|
||||
on(eventName, handler) {
|
||||
events.set(eventName, handler);
|
||||
},
|
||||
registerCommand(name, options) {
|
||||
commands.set(name, options);
|
||||
},
|
||||
appendEntry(customType, data) {
|
||||
appendedEntries.push({ customType, data });
|
||||
},
|
||||
sendUserMessage(text, options) {
|
||||
sentUserMessages.push({ text, options });
|
||||
},
|
||||
};
|
||||
|
||||
ponytailExtension(pi);
|
||||
return { events, commands, appendedEntries, sentUserMessages };
|
||||
}
|
||||
|
||||
function createCommandContext(overrides = {}) {
|
||||
return {
|
||||
isIdle: () => true,
|
||||
sessionManager: { getEntries: () => [] },
|
||||
ui: { notify() {} },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function withTempConfig(fn) {
|
||||
const tempConfigHome = mkdtempSync(join(tmpdir(), "ponytail-test-"));
|
||||
const previousXdg = process.env.XDG_CONFIG_HOME;
|
||||
process.env.XDG_CONFIG_HOME = tempConfigHome;
|
||||
|
||||
return Promise.resolve()
|
||||
.then(fn)
|
||||
.finally(() => {
|
||||
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
||||
else process.env.XDG_CONFIG_HOME = previousXdg;
|
||||
rmSync(tempConfigHome, { recursive: true, force: true });
|
||||
});
|
||||
}
|
||||
|
||||
test("extension registers Ponytail commands", () => {
|
||||
const { commands } = createPiHarness();
|
||||
|
||||
assert.deepEqual([...commands.keys()].sort(), ["ponytail", "ponytail-help", "ponytail-review"]);
|
||||
});
|
||||
|
||||
test("/ponytail updates session mode and injects instructions", async () => withTempConfig(async () => {
|
||||
const { commands, events, appendedEntries } = createPiHarness();
|
||||
const ctx = createCommandContext();
|
||||
|
||||
await events.get("session_start")({ reason: "startup" }, ctx);
|
||||
await commands.get("ponytail").handler("ultra", ctx);
|
||||
|
||||
assert.deepEqual(appendedEntries.at(-1), {
|
||||
customType: "ponytail-mode",
|
||||
data: { mode: "ultra" },
|
||||
});
|
||||
|
||||
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
||||
assert.ok(result.systemPrompt.includes("PONYTAIL MODE ACTIVE"));
|
||||
assert.ok(result.systemPrompt.includes("ultra"));
|
||||
}));
|
||||
|
||||
test("session_start restores latest persisted mode", async () => withTempConfig(async () => {
|
||||
const { events } = createPiHarness();
|
||||
const ctx = createCommandContext({
|
||||
sessionManager: {
|
||||
getEntries: () => [
|
||||
{ type: "custom", customType: "ponytail-mode", data: { mode: "lite" } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await events.get("session_start")({ reason: "resume" }, ctx);
|
||||
const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
||||
|
||||
assert.ok(result.systemPrompt.includes("lite"));
|
||||
}));
|
||||
|
||||
test("skill alias commands delegate to Pi skill commands", async () => {
|
||||
const { commands, sentUserMessages } = createPiHarness();
|
||||
const ctx = createCommandContext();
|
||||
|
||||
await commands.get("ponytail-review").handler("", ctx);
|
||||
await commands.get("ponytail-help").handler("", ctx);
|
||||
|
||||
assert.deepEqual(sentUserMessages.map((entry) => entry.text), [
|
||||
"/skill:ponytail-review",
|
||||
"/skill:ponytail-help",
|
||||
]);
|
||||
});
|
||||
|
||||
test("normal mode disables persistent instructions", async () => withTempConfig(async () => {
|
||||
const { commands, events } = createPiHarness();
|
||||
const ctx = createCommandContext();
|
||||
|
||||
await events.get("session_start")({ reason: "startup" }, ctx);
|
||||
await commands.get("ponytail").handler("ultra", ctx);
|
||||
await events.get("input")({ text: "normal mode", source: "interactive" }, ctx);
|
||||
|
||||
const disabled = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
|
||||
assert.equal(disabled, undefined);
|
||||
}));
|
||||
@@ -0,0 +1,68 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
filterSkillBodyForMode,
|
||||
parsePonytailCommand,
|
||||
readDefaultMode,
|
||||
resolveSessionMode,
|
||||
writeDefaultMode,
|
||||
} from "../index.js";
|
||||
|
||||
test("parsePonytailCommand falls back to full when invoked bare and default is off", () => {
|
||||
assert.deepEqual(parsePonytailCommand("", "off"), { type: "set-mode", mode: "full" });
|
||||
});
|
||||
|
||||
test("parsePonytailCommand parses modes, status, and default subcommand", () => {
|
||||
assert.deepEqual(parsePonytailCommand("ultra", "full"), { type: "set-mode", mode: "ultra" });
|
||||
assert.deepEqual(parsePonytailCommand("status", "full"), { type: "status" });
|
||||
assert.deepEqual(parsePonytailCommand("default lite", "full"), { type: "set-default", mode: "lite" });
|
||||
});
|
||||
|
||||
test("resolveSessionMode prefers latest persisted session mode", () => {
|
||||
const entries = [
|
||||
{ type: "custom", customType: "ponytail-mode", data: { mode: "lite" } },
|
||||
{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } },
|
||||
];
|
||||
|
||||
assert.equal(resolveSessionMode(entries, "full"), "ultra");
|
||||
});
|
||||
|
||||
test("readDefaultMode and writeDefaultMode use XDG config path", () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "ponytail-config-"));
|
||||
const previousXdg = process.env.XDG_CONFIG_HOME;
|
||||
const previousDefault = process.env.PONYTAIL_DEFAULT_MODE;
|
||||
const configPath = join(tempDir, "ponytail", "config.json");
|
||||
process.env.XDG_CONFIG_HOME = tempDir;
|
||||
delete process.env.PONYTAIL_DEFAULT_MODE;
|
||||
|
||||
try {
|
||||
assert.equal(readDefaultMode(), "full");
|
||||
assert.equal(writeDefaultMode("ultra"), "ultra");
|
||||
assert.equal(readDefaultMode(), "ultra");
|
||||
assert.ok(existsSync(configPath));
|
||||
assert.deepEqual(JSON.parse(readFileSync(configPath, "utf8")), { defaultMode: "ultra" });
|
||||
} finally {
|
||||
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
||||
else process.env.XDG_CONFIG_HOME = previousXdg;
|
||||
if (previousDefault === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
||||
else process.env.PONYTAIL_DEFAULT_MODE = previousDefault;
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("filterSkillBodyForMode keeps only requested intensity examples and rows", () => {
|
||||
const body = `---\nname: ponytail\n---\n| **lite** | keep lite |\n| **full** | keep full |\n| **ultra** | keep ultra |\n- lite: Lite example\n- full: Full example\n- ultra: Ultra example\nOther line`;
|
||||
|
||||
const filtered = filterSkillBodyForMode(body, "ultra");
|
||||
|
||||
assert.ok(!filtered.includes("keep lite"));
|
||||
assert.ok(!filtered.includes("keep full"));
|
||||
assert.ok(filtered.includes("keep ultra"));
|
||||
assert.ok(!filtered.includes("Lite example"));
|
||||
assert.ok(filtered.includes("Ultra example"));
|
||||
assert.ok(filtered.includes("Other line"));
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
|
||||
function read(relPath) {
|
||||
return fs.readFileSync(path.join(root, relPath), 'utf8').replace(/\r\n/g, '\n').trim();
|
||||
}
|
||||
|
||||
function stripFrontmatter(text) {
|
||||
return text.replace(/^---\n[\s\S]*?\n---\n*/, '').trim();
|
||||
}
|
||||
|
||||
const agents = read('AGENTS.md');
|
||||
const canonical = agents.replace(/\n\n\(Yes, this file also applies[\s\S]*?\)$/, '').trim();
|
||||
|
||||
// Compact copies: same body as AGENTS.md, host-specific frontmatter stripped.
|
||||
const copies = [
|
||||
['.cursor/rules/ponytail.mdc', stripFrontmatter],
|
||||
['.windsurf/rules/ponytail.md', text => text.trim()],
|
||||
['.clinerules/ponytail.md', text => text.trim()],
|
||||
['.github/copilot-instructions.md', text => text.trim()],
|
||||
['.kiro/steering/ponytail.md', stripFrontmatter],
|
||||
];
|
||||
|
||||
let failed = false;
|
||||
|
||||
for (const [relPath, normalize] of copies) {
|
||||
const actual = normalize(read(relPath));
|
||||
if (actual !== canonical) {
|
||||
console.error(`${relPath} drifted from AGENTS.md`);
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// SKILL.md is the runtime source of truth and is longer than the compact body,
|
||||
// so it cannot be byte-compared. ponytail: canary, not full equality. Assert the
|
||||
// load-bearing rules survive verbatim in both the source and AGENTS.md. Changing
|
||||
// a rule's wording trips this, which is the reminder to propagate it everywhere.
|
||||
// Upgrade path: generate the copies from SKILL.md if this ever misses a real drift.
|
||||
const INVARIANTS = [
|
||||
'naive heuristic', // ceiling-comment rule
|
||||
'ONE runnable check', // test reflex
|
||||
'flimsier algorithm', // robust-variant rule
|
||||
'input validation at trust boundaries', // the "not lazy about" clause
|
||||
];
|
||||
|
||||
const skill = read('skills/ponytail/SKILL.md');
|
||||
const sources = [['skills/ponytail/SKILL.md', skill], ['AGENTS.md', agents]];
|
||||
for (const phrase of INVARIANTS) {
|
||||
for (const [label, text] of sources) {
|
||||
if (!text.includes(phrase)) {
|
||||
console.error(`${label} is missing rule invariant: "${phrase}"`);
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
console.error('Update the copied rule text, AGENTS.md, or SKILL.md so the shared rules match.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Rule copies match AGENTS.md; ${INVARIANTS.length} rule invariants present in SKILL.md and AGENTS.md.`);
|
||||
@@ -29,6 +29,9 @@ Level sticks until changed or session end.
|
||||
| **ponytail-review** | `/ponytail-review` | Over-engineering review: `L42: yagni: factory, one product. Inline.` |
|
||||
| **ponytail-help** | `/ponytail-help` | This card. |
|
||||
|
||||
Codex uses `@ponytail`, `@ponytail-review`, and `@ponytail-help`; Claude Code
|
||||
uses the slash-command forms above.
|
||||
|
||||
## Deactivate
|
||||
|
||||
Say "stop ponytail" or "normal mode". Resume anytime with `/ponytail`.
|
||||
|
||||
@@ -50,5 +50,7 @@ If there is nothing to cut, say `Lean already. Ship.` and stop.
|
||||
## Boundaries
|
||||
|
||||
Complexity only — correctness bugs, security holes, and performance go to a
|
||||
normal review pass, not this one. Does not apply the fixes, only lists them.
|
||||
normal review pass, not this one. A single smoke test or `assert`-based
|
||||
self-check is the ponytail minimum, not bloat — never flag it for deletion.
|
||||
Does not apply the fixes, only lists them.
|
||||
"stop ponytail-review" or "normal mode": revert to verbose review style.
|
||||
|
||||
@@ -46,7 +46,8 @@ higher one and move on. The first lazy solution that works is the right one.
|
||||
- Deletion over addition. Boring over clever — clever is what someone decodes at 3am.
|
||||
- Fewest files possible. Shortest working diff wins.
|
||||
- Complex request? Ship the lazy version and question it in the same response — "Did X; Y covers it. Need full X? Say so." Never stall on an answer you can default.
|
||||
- Mark deliberate simplifications with a `ponytail:` comment (`// ponytail: this exists`) — simple reads as intent, not ignorance.
|
||||
- Two stdlib options, same size? Take the one that's correct on edge cases. Lazy means writing less code, not picking the flimsier algorithm.
|
||||
- Mark deliberate simplifications with a `ponytail:` comment (`// ponytail: this exists`) — simple reads as intent, not ignorance. Shortcut with a known ceiling (global lock, O(n²) scan, naive heuristic)? The comment names the ceiling and the upgrade path: `# ponytail: global lock — per-account locks if throughput matters`.
|
||||
|
||||
## Output
|
||||
|
||||
@@ -77,6 +78,12 @@ that prevents data loss, security measures, accessibility basics, anything
|
||||
explicitly requested. User insists on the full version → build it, no
|
||||
re-arguing.
|
||||
|
||||
Non-trivial logic (a branch, a loop, a parser, a money/security path) leaves
|
||||
ONE runnable check behind — the smallest thing that fails if the logic
|
||||
breaks: an `assert`-based `demo()`/`__main__` self-check or one small
|
||||
`test_*.py`. No frameworks, no fixtures, no per-function suites unless
|
||||
asked. Trivial one-liners need no test — YAGNI applies to tests too.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Ponytail governs what you build, not how you talk (pair with Caveman for
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
|
||||
function run(script, env, input = '') {
|
||||
return spawnSync(process.execPath, [path.join(root, 'hooks', script)], {
|
||||
env: { ...process.env, ...env },
|
||||
input,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
|
||||
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ponytail-hooks-'));
|
||||
const home = path.join(temp, 'home');
|
||||
const pluginData = path.join(temp, 'plugin-data');
|
||||
fs.mkdirSync(home, { recursive: true });
|
||||
|
||||
// USERPROFILE alongside HOME: os.homedir() reads USERPROFILE on Windows, HOME on POSIX.
|
||||
const codexEnv = {
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
PLUGIN_DATA: pluginData,
|
||||
PONYTAIL_DEFAULT_MODE: 'ultra',
|
||||
};
|
||||
const codexState = path.join(pluginData, '.ponytail-active');
|
||||
|
||||
let result = run('ponytail-activate.js', codexEnv);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(fs.readFileSync(codexState, 'utf8'), 'ultra');
|
||||
let output = JSON.parse(result.stdout);
|
||||
assert.equal(output.systemMessage, 'PONYTAIL:ULTRA');
|
||||
assert.match(
|
||||
output.hookSpecificOutput.additionalContext,
|
||||
/PONYTAIL MODE ACTIVE — level: ultra/,
|
||||
);
|
||||
|
||||
result = run(
|
||||
'ponytail-mode-tracker.js',
|
||||
codexEnv,
|
||||
JSON.stringify({ prompt: '@ponytail lite' }),
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(fs.readFileSync(codexState, 'utf8'), 'lite');
|
||||
output = JSON.parse(result.stdout);
|
||||
assert.equal(output.systemMessage, 'PONYTAIL:LITE');
|
||||
|
||||
result = run(
|
||||
'ponytail-mode-tracker.js',
|
||||
codexEnv,
|
||||
JSON.stringify({ prompt: 'normal mode' }),
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(fs.existsSync(codexState), false);
|
||||
output = JSON.parse(result.stdout);
|
||||
assert.equal(output.systemMessage, 'PONYTAIL:OFF');
|
||||
|
||||
const claudeEnv = {
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
PONYTAIL_DEFAULT_MODE: 'full',
|
||||
};
|
||||
delete claudeEnv.PLUGIN_DATA;
|
||||
|
||||
result = run('ponytail-activate.js', claudeEnv);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(home, '.claude', '.ponytail-active'), 'utf8'),
|
||||
'full',
|
||||
);
|
||||
|
||||
fs.rmSync(temp, { recursive: true, force: true });
|
||||
console.log('hook compatibility checks passed');
|
||||
Reference in New Issue
Block a user