Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
763e04deee | ||
|
|
dedc97ca7c | ||
|
|
6da37bfa7d | ||
|
|
215777d835 | ||
|
|
5eb1fd8b76 | ||
|
|
248a30b40b | ||
|
|
ee263e5708 | ||
|
|
0403c4dd50 | ||
|
|
ff5d0936be | ||
|
|
bd6176a9b3 | ||
|
|
cf9cbd531e |
@@ -5,11 +5,16 @@ You are a lazy senior developer. Lazy means efficient, not careless. The best co
|
|||||||
Before writing any code, stop at the first rung that holds:
|
Before writing any code, stop at the first rung that holds:
|
||||||
|
|
||||||
1. Does this need to be built at all? (YAGNI)
|
1. Does this need to be built at all? (YAGNI)
|
||||||
2. Does the standard library already do this? Use it.
|
2. Does it already exist in this codebase? Reuse the helper, util, or pattern that's already here, don't re-write it.
|
||||||
3. Does a native platform feature cover it? Use it.
|
3. Does the standard library already do this? Use it.
|
||||||
4. Does an already-installed dependency solve it? Use it.
|
4. Does a native platform feature cover it? Use it.
|
||||||
5. Can this be one line? Make it one line.
|
5. Does an already-installed dependency solve it? Use it.
|
||||||
6. Only then: write the minimum code that works.
|
6. Can this be one line? Make it one line.
|
||||||
|
7. Only then: write the minimum code that works.
|
||||||
|
|
||||||
|
The ladder runs after you understand the problem, not instead of it: read the task and the code it touches, trace the real flow end to end, then climb.
|
||||||
|
|
||||||
|
Bug fix = root cause, not symptom: a report names a symptom. Grep every caller of the function you touch and fix the shared function once — one guard there is a smaller diff than one per caller, and patching only the path the ticket names leaves a sibling caller still broken.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
@@ -17,8 +22,9 @@ Rules:
|
|||||||
- No new dependency if it can be avoided.
|
- No new dependency if it can be avoided.
|
||||||
- No boilerplate nobody asked for.
|
- No boilerplate nobody asked for.
|
||||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||||
|
- Shortest working diff wins, but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
|
||||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
- 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.
|
- 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.
|
- 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, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
Not lazy about: understanding the problem (read it fully and trace the real flow before picking a rung, a small diff you don't understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail",
|
"name": "ponytail",
|
||||||
"version": "4.7.0",
|
"version": "4.8.1",
|
||||||
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Dietrich Gebert",
|
"name": "Dietrich Gebert",
|
||||||
|
|||||||
+12
-6
@@ -5,11 +5,16 @@ You are a lazy senior developer. Lazy means efficient, not careless. The best co
|
|||||||
Before writing any code, stop at the first rung that holds:
|
Before writing any code, stop at the first rung that holds:
|
||||||
|
|
||||||
1. Does this need to be built at all? (YAGNI)
|
1. Does this need to be built at all? (YAGNI)
|
||||||
2. Does the standard library already do this? Use it.
|
2. Does it already exist in this codebase? Reuse the helper, util, or pattern that's already here, don't re-write it.
|
||||||
3. Does a native platform feature cover it? Use it.
|
3. Does the standard library already do this? Use it.
|
||||||
4. Does an already-installed dependency solve it? Use it.
|
4. Does a native platform feature cover it? Use it.
|
||||||
5. Can this be one line? Make it one line.
|
5. Does an already-installed dependency solve it? Use it.
|
||||||
6. Only then: write the minimum code that works.
|
6. Can this be one line? Make it one line.
|
||||||
|
7. Only then: write the minimum code that works.
|
||||||
|
|
||||||
|
The ladder runs after you understand the problem, not instead of it: read the task and the code it touches, trace the real flow end to end, then climb.
|
||||||
|
|
||||||
|
Bug fix = root cause, not symptom: a report names a symptom. Grep every caller of the function you touch and fix the shared function once — one guard there is a smaller diff than one per caller, and patching only the path the ticket names leaves a sibling caller still broken.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
@@ -17,8 +22,9 @@ Rules:
|
|||||||
- No new dependency if it can be avoided.
|
- No new dependency if it can be avoided.
|
||||||
- No boilerplate nobody asked for.
|
- No boilerplate nobody asked for.
|
||||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||||
|
- Shortest working diff wins, but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
|
||||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
- 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.
|
- 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.
|
- 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, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
Not lazy about: understanding the problem (read it fully and trace the real flow before picking a rung, a small diff you don't understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail",
|
"name": "ponytail",
|
||||||
"version": "4.7.0",
|
"version": "4.8.1",
|
||||||
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Dietrich Gebert",
|
"name": "Dietrich Gebert",
|
||||||
|
|||||||
@@ -11,11 +11,16 @@ You are a lazy senior developer. Lazy means efficient, not careless. The best co
|
|||||||
Before writing any code, stop at the first rung that holds:
|
Before writing any code, stop at the first rung that holds:
|
||||||
|
|
||||||
1. Does this need to be built at all? (YAGNI)
|
1. Does this need to be built at all? (YAGNI)
|
||||||
2. Does the standard library already do this? Use it.
|
2. Does it already exist in this codebase? Reuse the helper, util, or pattern that's already here, don't re-write it.
|
||||||
3. Does a native platform feature cover it? Use it.
|
3. Does the standard library already do this? Use it.
|
||||||
4. Does an already-installed dependency solve it? Use it.
|
4. Does a native platform feature cover it? Use it.
|
||||||
5. Can this be one line? Make it one line.
|
5. Does an already-installed dependency solve it? Use it.
|
||||||
6. Only then: write the minimum code that works.
|
6. Can this be one line? Make it one line.
|
||||||
|
7. Only then: write the minimum code that works.
|
||||||
|
|
||||||
|
The ladder runs after you understand the problem, not instead of it: read the task and the code it touches, trace the real flow end to end, then climb.
|
||||||
|
|
||||||
|
Bug fix = root cause, not symptom: a report names a symptom. Grep every caller of the function you touch and fix the shared function once — one guard there is a smaller diff than one per caller, and patching only the path the ticket names leaves a sibling caller still broken.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
@@ -23,8 +28,9 @@ Rules:
|
|||||||
- No new dependency if it can be avoided.
|
- No new dependency if it can be avoided.
|
||||||
- No boilerplate nobody asked for.
|
- No boilerplate nobody asked for.
|
||||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||||
|
- Shortest working diff wins, but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
|
||||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
- 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.
|
- 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.
|
- 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, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
Not lazy about: understanding the problem (read it fully and trace the real flow before picking a rung, a small diff you don't understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ You are a lazy senior developer. Lazy means efficient, not careless. The best co
|
|||||||
Before writing any code, stop at the first rung that holds:
|
Before writing any code, stop at the first rung that holds:
|
||||||
|
|
||||||
1. Does this need to be built at all? (YAGNI)
|
1. Does this need to be built at all? (YAGNI)
|
||||||
2. Does the standard library already do this? Use it.
|
2. Does it already exist in this codebase? Reuse the helper, util, or pattern that's already here, don't re-write it.
|
||||||
3. Does a native platform feature cover it? Use it.
|
3. Does the standard library already do this? Use it.
|
||||||
4. Does an already-installed dependency solve it? Use it.
|
4. Does a native platform feature cover it? Use it.
|
||||||
5. Can this be one line? Make it one line.
|
5. Does an already-installed dependency solve it? Use it.
|
||||||
6. Only then: write the minimum code that works.
|
6. Can this be one line? Make it one line.
|
||||||
|
7. Only then: write the minimum code that works.
|
||||||
|
|
||||||
|
The ladder runs after you understand the problem, not instead of it: read the task and the code it touches, trace the real flow end to end, then climb.
|
||||||
|
|
||||||
|
Bug fix = root cause, not symptom: a report names a symptom. Grep every caller of the function you touch and fix the shared function once — one guard there is a smaller diff than one per caller, and patching only the path the ticket names leaves a sibling caller still broken.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
@@ -17,8 +22,9 @@ Rules:
|
|||||||
- No new dependency if it can be avoided.
|
- No new dependency if it can be avoided.
|
||||||
- No boilerplate nobody asked for.
|
- No boilerplate nobody asked for.
|
||||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||||
|
- Shortest working diff wins, but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
|
||||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
- 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.
|
- 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.
|
- 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, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
Not lazy about: understanding the problem (read it fully and trace the real flow before picking a rung, a small diff you don't understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail",
|
"name": "ponytail",
|
||||||
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
||||||
"version": "4.7.0",
|
"version": "4.8.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Dietrich Gebert",
|
"name": "Dietrich Gebert",
|
||||||
"url": "https://github.com/DietrichGebert"
|
"url": "https://github.com/DietrichGebert"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ name: test
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
tags: ['v*']
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -25,5 +26,8 @@ jobs:
|
|||||||
- name: Check rule copies
|
- name: Check rule copies
|
||||||
run: node scripts/check-rule-copies.js
|
run: node scripts/check-rule-copies.js
|
||||||
|
|
||||||
|
- name: Check version consistency
|
||||||
|
run: node scripts/check-versions.js
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|||||||
@@ -10,11 +10,16 @@ You are a lazy senior developer. Lazy means efficient, not careless. The best co
|
|||||||
Before writing any code, stop at the first rung that holds:
|
Before writing any code, stop at the first rung that holds:
|
||||||
|
|
||||||
1. Does this need to be built at all? (YAGNI)
|
1. Does this need to be built at all? (YAGNI)
|
||||||
2. Does the standard library already do this? Use it.
|
2. Does it already exist in this codebase? Reuse the helper, util, or pattern that's already here, don't re-write it.
|
||||||
3. Does a native platform feature cover it? Use it.
|
3. Does the standard library already do this? Use it.
|
||||||
4. Does an already-installed dependency solve it? Use it.
|
4. Does a native platform feature cover it? Use it.
|
||||||
5. Can this be one line? Make it one line.
|
5. Does an already-installed dependency solve it? Use it.
|
||||||
6. Only then: write the minimum code that works.
|
6. Can this be one line? Make it one line.
|
||||||
|
7. Only then: write the minimum code that works.
|
||||||
|
|
||||||
|
The ladder runs after you understand the problem, not instead of it: read the task and the code it touches, trace the real flow end to end, then climb.
|
||||||
|
|
||||||
|
Bug fix = root cause, not symptom: a report names a symptom. Grep every caller of the function you touch and fix the shared function once — one guard there is a smaller diff than one per caller, and patching only the path the ticket names leaves a sibling caller still broken.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
@@ -22,8 +27,9 @@ Rules:
|
|||||||
- No new dependency if it can be avoided.
|
- No new dependency if it can be avoided.
|
||||||
- No boilerplate nobody asked for.
|
- No boilerplate nobody asked for.
|
||||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||||
|
- Shortest working diff wins, but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
|
||||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
- 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.
|
- 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.
|
- 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, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
Not lazy about: understanding the problem (read it fully and trace the real flow before picking a rung, a small diff you don't understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ End with `net: -<N> lines, -<M> deps possible.` Nothing to cut: `Lean already. S
|
|||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
Scope: over-engineering and complexity only. Correctness bugs, security holes,
|
Scope: over-engineering and complexity only. Correctness bugs, security holes,
|
||||||
and performance are explicitly out of scope — route them to a normal review
|
and performance are explicitly out of scope. Route them to a normal review
|
||||||
pass. Lists findings, applies nothing. One-shot.
|
pass. Lists findings, applies nothing. One-shot.
|
||||||
"stop ponytail-audit" or "normal mode" to revert.
|
"stop ponytail-audit" or "normal mode" to revert.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ the convention out of the ledger.
|
|||||||
|
|
||||||
One row per marker, grouped by file:
|
One row per marker, grouped by file:
|
||||||
|
|
||||||
`<file>:<line> — <what was simplified>. ceiling: <the limit named>. upgrade: <the trigger to revisit>.`
|
`<file>:<line>, <what was simplified>. ceiling: <the limit named>. upgrade: <the trigger to revisit>.`
|
||||||
|
|
||||||
The convention is `ponytail: <ceiling>, <upgrade path>`, so pull the ceiling
|
The convention is `ponytail: <ceiling>, <upgrade path>`, so pull the ceiling
|
||||||
and the trigger straight from the comment. Want an owner per row too? add
|
and the trigger straight from the comment. Want an owner per row too? add
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ If there is nothing to cut, say `Lean already. Ship.` and stop.
|
|||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
Scope: over-engineering and complexity only. Correctness bugs, security holes,
|
Scope: over-engineering and complexity only. Correctness bugs, security holes,
|
||||||
and performance are explicitly out of scope — route them to a normal review
|
and performance are explicitly out of scope. Route them to a normal review
|
||||||
pass, not this one. A single smoke test or `assert`-based
|
pass, not this one. A single smoke test or `assert`-based
|
||||||
self-check is the ponytail minimum, not bloat, never flag it for deletion.
|
self-check is the ponytail minimum, not bloat, never flag it for deletion.
|
||||||
Does not apply the fixes, only lists them.
|
Does not apply the fixes, only lists them.
|
||||||
|
|||||||
@@ -22,31 +22,31 @@ Switch: `/ponytail lite|full|ultra`.
|
|||||||
Stop at the first rung that holds:
|
Stop at the first rung that holds:
|
||||||
|
|
||||||
1. **Does this need to exist at all?** Speculative need = skip it, say so in one line. (YAGNI)
|
1. **Does this need to exist at all?** Speculative need = skip it, say so in one line. (YAGNI)
|
||||||
2. **Stdlib does it?** Use it.
|
2. **Already in this codebase?** A helper, util, type, or pattern that already lives here → reuse it. Look before you write; re-implementing what's a few files over is the most common slop.
|
||||||
3. **Native platform feature covers it?** `<input type="date">` over a picker lib, CSS over JS, DB constraint over app code.
|
3. **Stdlib does it?** Use it.
|
||||||
4. **Already-installed dependency solves it?** Use it. Never add a new one for what a few lines can do.
|
4. **Native platform feature covers it?** `<input type="date">` over a picker lib, CSS over JS, DB constraint over app code.
|
||||||
5. **Can it be one line?** One line.
|
5. **Already-installed dependency solves it?** Use it. Never add a new one for what a few lines can do.
|
||||||
6. **Only then:** the minimum code that works.
|
6. **Can it be one line?** One line.
|
||||||
|
7. **Only then:** the minimum code that works.
|
||||||
|
|
||||||
The ladder is a reflex, not a research project. Two rungs work → take the
|
The ladder is a reflex, not a research project — but it runs *after* you
|
||||||
higher one and move on. The first lazy solution that works is the right one.
|
understand the problem, not instead of it. Read the task and the code it
|
||||||
|
touches first, trace the real flow end to end, then climb. Two rungs work →
|
||||||
|
take the higher one and move on. The first lazy solution that works is the
|
||||||
|
right one — once you actually know what the change has to touch.
|
||||||
|
|
||||||
## Web tasks: rung 3 lookup
|
**Bug fix = root cause, not symptom.** A report names a symptom. Before you
|
||||||
|
edit, grep every caller of the function you're about to touch. The lazy fix IS
|
||||||
On web work, rung 3 is where the laziest win hides: a native element or CSS
|
the root-cause fix: one guard in the shared function is a smaller diff than a
|
||||||
behavior the agent forgot exists. If a web task turns on whether the platform
|
guard in every caller — and patching only the path the ticket names leaves
|
||||||
covers it (a date input, dialog, popover, view transition, container query),
|
every sibling caller still broken. Fix it once, where all callers route through.
|
||||||
and the `modern-web` CLI is available, look it up: `modern-web search "<task>"`,
|
|
||||||
then `modern-web retrieve <id>`. It is a lookup, not a license, the answer
|
|
||||||
still goes through the ladder. MWG suggests the cutting edge; you keep only the
|
|
||||||
rung that holds. Not installed? Skip it, the ladder runs fine without it.
|
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- No unrequested abstractions: no interface with one implementation, no factory for one product, no config for a value that never changes.
|
- No unrequested abstractions: no interface with one implementation, no factory for one product, no config for a value that never changes.
|
||||||
- No boilerplate, no scaffolding "for later", later can scaffold for itself.
|
- No boilerplate, no scaffolding "for later", later can scaffold for itself.
|
||||||
- Deletion over addition. Boring over clever, clever is what someone decodes at 3am.
|
- Deletion over addition. Boring over clever, clever is what someone decodes at 3am.
|
||||||
- Fewest files possible. Shortest working diff wins.
|
- Fewest files possible. Shortest working diff wins — but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
|
||||||
- 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.
|
- 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.
|
||||||
- Two stdlib options, same size? Take the one that's correct on edge cases. Lazy means writing less code, not picking the flimsier algorithm.
|
- 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`.
|
- 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`.
|
||||||
@@ -82,6 +82,12 @@ that prevents data loss, security measures, accessibility basics, anything
|
|||||||
explicitly requested. User insists on the full version → build it, no
|
explicitly requested. User insists on the full version → build it, no
|
||||||
re-arguing.
|
re-arguing.
|
||||||
|
|
||||||
|
Never lazy about understanding the problem. The ladder shortens the
|
||||||
|
solution, never the reading. Trace the whole thing first — every file the
|
||||||
|
change touches, the actual flow — before picking a rung. Laziness that skips
|
||||||
|
comprehension to ship a small diff is the dangerous kind: it dresses up as
|
||||||
|
efficiency and ships a confident wrong fix. Read fully, then be lazy.
|
||||||
|
|
||||||
Hardware is never the ideal on paper: a real clock drifts, a real sensor
|
Hardware is never the ideal on paper: a real clock drifts, a real sensor
|
||||||
reads off, a PCA9685 runs a few percent fast. Leave the calibration knob, not
|
reads off, a PCA9685 runs a few percent fast. Leave the calibration knob, not
|
||||||
just less code, the physical world needs tuning a minimal model can't see.
|
just less code, the physical world needs tuning a minimal model can't see.
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
description: "Harvest ponytail: comments into a tracked debt ledger"
|
description: "Harvest ponytail: comments into a tracked debt ledger"
|
||||||
---
|
---
|
||||||
|
|
||||||
Harvest every `ponytail:` comment in this repository into a debt ledger so deferrals do not rot into 'later means never'. Grep the whole tree for comment markers (grep -rnE '(#|//) ?ponytail:' ., skipping node_modules/.git/build output). One row per marker, grouped by file: <file>:<line> — <what was simplified>. ceiling: <the limit named in the comment>. upgrade: <the trigger to revisit>. Tag any marker that names no upgrade path or trigger as no-trigger, those rot silently. End with the count of markers and how many lack a trigger. If none: 'No ponytail: debt. Clean ledger.' Report only, change nothing.
|
Harvest every `ponytail:` comment in this repository into a debt ledger so deferrals do not rot into 'later means never'. Grep the whole tree for comment markers (grep -rnE '(#|//) ?ponytail:' ., skipping node_modules/.git/build output). One row per marker, grouped by file: <file>:<line>, <what was simplified>. ceiling: <the limit named in the comment>. upgrade: <the trigger to revisit>. Tag any marker that names no upgrade path or trigger as no-trigger, those rot silently. End with the count of markers and how many lack a trigger. If none: 'No ponytail: debt. Clean ledger.' Report only, change nothing.
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ You are a lazy senior developer. Lazy means efficient, not careless. The best co
|
|||||||
Before writing any code, stop at the first rung that holds:
|
Before writing any code, stop at the first rung that holds:
|
||||||
|
|
||||||
1. Does this need to be built at all? (YAGNI)
|
1. Does this need to be built at all? (YAGNI)
|
||||||
2. Does the standard library already do this? Use it.
|
2. Does it already exist in this codebase? Reuse the helper, util, or pattern that's already here, don't re-write it.
|
||||||
3. Does a native platform feature cover it? Use it.
|
3. Does the standard library already do this? Use it.
|
||||||
4. Does an already-installed dependency solve it? Use it.
|
4. Does a native platform feature cover it? Use it.
|
||||||
5. Can this be one line? Make it one line.
|
5. Does an already-installed dependency solve it? Use it.
|
||||||
6. Only then: write the minimum code that works.
|
6. Can this be one line? Make it one line.
|
||||||
|
7. Only then: write the minimum code that works.
|
||||||
|
|
||||||
|
The ladder runs after you understand the problem, not instead of it: read the task and the code it touches, trace the real flow end to end, then climb.
|
||||||
|
|
||||||
|
Bug fix = root cause, not symptom: a report names a symptom. Grep every caller of the function you touch and fix the shared function once — one guard there is a smaller diff than one per caller, and patching only the path the ticket names leaves a sibling caller still broken.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
@@ -17,8 +22,9 @@ Rules:
|
|||||||
- No new dependency if it can be avoided.
|
- No new dependency if it can be avoided.
|
||||||
- No boilerplate nobody asked for.
|
- No boilerplate nobody asked for.
|
||||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||||
|
- Shortest working diff wins, but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
|
||||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
- 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.
|
- 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.
|
- 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, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
Not lazy about: understanding the problem (read it fully and trace the real flow before picking a rung, a small diff you don't understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ You are a lazy senior developer. Lazy means efficient, not careless. The best co
|
|||||||
Before writing any code, stop at the first rung that holds:
|
Before writing any code, stop at the first rung that holds:
|
||||||
|
|
||||||
1. Does this need to be built at all? (YAGNI)
|
1. Does this need to be built at all? (YAGNI)
|
||||||
2. Does the standard library already do this? Use it.
|
2. Does it already exist in this codebase? Reuse the helper, util, or pattern that's already here, don't re-write it.
|
||||||
3. Does a native platform feature cover it? Use it.
|
3. Does the standard library already do this? Use it.
|
||||||
4. Does an already-installed dependency solve it? Use it.
|
4. Does a native platform feature cover it? Use it.
|
||||||
5. Can this be one line? Make it one line.
|
5. Does an already-installed dependency solve it? Use it.
|
||||||
6. Only then: write the minimum code that works.
|
6. Can this be one line? Make it one line.
|
||||||
|
7. Only then: write the minimum code that works.
|
||||||
|
|
||||||
|
The ladder runs after you understand the problem, not instead of it: read the task and the code it touches, trace the real flow end to end, then climb.
|
||||||
|
|
||||||
|
Bug fix = root cause, not symptom: a report names a symptom. Grep every caller of the function you touch and fix the shared function once — one guard there is a smaller diff than one per caller, and patching only the path the ticket names leaves a sibling caller still broken.
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
@@ -17,10 +22,11 @@ Rules:
|
|||||||
- No new dependency if it can be avoided.
|
- No new dependency if it can be avoided.
|
||||||
- No boilerplate nobody asked for.
|
- No boilerplate nobody asked for.
|
||||||
- Deletion over addition. Boring over clever. Fewest files possible.
|
- Deletion over addition. Boring over clever. Fewest files possible.
|
||||||
|
- Shortest working diff wins, but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
|
||||||
- Question complex requests: "Do you actually need X, or does Y cover it?"
|
- 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.
|
- 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.
|
- 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, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.
|
Not lazy about: understanding the problem (read it fully and trace the real flow before picking a rung, a small diff you don't understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs (the platform is never the spec ideal, a clock drifts, a sensor reads off), anything explicitly requested. Lazy code without its check is unfinished: 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.)
|
(Yes, this file also applies to agents working on the ponytail repo itself. Especially to them.)
|
||||||
|
|||||||
+10
-9
@@ -46,8 +46,6 @@ Con ponytail:
|
|||||||
|
|
||||||
Más sobrevivientes en [examples/](examples/).
|
Más sobrevivientes en [examples/](examples/).
|
||||||
|
|
||||||
> **Combina bien con** [Modern Web Guidance](https://github.com/GoogleChrome/modern-web-guidance) para trabajo web: ponytail decide *si* apoyarse en la plataforma, MWG es cómo el agente busca *qué* feature nativa hace el trabajo. Ver [examples/web-platform-lookup.md](examples/web-platform-lookup.md).
|
|
||||||
|
|
||||||
## Números
|
## Números
|
||||||
|
|
||||||
La medición honesta es un agente real haciendo trabajo real: una sesión headless de Claude Code editando [el template full-stack-fastapi de tiangolo](https://github.com/fastapi/full-stack-fastapi-template) (un repo real de FastAPI + React), evaluada sobre el `git diff` que deja. Doce tickets de feature, el mismo agente con y sin el skill, n=4, Haiku 4.5.
|
La medición honesta es un agente real haciendo trabajo real: una sesión headless de Claude Code editando [el template full-stack-fastapi de tiangolo](https://github.com/fastapi/full-stack-fastapi-template) (un repo real de FastAPI + React), evaluada sobre el `git diff` que deja. Doce tickets de feature, el mismo agente con y sin el skill, n=4, Haiku 4.5.
|
||||||
@@ -85,20 +83,23 @@ Antes de escribir código, el agente se detiene en el primer peldaño que aguant
|
|||||||
|
|
||||||
```
|
```
|
||||||
1. ¿Necesita existir esto? → no: omitirlo (YAGNI)
|
1. ¿Necesita existir esto? → no: omitirlo (YAGNI)
|
||||||
2. ¿Lo hace la stdlib? → úsala
|
2. ¿Ya existe en este código? → reúsalo, no lo reescribas
|
||||||
3. ¿Es una feature nativa? → úsala
|
3. ¿Lo hace la stdlib? → úsala
|
||||||
4. ¿Una dependencia ya instalada? → úsala
|
4. ¿Es una feature nativa? → úsala
|
||||||
5. ¿Cabe en una línea? → una línea
|
5. ¿Una dependencia ya instalada? → úsala
|
||||||
6. Solo entonces: el mínimo que funciona
|
6. ¿Cabe en una línea? → una línea
|
||||||
|
7. Solo entonces: el mínimo que funciona
|
||||||
```
|
```
|
||||||
|
|
||||||
|
La escalera se recorre *después* de entender el problema, no en su lugar: lee el código que toca el cambio y sigue el flujo real antes de elegir un peldaño. Flojo en la solución, nunca en la lectura.
|
||||||
|
|
||||||
Flojo, no negligente: la validación en límites de confianza, el manejo de pérdida de datos, la seguridad y la accesibilidad nunca están en riesgo.
|
Flojo, no negligente: la validación en límites de confianza, el manejo de pérdida de datos, la seguridad y la accesibilidad nunca están en riesgo.
|
||||||
|
|
||||||
## Instalación
|
## Instalación
|
||||||
|
|
||||||
El mayor esfuerzo que ponytail te va a pedir:
|
El mayor esfuerzo que ponytail te va a pedir:
|
||||||
|
|
||||||
Los plugins de Claude Code y Codex ejecutan dos pequeños lifecycle hooks de Node.js, así que `node` debe estar en tu PATH (nota para usuarios de Nix/nvm: debe estar en el PATH del shell no-interactivo). Si no lo está, los skills igualmente funcionan — la activación automática simplemente queda en silencio en vez de lanzar un error en cada prompt.
|
Los plugins de Claude Code y Codex ejecutan dos pequeños lifecycle hooks de Node.js, así que `node` debe estar en tu PATH (nota para usuarios de Nix/nvm: debe estar en el PATH del shell no-interactivo). Si no lo está, los skills igualmente funcionan, la activación automática simplemente queda en silencio en vez de lanzar un error en cada prompt.
|
||||||
|
|
||||||
### Claude Code
|
### Claude Code
|
||||||
|
|
||||||
@@ -226,7 +227,7 @@ node scripts/check-rule-copies.js
|
|||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
El paquete de skills de OpenClaw (`.openclaw/skills/`) se genera desde `skills/`; ejecuta `node scripts/build-openclaw-skills.js` después de cambiar un skill — la suite de tests falla si está desactualizado.
|
El paquete de skills de OpenClaw (`.openclaw/skills/`) se genera desde `skills/`; ejecuta `node scripts/build-openclaw-skills.js` después de cambiar un skill, la suite de tests falla si está desactualizado.
|
||||||
|
|
||||||
El benchmark de correctness lanza Python para las verificaciones de email y CSV; se prueba `python3` antes que `python`. Las verificaciones de CSV requieren `pandas` instalado localmente.
|
El benchmark de correctness lanza Python para las verificaciones de email y CSV; se prueba `python3` antes que `python`. Las verificaciones de CSV requieren `pandas` instalado localmente.
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ With ponytail:
|
|||||||
|
|
||||||
More survivors in [examples/](examples/).
|
More survivors in [examples/](examples/).
|
||||||
|
|
||||||
> **Pairs well with** [Modern Web Guidance](https://github.com/GoogleChrome/modern-web-guidance) for web work: ponytail decides *whether* to lean on the platform, MWG is how the agent looks up *which* native feature does the job. See [examples/web-platform-lookup.md](examples/web-platform-lookup.md).
|
|
||||||
|
|
||||||
## Numbers
|
## Numbers
|
||||||
|
|
||||||
The honest measurement is a real agent doing real work: a headless Claude Code session editing [tiangolo's full-stack-fastapi-template](https://github.com/fastapi/full-stack-fastapi-template) (a real FastAPI + React repo), scored on the `git diff` it leaves behind. Twelve feature tickets, the same agent with and without the skill, n=4, Haiku 4.5.
|
The honest measurement is a real agent doing real work: a headless Claude Code session editing [tiangolo's full-stack-fastapi-template](https://github.com/fastapi/full-stack-fastapi-template) (a real FastAPI + React repo), scored on the `git diff` it leaves behind. Twelve feature tickets, the same agent with and without the skill, n=4, Haiku 4.5.
|
||||||
@@ -85,13 +83,16 @@ Before writing code, the agent stops at the first rung that holds:
|
|||||||
|
|
||||||
```
|
```
|
||||||
1. Does this need to exist? → no: skip it (YAGNI)
|
1. Does this need to exist? → no: skip it (YAGNI)
|
||||||
2. Stdlib does it? → use it
|
2. Already in this codebase? → reuse it, don't rewrite
|
||||||
3. Native platform feature? → use it
|
3. Stdlib does it? → use it
|
||||||
4. Installed dependency? → use it
|
4. Native platform feature? → use it
|
||||||
5. One line? → one line
|
5. Installed dependency? → use it
|
||||||
6. Only then: the minimum that works
|
6. One line? → one line
|
||||||
|
7. Only then: the minimum that works
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The ladder runs *after* it understands the problem, not instead of it: it reads the code the change touches and traces the real flow before picking a rung. Lazy about the solution, never about reading.
|
||||||
|
|
||||||
Lazy, not negligent: trust-boundary validation, data-loss handling, security, and accessibility are never on the chopping block.
|
Lazy, not negligent: trust-boundary validation, data-loss handling, security, and accessibility are never on the chopping block.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
@@ -183,7 +184,7 @@ It reuses this repo's `gemini-extension.json`. One difference: Antigravity conve
|
|||||||
|
|
||||||
### CodeWhale
|
### CodeWhale
|
||||||
|
|
||||||
Reads `AGENTS.md` from the project root — zero setup. Copy [`AGENTS.md`](AGENTS.md) to your project, or run `codewhale` from a checkout of this repo. That's it.
|
Reads `AGENTS.md` from the project root, zero setup. Copy [`AGENTS.md`](AGENTS.md) to your project, or run `codewhale` from a checkout of this repo. That's it.
|
||||||
|
|
||||||
### OpenClaw
|
### OpenClaw
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Three arms (no skill, [caveman](https://github.com/JuliusBrussee/caveman), ponyt
|
|||||||
|
|
||||||
### Claude (Haiku / Sonnet / Opus)
|
### Claude (Haiku / Sonnet / Opus)
|
||||||
|
|
||||||
Requires an Anthropic API key and **Node.js ≥ 22.22.0** (promptfoo's engine constraint —
|
Requires an Anthropic API key and **Node.js ≥ 22.22.0** (promptfoo's engine constraint,
|
||||||
check with `node --version` and upgrade if needed):
|
check with `node --version` and upgrade if needed):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -92,11 +92,11 @@ python judge.py --run runs/<stamp> # score every workspace's source
|
|||||||
|
|
||||||
Fewer lines only counts as a win if the code still does the job. The LOC tier scores the open
|
Fewer lines only counts as a win if the code still does the job. The LOC tier scores the open
|
||||||
feature tasks on `git diff` alone, with no deterministic check that the asked feature was
|
feature tasks on `git diff` alone, with no deterministic check that the asked feature was
|
||||||
actually built — so an arm could "win" the LOC metric by shipping a stub. This pass closes that
|
actually built, so an arm could "win" the LOC metric by shipping a stub. This pass closes that
|
||||||
hole: the same auditable LLM judge (fixed model, temperature 0, published rubric) rates how
|
hole: the same auditable LLM judge (fixed model, temperature 0, published rubric) rates how
|
||||||
**fully** each submission implements its task. Rubric: `0` stub/placeholder, `1` partial (core
|
**fully** each submission implements its task. Rubric: `0` stub/placeholder, `1` partial (core
|
||||||
behavior missing), `2` mostly complete (a stated requirement missing), `3` fully implements the
|
behavior missing), `2` mostly complete (a stated requirement missing), `3` fully implements the
|
||||||
task. Read it **alongside** the LOC table — a low-LOC arm whose completeness also drops is doing
|
task. Read it **alongside** the LOC table, a low-LOC arm whose completeness also drops is doing
|
||||||
less, not less-bloated.
|
less, not less-bloated.
|
||||||
|
|
||||||
Validated like the over-engineering judge: `--selftest` requires the judge to rank a complete
|
Validated like the over-engineering judge: `--selftest` requires the judge to rank a complete
|
||||||
|
|||||||
@@ -89,11 +89,40 @@ def _count(p: Path, with_comments: bool):
|
|||||||
n += 1
|
n += 1
|
||||||
return n
|
return n
|
||||||
|
|
||||||
def code_stats(workdir: Path):
|
_SELFCHECK_DEFS = ("def demo(", "def _demo(", "def selfcheck(", "def _selfcheck(",
|
||||||
|
"def _check(", "def _smoke(", "def smoke(")
|
||||||
|
def _selfcheck_split(p: Path):
|
||||||
|
"""Split a produced .py file at the first TOP-LEVEL self-check marker (a `__main__` guard or a
|
||||||
|
demo()/selfcheck() function) through end of file. Returns (src_total, src_code, sc_total,
|
||||||
|
sc_code), counted like _count. On a surgical task that delivers ONE function, an in-file self-
|
||||||
|
check is the runnable check ponytail's rule asks for -- a positive signal, not source bloat --
|
||||||
|
so it is split off here and counted as test LOC instead of penalising the arm that wrote it."""
|
||||||
|
try: lines = p.read_text(encoding="utf-8", errors="ignore").splitlines()
|
||||||
|
except Exception: return 0, 0, 0, 0
|
||||||
|
start = None
|
||||||
|
for i, ln in enumerate(lines):
|
||||||
|
if ln[:1] not in (" ", "\t") and (ln.startswith("if __name__") or ln.startswith(_SELFCHECK_DEFS)):
|
||||||
|
start = i; break
|
||||||
|
def cnt(seq):
|
||||||
|
t = c = 0
|
||||||
|
for ln in seq:
|
||||||
|
s = ln.strip()
|
||||||
|
if not s: continue
|
||||||
|
t += 1
|
||||||
|
if not s.startswith(("#", "//", "*", "/*", "*/")): c += 1
|
||||||
|
return t, c
|
||||||
|
if start is None:
|
||||||
|
t, c = cnt(lines); return t, c, 0, 0
|
||||||
|
t, c = cnt(lines[:start]); st, sc = cnt(lines[start:])
|
||||||
|
return t, c, st, sc
|
||||||
|
|
||||||
|
def code_stats(workdir: Path, selfcheck_as_test: bool = False):
|
||||||
"""LOC over code-extension source files only (generated images/data can't pollute it).
|
"""LOC over code-extension source files only (generated images/data can't pollute it).
|
||||||
total_loc counts every non-blank line including comments and docstrings -- the bloat a vibe
|
total_loc counts every non-blank line including comments and docstrings -- the bloat a vibe
|
||||||
baseline actually produces. src_loc is code-only, for the breakdown. Tests tracked separately,
|
baseline actually produces. src_loc is code-only, for the breakdown. Tests tracked separately,
|
||||||
never as bloat."""
|
never as bloat. selfcheck_as_test (surgical tasks): an in-file __main__/demo() self-check is
|
||||||
|
reclassified from source to test, so following ponytail's 'leave a runnable check' rule is not
|
||||||
|
counted as code bloat against it."""
|
||||||
fixture = set() # files that were seeded, not delivered
|
fixture = set() # files that were seeded, not delivered
|
||||||
fm = workdir / "_fixture_files.json"
|
fm = workdir / "_fixture_files.json"
|
||||||
if fm.exists():
|
if fm.exists():
|
||||||
@@ -105,10 +134,19 @@ def code_stats(workdir: Path):
|
|||||||
and not p.name.startswith((".", "_")) and _rel(p) not in fixture]
|
and not p.name.startswith((".", "_")) and _rel(p) not in fixture]
|
||||||
src = [p for p in files if not _is_test(p, workdir)]
|
src = [p for p in files if not _is_test(p, workdir)]
|
||||||
tst = [p for p in files if _is_test(p, workdir)]
|
tst = [p for p in files if _is_test(p, workdir)]
|
||||||
|
test_loc = sum(_count(p, True) for p in tst)
|
||||||
|
if selfcheck_as_test:
|
||||||
|
total = code = sc_test = 0
|
||||||
|
for p in src:
|
||||||
|
t, c, st, _ = _selfcheck_split(p)
|
||||||
|
total += t; code += c; sc_test += st
|
||||||
|
return {"files": len(files), "src_files": len(src),
|
||||||
|
"total_loc": total, "src_loc": code,
|
||||||
|
"test_files": len(tst), "test_loc": test_loc + sc_test}
|
||||||
return {"files": len(files), "src_files": len(src),
|
return {"files": len(files), "src_files": len(src),
|
||||||
"total_loc": sum(_count(p, True) for p in src), # incl comments + docstrings (the bloat)
|
"total_loc": sum(_count(p, True) for p in src), # incl comments + docstrings (the bloat)
|
||||||
"src_loc": sum(_count(p, False) for p in src), # code only
|
"src_loc": sum(_count(p, False) for p in src), # code only
|
||||||
"test_files": len(tst), "test_loc": sum(_count(p, True) for p in tst)}
|
"test_files": len(tst), "test_loc": test_loc}
|
||||||
|
|
||||||
def _git(workdir, *args):
|
def _git(workdir, *args):
|
||||||
return subprocess.run([shutil.which("git") or "git", *args], cwd=str(workdir),
|
return subprocess.run([shutil.which("git") or "git", *args], cwd=str(workdir),
|
||||||
@@ -151,7 +189,9 @@ def selftest():
|
|||||||
axis = task.get("axis", "safe")
|
axis = task.get("axis", "safe")
|
||||||
for kind in ("good", "bad"):
|
for kind in ("good", "bad"):
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
(Path(d) / task["file"]).write_text(task[kind], encoding="utf-8")
|
for fn, content in task.get("seed", {}).items(): # seed siblings (a helper module
|
||||||
|
(Path(d) / fn).write_text(content, encoding="utf-8") # the ref imports) too
|
||||||
|
(Path(d) / task["file"]).write_text(task[kind], encoding="utf-8") # entry = the ref
|
||||||
r = task["score"](Path(d))
|
r = task["score"](Path(d))
|
||||||
ok = (r["correct"] == 1 and r["safe"] == 1) if kind == "good" else (r[axis] == 0)
|
ok = (r["correct"] == 1 and r["safe"] == 1) if kind == "good" else (r[axis] == 0)
|
||||||
print(f"{'ok ' if ok else 'XX '} {tid:12} {kind:4} correct={r['correct']} "
|
print(f"{'ok ' if ok else 'XX '} {tid:12} {kind:4} correct={r['correct']} "
|
||||||
@@ -205,7 +245,8 @@ def score_workspace(task_id, arm, model, workdir: Path):
|
|||||||
"cache_tokens": (u.get("cache_read_input_tokens") or 0) + (u.get("cache_creation_input_tokens") or 0)}
|
"cache_tokens": (u.get("cache_read_input_tokens") or 0) + (u.get("cache_creation_input_tokens") or 0)}
|
||||||
result_text = j.get("result", "")
|
result_text = j.get("result", "")
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
stats = git_diff_stats(workdir) if TASKS[task_id].get("fixture") else code_stats(workdir)
|
surgical = not TASKS[task_id].get("open") and not TASKS[task_id].get("fixture")
|
||||||
|
stats = git_diff_stats(workdir) if TASKS[task_id].get("fixture") else code_stats(workdir, selfcheck_as_test=surgical)
|
||||||
# open/explain tasks answer in the chat, not a file. If no source file was written, count the
|
# open/explain tasks answer in the chat, not a file. If no source file was written, count the
|
||||||
# code the agent delivered in its chat answer so the comparison isn't a false zero.
|
# code the agent delivered in its chat answer so the comparison isn't a false zero.
|
||||||
if TASKS[task_id].get("open") and stats["total_loc"] == 0 and result_text:
|
if TASKS[task_id].get("open") and stats["total_loc"] == 0 and result_text:
|
||||||
|
|||||||
+397
-1
@@ -20,7 +20,7 @@ Task fields:
|
|||||||
score : (workdir) -> {correct, safe, reason}
|
score : (workdir) -> {correct, safe, reason}
|
||||||
good/bad : reference implementations for the selftest
|
good/bad : reference implementations for the selftest
|
||||||
"""
|
"""
|
||||||
import hashlib, hmac, importlib.util, inspect, json, os, py_compile, sqlite3, tempfile
|
import hashlib, hmac, importlib, importlib.util, inspect, json, os, py_compile, sqlite3, sys, tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Real-repo fixture: tiangolo/full-stack-fastapi-template @ cd83fc1 (v0.10.0, MIT), cloned locally.
|
# Real-repo fixture: tiangolo/full-stack-fastapi-template @ cd83fc1 (v0.10.0, MIT), cloned locally.
|
||||||
@@ -439,8 +439,372 @@ def score_fixture(workdir):
|
|||||||
return {"correct": 1 if new else 0, "safe": 1,
|
return {"correct": 1 if new else 0, "safe": 1,
|
||||||
"reason": f"{len(new)} new file(s)" if new else "no new file"}
|
"reason": f"{len(new)} new file(s)" if new else "no new file"}
|
||||||
|
|
||||||
|
# ======================================================================================
|
||||||
|
# todo-null -- in-memory Todo REST API. The trust-boundary trap is a POST body of literal
|
||||||
|
# `null`: JSON.parse("null") returns null, which slips past a try/catch around parsing, then
|
||||||
|
# `body.title` throws on null and (in an async handler, unhandled) takes the whole process
|
||||||
|
# down -- a one-request DoS. The lazy-but-plausible version guards "missing title" but not
|
||||||
|
# "body is not an object". This is the exact bug an external benchmark hit (task-02).
|
||||||
|
# ======================================================================================
|
||||||
|
TODO_SEED = (
|
||||||
|
"// Implement an in-memory Todo REST API with the built-in http module only.\n"
|
||||||
|
"// Listen on process.env.PORT || 3000.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# good: rejects a non-object body (incl. null) with 400 and stays up.
|
||||||
|
TODO_GOOD = r"""'use strict';
|
||||||
|
const http = require('http');
|
||||||
|
const todos = new Map(); let nextId = 1;
|
||||||
|
const send = (res, c, b) => { res.writeHead(c, {'Content-Type':'application/json'}); res.end(b === undefined ? '' : JSON.stringify(b)); };
|
||||||
|
const readJson = req => new Promise((resolve, reject) => {
|
||||||
|
let d = ''; req.on('data', c => d += c);
|
||||||
|
req.on('end', () => { if (d.trim() === '') return resolve({}); try { resolve(JSON.parse(d)); } catch { reject(new Error('bad json')); } });
|
||||||
|
req.on('error', reject);
|
||||||
|
});
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
const path = (req.url || '/').replace(/\/+$/, '') || '/';
|
||||||
|
const m = path.match(/^\/todos(?:\/(\d+))?$/);
|
||||||
|
if (!m) return send(res, 404, { error: 'not found' });
|
||||||
|
const id = m[1] ? Number(m[1]) : null;
|
||||||
|
if (id === null) {
|
||||||
|
if (req.method === 'GET') return send(res, 200, [...todos.values()]);
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
let body;
|
||||||
|
try { body = await readJson(req); } catch (e) { return send(res, 400, { error: e.message }); }
|
||||||
|
if (body === null || typeof body !== 'object' || Array.isArray(body)) return send(res, 400, { error: 'body must be an object' });
|
||||||
|
if (typeof body.title !== 'string' || body.title.trim() === '') return send(res, 400, { error: 'title required' });
|
||||||
|
const t = { id: nextId++, title: body.title, done: false }; todos.set(t.id, t); return send(res, 201, t);
|
||||||
|
}
|
||||||
|
return send(res, 405, { error: 'method not allowed' });
|
||||||
|
}
|
||||||
|
const t = todos.get(id);
|
||||||
|
if (req.method === 'GET') return t ? send(res, 200, t) : send(res, 404, { error: 'not found' });
|
||||||
|
if (req.method === 'DELETE') return todos.delete(id) ? send(res, 204) : send(res, 404, { error: 'not found' });
|
||||||
|
return send(res, 405, { error: 'method not allowed' });
|
||||||
|
});
|
||||||
|
server.listen(process.env.PORT || 3000);
|
||||||
|
"""
|
||||||
|
|
||||||
|
# bad: happy-path correct, but only guards "missing title" -- a `null` body reaches body.title
|
||||||
|
# and throws in the async handler -> unhandled rejection -> process exits. The benchmark's bug.
|
||||||
|
TODO_BAD = r"""'use strict';
|
||||||
|
const http = require('http');
|
||||||
|
const todos = new Map(); let nextId = 1;
|
||||||
|
const send = (res, c, b) => { res.writeHead(c, {'Content-Type':'application/json'}); res.end(b === undefined ? '' : JSON.stringify(b)); };
|
||||||
|
const readJson = req => new Promise((resolve, reject) => {
|
||||||
|
let d = ''; req.on('data', c => d += c);
|
||||||
|
req.on('end', () => { if (d.trim() === '') return resolve({}); try { resolve(JSON.parse(d)); } catch { reject(new Error('bad json')); } });
|
||||||
|
req.on('error', reject);
|
||||||
|
});
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
const path = (req.url || '/').replace(/\/+$/, '') || '/';
|
||||||
|
const m = path.match(/^\/todos(?:\/(\d+))?$/);
|
||||||
|
if (!m) return send(res, 404, { error: 'not found' });
|
||||||
|
const id = m[1] ? Number(m[1]) : null;
|
||||||
|
if (id === null) {
|
||||||
|
if (req.method === 'GET') return send(res, 200, [...todos.values()]);
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
let body;
|
||||||
|
try { body = await readJson(req); } catch (e) { return send(res, 400, { error: e.message }); }
|
||||||
|
if (typeof body.title !== 'string' || body.title.trim() === '') return send(res, 400, { error: 'title required' });
|
||||||
|
const t = { id: nextId++, title: body.title, done: false }; todos.set(t.id, t); return send(res, 201, t);
|
||||||
|
}
|
||||||
|
return send(res, 405, { error: 'method not allowed' });
|
||||||
|
}
|
||||||
|
const t = todos.get(id);
|
||||||
|
if (req.method === 'GET') return t ? send(res, 200, t) : send(res, 404, { error: 'not found' });
|
||||||
|
return send(res, 405, { error: 'method not allowed' });
|
||||||
|
});
|
||||||
|
server.listen(process.env.PORT || 3000);
|
||||||
|
"""
|
||||||
|
|
||||||
|
def score_todo(workdir):
|
||||||
|
"""Boot the produced Todo server on a free port; a POST body of literal `null` must NOT
|
||||||
|
crash it. correct = normal create/list works; safe = server survives the null POST."""
|
||||||
|
import socket, subprocess, time, json as _json, shutil as _sh, urllib.request, urllib.error
|
||||||
|
srv = workdir / "server.js"
|
||||||
|
if not srv.exists():
|
||||||
|
js = [p for p in workdir.glob("*.js") if not p.name.startswith(("_", "."))]
|
||||||
|
if len(js) != 1: return _fail("server.js missing")
|
||||||
|
srv = js[0]
|
||||||
|
node = _sh.which("node")
|
||||||
|
if not node: return _fail("node not on PATH")
|
||||||
|
s = socket.socket(); s.bind(("127.0.0.1", 0)); port = s.getsockname()[1]; s.close()
|
||||||
|
base = f"http://127.0.0.1:{port}"
|
||||||
|
def req(method, path, body=None, raw=None):
|
||||||
|
data = raw.encode() if raw is not None else (_json.dumps(body).encode() if body is not None else None)
|
||||||
|
r = urllib.request.Request(base + path, data=data, method=method,
|
||||||
|
headers={"Content-Type": "application/json"})
|
||||||
|
with urllib.request.urlopen(r, timeout=4) as resp:
|
||||||
|
return resp.status, resp.read().decode()
|
||||||
|
env = {**os.environ, "PORT": str(port)}
|
||||||
|
proc = subprocess.Popen([node, str(srv)], cwd=str(workdir), env=env,
|
||||||
|
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
||||||
|
try:
|
||||||
|
up = False
|
||||||
|
for _ in range(40):
|
||||||
|
if proc.poll() is not None: return _fail("server exited on startup")
|
||||||
|
try:
|
||||||
|
req("GET", "/todos"); up = True; break
|
||||||
|
except Exception: time.sleep(0.1)
|
||||||
|
if not up: return _fail("server never came up")
|
||||||
|
try:
|
||||||
|
st, _ = req("POST", "/todos", {"title": "buy milk"})
|
||||||
|
correct = (st == 201)
|
||||||
|
st2, body2 = req("GET", "/todos")
|
||||||
|
correct = correct and st2 == 200 and "buy milk" in body2
|
||||||
|
except Exception:
|
||||||
|
correct = False
|
||||||
|
try:
|
||||||
|
req("POST", "/todos", raw="null")
|
||||||
|
except urllib.error.HTTPError:
|
||||||
|
pass # a 4xx is fine -- it responded
|
||||||
|
except Exception:
|
||||||
|
pass # connection reset -- decide by whether the process died
|
||||||
|
time.sleep(0.3)
|
||||||
|
safe = proc.poll() is None # process still alive == survived the null POST
|
||||||
|
if safe:
|
||||||
|
try: req("GET", "/todos")
|
||||||
|
except Exception: safe = False
|
||||||
|
return _ok(correct, safe, "survived null POST" if safe else "crashed on null POST")
|
||||||
|
finally:
|
||||||
|
try: proc.kill()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
# ======================================================================================
|
||||||
|
# QUALITY TIER -- probes the two behaviors fixed in #245 (understand/trace before patching)
|
||||||
|
# and #217 (reuse what already lives in the codebase). Same shape as the safety tier: the `bad`
|
||||||
|
# ref is the lazy-but-plausible version -- correct on the happy path, but it cuts the corner the
|
||||||
|
# fix is about. axis="safe" carries the QUALITY signal (reuse / root-cause), so a working-but-
|
||||||
|
# low-quality answer is caught the way an unsafe one is.
|
||||||
|
#
|
||||||
|
# Two design choices make these DISCRIMINATE (an earlier in-file version had every arm reuse the
|
||||||
|
# helper, so the arms tied):
|
||||||
|
# - reuse tasks keep the helper in a SEPARATE module the agent has to read the project to find
|
||||||
|
# (that is exactly how #217 slop happens), and give it a DISTINCTIVE behavior, so a re-
|
||||||
|
# implementation diverges observably instead of needing a brittle spy to catch.
|
||||||
|
# - trace tasks route the named symptom and an UN-named sibling through a shared helper. The lazy
|
||||||
|
# fix patches the named caller; the scorer exercises the sibling, which only a flow-tracing fix
|
||||||
|
# (repair the shared helper) gets right.
|
||||||
|
# ======================================================================================
|
||||||
|
|
||||||
|
def _import_pkg(workdir, modname, also=()):
|
||||||
|
"""Import a produced module by name with workdir on sys.path, so its own intra-repo imports
|
||||||
|
(`from textutils import slugify`) resolve. Fresh each call: drop cached names first."""
|
||||||
|
wd = str(workdir)
|
||||||
|
if wd not in sys.path: sys.path.insert(0, wd)
|
||||||
|
for m in (modname,) + tuple(also): sys.modules.pop(m, None)
|
||||||
|
try:
|
||||||
|
return importlib.import_module(modname)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# --- #217a reuse-slug: the project slugifies in textutils.py, and its slugify transliterates
|
||||||
|
# accents (Cafe, not Caf). unique_slug must reuse it so slugs stay consistent; a hand-rolled regex
|
||||||
|
# silently diverges on any accented title. correct = ASCII titles (both agree); safe(reuse) = an
|
||||||
|
# accented title slugs the project's way.
|
||||||
|
def score_reuse_slug(workdir):
|
||||||
|
mod = _import_pkg(workdir, "articles", also=("textutils",))
|
||||||
|
if mod is None: return _fail("articles.py missing or import error")
|
||||||
|
fn = _find(mod, ["unique_slug"])
|
||||||
|
if fn is None: return _fail("no unique_slug")
|
||||||
|
try:
|
||||||
|
correct = (fn("Hello, World!", set()) == "hello-world"
|
||||||
|
and fn("Hello, World!", {"hello-world"}) == "hello-world-2")
|
||||||
|
except Exception as e:
|
||||||
|
return _fail(f"correctness raised: {e}")
|
||||||
|
try:
|
||||||
|
reused = (fn("Café Olé", set()) == "cafe-ole") # only the project's slugify transliterates
|
||||||
|
except Exception:
|
||||||
|
reused = False
|
||||||
|
return _ok(correct, reused, "reused project slugify" if reused else "re-implemented slug (diverges on accents)")
|
||||||
|
|
||||||
|
REUSE_SLUG_HELPER = (
|
||||||
|
"import re, unicodedata\n\n"
|
||||||
|
"def slugify(title):\n"
|
||||||
|
' """Project-wide slug: transliterate accents to ASCII, then hyphenate. Use this so every\n'
|
||||||
|
' slug in the app is built the same way."""\n'
|
||||||
|
" ascii_title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore').decode()\n"
|
||||||
|
' return re.sub(r"[^a-z0-9]+", "-", ascii_title.lower()).strip("-")\n\n'
|
||||||
|
"def truncate(text, length=80):\n"
|
||||||
|
' """Trim text to length, adding an ellipsis if it was longer."""\n'
|
||||||
|
" return text if len(text) <= length else text[: length - 1].rstrip() + '\\u2026'\n"
|
||||||
|
)
|
||||||
|
REUSE_SLUG_SEED = (
|
||||||
|
"def unique_slug(title, taken):\n"
|
||||||
|
' """Return a URL slug for `title` not already in `taken` (a set of slugs in use). If the\n'
|
||||||
|
' base slug is taken, append -2, -3, ... until one is free. Slugs must match how the rest\n'
|
||||||
|
' of the project builds them."""\n'
|
||||||
|
" raise NotImplementedError\n"
|
||||||
|
)
|
||||||
|
_SLUG_TAIL = (
|
||||||
|
" if base not in taken:\n"
|
||||||
|
" return base\n"
|
||||||
|
" i = 2\n"
|
||||||
|
" while f'{base}-{i}' in taken:\n"
|
||||||
|
" i += 1\n"
|
||||||
|
" return f'{base}-{i}'\n"
|
||||||
|
)
|
||||||
|
REUSE_SLUG_GOOD = ("from textutils import slugify\n\n" + REUSE_SLUG_SEED).replace(
|
||||||
|
" raise NotImplementedError\n", " base = slugify(title)\n" + _SLUG_TAIL)
|
||||||
|
REUSE_SLUG_BAD = ("import re\n\n" + REUSE_SLUG_SEED).replace(
|
||||||
|
" raise NotImplementedError\n",
|
||||||
|
' base = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")\n' + _SLUG_TAIL)
|
||||||
|
|
||||||
|
# --- #217b reuse-money: the project formats currency in money.py, and format_money inserts a
|
||||||
|
# thousands separator ($1,234.56). line_item must reuse it; a hand-rolled f-string drops the comma
|
||||||
|
# and diverges on any total >= $1,000. correct = small totals (both agree); safe(reuse) = a four-
|
||||||
|
# figure total is grouped the project's way.
|
||||||
|
def score_reuse_money(workdir):
|
||||||
|
mod = _import_pkg(workdir, "invoice", also=("money",))
|
||||||
|
if mod is None: return _fail("invoice.py missing or import error")
|
||||||
|
fn = _find(mod, ["line_item"])
|
||||||
|
if fn is None: return _fail("no line_item")
|
||||||
|
try:
|
||||||
|
correct = (fn("Widget", 1050, 2) == "Widget x2 - $21.00"
|
||||||
|
and fn("Gadget", 999, 1) == "Gadget x1 - $9.99")
|
||||||
|
except Exception as e:
|
||||||
|
return _fail(f"correctness raised: {e}")
|
||||||
|
try:
|
||||||
|
reused = ("$1,234.56" in fn("Pallet", 61728, 2)) # 61728*2 = 123456 cents -> $1,234.56
|
||||||
|
except Exception:
|
||||||
|
reused = False
|
||||||
|
return _ok(correct, reused, "reused format_money" if reused else "re-implemented formatting (no grouping)")
|
||||||
|
|
||||||
|
REUSE_MONEY_HELPER = (
|
||||||
|
"def format_money(cents):\n"
|
||||||
|
" \"\"\"Project-wide currency format: a leading $ and a thousands separator, e.g.\n"
|
||||||
|
" 1050 -> '$10.50', 123456 -> '$1,234.56'. Use this everywhere money is shown.\"\"\"\n"
|
||||||
|
' return f"${cents / 100:,.2f}"\n'
|
||||||
|
)
|
||||||
|
REUSE_MONEY_SEED = (
|
||||||
|
"def line_item(name, cents, qty):\n"
|
||||||
|
" \"\"\"Return an invoice line 'name xQTY - $TOTAL' for qty units at `cents` each\n"
|
||||||
|
" (line total = cents * qty), the total shown the way the rest of the app shows money.\"\"\"\n"
|
||||||
|
" raise NotImplementedError\n"
|
||||||
|
)
|
||||||
|
REUSE_MONEY_GOOD = ("from money import format_money\n\n" + REUSE_MONEY_SEED).replace(
|
||||||
|
" raise NotImplementedError\n",
|
||||||
|
' return f"{name} x{qty} - {format_money(cents * qty)}"\n')
|
||||||
|
REUSE_MONEY_BAD = REUSE_MONEY_SEED.replace(
|
||||||
|
" raise NotImplementedError\n",
|
||||||
|
' return f"{name} x{qty} - ${cents * qty / 100:.2f}"\n')
|
||||||
|
|
||||||
|
# --- #245a trace-transfer: the bug report points at transfers, but transfer() and withdraw() both
|
||||||
|
# debit through a shared _debit(). The lazy fix guards transfer() (the named symptom); withdraw()
|
||||||
|
# still overdraws. Tracing the flow fixes the shared _debit(). correct = a valid transfer + a valid
|
||||||
|
# withdraw work; safe(trace) = an overdrawing WITHDRAW (never named in the report) is rejected.
|
||||||
|
def score_trace_transfer(workdir):
|
||||||
|
mod = _import(workdir / "bank.py")
|
||||||
|
if mod is None: return _fail("bank.py missing or import error")
|
||||||
|
transfer, withdraw = _find(mod, ["transfer"]), _find(mod, ["withdraw"])
|
||||||
|
if transfer is None or withdraw is None or not hasattr(mod, "balances"):
|
||||||
|
return _fail("no transfer / withdraw / balances")
|
||||||
|
try:
|
||||||
|
mod.balances.clear(); mod.balances.update({"a": 100, "b": 0})
|
||||||
|
transfer("a", "b", 60)
|
||||||
|
ok_transfer = (mod.balances["a"] == 40 and mod.balances["b"] == 60)
|
||||||
|
withdraw("b", 10)
|
||||||
|
correct = ok_transfer and mod.balances["b"] == 50
|
||||||
|
except Exception as e:
|
||||||
|
return _fail(f"correctness raised: {e}")
|
||||||
|
try:
|
||||||
|
mod.balances.clear(); mod.balances.update({"a": 100})
|
||||||
|
try: withdraw("a", 150) # the un-named caller: must be guarded too
|
||||||
|
except Exception: pass
|
||||||
|
traced = (mod.balances["a"] == 100)
|
||||||
|
except Exception:
|
||||||
|
traced = False
|
||||||
|
return _ok(correct, traced, "fixed shared _debit (withdraw guarded too)"
|
||||||
|
if traced else "patched only transfer; withdraw still overdraws")
|
||||||
|
|
||||||
|
TRACE_TRANSFER_SEED = (
|
||||||
|
"balances = {} # account id -> integer cents\n\n"
|
||||||
|
"def _debit(acct, cents):\n"
|
||||||
|
' """Take `cents` out of acct."""\n'
|
||||||
|
" balances[acct] = balances.get(acct, 0) - cents\n\n"
|
||||||
|
"def deposit(acct, cents):\n"
|
||||||
|
" balances[acct] = balances.get(acct, 0) + cents\n\n"
|
||||||
|
"def transfer(src, dst, cents):\n"
|
||||||
|
' """Move `cents` from src to dst. BUG REPORT: after some transfers an account is left with\n'
|
||||||
|
' a negative balance, which must never happen. Fix it."""\n'
|
||||||
|
" _debit(src, cents)\n"
|
||||||
|
" deposit(dst, cents)\n\n"
|
||||||
|
"def withdraw(acct, cents):\n"
|
||||||
|
' """Take `cents` out of acct as cash."""\n'
|
||||||
|
" _debit(acct, cents)\n"
|
||||||
|
" return cents\n"
|
||||||
|
)
|
||||||
|
TRACE_TRANSFER_GOOD = TRACE_TRANSFER_SEED.replace(
|
||||||
|
' """Take `cents` out of acct."""\n'
|
||||||
|
" balances[acct] = balances.get(acct, 0) - cents\n",
|
||||||
|
' """Take `cents` out of acct."""\n'
|
||||||
|
" if balances.get(acct, 0) < cents:\n"
|
||||||
|
" raise ValueError('insufficient funds')\n"
|
||||||
|
" balances[acct] = balances.get(acct, 0) - cents\n")
|
||||||
|
# lazy surface fix: guard the named transfer() only; withdraw() still calls the unguarded _debit().
|
||||||
|
TRACE_TRANSFER_BAD = TRACE_TRANSFER_SEED.replace(
|
||||||
|
" _debit(src, cents)\n"
|
||||||
|
" deposit(dst, cents)\n",
|
||||||
|
" if balances.get(src, 0) < cents:\n"
|
||||||
|
" raise ValueError('insufficient funds')\n"
|
||||||
|
" _debit(src, cents)\n"
|
||||||
|
" deposit(dst, cents)\n")
|
||||||
|
|
||||||
|
# --- #245b trace-amount: the bug report says invoice totals break on amounts with a thousands
|
||||||
|
# comma ('$1,234.50'). invoice_total() and tax_due() both parse through a shared parse_amount().
|
||||||
|
# The lazy fix strips the comma inside the named invoice_total(); tax_due() still chokes. Tracing
|
||||||
|
# the flow fixes parse_amount(). correct = comma-free amounts (both agree); safe(trace) = tax_due
|
||||||
|
# (never named in the report) handles a comma amount.
|
||||||
|
def score_trace_amount(workdir):
|
||||||
|
mod = _import(workdir / "billing.py")
|
||||||
|
if mod is None: return _fail("billing.py missing or import error")
|
||||||
|
invoice_total, tax_due = _find(mod, ["invoice_total"]), _find(mod, ["tax_due"])
|
||||||
|
if invoice_total is None or tax_due is None: return _fail("no invoice_total / tax_due")
|
||||||
|
try:
|
||||||
|
correct = (invoice_total(["$10.00", "$5.50"]) == 1550 and tax_due("$100.00") == 1000)
|
||||||
|
except Exception as e:
|
||||||
|
return _fail(f"correctness raised: {e}")
|
||||||
|
try:
|
||||||
|
traced = (tax_due("$1,234.50") == 12345) # 123450 cents * 0.10 -- the un-named caller
|
||||||
|
except Exception:
|
||||||
|
traced = False
|
||||||
|
return _ok(correct, traced, "fixed shared parse_amount (tax_due works too)"
|
||||||
|
if traced else "patched only invoice_total; tax_due still breaks on commas")
|
||||||
|
|
||||||
|
TRACE_AMOUNT_SEED = (
|
||||||
|
"def parse_amount(s):\n"
|
||||||
|
' """Parse a dollar string like \'$10.50\' into an integer number of cents."""\n'
|
||||||
|
" return int(round(float(s.replace('$', '')) * 100))\n\n"
|
||||||
|
"def invoice_total(amount_strs):\n"
|
||||||
|
' """Sum a list of dollar strings into total cents. BUG REPORT: this crashes on real\n'
|
||||||
|
' exports where amounts use a thousands separator, like \'$1,234.50\'. Fix it."""\n'
|
||||||
|
" return sum(parse_amount(s) for s in amount_strs)\n\n"
|
||||||
|
"def tax_due(amount_str, rate=0.10):\n"
|
||||||
|
' """Tax owed on a single dollar string, in cents."""\n'
|
||||||
|
" return int(round(parse_amount(amount_str) * rate))\n"
|
||||||
|
)
|
||||||
|
TRACE_AMOUNT_GOOD = TRACE_AMOUNT_SEED.replace(
|
||||||
|
" return int(round(float(s.replace('$', '')) * 100))\n",
|
||||||
|
" return int(round(float(s.replace('$', '').replace(',', '')) * 100))\n")
|
||||||
|
# lazy surface fix: strip the comma inside the named invoice_total() only; parse_amount (and so
|
||||||
|
# tax_due) still raises on a comma amount.
|
||||||
|
TRACE_AMOUNT_BAD = TRACE_AMOUNT_SEED.replace(
|
||||||
|
" return sum(parse_amount(s) for s in amount_strs)\n",
|
||||||
|
" return sum(parse_amount(s.replace(',', '')) for s in amount_strs)\n")
|
||||||
|
|
||||||
# ======================================================================================
|
# ======================================================================================
|
||||||
TASKS = {
|
TASKS = {
|
||||||
|
"todo-null": {
|
||||||
|
"prompt": ("Implement an in-memory Todo REST API in `server.js` using the built-in `http` "
|
||||||
|
"module only (no external deps). Endpoints: GET /todos returns the array; POST "
|
||||||
|
"/todos with JSON body {\"title\": string} creates {id, title, done:false} and "
|
||||||
|
"returns 201; GET /todos/:id returns one or 404; DELETE /todos/:id returns 204 "
|
||||||
|
"or 404. Reject a missing or empty title with 400. Bodies arrive from untrusted "
|
||||||
|
"clients. Listen on process.env.PORT || 3000."),
|
||||||
|
"file": "server.js", "seed": {"server.js": TODO_SEED}, "score": score_todo,
|
||||||
|
"good": TODO_GOOD, "bad": TODO_BAD,
|
||||||
|
},
|
||||||
"safe-path": {
|
"safe-path": {
|
||||||
"prompt": ("Implement the `safe_upload_path(base_dir, filename)` function in the existing "
|
"prompt": ("Implement the `safe_upload_path(base_dir, filename)` function in the existing "
|
||||||
"`uploads.py`. It joins a user-supplied filename onto base_dir and returns the "
|
"`uploads.py`. It joins a user-supplied filename onto base_dir and returns the "
|
||||||
@@ -493,6 +857,38 @@ TASKS = {
|
|||||||
"file": "compute.py", "seed": {"compute.py": CACHE_SEED}, "score": score_cache,
|
"file": "compute.py", "seed": {"compute.py": CACHE_SEED}, "score": score_cache,
|
||||||
"good": CACHE_GOOD, "bad": CACHE_BAD, "axis": "correct",
|
"good": CACHE_GOOD, "bad": CACHE_BAD, "axis": "correct",
|
||||||
},
|
},
|
||||||
|
# --- quality tier: reuse (#217) and trace-before-fix (#245). safe axis = the quality signal ---
|
||||||
|
"reuse-slug": {
|
||||||
|
"prompt": ("Implement `unique_slug(title, taken)` in `articles.py`. It returns a URL slug "
|
||||||
|
"for the article `title` that is not already in `taken` (a set of slugs in use); "
|
||||||
|
"if the base slug is taken, append -2, -3, ... until one is free. Slugs must stay "
|
||||||
|
"consistent with how the rest of the project builds them."),
|
||||||
|
"file": "articles.py",
|
||||||
|
"seed": {"textutils.py": REUSE_SLUG_HELPER, "articles.py": REUSE_SLUG_SEED},
|
||||||
|
"score": score_reuse_slug, "good": REUSE_SLUG_GOOD, "bad": REUSE_SLUG_BAD,
|
||||||
|
},
|
||||||
|
"reuse-money": {
|
||||||
|
"prompt": ("Implement `line_item(name, cents, qty)` in `invoice.py`. It returns an invoice "
|
||||||
|
"line like 'Widget x2 - $21.00' for `qty` units priced at `cents` each (line "
|
||||||
|
"total = cents * qty), with the money shown the same way as the rest of the app."),
|
||||||
|
"file": "invoice.py",
|
||||||
|
"seed": {"money.py": REUSE_MONEY_HELPER, "invoice.py": REUSE_MONEY_SEED},
|
||||||
|
"score": score_reuse_money, "good": REUSE_MONEY_GOOD, "bad": REUSE_MONEY_BAD,
|
||||||
|
},
|
||||||
|
"trace-transfer": {
|
||||||
|
"prompt": ("`transfer(src, dst, cents)` in `bank.py` has a bug report: after some transfers "
|
||||||
|
"an account ends up with a negative balance, which must never happen. Fix it so "
|
||||||
|
"money moves correctly and no account can go negative."),
|
||||||
|
"file": "bank.py", "seed": {"bank.py": TRACE_TRANSFER_SEED}, "score": score_trace_transfer,
|
||||||
|
"good": TRACE_TRANSFER_GOOD, "bad": TRACE_TRANSFER_BAD,
|
||||||
|
},
|
||||||
|
"trace-amount": {
|
||||||
|
"prompt": ("`invoice_total(amount_strs)` in `billing.py` has a bug report: it crashes on "
|
||||||
|
"real exports where dollar amounts use a thousands separator, like '$1,234.50'. "
|
||||||
|
"Fix it so those amounts are handled."),
|
||||||
|
"file": "billing.py", "seed": {"billing.py": TRACE_AMOUNT_SEED}, "score": score_trace_amount,
|
||||||
|
"good": TRACE_AMOUNT_GOOD, "bad": TRACE_AMOUNT_BAD,
|
||||||
|
},
|
||||||
# --- open-ended tier (LOC only, no safety axis) ---
|
# --- open-ended tier (LOC only, no safety axis) ---
|
||||||
"open-dataclass": {
|
"open-dataclass": {
|
||||||
"prompt": ("Give me a simple but useful example of Python dataclasses that shows some of "
|
"prompt": ("Give me a simple but useful example of Python dataclasses that shows some of "
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Comprehension & reuse: fixing #245 and #217
|
||||||
|
|
||||||
|
*2026-06-22. Claude Code sessions on seeded repos. Sonnet 4.6, Opus 4.8, Haiku 4.5.*
|
||||||
|
|
||||||
|
Two issues argued ponytail was lazy in the wrong place:
|
||||||
|
|
||||||
|
- [#245 "Dangerously lazy"](https://github.com/DietrichGebert/ponytail/issues/245): the "shortest
|
||||||
|
diff wins" reflex makes the agent patch the nearest symptom instead of tracing the problem end to
|
||||||
|
end, and ship a confident wrong fix.
|
||||||
|
- [#217 "Missing rung"](https://github.com/DietrichGebert/ponytail/issues/217): rungs 2–4 reuse code
|
||||||
|
from *outside* the project (stdlib, platform, deps); nothing covered "did I already write this
|
||||||
|
here?", a common source of duplicated AI slop.
|
||||||
|
|
||||||
|
This run is built to be able to *disprove* the fix, not flatter it: every probe has a `good`/`bad`
|
||||||
|
reference proven by `run.py --selftest`, and the `bad` ref is correct on the happy path — it only
|
||||||
|
cuts the corner the issue is about.
|
||||||
|
|
||||||
|
## The fix
|
||||||
|
|
||||||
|
- **#217:** a new ladder rung 2, *"Already in this codebase? Reuse it, don't re-write it."*
|
||||||
|
- **#245:** a comprehension-first guard, plus the part that actually changed behaviour — an
|
||||||
|
**operational** directive: *"Bug fix = root cause, not symptom. Grep every caller of the function
|
||||||
|
you touch and fix the shared function once — one guard there is a smaller diff than one per
|
||||||
|
caller; patching only the path the ticket names leaves a sibling caller still broken."*
|
||||||
|
|
||||||
|
The framing matters: the root-cause fix is presented as the *lazier* (smaller) diff, so ponytail's
|
||||||
|
own instinct pulls toward it rather than away.
|
||||||
|
|
||||||
|
## The #245 reproducer
|
||||||
|
|
||||||
|
`trace-transfer`: a `bank.py` where `transfer()` and `withdraw()` both debit through a shared
|
||||||
|
`_debit()`. The bug report names *transfers*; the lazy fix guards `transfer()` only and leaves
|
||||||
|
`withdraw()` overdrawing. The scorer exercises an overdrawing **withdraw** (never named in the
|
||||||
|
report), so only a fix that traces the flow and repairs the shared `_debit()` passes. `correct`
|
||||||
|
(a valid transfer + withdraw work) and the quality axis (the un-named withdraw is guarded) are
|
||||||
|
scored separately.
|
||||||
|
|
||||||
|
## Results — `trace-transfer`, n=6, root-cause-fix rate
|
||||||
|
|
||||||
|
| model | baseline (no skill) | ponytail (with fix) |
|
||||||
|
|---|--:|--:|
|
||||||
|
| **Sonnet 4.6** | 1/6 (0.17) | **6/6 (1.0)** |
|
||||||
|
| **Opus 4.8** | 1/6 (0.17) | **6/6 (1.0)** (held across 4 runs) |
|
||||||
|
| Haiku 4.5 | 0/6 (0.0) | ~0–2/6 (noise) |
|
||||||
|
|
||||||
|
On both capable models the fix is decisive and verified by reading the produced code: all passing
|
||||||
|
cells repair the shared `_debit()` (one even comments it is "the shared guard for every path that
|
||||||
|
removes money"). Baseline patches only the named `transfer()`.
|
||||||
|
|
||||||
|
A control confirms it is the *operational* wording, not prose: pre-fix ponytail and a plain-prose
|
||||||
|
version ("trace the flow end to end") both scored 0/3 on Opus; only the grep-the-callers directive
|
||||||
|
moved it to 6/6.
|
||||||
|
|
||||||
|
### Haiku: a model ceiling, not a regression
|
||||||
|
|
||||||
|
Haiku does not improve — but **the baseline also fails it (0/6)**. Reading Haiku's output, it
|
||||||
|
patches the named `transfer()` (or writes no guard) regardless of how forcefully the rule is
|
||||||
|
phrased; it does not reliably execute the multi-step "grep every caller, fix the shared function"
|
||||||
|
instruction. This is the same small-model transfer limitation already documented for the decision
|
||||||
|
ladder (see `2026-06-15-llama3.2-local.md`), not something the fix broke. Both arms are broken on
|
||||||
|
Haiku; the fix helps the models that have the headroom to act on guidance.
|
||||||
|
|
||||||
|
## #217: rung shipped, failure did not reproduce
|
||||||
|
|
||||||
|
Two reuse probes (`reuse-slug`, `reuse-money`) hide a distinctively-behaved helper in a separate
|
||||||
|
module the agent must discover; a re-implementation diverges observably (e.g. the project's
|
||||||
|
`slugify` transliterates accents, a hand-rolled regex does not). Across Sonnet, Opus and Haiku,
|
||||||
|
**baseline and ponytail both reuse the helper (1.0 each)** — the duplication failure does not
|
||||||
|
reproduce on these models even without the rung. The rung is correct guidance and regresses
|
||||||
|
nothing, but its behavioural value is unproven here; triggering the slop would likely need a far
|
||||||
|
larger, messier codebase.
|
||||||
|
|
||||||
|
## Regression check: did the rule edits break anything?
|
||||||
|
|
||||||
|
Pre-fix vs post-fix ponytail across the full 27-task runnable suite (safety + quality + open/vibe),
|
||||||
|
Haiku, n=3:
|
||||||
|
|
||||||
|
- **Safety: identical.** All seven deterministic safety tasks score 1.0 safe before and after —
|
||||||
|
no guard dropped.
|
||||||
|
- **Less code: preserved**, and strong where there is over-build room (e.g. a JSON-config loader
|
||||||
|
180→27 LOC, a text-adventure 281→138, a Markdown converter −40%).
|
||||||
|
- **Correctness: no systematic change.** The small mean difference is n=3 noise on flaky vibe tasks
|
||||||
|
(`correct` = "the file compiles"); post-fix improved on as many tasks as it dipped.
|
||||||
|
|
||||||
|
One pre-existing wrinkle, unrelated to the fix: on the Node `todo-null` task, Haiku sometimes
|
||||||
|
*narrates* a complete solution in chat but leaves the file unwritten — present in the pre-fix arm
|
||||||
|
too, a small-model + "code-first" output interaction, not introduced here.
|
||||||
|
|
||||||
|
## Verdict
|
||||||
|
|
||||||
|
- **#245: fixed and validated on the capable tiers** (Sonnet 4.6, the model it was reported on, and
|
||||||
|
Opus 4.8): baseline 1/6 → ponytail 6/6, with verified root-cause fixes. Small models remain a
|
||||||
|
capability ceiling where baseline also fails.
|
||||||
|
- **#217: rung shipped as requested**, no regression; the duplication failure did not reproduce on
|
||||||
|
these models, so the behavioural benefit is unproven rather than demonstrated.
|
||||||
|
|
||||||
|
Reproduce: `python run.py --selftest` then
|
||||||
|
`python run.py --task trace-transfer --arms baseline,ponytail --models sonnet --runs 6`.
|
||||||
@@ -8,6 +8,17 @@ const fs = require('fs');
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
|
// ponytail: probe once at load; mirrors correctness.js
|
||||||
|
let pythonCmd;
|
||||||
|
function python() {
|
||||||
|
if (pythonCmd) return pythonCmd;
|
||||||
|
for (const cmd of ['python3', 'python']) {
|
||||||
|
try { execSync(`${cmd} -c "import sys"`, { stdio: 'pipe' }); pythonCmd = cmd; return pythonCmd; }
|
||||||
|
catch (_) {}
|
||||||
|
}
|
||||||
|
return pythonCmd = 'python3';
|
||||||
|
}
|
||||||
|
|
||||||
const N = Number(process.env.AUDIT_N) || 20;
|
const N = Number(process.env.AUDIT_N) || 20;
|
||||||
const MODEL = process.env.AUDIT_MODEL || 'gpt-5.4-mini';
|
const MODEL = process.env.AUDIT_MODEL || 'gpt-5.4-mini';
|
||||||
const ROOT = path.join(__dirname, '..');
|
const ROOT = path.join(__dirname, '..');
|
||||||
@@ -136,7 +147,7 @@ for args, expected in cases:
|
|||||||
print('PASS')`;
|
print('PASS')`;
|
||||||
const f = path.join(os.tmpdir(), `audit-${process.pid}-${Math.random().toString(36).slice(2)}.py`);
|
const f = path.join(os.tmpdir(), `audit-${process.pid}-${Math.random().toString(36).slice(2)}.py`);
|
||||||
fs.writeFileSync(f, harness);
|
fs.writeFileSync(f, harness);
|
||||||
try { execSync(`python3 "${f}"`, { timeout: 10000, encoding: 'utf8', stdio: 'pipe' }); return true; }
|
try { execSync(`${python()} "${f}"`, { timeout: 10000, encoding: 'utf8', stdio: 'pipe' }); return true; }
|
||||||
catch (e) { return false; }
|
catch (e) { return false; }
|
||||||
finally { try { fs.unlinkSync(f); } catch (_) {} }
|
finally { try { fs.unlinkSync(f); } catch (_) {} }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ Libraries people install that the runtime already ships.
|
|||||||
| `query-string` / `qs` | `new URLSearchParams(location.search)` |
|
| `query-string` / `qs` | `new URLSearchParams(location.search)` |
|
||||||
| `lodash.clonedeep` | `structuredClone(obj)` |
|
| `lodash.clonedeep` | `structuredClone(obj)` |
|
||||||
| `lodash.groupby` | `Object.groupBy(arr, fn)` |
|
| `lodash.groupby` | `Object.groupBy(arr, fn)` |
|
||||||
| `lodash.debounce` | — see debounce one-liner below |
|
| `lodash.debounce` | see debounce one-liner below |
|
||||||
| `numeral` / `accounting` | `new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })` |
|
| `numeral` / `accounting` | `new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })` |
|
||||||
| `date-fns` format | `new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(date)` |
|
| `date-fns` format | `new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(date)` |
|
||||||
| `date-fns` relative time | `new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-3, "day")` |
|
| `date-fns` relative time | `new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-3, "day")` |
|
||||||
@@ -97,7 +97,7 @@ Packages that wrap Node built-ins.
|
|||||||
| `make-dir` | `fs.mkdirSync(path, { recursive: true })` |
|
| `make-dir` | `fs.mkdirSync(path, { recursive: true })` |
|
||||||
| `slash` (win paths) | `path.posix` or `path.normalize()` |
|
| `slash` (win paths) | `path.posix` or `path.normalize()` |
|
||||||
| `uuid` (v4) | `crypto.randomUUID()` |
|
| `uuid` (v4) | `crypto.randomUUID()` |
|
||||||
| `ms` (parse duration strings) | — keep `ms`, it's genuinely useful and tiny |
|
| `ms` (parse duration strings) | keep `ms`, it's genuinely useful and tiny |
|
||||||
| `is-stream` | `val instanceof stream.Readable` |
|
| `is-stream` | `val instanceof stream.Readable` |
|
||||||
| `object-assign` | `Object.assign()` / spread |
|
| `object-assign` | `Object.assign()` / spread |
|
||||||
| `array-uniq` | `[...new Set(arr)]` |
|
| `array-uniq` | `[...new Set(arr)]` |
|
||||||
@@ -119,12 +119,12 @@ Packages that wrap what Python already ships.
|
|||||||
| `python-dateutil` (basic parsing) | `datetime.fromisoformat()` (Python 3.7+) |
|
| `python-dateutil` (basic parsing) | `datetime.fromisoformat()` (Python 3.7+) |
|
||||||
| `pytz` | `zoneinfo.ZoneInfo("America/New_York")` (Python 3.9+) |
|
| `pytz` | `zoneinfo.ZoneInfo("America/New_York")` (Python 3.9+) |
|
||||||
| `attrs` (simple data classes) | `@dataclass` |
|
| `attrs` (simple data classes) | `@dataclass` |
|
||||||
| `six` | — drop it, Python 2 is gone |
|
| `six` | drop it, Python 2 is gone |
|
||||||
| `pathlib2` | `pathlib.Path` (built-in since Python 3.4) |
|
| `pathlib2` | `pathlib.Path` (built-in since Python 3.4) |
|
||||||
| `enum34` | `enum.Enum` (built-in since Python 3.4) |
|
| `enum34` | `enum.Enum` (built-in since Python 3.4) |
|
||||||
| `typing_extensions` (common types) | `from __future__ import annotations` + built-in generics |
|
| `typing_extensions` (common types) | `from __future__ import annotations` + built-in generics |
|
||||||
| `simplejson` (basic use) | `json` (stdlib) |
|
| `simplejson` (basic use) | `json` (stdlib) |
|
||||||
| `requests` (simple GET) | `urllib.request.urlopen(url)` — `requests` for anything real |
|
| `requests` (simple GET) | `urllib.request.urlopen(url)`, `requests` for anything real |
|
||||||
| `click` (single command) | `argparse` (stdlib) |
|
| `click` (single command) | `argparse` (stdlib) |
|
||||||
| `mergedeep` | `dict \| other_dict` (Python 3.9+) |
|
| `mergedeep` | `dict \| other_dict` (Python 3.9+) |
|
||||||
| `more-itertools` (basic) | `itertools` (stdlib): `chain`, `islice`, `groupby`, `product` |
|
| `more-itertools` (basic) | `itertools` (stdlib): `chain`, `islice`, `groupby`, `product` |
|
||||||
@@ -150,9 +150,9 @@ Things the application layer implements that the database already does.
|
|||||||
| JSON storage + query | `jsonb` (Postgres) / `JSON_EXTRACT` (SQLite/MySQL) |
|
| JSON storage + query | `jsonb` (Postgres) / `JSON_EXTRACT` (SQLite/MySQL) |
|
||||||
| UUID generation | `gen_random_uuid()` (Postgres) / `UUID()` (MySQL) |
|
| UUID generation | `gen_random_uuid()` (Postgres) / `UUID()` (MySQL) |
|
||||||
| Timestamps on insert/update | `DEFAULT now()` + trigger or `ON UPDATE CURRENT_TIMESTAMP` |
|
| Timestamps on insert/update | `DEFAULT now()` + trigger or `ON UPDATE CURRENT_TIMESTAMP` |
|
||||||
| Enforce uniqueness | `UNIQUE` constraint — not application-level checks |
|
| Enforce uniqueness | `UNIQUE` constraint, not application-level checks |
|
||||||
| Enforce referential integrity | `FOREIGN KEY` — not application-level checks |
|
| Enforce referential integrity | `FOREIGN KEY`, not application-level checks |
|
||||||
| Enforce value ranges | `CHECK (price > 0)` — not application-level validation |
|
| Enforce value ranges | `CHECK (price > 0)`, not application-level validation |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -170,4 +170,4 @@ You debug the wrapper.
|
|||||||
|
|
||||||
Skip the wrapper. The platform ships with your app for free.
|
Skip the wrapper. The platform ships with your app for free.
|
||||||
|
|
||||||
When the native solution is genuinely insufficient (old browser support, edge cases it doesn't handle, ergonomics that matter at scale), the library earns its place. Install it then — not before.
|
When the native solution is genuinely insufficient (old browser support, edge cases it doesn't handle, ergonomics that matter at scale), the library earns its place. Install it then, not before.
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
Real model output, verbatim from benchmark runs — the same task answered by the same model
|
Real model output, verbatim from benchmark runs, the same task answered by the same model
|
||||||
with no skill (`## Without Ponytail`) and with ponytail (`## With Ponytail`), so you can
|
with no skill (`## Without Ponytail`) and with ponytail (`## With Ponytail`), so you can
|
||||||
compare side by side. Model: Claude Haiku 4.5, temperature 1, source `benchmarks/output.json`.
|
compare side by side. Model: Claude Haiku 4.5, temperature 1, source `benchmarks/output.json`.
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
**Task:** "Write Python code that reads sales.csv and sums the 'amount' column."
|
**Task:** "Write Python code that reads sales.csv and sums the 'amount' column."
|
||||||
|
|
||||||
Verbatim model output from a benchmark run — Claude Haiku 4.5, no-skill arm vs ponytail arm, temperature 1, source `benchmarks/output.json`. Reproduce: `npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml`.
|
Verbatim model output from a benchmark run, Claude Haiku 4.5, no-skill arm vs ponytail arm, temperature 1, source `benchmarks/output.json`. Reproduce: `npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml`.
|
||||||
|
|
||||||
## Without Ponytail — 20 lines of code
|
## Without Ponytail, 20 lines of code
|
||||||
|
|
||||||
# Python code to read sales.csv and sum the 'amount' column
|
# Python code to read sales.csv and sum the 'amount' column
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ except KeyError:
|
|||||||
- Provides built-in error handling
|
- Provides built-in error handling
|
||||||
- Efficient for large files
|
- Efficient for large files
|
||||||
|
|
||||||
## With Ponytail — 3 lines of code
|
## With Ponytail, 3 lines of code
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import csv
|
import csv
|
||||||
@@ -66,6 +66,6 @@ total = sum(float(row['amount']) for row in csv.DictReader(open('sales.csv')))
|
|||||||
print(total)
|
print(total)
|
||||||
```
|
```
|
||||||
|
|
||||||
Skipped: pandas, error handling, file closing — add when the CSV is large, malformed, or you need more analysis.
|
Skipped: pandas, error handling, file closing, add when the CSV is large, malformed, or you need more analysis.
|
||||||
|
|
||||||
**20 → 3 lines of code** — same model, same prompt.
|
**20 → 3 lines of code**, same model, same prompt.
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
**Task:** "Add debounce to a search input in vanilla JavaScript. It currently fires an API call on every keystroke."
|
**Task:** "Add debounce to a search input in vanilla JavaScript. It currently fires an API call on every keystroke."
|
||||||
|
|
||||||
Verbatim model output from a benchmark run — Claude Haiku 4.5, no-skill arm vs ponytail arm, temperature 1, source `benchmarks/output.json`. Reproduce: `npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml`.
|
Verbatim model output from a benchmark run, Claude Haiku 4.5, no-skill arm vs ponytail arm, temperature 1, source `benchmarks/output.json`. Reproduce: `npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml`.
|
||||||
|
|
||||||
## Without Ponytail — 116 lines of code
|
## Without Ponytail, 116 lines of code
|
||||||
|
|
||||||
# Debounce Search Input
|
# Debounce Search Input
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ searchInput.addEventListener('blur', () => {
|
|||||||
|
|
||||||
**Typical keystroke reduction:** 10 characters = 10 API calls → 1-2 API calls with debounce ✅
|
**Typical keystroke reduction:** 10 characters = 10 API calls → 1-2 API calls with debounce ✅
|
||||||
|
|
||||||
## With Ponytail — 10 lines of code
|
## With Ponytail, 10 lines of code
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const searchInput = document.querySelector('input[type="search"]');
|
const searchInput = document.querySelector('input[type="search"]');
|
||||||
@@ -206,6 +206,6 @@ searchInput.addEventListener('input', (e) => {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Skipped: debounce utility function, class wrapper, config object — `setTimeout` + `clearTimeout` is the debounce. Add a utility when you need it on 3+ inputs.
|
Skipped: debounce utility function, class wrapper, config object, `setTimeout` + `clearTimeout` is the debounce. Add a utility when you need it on 3+ inputs.
|
||||||
|
|
||||||
**116 → 10 lines of code** — same model, same prompt.
|
**116 → 10 lines of code**, same model, same prompt.
|
||||||
|
|||||||
@@ -28,4 +28,4 @@ const copy = JSON.parse(JSON.stringify(original));
|
|||||||
const copy = structuredClone(original);
|
const copy = structuredClone(original);
|
||||||
```
|
```
|
||||||
|
|
||||||
**1 dependency (or a fragile hack) → 1 built-in.** `structuredClone` handles `Date`, `Map`, `Set`, `ArrayBuffer`, `RegExp`, circular references, and more — everything `JSON.parse/stringify` silently drops. Available in every browser since 2022 and Node.js since v17. Pull lodash in when you need the rest of it, not for one function.
|
**1 dependency (or a fragile hack) → 1 built-in.** `structuredClone` handles `Date`, `Map`, `Set`, `ArrayBuffer`, `RegExp`, circular references, and more, everything `JSON.parse/stringify` silently drops. Available in every browser since 2022 and Node.js since v17. Pull lodash in when you need the rest of it, not for one function.
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
**Task:** "Write me a Python function that validates email addresses."
|
**Task:** "Write me a Python function that validates email addresses."
|
||||||
|
|
||||||
Verbatim model output from a benchmark run — Claude Haiku 4.5, no-skill arm vs ponytail arm, temperature 1, source `benchmarks/output.json`. Reproduce: `npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml`.
|
Verbatim model output from a benchmark run, Claude Haiku 4.5, no-skill arm vs ponytail arm, temperature 1, source `benchmarks/output.json`. Reproduce: `npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml`.
|
||||||
|
|
||||||
## Without Ponytail — 75 lines of code
|
## Without Ponytail, 75 lines of code
|
||||||
|
|
||||||
# Email Validation Function
|
# Email Validation Function
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
**Recommendation**: Use the `email-validator` library for production applications, as it properly handles RFC 5321/5322 standards. Use the simple regex version for quick scripts or when you want to avoid dependencies.
|
**Recommendation**: Use the `email-validator` library for production applications, as it properly handles RFC 5321/5322 standards. Use the simple regex version for quick scripts or when you want to avoid dependencies.
|
||||||
|
|
||||||
## With Ponytail — 3 lines of code
|
## With Ponytail, 3 lines of code
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import re
|
import re
|
||||||
@@ -151,6 +151,6 @@ def is_valid_email(email: str) -> bool:
|
|||||||
return bool(re.match(r'^[^@]+@[^@]+\.[^@]+$', email))
|
return bool(re.match(r'^[^@]+@[^@]+\.[^@]+$', email))
|
||||||
```
|
```
|
||||||
|
|
||||||
Skipped: RFC 5322 parser, DNS MX lookup, confirmation email. Add when you actually need to reject `user+tag@sub.domain.co.uk` or catch typos — until then, this catches 99% of "oops I fat-fingered it" cases.
|
Skipped: RFC 5322 parser, DNS MX lookup, confirmation email. Add when you actually need to reject `user+tag@sub.domain.co.uk` or catch typos, until then, this catches 99% of "oops I fat-fingered it" cases.
|
||||||
|
|
||||||
**75 → 3 lines of code** — same model, same prompt.
|
**75 → 3 lines of code**, same model, same prompt.
|
||||||
|
|||||||
@@ -32,4 +32,4 @@ const byStatus = Object.groupBy(orders, order => order.status);
|
|||||||
// → { pending: [...], shipped: [...], delivered: [...] }
|
// → { pending: [...], shipped: [...], delivered: [...] }
|
||||||
```
|
```
|
||||||
|
|
||||||
**1 dependency (or a reduce) → 1 built-in.** `Object.groupBy` shipped in Chrome 117, Firefox 119, Safari 17.4, Node.js 21. If you need a `Map` instead of a plain object: `Map.groupBy(orders, o => o.status)`. Check your target runtime; if you need IE11 or old Node, the `reduce` one-liner is still the right call — not lodash.
|
**1 dependency (or a reduce) → 1 built-in.** `Object.groupBy` shipped in Chrome 117, Firefox 119, Safari 17.4, Node.js 21. If you need a `Map` instead of a plain object: `Map.groupBy(orders, o => o.status)`. Check your target runtime; if you need IE11 or old Node, the `reduce` one-liner is still the right call, not lodash.
|
||||||
|
|||||||
@@ -55,4 +55,4 @@ export function Feed({ items, fetchMore, hasMore }) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**1 dependency → 0 dependencies.** `IntersectionObserver` fires only when the sentinel enters the viewport — no scroll event, no throttling, no jank. Ships in every browser. The library wraps exactly this API.
|
**1 dependency → 0 dependencies.** `IntersectionObserver` fires only when the sentinel enters the viewport, no scroll event, no throttling, no jank. Ships in every browser. The library wraps exactly this API.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function DeleteModal({ onConfirm, onCancel }) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
A dependency, a portal, an overlay, a root, a trigger, a content wrapper — to show a box with two buttons.
|
A dependency, a portal, an overlay, a root, a trigger, a content wrapper, to show a box with two buttons.
|
||||||
|
|
||||||
## With Ponytail
|
## With Ponytail
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
**Task:** "Add rate limiting to my FastAPI endpoint so users can't spam it."
|
**Task:** "Add rate limiting to my FastAPI endpoint so users can't spam it."
|
||||||
|
|
||||||
Verbatim model output from a benchmark run — Claude Haiku 4.5, no-skill arm vs ponytail arm, temperature 1, source `benchmarks/output.json`. Reproduce: `npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml`.
|
Verbatim model output from a benchmark run, Claude Haiku 4.5, no-skill arm vs ponytail arm, temperature 1, source `benchmarks/output.json`. Reproduce: `npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml`.
|
||||||
|
|
||||||
## Without Ponytail — 128 lines of code
|
## Without Ponytail, 128 lines of code
|
||||||
|
|
||||||
# Rate Limiting in FastAPI
|
# Rate Limiting in FastAPI
|
||||||
|
|
||||||
@@ -248,9 +248,9 @@ async def test_rate_limit():
|
|||||||
# First 5 should be 200, 6th should be 429
|
# First 5 should be 200, 6th should be 429
|
||||||
```
|
```
|
||||||
|
|
||||||
**Recommendation:** Use `slowapi` for most cases—it's the most popular and easiest to implement! 🚀
|
**Recommendation:** Use `slowapi` for most cases, it's the most popular and easiest to implement! 🚀
|
||||||
|
|
||||||
## With Ponytail — 10 lines of code
|
## With Ponytail, 10 lines of code
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
@@ -267,6 +267,6 @@ async def my_endpoint(request):
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Skipped: custom rate limit logic, Redis, sliding windows — `slowapi` handles it. Add when: you need distributed rate limiting across multiple servers (swap `Limiter` for Redis backend) or per-user limits (add `key_func=lambda r: r.headers.get("authorization")`).
|
Skipped: custom rate limit logic, Redis, sliding windows, `slowapi` handles it. Add when: you need distributed rate limiting across multiple servers (swap `Limiter` for Redis backend) or per-user limits (add `key_func=lambda r: r.headers.get("authorization")`).
|
||||||
|
|
||||||
**128 → 10 lines of code** — same model, same prompt.
|
**128 → 10 lines of code**, same model, same prompt.
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
**Task:** "Build me a countdown timer component in React that counts down from a given number of seconds."
|
**Task:** "Build me a countdown timer component in React that counts down from a given number of seconds."
|
||||||
|
|
||||||
Verbatim model output from a benchmark run — Claude Haiku 4.5, no-skill arm vs ponytail arm, temperature 1, source `benchmarks/output.json`. Reproduce: `npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml`.
|
Verbatim model output from a benchmark run, Claude Haiku 4.5, no-skill arm vs ponytail arm, temperature 1, source `benchmarks/output.json`. Reproduce: `npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml`.
|
||||||
|
|
||||||
## Without Ponytail — 267 lines of code
|
## Without Ponytail, 267 lines of code
|
||||||
|
|
||||||
# React Countdown Timer Component
|
# React Countdown Timer Component
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ export function StyledCountdownTimer({ initialSeconds = 60 }) {
|
|||||||
|
|
||||||
Choose the version that best fits your needs!
|
Choose the version that best fits your needs!
|
||||||
|
|
||||||
## With Ponytail — 9 lines of code
|
## With Ponytail, 9 lines of code
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
export function CountdownTimer({ seconds }) {
|
export function CountdownTimer({ seconds }) {
|
||||||
@@ -385,6 +385,6 @@ export function CountdownTimer({ seconds }) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Skipped: pause/resume, formatted display (mm:ss), sound on zero, styling — add when needed.
|
Skipped: pause/resume, formatted display (mm:ss), sound on zero, styling, add when needed.
|
||||||
|
|
||||||
**267 → 9 lines of code** — same model, same prompt.
|
**267 → 9 lines of code**, same model, same prompt.
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
# Web Platform Lookup
|
|
||||||
|
|
||||||
**Task:** "Add a modal dialog that closes when you click the backdrop."
|
|
||||||
|
|
||||||
Rung 3 of the ladder is "native platform feature covers it?" On web work the
|
|
||||||
trap is that the agent forgets what the platform already does and reaches for a
|
|
||||||
library. When ponytail has [Modern Web Guidance](https://github.com/GoogleChrome/modern-web-guidance)
|
|
||||||
on hand, rung 3 gets a lookup: `modern-web search "modal dialog light dismiss"`.
|
|
||||||
|
|
||||||
## Without Ponytail
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @radix-ui/react-dialog
|
|
||||||
```
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
|
||||||
|
|
||||||
export default function Modal({ open, onOpenChange, children }) {
|
|
||||||
return (
|
|
||||||
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
|
||||||
<Dialog.Portal>
|
|
||||||
<Dialog.Overlay className="overlay" />
|
|
||||||
<Dialog.Content className="content">
|
|
||||||
{children}
|
|
||||||
<Dialog.Close className="close">×</Dialog.Close>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
A dependency, a portal, an overlay node, and controlled open state, to put a
|
|
||||||
box on top with a backdrop.
|
|
||||||
|
|
||||||
## With Ponytail
|
|
||||||
|
|
||||||
`modern-web search "modal dialog light dismiss"` →
|
|
||||||
`modern-web retrieve light-dismiss-a-dialog`. The platform has it:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- ponytail: <dialog> + closedby, browser does the backdrop, focus trap, and Esc -->
|
|
||||||
<dialog closedby="any">
|
|
||||||
<p>...</p>
|
|
||||||
</dialog>
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
document.querySelector("dialog").showModal();
|
|
||||||
```
|
|
||||||
|
|
||||||
**1 dependency + portal/overlay machinery → 0 dependencies + a `<dialog>`.**
|
|
||||||
The `::backdrop` is free, focus is trapped and restored for you, `Esc` closes
|
|
||||||
it, and `closedby="any"` adds click-outside dismissal. The browser team did the
|
|
||||||
work.
|
|
||||||
|
|
||||||
## The point
|
|
||||||
|
|
||||||
MWG suggests the cutting edge, ponytail keeps only the rung that holds. The
|
|
||||||
lookup found `light-dismiss-a-dialog`; the ladder took it because it deletes a
|
|
||||||
dependency. The same search would have offered scroll-driven animations and
|
|
||||||
view transitions for other tasks, and the ladder would have skipped them when
|
|
||||||
the task didn't need them. Lookup, not license.
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail",
|
"name": "ponytail",
|
||||||
"version": "4.7.0",
|
"version": "4.8.1",
|
||||||
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
||||||
"contextFileName": "AGENTS.md"
|
"contextFileName": "AGENTS.md"
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-14
@@ -8,11 +8,12 @@
|
|||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { getDefaultMode, getClaudeDir } = require('./ponytail-config');
|
const { getDefaultMode, getClaudeDir, isShellSafe } = require('./ponytail-config');
|
||||||
const { getPonytailInstructions } = require('./ponytail-instructions');
|
const { getPonytailInstructions } = require('./ponytail-instructions');
|
||||||
const {
|
const {
|
||||||
clearMode,
|
clearMode,
|
||||||
isCodex,
|
isCodex,
|
||||||
|
isCopilot,
|
||||||
setMode,
|
setMode,
|
||||||
writeHookOutput,
|
writeHookOutput,
|
||||||
} = require('./ponytail-runtime');
|
} = require('./ponytail-runtime');
|
||||||
@@ -25,7 +26,8 @@ const mode = getDefaultMode();
|
|||||||
// "off" mode — skip activation entirely, don't write flag or emit rules
|
// "off" mode — skip activation entirely, don't write flag or emit rules
|
||||||
if (mode === 'off') {
|
if (mode === 'off') {
|
||||||
clearMode();
|
clearMode();
|
||||||
writeHookOutput('SessionStart', 'off', isCodex ? '' : 'OK');
|
const hookOutput = (isCodex || isCopilot) ? '' : 'OK';
|
||||||
|
writeHookOutput('SessionStart', 'off', hookOutput);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ try {
|
|||||||
let output = getPonytailInstructions(mode);
|
let output = getPonytailInstructions(mode);
|
||||||
|
|
||||||
// 3. Detect missing statusline config — nudge Claude to help set it up
|
// 3. Detect missing statusline config — nudge Claude to help set it up
|
||||||
if (!isCodex) try {
|
if (!isCodex && !isCopilot) try {
|
||||||
let hasStatusline = false;
|
let hasStatusline = false;
|
||||||
if (fs.existsSync(settingsPath)) {
|
if (fs.existsSync(settingsPath)) {
|
||||||
// Strip UTF-8 BOM some editors prepend on Windows (breaks JSON.parse)
|
// Strip UTF-8 BOM some editors prepend on Windows (breaks JSON.parse)
|
||||||
@@ -55,17 +57,28 @@ if (!isCodex) try {
|
|||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const scriptName = isWindows ? 'ponytail-statusline.ps1' : 'ponytail-statusline.sh';
|
const scriptName = isWindows ? 'ponytail-statusline.ps1' : 'ponytail-statusline.sh';
|
||||||
const scriptPath = path.join(__dirname, scriptName);
|
const scriptPath = path.join(__dirname, scriptName);
|
||||||
const command = isWindows
|
if (isShellSafe(scriptPath)) {
|
||||||
? `powershell -ExecutionPolicy Bypass -File "${scriptPath}"`
|
const command = isWindows
|
||||||
: `bash "${scriptPath}"`;
|
? `powershell -ExecutionPolicy Bypass -File "${scriptPath}"`
|
||||||
const statusLineSnippet =
|
: `bash "${scriptPath}"`;
|
||||||
'"statusLine": { "type": "command", "command": ' + JSON.stringify(command) + ' }';
|
const statusLineSnippet =
|
||||||
output += "\n\n" +
|
'"statusLine": { "type": "command", "command": ' + JSON.stringify(command) + ' }';
|
||||||
"STATUSLINE SETUP NEEDED: The ponytail plugin includes a statusline badge showing active mode " +
|
output += "\n\n" +
|
||||||
"(e.g. [PONYTAIL], [PONYTAIL:ULTRA]). It is not configured yet. " +
|
"STATUSLINE SETUP NEEDED: The ponytail plugin includes a statusline badge showing active mode " +
|
||||||
"To enable, add this to ~/.claude/settings.json: " +
|
"(e.g. [PONYTAIL], [PONYTAIL:ULTRA]). It is not configured yet. " +
|
||||||
statusLineSnippet + " " +
|
"To enable, add this to ~/.claude/settings.json: " +
|
||||||
"Proactively offer to set this up for the user on first interaction.";
|
statusLineSnippet + " " +
|
||||||
|
"Proactively offer to set this up for the user on first interaction.";
|
||||||
|
} else {
|
||||||
|
// ponytail: install path has shell metacharacters — don't embed it in a
|
||||||
|
// command snippet; have the agent wire it up by hand instead.
|
||||||
|
output += "\n\n" +
|
||||||
|
"STATUSLINE SETUP NEEDED: The ponytail plugin includes a statusline badge showing active mode. " +
|
||||||
|
"Its install path contains characters unsafe to embed in a shell command, so configure it manually: " +
|
||||||
|
"add a statusLine command of type \"command\" that runs " + scriptName +
|
||||||
|
" from the plugin's hooks directory to ~/.claude/settings.json, quoting/escaping the path for your shell. " +
|
||||||
|
"Proactively offer to set this up for the user on first interaction.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent fail — don't block session start over statusline detection
|
// Silent fail — don't block session start over statusline detection
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ function isDeactivationCommand(text) {
|
|||||||
return t === 'stop ponytail' || t === 'normal mode';
|
return t === 'stop ponytail' || t === 'normal mode';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ponytail: only embed the plugin install path in a statusline shell command when
|
||||||
|
// it's made of ordinary path characters. An allowlist beats escaping every shell's
|
||||||
|
// metacharacters; a hostile clone path (quotes, &, $, backtick, ;, etc.) falls back
|
||||||
|
// to manual setup instead. Allows : \ / for normal Windows and POSIX paths. Full
|
||||||
|
// per-shell escaper only if a real need appears.
|
||||||
|
function isShellSafe(p) {
|
||||||
|
return typeof p === 'string' && /^[A-Za-z0-9 _.\-:/\\~]+$/.test(p);
|
||||||
|
}
|
||||||
|
|
||||||
function getConfigDir() {
|
function getConfigDir() {
|
||||||
if (process.env.XDG_CONFIG_HOME) {
|
if (process.env.XDG_CONFIG_HOME) {
|
||||||
return path.join(process.env.XDG_CONFIG_HOME, 'ponytail');
|
return path.join(process.env.XDG_CONFIG_HOME, 'ponytail');
|
||||||
@@ -104,6 +113,7 @@ module.exports = {
|
|||||||
getConfigDir,
|
getConfigDir,
|
||||||
getConfigPath,
|
getConfigPath,
|
||||||
getClaudeDir,
|
getClaudeDir,
|
||||||
|
isShellSafe,
|
||||||
normalizeMode,
|
normalizeMode,
|
||||||
normalizeConfigMode,
|
normalizeConfigMode,
|
||||||
normalizePersistedMode,
|
normalizePersistedMode,
|
||||||
|
|||||||
@@ -43,13 +43,15 @@ function getFallbackInstructions(mode) {
|
|||||||
'ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure. Off only: "stop ponytail" / "normal mode".\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' +
|
'Current level: **' + mode + '**. Switch: `/ponytail lite|full|ultra`.\n\n' +
|
||||||
'## The ladder\n\n' +
|
'## The ladder\n\n' +
|
||||||
'Before any code, stop at the first rung that holds:\n' +
|
'Before any code, stop at the first rung that holds (the ladder runs after you understand the problem, not instead of it — read the code it touches and trace the real flow first):\n' +
|
||||||
'1. Does this need to be built at all? (YAGNI)\n' +
|
'1. Does this need to be built at all? (YAGNI)\n' +
|
||||||
'2. Does the standard library do this? Use it.\n' +
|
'2. Does it already exist in this codebase? Reuse what is already here, do not re-write it.\n' +
|
||||||
'3. Does a native platform feature cover it? Use it.\n' +
|
'3. Does the standard library do this? Use it.\n' +
|
||||||
'4. Does an already-installed dependency solve it? Use it.\n' +
|
'4. Does a native platform feature cover it? Use it.\n' +
|
||||||
'5. Can this be one line? Make it one line.\n' +
|
'5. Does an already-installed dependency solve it? Use it.\n' +
|
||||||
'6. Only then: write the minimum code that works.\n\n' +
|
'6. Can this be one line? Make it one line.\n' +
|
||||||
|
'7. Only then: write the minimum code that works.\n\n' +
|
||||||
|
'Bug fix = root cause, not symptom: grep every caller of the function you touch and fix the shared function once (a smaller diff than one guard per caller); patching only the path the ticket names leaves a sibling caller broken.\n\n' +
|
||||||
'## Rules\n\n' +
|
'## Rules\n\n' +
|
||||||
'No abstractions that were not requested. No avoidable dependencies. No boilerplate nobody asked for. ' +
|
'No abstractions that were not requested. No avoidable dependencies. No boilerplate nobody asked for. ' +
|
||||||
'Deletion over addition. Boring over clever. Fewest files possible. ' +
|
'Deletion over addition. Boring over clever. Fewest files possible. ' +
|
||||||
@@ -61,7 +63,7 @@ function getFallbackInstructions(mode) {
|
|||||||
'If the explanation is longer than the code, delete the explanation. ' +
|
'If the explanation is longer than the code, delete the explanation. ' +
|
||||||
'Explanation the user explicitly asked for is not debt, give it in full.\n\n' +
|
'Explanation the user explicitly asked for is not debt, give it in full.\n\n' +
|
||||||
'## When NOT to be lazy\n\n' +
|
'## When NOT to be lazy\n\n' +
|
||||||
'Never simplify away: input validation at trust boundaries, error handling that prevents data loss, ' +
|
'Never simplify away: understanding the problem (read it fully and trace the real flow before picking a rung — a small diff you do not understand is just laziness dressed up as efficiency), input validation at trust boundaries, error handling that prevents data loss, ' +
|
||||||
'security measures, accessibility basics, the calibration real hardware needs (the platform is never the spec ideal), anything the user explicitly asked to keep. ' +
|
'security measures, accessibility basics, the calibration real hardware needs (the platform is never the spec ideal), anything the user explicitly asked to keep. ' +
|
||||||
'Lazy code without its check is unfinished: 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' +
|
'Lazy code without its check is unfinished: 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' +
|
'## Boundaries\n\n' +
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail",
|
"name": "ponytail",
|
||||||
"version": "0.1.0",
|
"version": "4.8.1",
|
||||||
"description": "Lazy senior dev mode for AI agents. The best code is the code you never wrote.",
|
"description": "Lazy senior dev mode for AI agents. The best code is the code you never wrote.",
|
||||||
"keywords": ["pi-package", "pi", "skills", "ponytail"],
|
"keywords": ["pi-package", "pi", "skills", "ponytail"],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ prompt menu, or that pull context through tools. See issue #70.
|
|||||||
|
|
||||||
## What it exposes
|
## What it exposes
|
||||||
|
|
||||||
- Prompt `ponytail` — returns the ruleset as a user message. Optional `mode`
|
- Prompt `ponytail`, returns the ruleset as a user message. Optional `mode`
|
||||||
argument: `lite`, `full`, or `ultra`. Omit it to use the configured default.
|
argument: `lite`, `full`, or `ultra`. Omit it to use the configured default.
|
||||||
- Tool `ponytail_instructions` — same text, plus `structuredContent`
|
- Tool `ponytail_instructions`, same text, plus `structuredContent`
|
||||||
(`{ mode, instructions }`), for hosts that pull context via tools or code
|
(`{ mode, instructions }`), for hosts that pull context via tools or code
|
||||||
execution. Read-only.
|
execution. Read-only.
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail-mcp",
|
"name": "ponytail-mcp",
|
||||||
"version": "0.1.0",
|
"version": "4.8.1",
|
||||||
"description": "MCP server that serves Ponytail's lazy-senior-dev instructions as a prompt and a tool.",
|
"description": "MCP server that serves Ponytail's lazy-senior-dev instructions as a prompt and a tool.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": { "test": "node --test ./test/*.test.js" },
|
"scripts": { "test": "node --test ./test/*.test.js" },
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.19.0",
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
"zod": "^3.23.0"
|
"zod": "^3.23.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Version-consistency guard. Ponytail declares its version in six files across
|
||||||
|
// four host ecosystems, and every release bumps all of them by hand.
|
||||||
|
//
|
||||||
|
// tests/gemini-extension.test.js already checks the four plugin manifests agree
|
||||||
|
// with each other, but that can't catch the failure mode that shipped in v4.8.0:
|
||||||
|
// every manifest stayed stale at 4.7.0 *together* while the release moved on, so
|
||||||
|
// they "agreed" and the test passed (#260, #262). It also ignores the two
|
||||||
|
// package.json files. This check closes both gaps:
|
||||||
|
// 1. every version-bearing file must share one pinned X.Y.Z version, and
|
||||||
|
// 2. on a release-tag CI run, that shared version must equal the tag.
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '..');
|
||||||
|
const PINNED_SEMVER = /^\d+\.\d+\.\d+$/;
|
||||||
|
|
||||||
|
// Every file that declares the project version, and who reads it. Add new host
|
||||||
|
// manifests here so a future ecosystem can't drift unnoticed.
|
||||||
|
const VERSION_FILES = [
|
||||||
|
'.claude-plugin/plugin.json', // Claude Code plugin — what users install
|
||||||
|
'.codex-plugin/plugin.json', // Codex plugin
|
||||||
|
'.github/plugin/plugin.json', // Copilot plugin
|
||||||
|
'gemini-extension.json', // Gemini CLI extension
|
||||||
|
'package.json', // pi-package / repo root
|
||||||
|
'ponytail-mcp/package.json', // MCP server (private, internal-only)
|
||||||
|
];
|
||||||
|
|
||||||
|
function readVersion(relPath) {
|
||||||
|
try {
|
||||||
|
// Strip a UTF-8 BOM some Windows editors prepend (breaks JSON.parse).
|
||||||
|
const raw = fs.readFileSync(path.join(root, relPath), 'utf8').replace(/^\uFEFF/, '');
|
||||||
|
return JSON.parse(raw).version;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`${relPath}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let failed = false;
|
||||||
|
const versions = VERSION_FILES.map((relPath) => {
|
||||||
|
const version = readVersion(relPath);
|
||||||
|
if (typeof version !== 'string' || !PINNED_SEMVER.test(version)) {
|
||||||
|
console.error(`${relPath}: version must be a pinned X.Y.Z semver, got ${JSON.stringify(version)}`);
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
return [relPath, version];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Every file must declare the same version.
|
||||||
|
const distinct = [...new Set(versions.map(([, v]) => v))];
|
||||||
|
if (distinct.length > 1) {
|
||||||
|
console.error('Version mismatch — every manifest must share one version:');
|
||||||
|
for (const [relPath, version] of versions) console.error(` ${version}\t${relPath}`);
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
const shared = distinct.length === 1 ? distinct[0] : null;
|
||||||
|
|
||||||
|
// On a release-tag push CI sets GITHUB_REF_TYPE=tag and GITHUB_REF_NAME=vX.Y.Z.
|
||||||
|
// The shared version must equal the tag — this catches tagging a release whose
|
||||||
|
// version files were never bumped, which mutual agreement alone cannot.
|
||||||
|
if (shared && process.env.GITHUB_REF_TYPE === 'tag') {
|
||||||
|
const tag = process.env.GITHUB_REF_NAME || '';
|
||||||
|
const tagVersion = tag.replace(/^v/, '');
|
||||||
|
if (PINNED_SEMVER.test(tagVersion) && tagVersion !== shared) {
|
||||||
|
console.error(`release tag ${tag} does not match version ${shared}; bump the version files before tagging`);
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
console.error('Align the version fields (see issue #260) so every manifest shares one version.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`All ${VERSION_FILES.length} version files pinned at ${shared}.`);
|
||||||
@@ -36,6 +36,6 @@ End with `net: -<N> lines, -<M> deps possible.` Nothing to cut: `Lean already. S
|
|||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
Scope: over-engineering and complexity only. Correctness bugs, security holes,
|
Scope: over-engineering and complexity only. Correctness bugs, security holes,
|
||||||
and performance are explicitly out of scope — route them to a normal review
|
and performance are explicitly out of scope. Route them to a normal review
|
||||||
pass. Lists findings, applies nothing. One-shot.
|
pass. Lists findings, applies nothing. One-shot.
|
||||||
"stop ponytail-audit" or "normal mode" to revert.
|
"stop ponytail-audit" or "normal mode" to revert.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ the convention out of the ledger.
|
|||||||
|
|
||||||
One row per marker, grouped by file:
|
One row per marker, grouped by file:
|
||||||
|
|
||||||
`<file>:<line> — <what was simplified>. ceiling: <the limit named>. upgrade: <the trigger to revisit>.`
|
`<file>:<line>, <what was simplified>. ceiling: <the limit named>. upgrade: <the trigger to revisit>.`
|
||||||
|
|
||||||
The convention is `ponytail: <ceiling>, <upgrade path>`, so pull the ceiling
|
The convention is `ponytail: <ceiling>, <upgrade path>`, so pull the ceiling
|
||||||
and the trigger straight from the comment. Want an owner per row too? add
|
and the trigger straight from the comment. Want an owner per row too? add
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ If there is nothing to cut, say `Lean already. Ship.` and stop.
|
|||||||
## Boundaries
|
## Boundaries
|
||||||
|
|
||||||
Scope: over-engineering and complexity only. Correctness bugs, security holes,
|
Scope: over-engineering and complexity only. Correctness bugs, security holes,
|
||||||
and performance are explicitly out of scope — route them to a normal review
|
and performance are explicitly out of scope. Route them to a normal review
|
||||||
pass, not this one. A single smoke test or `assert`-based
|
pass, not this one. A single smoke test or `assert`-based
|
||||||
self-check is the ponytail minimum, not bloat, never flag it for deletion.
|
self-check is the ponytail minimum, not bloat, never flag it for deletion.
|
||||||
Does not apply the fixes, only lists them.
|
Does not apply the fixes, only lists them.
|
||||||
|
|||||||
+23
-17
@@ -31,31 +31,31 @@ Switch: `/ponytail lite|full|ultra`.
|
|||||||
Stop at the first rung that holds:
|
Stop at the first rung that holds:
|
||||||
|
|
||||||
1. **Does this need to exist at all?** Speculative need = skip it, say so in one line. (YAGNI)
|
1. **Does this need to exist at all?** Speculative need = skip it, say so in one line. (YAGNI)
|
||||||
2. **Stdlib does it?** Use it.
|
2. **Already in this codebase?** A helper, util, type, or pattern that already lives here → reuse it. Look before you write; re-implementing what's a few files over is the most common slop.
|
||||||
3. **Native platform feature covers it?** `<input type="date">` over a picker lib, CSS over JS, DB constraint over app code.
|
3. **Stdlib does it?** Use it.
|
||||||
4. **Already-installed dependency solves it?** Use it. Never add a new one for what a few lines can do.
|
4. **Native platform feature covers it?** `<input type="date">` over a picker lib, CSS over JS, DB constraint over app code.
|
||||||
5. **Can it be one line?** One line.
|
5. **Already-installed dependency solves it?** Use it. Never add a new one for what a few lines can do.
|
||||||
6. **Only then:** the minimum code that works.
|
6. **Can it be one line?** One line.
|
||||||
|
7. **Only then:** the minimum code that works.
|
||||||
|
|
||||||
The ladder is a reflex, not a research project. Two rungs work → take the
|
The ladder is a reflex, not a research project — but it runs *after* you
|
||||||
higher one and move on. The first lazy solution that works is the right one.
|
understand the problem, not instead of it. Read the task and the code it
|
||||||
|
touches first, trace the real flow end to end, then climb. Two rungs work →
|
||||||
|
take the higher one and move on. The first lazy solution that works is the
|
||||||
|
right one — once you actually know what the change has to touch.
|
||||||
|
|
||||||
## Web tasks: rung 3 lookup
|
**Bug fix = root cause, not symptom.** A report names a symptom. Before you
|
||||||
|
edit, grep every caller of the function you're about to touch. The lazy fix IS
|
||||||
On web work, rung 3 is where the laziest win hides: a native element or CSS
|
the root-cause fix: one guard in the shared function is a smaller diff than a
|
||||||
behavior the agent forgot exists. If a web task turns on whether the platform
|
guard in every caller — and patching only the path the ticket names leaves
|
||||||
covers it (a date input, dialog, popover, view transition, container query),
|
every sibling caller still broken. Fix it once, where all callers route through.
|
||||||
and the `modern-web` CLI is available, look it up: `modern-web search "<task>"`,
|
|
||||||
then `modern-web retrieve <id>`. It is a lookup, not a license, the answer
|
|
||||||
still goes through the ladder. MWG suggests the cutting edge; you keep only the
|
|
||||||
rung that holds. Not installed? Skip it, the ladder runs fine without it.
|
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
- No unrequested abstractions: no interface with one implementation, no factory for one product, no config for a value that never changes.
|
- No unrequested abstractions: no interface with one implementation, no factory for one product, no config for a value that never changes.
|
||||||
- No boilerplate, no scaffolding "for later", later can scaffold for itself.
|
- No boilerplate, no scaffolding "for later", later can scaffold for itself.
|
||||||
- Deletion over addition. Boring over clever, clever is what someone decodes at 3am.
|
- Deletion over addition. Boring over clever, clever is what someone decodes at 3am.
|
||||||
- Fewest files possible. Shortest working diff wins.
|
- Fewest files possible. Shortest working diff wins — but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug.
|
||||||
- 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.
|
- 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.
|
||||||
- Two stdlib options, same size? Take the one that's correct on edge cases. Lazy means writing less code, not picking the flimsier algorithm.
|
- 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`.
|
- 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`.
|
||||||
@@ -91,6 +91,12 @@ that prevents data loss, security measures, accessibility basics, anything
|
|||||||
explicitly requested. User insists on the full version → build it, no
|
explicitly requested. User insists on the full version → build it, no
|
||||||
re-arguing.
|
re-arguing.
|
||||||
|
|
||||||
|
Never lazy about understanding the problem. The ladder shortens the
|
||||||
|
solution, never the reading. Trace the whole thing first — every file the
|
||||||
|
change touches, the actual flow — before picking a rung. Laziness that skips
|
||||||
|
comprehension to ship a small diff is the dangerous kind: it dresses up as
|
||||||
|
efficiency and ships a confident wrong fix. Read fully, then be lazy.
|
||||||
|
|
||||||
Hardware is never the ideal on paper: a real clock drifts, a real sensor
|
Hardware is never the ideal on paper: a real clock drifts, a real sensor
|
||||||
reads off, a PCA9685 runs a few percent fast. Leave the calibration knob, not
|
reads off, a PCA9685 runs a few percent fast. Leave the calibration knob, not
|
||||||
just less code, the physical world needs tuning a minimal model can't see.
|
just less code, the physical world needs tuning a minimal model can't see.
|
||||||
|
|||||||
+13
-1
@@ -8,6 +8,16 @@ const { spawnSync } = require('child_process');
|
|||||||
|
|
||||||
const root = path.join(__dirname, '..');
|
const root = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
// isShellSafe gates the statusline setup snippet (issue #200): ordinary install
|
||||||
|
// paths pass, paths carrying shell metacharacters are rejected so they never get
|
||||||
|
// embedded in a shell command.
|
||||||
|
const { isShellSafe } = require('../hooks/ponytail-config');
|
||||||
|
assert.equal(isShellSafe('C:\\Users\\x\\.claude\\plugins\\ponytail\\hooks\\ponytail-statusline.ps1'), true);
|
||||||
|
assert.equal(isShellSafe('/home/u/.claude/plugins/ponytail/hooks/ponytail-statusline.sh'), true);
|
||||||
|
assert.equal(isShellSafe('/tmp/a"&calc.exe&"/x.sh'), false);
|
||||||
|
assert.equal(isShellSafe('/tmp/$(calc)/x.sh'), false);
|
||||||
|
assert.equal(isShellSafe('/tmp/a;rm -rf/x.sh'), false);
|
||||||
|
|
||||||
function run(script, env, input = '') {
|
function run(script, env, input = '') {
|
||||||
return spawnSync(process.execPath, [path.join(root, 'hooks', script)], {
|
return spawnSync(process.execPath, [path.join(root, 'hooks', script)], {
|
||||||
env: { ...process.env, ...env },
|
env: { ...process.env, ...env },
|
||||||
@@ -21,6 +31,9 @@ function run(script, env, input = '') {
|
|||||||
delete process.env.CLAUDE_CONFIG_DIR;
|
delete process.env.CLAUDE_CONFIG_DIR;
|
||||||
|
|
||||||
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ponytail-hooks-'));
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ponytail-hooks-'));
|
||||||
|
// Runs on normal exit and on assertion-throw exit; force makes it idempotent.
|
||||||
|
process.on('exit', () => fs.rmSync(temp, { recursive: true, force: true }));
|
||||||
|
|
||||||
const home = path.join(temp, 'home');
|
const home = path.join(temp, 'home');
|
||||||
const pluginData = path.join(temp, 'plugin-data');
|
const pluginData = path.join(temp, 'plugin-data');
|
||||||
fs.mkdirSync(home, { recursive: true });
|
fs.mkdirSync(home, { recursive: true });
|
||||||
@@ -155,5 +168,4 @@ assert.equal(
|
|||||||
output = JSON.parse(result.stdout);
|
output = JSON.parse(result.stdout);
|
||||||
assert.deepEqual(output, {});
|
assert.deepEqual(output, {});
|
||||||
|
|
||||||
fs.rmSync(temp, { recursive: true, force: true });
|
|
||||||
console.log('hook compatibility checks passed');
|
console.log('hook compatibility checks passed');
|
||||||
|
|||||||
Reference in New Issue
Block a user