Compare commits

..
7 Commits
Author SHA1 Message Date
DietrichGebert 1c420ad2f3 chore: untrack one-off social images committed by mistake (#46)
A `git add -A` during the v4.4.0 bump accidentally tracked the announcement
art (announce/changelog/ponytail-*.gif) in the repo root. Untrack them and
gitignore the pattern; they stay on disk for posting but out of the repo.
2026-06-15 03:03:33 +02:00
DietrichGebertandClaude Opus 4.8 e27180633f chore: bump version to 4.4.0 (#45)
Release-prep bump across the Claude Code, Codex, and Gemini manifests for
v4.4.0.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 03:01:53 +02:00
DietrichGebert 4949910587 docs: use the dark logo in the README header on dark themes (#44)
The header showed logo.png (black on transparent), which nearly vanishes in
GitHub's dark theme. Wrap it in a <picture> so dark-theme viewers get the
contoured logo-dark.png and light-theme viewers keep the original.
2026-06-15 02:59:30 +02:00
706bd2795c feat: add a dark-background logo (#43)
A dark-bg-ready variant of the mark: white face fill plus a die-cut white
contour so it reads on dark backgrounds, where logo.png (black on transparent)
and the social-preview face do not. Ships as SVG (scalable, white + black
layers) and a 1085x1241 PNG.

Contributed by @pixexid in #42.

Co-authored-by: pixexid <54691335+pixexid@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 02:54:23 +02:00
DietrichGebertandClaude Opus 4.8 e733c6b40b docs: commands reference + portability accuracy (#41)
* docs: add a Commands reference to the README

The commands were only mentioned scattered through prose, and two install
blurbs had gone stale (OpenCode omitted /ponytail-debt, Gemini omitted audit
and debt). Add one canonical Commands table (all five commands + what each
does + which hosts support them) and point the install blurbs at it so they
stop drifting as commands are added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: add pi to the portability table, de-stale the Gemini row

pi was a supported integration (pi-extension, README install, registers all
the commands) but had no row in the Supported Adapters table. The Gemini row
also enumerated an outdated command list; point it at commands/*.toml
generically so it stops drifting.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 02:44:43 +02:00
DietrichGebertandClaude Opus 4.8 d9e1480c74 feat: add ponytail-debt skill (#40)
Closes the last gap from the field review: deferral creep. /ponytail-debt
greps the repo for `ponytail:` comment markers and prints a ledger
(file:line, what was simplified, ceiling, upgrade trigger), flagging any
marker with no trigger as the rot risk. One-shot, reports only.

Full parity like ponytail-audit: skill + commands/.toml + .opencode/.md + pi
registerCommand (+ test) + agent-portability + README.

Verified: tests 32/32 (pi command list updated), rule check green, the scan
finds the repo's real markers, and a live end-to-end run produced a correct
ledger (2 markers, 1 no-trigger, prose/examples excluded).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 02:32:45 +02:00
DietrichGebertandClaude Opus 4.8 f3da910b4f feat: refine ruleset from a full-project field review (#39)
* feat: refine ruleset from a full-project field review

A reviewer ran ponytail across a 9-phase rewrite (protocol, PC app, simulator,
RPi daemon, ESP32 firmware) and flagged three gaps. All three land in SKILL.md
and propagate to AGENTS.md + the rule copies:

- Promote the one-runnable-check rule to a headline ("Lazy code without its
  check is unfinished"), enforced as a check-rule-copies invariant.
- Hardware carve-out in "When NOT to be lazy": a real device is never the spec
  ideal (clock drift, sensor offset), leave the calibration knob.
- Clarify the Output rule: explanation the user explicitly asked for is not
  debt, only unrequested prose is.

Fallback instructions kept in sync. Rule-copy check + tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: add a behavior gate proving the refinements actually fire

The refinements were verified as injected text, but injected != behavioral.
This adds a behavior eval that probes each refined rule on a task that should
trigger it:

- hardware    -> does the output leave a calibration knob?
- explanation -> when a write-up is explicitly requested, is it given in full?
- onecheck    -> is a runnable check left behind?

benchmarks/behavior.yaml runs the probes (baseline vs ponytail arm); the
grader benchmarks/behavior.js is proven by tests/behavior.test.js (8 cases,
RED/GREEN, no API key, runs in CI). Live-confirmed: the model under the
current ruleset passes all three gates, graded by the same grader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 02:02:50 +02:00
25 changed files with 406 additions and 28 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ponytail", "name": "ponytail",
"version": "4.3.0", "version": "4.4.0",
"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",
+1 -1
View File
@@ -21,4 +21,4 @@ Rules:
- 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, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test. 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.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ponytail", "name": "ponytail",
"version": "4.3.0", "version": "4.4.0",
"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",
+1 -1
View File
@@ -27,4 +27,4 @@ Rules:
- 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, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test. 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.
+1 -1
View File
@@ -21,4 +21,4 @@ Rules:
- 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, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test. 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.
+5
View File
@@ -9,3 +9,8 @@ node_modules/
# promptfoo eval artifacts # promptfoo eval artifacts
.promptfoo/ .promptfoo/
benchmarks/output* benchmarks/output*
# one-off social/announcement art, not repo content
announce-*.png
changelog-*.png
ponytail-*.gif
+1 -1
View File
@@ -26,4 +26,4 @@ Rules:
- 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, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test. 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.
+5
View File
@@ -0,0 +1,5 @@
---
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.
+1 -1
View File
@@ -21,4 +21,4 @@ Rules:
- 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, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test. 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.
+1 -1
View File
@@ -21,6 +21,6 @@ Rules:
- 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, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind, the smallest thing that fails if the logic breaks (an assert-based demo/self-check or one small test file; no frameworks, no fixtures). Trivial one-liners need no test. 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.
(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.)
+19 -8
View File
@@ -1,5 +1,8 @@
<p align="center"> <p align="center">
<img src="assets/logo.png" width="220" alt="Ponytail, the lazy senior dev"> <picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.png">
<img src="assets/logo.png" width="220" alt="Ponytail, the lazy senior dev">
</picture>
</p> </p>
<h1 align="center">Ponytail</h1> <h1 align="center">Ponytail</h1>
@@ -99,7 +102,7 @@ Run OpenCode from a checkout of this repo (the plugin reuses its `hooks/` and `s
{ "plugin": ["./.opencode/plugins/ponytail.mjs"] } { "plugin": ["./.opencode/plugins/ponytail.mjs"] }
``` ```
Injects the ruleset every turn at the active level; adds `/ponytail`, `/ponytail-review`, and `/ponytail-audit`. OpenCode also auto-loads this repo's `AGENTS.md`, so the rules hold even without the plugin. The plugin adds the `lite/full/ultra/off` levels. Injects the ruleset every turn at the active level; adds the `/ponytail` commands (see [Commands](#commands)). OpenCode also auto-loads this repo's `AGENTS.md`, so the rules hold even without the plugin. The plugin adds the `lite/full/ultra/off` levels.
### Gemini CLI ### Gemini CLI
@@ -107,15 +110,11 @@ Injects the ruleset every turn at the active level; adds `/ponytail`, `/ponytail
gemini extensions install https://github.com/DietrichGebert/ponytail gemini extensions install https://github.com/DietrichGebert/ponytail
``` ```
Loads the ruleset as always-on context every session and registers `/ponytail` and `/ponytail-review`; the `skills/` ship too, activated when a task needs them. Loads the ruleset as always-on context every session and registers the `/ponytail` commands; the `skills/` ship too, activated when a task needs them.
That was it. He'd be proud. He won't say it. That was it. He'd be proud. He won't say it.
Active every session. `/ponytail-review` finds what to delete in your diff, `/ponytail-audit` does the same for the whole repo. `/ponytail ultra` exists for when the codebase has wronged you personally. `/ponytail-help` explains the rest. Active every session, with a handful of commands (see [Commands](#commands)). `/ponytail ultra` exists for when the codebase has wronged you personally. Startup and mode-change text shows the current mode.
In Codex, invoke the skills as `@ponytail`, `@ponytail-review`,
`@ponytail-audit`, and `@ponytail-help`. Startup and mode-change text shows the
current mode.
Cursor, Windsurf, Cline, Copilot, Aider, Kiro: copy the matching rules file from this repo ([`.cursor/rules/`](.cursor/rules/), [`.windsurf/rules/`](.windsurf/rules/), [`.clinerules/`](.clinerules/), [`.github/copilot-instructions.md`](.github/copilot-instructions.md), [`AGENTS.md`](AGENTS.md), [`.kiro/steering/`](.kiro/steering/)). Cursor, Windsurf, Cline, Copilot, Aider, Kiro: copy the matching rules file from this repo ([`.cursor/rules/`](.cursor/rules/), [`.windsurf/rules/`](.windsurf/rules/), [`.clinerules/`](.clinerules/), [`.github/copilot-instructions.md`](.github/copilot-instructions.md), [`AGENTS.md`](AGENTS.md), [`.kiro/steering/`](.kiro/steering/)).
@@ -127,6 +126,18 @@ Antigravity and VS Code with the Codex extension: both read `AGENTS.md`, which t
Which files map to which agent: [Agent portability](docs/agent-portability.md). Which files map to which agent: [Agent portability](docs/agent-portability.md).
## Commands
| Command | What it does |
|---------|--------------|
| `/ponytail [lite \| full \| ultra \| off]` | Set the intensity, or turn it off. No argument reports the current level. |
| `/ponytail-review` | Review the current diff for over-engineering, hands back a delete-list. |
| `/ponytail-audit` | Audit the whole repo for over-engineering, not just the diff. |
| `/ponytail-debt` | Harvest the `ponytail:` shortcuts you've deferred into a ledger, so "later" doesn't become "never". |
| `/ponytail-help` | Quick reference for the commands above. |
Commands need a skill-capable host (Claude Code, Codex, OpenCode, Gemini, pi). In Codex they're skills, invoke with `@` (`@ponytail-review`). The instruction-only adapters (Cursor, Windsurf, Cline, Copilot, Kiro, Antigravity) load the always-on ruleset without the commands.
## Development ## Development
When changing the compact rule text, keep the agent copies aligned: When changing the compact rule text, keep the agent copies aligned:
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 84 KiB

+58
View File
@@ -0,0 +1,58 @@
// Behavior gate: does the ponytail ruleset actually PRODUCE its refined
// behaviors, not just carry the text? One check per probe (vars.probe), each
// targeting a rule that a field review (rcstack, phases 0-8) showed mattered:
// hardware - "hardware is never the spec ideal, leave the calibration knob"
// explanation - "explanation the user explicitly asked for is not debt"
// onecheck - "lazy code without its check is unfinished"
//
// Heuristic graders, same spirit as loc.js / correctness.js. The graders
// themselves are proven by tests/behavior.test.js (RED/GREEN, no API key).
//
// Metric: `behavior` (1 = behavior present, 0 = absent).
function codeOf(text) {
return [...String(text || '').matchAll(/```[\w-]*\n([\s\S]*?)```/g)].map((m) => m[1]).join('\n');
}
function proseOf(text) {
return String(text || '').replace(/```[\s\S]*?```/g, ' ').replace(/\s+/g, ' ').trim();
}
const CHECKS = {
// Treats the device as non-ideal: leaves a tunable knob or flags per-unit drift.
// A passing mention of "calibration" is not enough; it must be actionable.
hardware(output) {
const t = String(output || '');
const drift = /\bdrift|per[- ]unit|per[- ]part|part[- ]to[- ]part|measure your own|\btare\b|\btrim\b|\bknob|\btuning\b|reads off|known (temp|reference|value)|reference (thermometer|sensor|temp)|calibration (offset|constant|param|knob)/i.test(t);
return drift
? { pass: true, reason: 'Leaves a calibration knob / flags per-unit drift.' }
: { pass: false, reason: 'Treats the hardware as ideal; no calibration knob.' };
},
// Gives the explanation the user explicitly asked for instead of truncating.
explanation(output) {
const p = proseOf(output);
const words = p ? p.split(' ').length : 0;
const structured = /(\d+[.)]\s|[-*]\s)/.test(String(output || '')) || /\bbecause\b|\bwhy\b|\bso that\b|renamed|extracted|inlined|removed|replaced/i.test(p);
return words >= 45 && structured
? { pass: true, reason: `Gave the requested write-up (${words} words of prose).` }
: { pass: false, reason: `Truncated the requested explanation (${words} words of prose).` };
},
// Leaves ONE runnable check behind for non-trivial logic.
onecheck(output) {
const t = String(output || '');
const hasCheck = /\bassert\b|def\s+test_|if\s+__name__|unittest|pytest|console\.assert|\bexpect\(|\bdescribe\(|\bit\(/.test(t);
return hasCheck
? { pass: true, reason: 'Left a runnable check (assert/test/demo).' }
: { pass: false, reason: 'No runnable check left behind.' };
},
};
module.exports = (output, context) => {
const probe = context && context.vars && context.vars.probe;
const check = CHECKS[probe];
if (!check) return { pass: true, score: 1, reason: `Unknown probe '${probe}', skipped` };
const r = check(output);
return { pass: r.pass, score: r.pass ? 1 : 0, reason: r.reason };
};
+40
View File
@@ -0,0 +1,40 @@
# Ponytail behavior gates: does the ruleset actually produce its refined
# behaviors (not just carry the text)? Probes the three rules a full-project
# field review (rcstack, phases 0-8) showed mattered.
#
# Run: npx promptfoo@latest eval -c benchmarks/behavior.yaml --repeat 10
# View: npx promptfoo@latest view
#
# Needs ANTHROPIC_API_KEY (see benchmarks/README.md). The grader (behavior.js)
# is proven separately by tests/behavior.test.js, which needs no API key.
#
# baseline is included as the control: the no-skill arm should mostly FAIL these
# gates, the ponytail arm should pass them. That delta is the point.
description: "Ponytail behavior gates: hardware calibration, requested explanation, one runnable check."
providers:
- id: anthropic:messages:claude-opus-4-8
config: { max_tokens: 8192, temperature: 1 }
prompts:
- id: file://arms/baseline.js
label: baseline (no skill)
- id: file://arms/ponytail.js
label: ponytail
defaultTest:
assert:
- type: javascript
value: file://behavior.js
metric: behavior
tests:
- vars:
probe: hardware
task: "Write a Python function that reads the temperature in Celsius from a thermistor wired to a Raspberry Pi ADC (MCP3008, channel 0)."
- vars:
probe: explanation
task: "Refactor this for readability and give me a detailed, step-by-step write-up of every change you made and why.\n\ndef p(d):\n r = []\n for x in d:\n if x.get('a') and x['a'] > 0:\n r.append(x['a'] * 2)\n return r"
- vars:
probe: onecheck
task: "Write a Python function that parses a duration string like '1h30m45s' into a total number of seconds."
+2
View File
@@ -0,0 +1,2 @@
description = "Harvest ponytail: comments into a tracked debt ledger"
prompt = "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."
+3 -1
View File
@@ -11,7 +11,8 @@ to load in a given agent.
| Claude Code | `.claude-plugin/`, `commands/`, `hooks/` | Full plugin install with session activation, mode tracking, commands, and statusline support. | | Claude Code | `.claude-plugin/`, `commands/`, `hooks/` | Full plugin install with session activation, mode tracking, commands, and statusline support. |
| Codex | `.codex-plugin/plugin.json`, `hooks/hooks.json`, `hooks/`, `skills/` | Plugin install with the same skills plus lifecycle hooks for activation and mode tracking. | | Codex | `.codex-plugin/plugin.json`, `hooks/hooks.json`, `hooks/`, `skills/` | Plugin install with the same skills plus lifecycle hooks for activation and mode tracking. |
| OpenCode | `.opencode/plugins/ponytail.mjs`, `.opencode/command/`, `hooks/`, `skills/` | Server plugin injects the ruleset each turn via `experimental.chat.system.transform` and persists `/ponytail` switches; reuses the shared instruction builder. | | OpenCode | `.opencode/plugins/ponytail.mjs`, `.opencode/command/`, `hooks/`, `skills/` | Server plugin injects the ruleset each turn via `experimental.chat.system.transform` and persists `/ponytail` switches; reuses the shared instruction builder. |
| Gemini CLI | `gemini-extension.json`, `AGENTS.md`, `commands/`, `skills/` | Extension manifest points `contextFileName` at `AGENTS.md` for always-on rules, and reuses the existing `commands/*.toml` (`/ponytail`, `/ponytail-review`) and `skills/`, which Gemini CLI auto-discovers. | | pi | `pi-extension/`, `skills/`, `hooks/` | Package extension: injects the ruleset each turn through the shared instruction builder and registers the `/ponytail` commands. |
| Gemini CLI | `gemini-extension.json`, `AGENTS.md`, `commands/`, `skills/` | Extension manifest points `contextFileName` at `AGENTS.md` for always-on rules, and reuses the existing `commands/*.toml` and `skills/`, which Gemini CLI auto-discovers. |
| Cursor | `.cursor/rules/ponytail.mdc` | Always-on project rule. | | Cursor | `.cursor/rules/ponytail.mdc` | Always-on project rule. |
| Windsurf | `.windsurf/rules/ponytail.md` | Project rule. | | Windsurf | `.windsurf/rules/ponytail.md` | Project rule. |
| Cline | `.clinerules/ponytail.md` | Project rule. | | Cline | `.clinerules/ponytail.md` | Project rule. |
@@ -33,5 +34,6 @@ instructions, keep its copied rule text aligned with `AGENTS.md`.
- `skills/ponytail/SKILL.md`: lazy senior dev mode - `skills/ponytail/SKILL.md`: lazy senior dev mode
- `skills/ponytail-review/SKILL.md`: over-engineering review - `skills/ponytail-review/SKILL.md`: over-engineering review
- `skills/ponytail-audit/SKILL.md`: whole-repo over-engineering audit - `skills/ponytail-audit/SKILL.md`: whole-repo over-engineering audit
- `skills/ponytail-debt/SKILL.md`: harvest `ponytail:` shortcuts into a tracked ledger
- `skills/ponytail-help/SKILL.md`: quick reference - `skills/ponytail-help/SKILL.md`: quick reference
- `AGENTS.md`: compact always-on instruction set for agents without skill support - `AGENTS.md`: compact always-on instruction set for agents without skill support
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ponytail", "name": "ponytail",
"version": "4.3.0", "version": "4.4.0",
"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"
} }
+4 -3
View File
@@ -58,11 +58,12 @@ function getFallbackInstructions(mode) {
'Mark intentional simplifications with a `ponytail:` comment — a shortcut with a known ceiling names the ceiling and the upgrade path in the comment.\n\n' + 'Mark intentional simplifications with a `ponytail:` comment — a shortcut with a known ceiling names the ceiling and the upgrade path in the comment.\n\n' +
'## Output\n\n' + '## Output\n\n' +
'Code first. Then at most three short lines: what was skipped, when to add it. ' + 'Code first. Then at most three short lines: what was skipped, when to add it. ' +
'If the explanation is longer than the code, delete the explanation.\n\n' + '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' +
'## 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: input validation at trust boundaries, error handling that prevents data loss, ' +
'security measures, accessibility basics, 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. ' +
'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' +
'Ponytail governs what you build, not how you talk. "stop ponytail" or "normal mode": revert. Level persists until changed or session end.'; 'Ponytail governs what you build, not how you talk. "stop ponytail" or "normal mode": revert. Level persists until changed or session end.';
} }
+5
View File
@@ -119,6 +119,11 @@ export default function ponytailExtension(pi) {
handler: (_args, ctx) => sendAlias("/skill:ponytail-audit", "", ctx), handler: (_args, ctx) => sendAlias("/skill:ponytail-audit", "", ctx),
}); });
pi.registerCommand("ponytail-debt", {
description: "Run /skill:ponytail-debt",
handler: (_args, ctx) => sendAlias("/skill:ponytail-debt", "", ctx),
});
pi.registerCommand("ponytail-help", { pi.registerCommand("ponytail-help", {
description: "Run /skill:ponytail-help", description: "Run /skill:ponytail-help",
handler: (_args, ctx) => sendAlias("/skill:ponytail-help", "", ctx), handler: (_args, ctx) => sendAlias("/skill:ponytail-help", "", ctx),
+3 -1
View File
@@ -57,7 +57,7 @@ function withTempConfig(fn) {
test("extension registers Ponytail commands", () => { test("extension registers Ponytail commands", () => {
const { commands } = createPiHarness(); const { commands } = createPiHarness();
assert.deepEqual([...commands.keys()].sort(), ["ponytail", "ponytail-audit", "ponytail-help", "ponytail-review"]); assert.deepEqual([...commands.keys()].sort(), ["ponytail", "ponytail-audit", "ponytail-debt", "ponytail-help", "ponytail-review"]);
}); });
test("/ponytail updates session mode and injects instructions", async () => withTempConfig(async () => { test("/ponytail updates session mode and injects instructions", async () => withTempConfig(async () => {
@@ -99,11 +99,13 @@ test("skill alias commands delegate to Pi skill commands", async () => {
await commands.get("ponytail-review").handler("", ctx); await commands.get("ponytail-review").handler("", ctx);
await commands.get("ponytail-audit").handler("", ctx); await commands.get("ponytail-audit").handler("", ctx);
await commands.get("ponytail-debt").handler("", ctx);
await commands.get("ponytail-help").handler("", ctx); await commands.get("ponytail-help").handler("", ctx);
assert.deepEqual(sentUserMessages.map((entry) => entry.text), [ assert.deepEqual(sentUserMessages.map((entry) => entry.text), [
"/skill:ponytail-review", "/skill:ponytail-review",
"/skill:ponytail-audit", "/skill:ponytail-audit",
"/skill:ponytail-debt",
"/skill:ponytail-help", "/skill:ponytail-help",
]); ]);
}); });
+1
View File
@@ -44,6 +44,7 @@ const INVARIANTS = [
'ONE runnable check', // test reflex 'ONE runnable check', // test reflex
'flimsier algorithm', // robust-variant rule 'flimsier algorithm', // robust-variant rule
'input validation at trust boundaries', // the "not lazy about" clause 'input validation at trust boundaries', // the "not lazy about" clause
'Lazy code without its check is unfinished', // one-check promoted to headline
]; ];
const skill = read('skills/ponytail/SKILL.md'); const skill = read('skills/ponytail/SKILL.md');
+44
View File
@@ -0,0 +1,44 @@
---
name: ponytail-debt
description: >
Harvest every `ponytail:` comment in the codebase into a debt ledger, so the
deliberate shortcuts and deferrals ponytail leaves behind get tracked instead
of rotting into "later means never". Use when the user says "ponytail debt",
"/ponytail-debt", "what did ponytail defer", "list the shortcuts", "ponytail
ledger", or "what did we mark to do later". One-shot report, changes nothing.
---
Every deliberate ponytail shortcut is marked with a `ponytail:` comment naming
its ceiling and upgrade path. This collects them into one ledger so a deferral
can't quietly become permanent.
## Scan
Grep the repo for comment markers, skipping `node_modules`, `.git`, and build
output:
`grep -rnE '(#|//) ?ponytail:' .` (add other comment prefixes if your stack uses them)
Each hit is one ledger row. The comment prefix keeps prose that merely mentions
the convention out of the ledger.
## Output
One row per marker, grouped by file:
`<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
and the trigger straight from the comment. Want an owner per row too? add
`git blame -L<line>,<line>`.
Flag the rot risk: any `ponytail:` comment that names no upgrade path or
trigger gets a `no-trigger` tag, those are the ones that silently rot.
End with `<N> markers, <M> with no trigger.` Nothing found: `No ponytail: debt. Clean ledger.`
## Boundaries
Reads and reports only, changes nothing. To persist it, ask and it writes the
ledger to a file (e.g. `PONYTAIL-DEBT.md`). One-shot. "stop ponytail-debt" or
"normal mode" to revert.
+13 -6
View File
@@ -54,7 +54,9 @@ higher one and move on. The first lazy solution that works is the right one.
Code first. Then at most three short lines: what was skipped, when to add it. Code first. Then at most three short lines: what was skipped, when to add it.
No essays, no feature tours, no design notes. If the explanation is longer No essays, no feature tours, no design notes. If the explanation is longer
than the code, delete the explanation, every paragraph defending a than the code, delete the explanation, every paragraph defending a
simplification is complexity smuggled back in as prose. simplification is complexity smuggled back in as prose. Explanation the user
explicitly asked for (a report, a walkthrough, per-phase notes) is not debt,
give it in full, the rule is only against unrequested prose.
Pattern: `[code] → skipped: [X], add when [Y].` Pattern: `[code] → skipped: [X], add when [Y].`
@@ -78,11 +80,16 @@ 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.
Non-trivial logic (a branch, a loop, a parser, a money/security path) leaves Hardware is never the ideal on paper: a real clock drifts, a real sensor
ONE runnable check behind, the smallest thing that fails if the logic reads off, a PCA9685 runs a few percent fast. Leave the calibration knob, not
breaks: an `assert`-based `demo()`/`__main__` self-check or one small just less code, the physical world needs tuning a minimal model can't see.
`test_*.py`. No frameworks, no fixtures, no per-function suites unless
asked. Trivial one-liners need no test, YAGNI applies to tests too. Lazy code without its check is unfinished. Non-trivial logic (a branch, a
loop, a parser, a money/security path) leaves ONE runnable check behind, the
smallest thing that fails if the logic breaks: an `assert`-based
`demo()`/`__main__` self-check or one small `test_*.py`. No frameworks, no
fixtures, no per-function suites unless asked. Trivial one-liners need no
test, YAGNI applies to tests too.
## Boundaries ## Boundaries
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env node
// Unit test for the behavior gate (benchmarks/behavior.js). Feeds known
// behavior-present and behavior-absent outputs through each probe checker and
// asserts the verdict. Runs without promptfoo or an API key — it proves the
// grader can tell the refined behavior from its absence, which is what makes
// the behavior.yaml eval trustworthy.
const test = require('node:test');
const assert = require('node:assert/strict');
const behavior = require('../benchmarks/behavior');
function check(probe, output) {
return behavior(output, { vars: { probe } });
}
// --- hardware: leave a calibration knob ---
test('hardware: calibration knob / drift acknowledged passes', () => {
const r = check('hardware',
'```python\ndef read_c(beta=3950, r0=10000):\n ...\n```\n' +
'Notes: beta/r0 drift part-to-part, measure your own r0 at a known temp.');
assert.equal(r.pass, true);
assert.equal(r.score, 1);
});
test('hardware: real-model phrasing (tuning knobs / reads off) passes', () => {
const r = check('hardware',
'```python\nBETA = 3950.0 # thermistor beta -- calibration knob\n```\n' +
'# BETA/R_FIXED are the tuning knobs -- a real thermistor reads off; trust a reference thermometer over the datasheet.');
assert.equal(r.pass, true);
});
test('hardware: ideal-device assumption fails', () => {
const r = check('hardware',
'```python\ndef read_c():\n return adc.read(0) * 0.1\n```\n' +
'Notes: converts the raw ADC reading straight to Celsius.');
assert.equal(r.pass, false);
assert.equal(r.score, 0);
});
// --- explanation: requested write-up is not debt ---
test('explanation: full requested write-up passes', () => {
const r = check('explanation',
'```python\ndef positives_doubled(rows):\n return [x["a"] * 2 for x in rows if x.get("a", 0) > 0]\n```\n' +
'1. Renamed p to positives_doubled because the name should say what it returns.\n' +
'2. Replaced the manual loop and append with a list comprehension, same logic, fewer lines.\n' +
'3. Used x.get("a", 0) so a missing key is treated as zero instead of raising.\n' +
'4. Kept the > 0 filter; the behavior is unchanged, only the shape is clearer.');
assert.equal(r.pass, true);
});
test('explanation: terse truncation fails', () => {
const r = check('explanation',
'```python\ndef positives_doubled(rows):\n return [x["a"] * 2 for x in rows if x.get("a", 0) > 0]\n```\n' +
'skipped: the loop. comprehension covers it.');
assert.equal(r.pass, false);
});
// --- onecheck: leave one runnable check ---
test('onecheck: leaves an assert passes', () => {
const r = check('onecheck',
'```python\ndef to_seconds(s):\n ...\n\nassert to_seconds("1h30m") == 5400\n```');
assert.equal(r.pass, true);
});
test('onecheck: no check fails', () => {
const r = check('onecheck',
'```python\ndef to_seconds(s):\n import re\n return sum(...)\n```');
assert.equal(r.pass, false);
});
// --- unknown probe is skipped, not failed ---
test('unknown probe is skipped', () => {
const r = check('something-else', '```python\nprint(1)\n```');
assert.equal(r.pass, true);
assert.match(r.reason, /skipped/i);
});