Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .openclaw/skills/ponytail-help/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ write flag files, or persist anything.

| Level | Trigger | What change |
|-------|---------|-------------|
| **Lite** | `/ponytail lite` | Build what's asked, name the lazier alternative in one line. |
| **Full** | `/ponytail` | The ladder enforced: YAGNI → stdlib → native → one line → minimum. Default. |
| **Ultra** | `/ponytail ultra` | YAGNI extremist. Deletion before addition. Challenges requirements before building. |
| **Lite** | `/ponytail lite` | Advisory: build what's asked, name the lazier alternative in one line, user picks. |
| **Full** | `/ponytail` | Enforced default: the ladder (YAGNI → stdlib → native → one line → minimum) is binding. |
| **Ultra** | `/ponytail ultra` | Deletion-first: YAGNI extremist, challenge the requirement before adding. |

Level sticks until changed or session end.

Expand Down
41 changes: 35 additions & 6 deletions .openclaw/skills/ponytail/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,45 @@ Pattern: `[code] → skipped: [X], add when [Y].`

## Intensity

| Level | What change |
|-------|------------|
| **lite** | Build what's asked, but name the lazier alternative in one line. User picks. |
| **full** | The ladder enforced. Stdlib and native first. Shortest diff, shortest explanation. Default. |
| **ultra** | YAGNI extremist. Deletion before addition. Ship the one-liner and challenge the rest of the requirement in the same breath. |
Your active level governs how hard the ladder is applied. The level text below
is not optional: it defines what the active level requires of you. Core rules
above always apply, at the strength your active level defines below.

<!-- mode: lite -->
**lite — advisory.** Build what's asked. Then name the lazier alternative in
one line, and let the user pick. Do not impose unrequested laziness: if the
user asked for the full version, build it without re-arguing. The ladder is a
suggestion you surface, not a gate you enforce.

**Lite overrides.** In lite, these core rules are advisory, not binding:
surface the higher ladder rung as a suggestion rather than enforcing it;
"Deletion over addition" applies only when the user did not ask for the full
version; "No boilerplate, no scaffolding" gets flagged, not refused; "Complex
request? Ship the lazy version and question it in the same response" names the
alternative and lets the user pick. Everything else in Core rules above still
applies unless this block overrides it.

Example: "Add a cache for these API responses."
- lite: "Done, cache added. FYI: `functools.lru_cache` covers this in one line if you'd rather not own a cache class."
<!-- /mode: lite -->

<!-- mode: full -->
**full — enforced default.** The ladder is enforced: YAGNI → stdlib → native →
one line → minimum. Stdlib and native first. Shortest diff, shortest
explanation. Question complex requests in the same response.

- full: "`@lru_cache(maxsize=1000)` on the fetch function. Skipped custom cache class, add when lru_cache measurably falls short."
<!-- /mode: full -->

<!-- mode: ultra -->
**ultra — deletion-first.** YAGNI extremist. Deletion before addition. Ship
the one-liner and challenge the rest of the requirement in the same breath.
Refuse speculative scaffolding outright: no placeholder modules, no
"for later" abstractions, no config for values that never change. If a
requirement can be served by deleting or reusing rather than writing, do that
and say so.

- ultra: "No cache until a profiler says so. When it does: `@lru_cache`. A hand-rolled TTL cache class is a bug farm with a hit rate."
<!-- /mode: ultra -->

## When NOT to be lazy

Expand Down
2 changes: 1 addition & 1 deletion .opencode/command/ponytail.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
description: Switch ponytail intensity level (lite/full/ultra/off)
---

Switch to ponytail $ARGUMENTS mode. If no level specified, use full. Lazy senior dev mode, before any code: does it need to exist at all (YAGNI)? Does the standard library do it? A native platform feature? Can it be one line? Build the minimum that works. No unrequested abstractions, no avoidable dependencies, no boilerplate. Mark deliberate simplifications that cut a real corner with a known ceiling using a ponytail: comment that names the ceiling and upgrade path.
Switch to ponytail $ARGUMENTS mode. If no level specified, use full. Levels are behavior dials: lite = advisory (build what's asked, name the lazier alternative, user picks), full = enforced default (ladder binding), ultra = deletion-first (YAGNI extremist, challenge the requirement before adding). Lazy senior dev mode, before any code: does it need to exist at all (YAGNI)? Does the standard library do it? A native platform feature? Can it be one line? Build the minimum that works. No unrequested abstractions, no avoidable dependencies, no boilerplate. Mark deliberate simplifications that cut a real corner with a known ceiling using a ponytail: comment that names the ceiling and upgrade path.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ That was it. He'd be proud. He won't say it.

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.

The three levels are real behavior dials, not tone: **lite** is advisory (build what's asked, name the lazier alternative, user picks), **full** is the enforced default (the ladder is binding), and **ultra** is deletion-first (YAGNI extremist, challenges the requirement before adding). Switching mid-session re-injects the new level's ruleset immediately on Claude Code and Codex; Copilot applies the switch at the next session start.

Set the level for every new session with the `PONYTAIL_DEFAULT_MODE` env var (`lite`/`full`/`ultra`/`off`), or a `defaultMode` field in `~/.config/ponytail/config.json` (`%APPDATA%\ponytail\config.json` on Windows). The default is `full`.

While active, the ruleset is also injected into every subagent spawned via the Agent tool. To scope that to specific agent types (say, keep it off read-only search agents), set the `PONYTAIL_SUBAGENT_MATCHER` env var to a regex tested against the subagent's `agent_type`. It is unanchored and case-insensitive: `explore|general` matches either, `^general$` is exact, and plugin agent types look like `plugin:name`. Unset means inject into every subagent (the default); an invalid regex, or a subagent whose type the platform doesn't report, also falls back to injecting.
Expand Down
74 changes: 66 additions & 8 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,35 +67,93 @@ def _strip_frontmatter(text: str) -> str:
return re.sub(r"^---[\s\S]*?---\s*", "", text or "", count=1)


# Mode block markers (KTD1): mirror the JS filter's convention exactly. A block
# whose mode is not the effective mode is dropped entirely; marker lines
# themselves are stripped from output.
_MODE_BLOCK_OPEN_RE = re.compile(r"^<!--\s*mode:\s*([a-z]+)\s*-->\s*$")
_MODE_BLOCK_CLOSE_RE = re.compile(r"^<!--\s*/mode:\s*([a-z]+)\s*-->\s*$")


def _filter_skill_body_for_mode(body: str, mode: str) -> str:
effective = _normalize_runtime_mode(mode) or DEFAULT_MODE
lines = []
out: list[str] = []
skip = False
for line in _strip_frontmatter(body).splitlines():
open_match = _MODE_BLOCK_OPEN_RE.match(line)
if open_match:
skip = (_normalize_runtime_mode(open_match.group(1)) or open_match.group(1)) != effective
continue # strip the marker line itself

close_match = _MODE_BLOCK_CLOSE_RE.match(line)
if close_match:
skip = False
continue # strip the marker line itself

if skip:
continue # drop the whole non-active block

# Preserve the original stateless line-drop as a fallback for content
# that does not use blocks (KTD2): a bold table row or quoted worked
# example whose label is a mode other than the effective one is dropped.
# The quote after the colon is load-bearing: it distinguishes a real
# per-mode worked example (`- lite: "..."`) from an ordinary rule bullet
# that merely starts with a mode word (e.g. "- Full: ..."), which must
# survive in every mode. This mirrors the JS filter's quote requirement.
table_label = re.match(r"^\|\s*\*\*(.+?)\*\*\s*\|", line)
if table_label:
label_mode = _normalize_runtime_mode(table_label.group(1))
if label_mode and label_mode != effective:
continue

example_label = re.match(r"^-\s*([^:]+):\s*", line)
example_label = re.match(r'^-\s*([^:]+):\s*"', line)
if example_label:
label_mode = _normalize_runtime_mode(example_label.group(1))
if label_mode and label_mode != effective:
continue

lines.append(line)
return "\n".join(lines)
out.append(line)
return "\n".join(out)


def _fallback_instructions(mode: str) -> str:
# One per-level enforcement line (R9): the failure path must not silently
# reproduce the 96%-identical behavior the levels fix. Mirrors the JS
# fallback's stance line.
stances = {
"lite": "advisory — build what is asked, name the lazier alternative, user picks.",
"ultra": "deletion-first — YAGNI extremist, challenge the requirement before adding.",
}
stance = stances.get(mode, "enforced — the ladder and rules below are binding.")
# Lite is advisory (R2): the fallback body must not re-impose the enforced
# ladder on the failure path — a stance that says "user picks" followed by
# binding mandates silently reproduces full-level enforcement. Full and
# ultra keep the enforced body; ultra keeps its deletion-first stance.
# Mirrors the JS fallback's mode-conditional body.
if mode == "lite":
ladder_rules = (
"Before any code, consider the lazy option first: does this need to "
"exist (YAGNI)? Does it already exist in this codebase? Does the "
"stdlib or a native platform feature cover it? Can it be one line? "
"Name the lazier alternative in one line and let the user pick. "
"Build what was asked. Avoid unrequested abstractions, avoidable "
"dependencies, and boilerplate unless the user asked for them. "
"Deletion over addition and boring over clever are advisory here, "
"not mandates — name the lazier option in the same response and let "
"the user pick."
)
else:
ladder_rules = (
"Before any code, stop at the first rung that holds: YAGNI, stdlib, "
"native platform, installed dependency, one line, then minimum code. "
"No unrequested abstractions, avoidable dependencies, boilerplate, or "
"speculative scaffolding. Deletion over addition. Boring over clever."
)
return (
f"PONYTAIL MODE ACTIVE — level: {mode}\n\n"
"You are a lazy senior developer. Lazy means efficient, not careless. "
"The best code is the code never written.\n\n"
"Before any code, stop at the first rung that holds: YAGNI, stdlib, "
"native platform, installed dependency, one line, then minimum code. "
"No unrequested abstractions, avoidable dependencies, boilerplate, or "
"speculative scaffolding. Deletion over addition. Boring over clever. "
f"Level stance: {stance}\n\n"
f"{ladder_rules} "
"Do not simplify away trust-boundary validation, data-loss handling, "
"security, accessibility, explicitly requested behavior, or one small "
"runnable check for non-trivial logic."
Expand Down
3 changes: 2 additions & 1 deletion benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ Versus baseline, ponytail writes **80-94% less code**, costs **42-75% less**, an
Run by other people, not by us, on their own harnesses and machines. Linked for
transparency: the numbers are theirs, may shift between runs, and are corroboration
rather than official figures. Only plugin-installed runs are listed, since pasting
`SKILL.md` into a prompt is a rough approximation of `full` and skews the result.
`SKILL.md` into a prompt is a rough approximation of `full` and skews the result. The bundled
arms use the mode-filtered full builder, the production injection path.

| Source | Method | Headline | Date |
|---|---|---|---|
Expand Down
4 changes: 3 additions & 1 deletion benchmarks/agentic/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
RUNS_DIR = Path(__file__).resolve().parent / "runs"

def _skill(rel): return (ROOT / rel).read_text(encoding="utf-8")

ARMS = {
"baseline": lambda: None,
"ponytail": lambda: _skill("skills/ponytail/SKILL.md"),
# ponytail activates via --plugin-dir (PLUGIN_ARMS), so this raw-prompt entry is never used.
"ponytail": lambda: None,
"caveman": lambda: _skill("benchmarks/arms/caveman-SKILL.md"),
"yagni": lambda: "Follow YAGNI principles.",
"yagni-oneliner": lambda: "Follow YAGNI principles, and prefer one-liner solutions.",
Expand Down
10 changes: 6 additions & 4 deletions benchmarks/arms/ponytail.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Ponytail arm: the repo's own SKILL.md (full) as the system prompt. Single source of truth.
const fs = require('fs');
const path = require('path');
const system = fs.readFileSync(path.join(__dirname, '..', '..', 'skills', 'ponytail', 'SKILL.md'), 'utf8');
// Ponytail arm: the repo's own full-level ruleset as the system prompt.
// Uses the mode-filtered builder (not the raw SKILL.md, which since #664
// carries the union of all three levels plus gating markers) so the arm
// measures "the enforced default" — the production injection path.
const { getPonytailInstructions } = require('../../hooks/ponytail-instructions');
const system = getPonytailInstructions('full');
module.exports = ({ vars }) => [
{ role: 'system', content: system },
{ role: 'user', content: vars.task },
Expand Down
9 changes: 8 additions & 1 deletion benchmarks/benchmark-local.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,17 @@


def load_arms():
# ponytail uses the mode-filtered full ruleset (the production injection
# path), not the raw SKILL.md, which since #664 carries the union of all
# three levels plus gating markers.
import importlib.util
spec = importlib.util.spec_from_file_location("ponytail_hermes_plugin", ROOT / "__init__.py")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return {
"baseline": None,
"caveman": (ROOT / "benchmarks/arms/caveman-SKILL.md").read_text(encoding="utf-8"),
"ponytail": (ROOT / "skills/ponytail/SKILL.md").read_text(encoding="utf-8"),
"ponytail": mod.build_injected_context("full"),
}


Expand Down
4 changes: 3 additions & 1 deletion benchmarks/claude-email.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Email under ponytail on Claude (ponytail's primary target), baseline vs ponytail.
const fs = require('fs'), path = require('path');
const { checkPy, pyBlock, TASKS } = require('./robustness-audit.js');
const skill = fs.readFileSync(path.join(__dirname, '..', 'skills', 'ponytail', 'SKILL.md'), 'utf8');
// Use the mode-filtered full ruleset (not the raw SKILL.md, which since #664
// carries the union of all three levels plus gating markers).
const skill = require('../hooks/ponytail-instructions').getPonytailInstructions('full');
const email = TASKS.find(t => t.name === 'email');
const N = Number(process.env.CE_N) || 40;
const MODELS = (process.env.CE_MODELS || 'claude-haiku-4-5-20251001,claude-sonnet-4-6,claude-opus-4-8').split(',');
Expand Down
4 changes: 3 additions & 1 deletion benchmarks/model-email.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Cross-model email rate at high n: is the parseaddr quirk gpt-5.4-mini-specific?
const fs = require('fs'), path = require('path');
const { checkPy, pyBlock, TASKS } = require('./robustness-audit.js');
const skill = fs.readFileSync(path.join(__dirname, '..', 'skills', 'ponytail', 'SKILL.md'), 'utf8');
// Use the mode-filtered full ruleset (not the raw SKILL.md, which since #664
// carries the union of all three levels plus gating markers).
const skill = require('../hooks/ponytail-instructions').getPonytailInstructions('full');
const email = TASKS.find(t => t.name === 'email');
const N = Number(process.env.ME_N) || 100;
const MODELS = (process.env.ME_MODELS || 'gpt-4.1-mini,gpt-5.4-mini').split(',');
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"configs": [
"baseline — no skill",
"caveman — caveman SKILL.md (full) as operating instructions",
"ponytail — ponytail SKILL.md (full) as operating instructions"
"ponytail — ponytail full-level ruleset (mode-filtered builder) as operating instructions"
],
"tasks": [
{ "id": "email", "prompt": "Write me a Python function that validates email addresses." },
Expand Down
5 changes: 4 additions & 1 deletion benchmarks/robustness-audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ try {
.map(l => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim()]; }));
} catch (_) { /* no .env — fine for --selftest */ }
const KEY = process.env.OPENAI_API_KEY || kv.OPENAI_API_KEY;
const SKILL = fs.readFileSync(path.join(ROOT, 'skills', 'ponytail', 'SKILL.md'), 'utf8');
// Use the mode-filtered full ruleset (not the raw SKILL.md, which since #664
// carries the union of all three levels plus gating markers) so the audit
// measures the production injection path.
const SKILL = require('../hooks/ponytail-instructions').getPonytailInstructions('full');

// task = { name, prompt, names, arity, cases: [[argsArray, expected], ...], good, bad }
const TASKS = [
Expand Down
2 changes: 1 addition & 1 deletion commands/ponytail.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
description = "Switch ponytail intensity level (lite/full/ultra/off)"
prompt = "Switch to ponytail {{args}} mode. If no level specified, use full. Lazy senior dev mode, before any code: does it need to exist at all (YAGNI)? Does the standard library do it? A native platform feature? Can it be one line? Build the minimum that works. No unrequested abstractions, no avoidable dependencies, no boilerplate. Mark deliberate simplifications that cut a real corner with a known ceiling using a ponytail: comment that names the ceiling and upgrade path."
prompt = "Switch to ponytail {{args}} mode. If no level specified, use full. Levels are behavior dials: lite = advisory (build what's asked, name the lazier alternative, user picks), full = enforced default (ladder binding), ultra = deletion-first (YAGNI extremist, challenge the requirement before adding). Lazy senior dev mode, before any code: does it need to exist at all (YAGNI)? Does the standard library do it? A native platform feature? Can it be one line? Build the minimum that works. No unrequested abstractions, no avoidable dependencies, no boilerplate. Mark deliberate simplifications that cut a real corner with a known ceiling using a ponytail: comment that names the ceiling and upgrade path."
8 changes: 8 additions & 0 deletions docs/agent-portability.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ Keep adapters thin. When a host supports skills or hooks, point it at the
existing `skills/` and `hooks/` files. When a host only supports project
instructions, keep its copied rule text aligned with `AGENTS.md`.

**Instruction-tier boundary:** hosts that load `AGENTS.md` or a compact copy
(Cursor, Windsurf, Cline, Copilot-chat, Kiro, Zed, CodeWhale, Swival, Junie,
Amp, Jules, Antigravity, VS Code + Codex extension, generic agents) have no
mode state and no `/ponytail` command, so they always receive the static
**full** ruleset — they cannot differentiate lite/ultra. Level switching is a
plugin-tier capability only (Claude Code, Codex, Copilot CLI, Qoder plugin,
pi, OpenCode, Hermes, MCP).

## Portable Behavior

- `skills/ponytail/SKILL.md`: lazy senior dev mode
Expand Down
9 changes: 8 additions & 1 deletion hooks/ponytail-activate.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@ const {
clearMode,
isCodex,
isCopilot,
readMode,
setMode,
writeHookOutput,
} = require('./ponytail-runtime');

const claudeDir = getClaudeDir();
const settingsPath = path.join(claudeDir, 'settings.json');

const mode = getDefaultMode();
// Copilot sessions can switch level mid-session (/ponytail <level>); the
// switch persists a session flag, and Copilot's writeHookOutput drops all
// non-SessionStart output, so the change only takes effect on the next
// SessionStart. Honor the persisted flag there; Claude Code and Codex keep
// starting at the configured default. 'off' never reaches the flag (it clears
// it), so readMode() can only return a real level or null.
const mode = isCopilot ? (readMode() || getDefaultMode()) : getDefaultMode();

// "off" mode — skip activation entirely, don't write flag or emit rules
if (mode === 'off') {
Expand Down
Loading