456 lines
15 KiB
Markdown
456 lines
15 KiB
Markdown
# Ponytail Plugin — Technická analýza integrace
|
|
|
|
**Zdroj:** https://github.com/DietrichGebert/ponytail/tree/main
|
|
**Verze:** 4.8.3 | **Licence:** MIT
|
|
**Autor:** Dietrich Gebert
|
|
|
|
---
|
|
|
|
## 1. Co to je
|
|
|
|
Ponytail je **multi-platformní plugin pro coding agenty** (Claude Code, Codex CLI, Pi, OpenCode, Gemini CLI) který vynucuje tzv. **"lazy senior dev" mód**. Cíl: donutit LLM agenta, aby generoval **nejmenší správné řešení** — YAGNI, stdlib first, žádné nevyžádané abstrakce, žádné předčasné generalizace.
|
|
|
|
---
|
|
|
|
## 2. Architektura — jeden zdroj pravdy, více platforem
|
|
|
|
Plugin používá **sdílené jádro** (`hooks/`, `AGENTS.md`, `skills/`) a pro každou platformu jen tenkou adaptační vrstvu:
|
|
|
|
```
|
|
ponytail/
|
|
├── AGENTS.md # Hlavní ruleset (lazy senior dev principles)
|
|
├── skills/ponytail/SKILL.md # Skill definice pro agenta
|
|
├── hooks/ # Sdílené JS moduly (Node.js)
|
|
│ ├── ponytail-instructions.js # Builder instrukcí podle módu
|
|
│ ├── ponytail-config.js # Cesty, default módy
|
|
│ ├── ponytail-runtime.js # Flag file I/O, output formáty
|
|
│ ├── ponytail-activate.js # SessionStart hook
|
|
│ ├── ponytail-subagent.js # SubagentStart hook
|
|
│ ├── ponytail-mode-tracker.js # CommandExecute hook (/ponytail)
|
|
│ └── ponytail-statusline.sh # Bash statusline indikátor
|
|
├── .claude-plugin/plugin.json # Claude Code manifest
|
|
├── .codex-plugin/plugin.json # Codex CLI manifest
|
|
├── pi-extension/index.js # Pi coding agent plugin
|
|
├── .opencode/plugins/ponytail.mjs # OpenCode plugin (ESM)
|
|
└── gemini-extension.json # Gemini CLI manifest
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Claude Code integrace
|
|
|
|
### 3.1 Manifest (`.claude-plugin/plugin.json`)
|
|
|
|
```json
|
|
{
|
|
"name": "ponytail",
|
|
"version": "4.8.3",
|
|
"description": "Lazy senior dev mode...",
|
|
"skills": "./skills/",
|
|
"hooks": "./hooks/claude-codex-hooks.json",
|
|
"interface": {
|
|
"displayName": "Ponytail",
|
|
"shortDescription": "Lazy senior developer mode",
|
|
"capabilities": ["Instructions", "Lifecycle hooks"],
|
|
"defaultPrompt": [
|
|
"Use Ponytail mode for this task.",
|
|
"Review this diff for over-engineering."
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Klíčové pole:** `hooks` → ukazuje na `claude-codex-hooks.json` který definuje **lifecycle hook bindings**.
|
|
|
|
### 3.2 Lifecycle Hooks (`hooks/claude-codex-hooks.json`)
|
|
|
|
```json
|
|
{
|
|
"hooks": [
|
|
{
|
|
"event": "SessionStart",
|
|
"script": "./hooks/ponytail-activate.js",
|
|
"description": "Inject ponytail instructions at session start if active"
|
|
},
|
|
{
|
|
"event": "SubagentStart",
|
|
"script": "./hooks/ponytail-subagent.js",
|
|
"description": "Pass ponytail context to subagents"
|
|
},
|
|
{
|
|
"event": "CommandExecute",
|
|
"script": "./hooks/ponytail-mode-tracker.js",
|
|
"description": "Track /ponytail mode switches"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Tři hook body:**
|
|
|
|
| Event | Script | Co dělá |
|
|
|-------|--------|---------|
|
|
| `SessionStart` | `ponytail-activate.js` | Při startu session zkontroluje `.ponytail-active` flag; pokud je aktivní, injectuje instrukce do system promptu |
|
|
| `SubagentStart` | `ponytail-subagent.js` | Při spuštění subagentu předá ponytail kontext, aby i subagent dodržoval pravidla |
|
|
| `CommandExecute` | `ponytail-mode-tracker.js` | Zachytává `/ponytail <mode>` příkazy a persistuje mód do flag file |
|
|
|
|
### 3.3 SessionStart hook (`ponytail-activate.js`)
|
|
|
|
```javascript
|
|
const { readMode } = require('./ponytail-runtime');
|
|
const { getPonytailInstructions } = require('./ponytail-instructions');
|
|
|
|
function main() {
|
|
const mode = readMode();
|
|
if (!mode || mode === 'off') return;
|
|
const instructions = getPonytailInstructions(mode);
|
|
process.stdout.write(instructions);
|
|
}
|
|
main();
|
|
```
|
|
|
|
**Mechanismus:**
|
|
1. Při každém startu Claude Code session se spustí tento skript
|
|
2. Přečte `~/.claude/.ponytail-active` (nebo `CLAUDE_CONFIG_DIR`)
|
|
3. Pokud je mód `full` nebo `review`, vypíše instrukce na stdout
|
|
4. Claude Code tyto instrukce **připojí k system promptu**
|
|
|
|
### 3.4 SubagentStart hook (`ponytail-subagent.js`)
|
|
|
|
```javascript
|
|
const { readMode } = require('./ponytail-runtime');
|
|
const { getPonytailInstructions } = require('./ponytail-instructions');
|
|
|
|
function main() {
|
|
const mode = readMode();
|
|
if (!mode || mode === 'off') {
|
|
process.stdout.write(JSON.stringify({}));
|
|
return;
|
|
}
|
|
const instructions = getPonytailInstructions(mode);
|
|
process.stdout.write(JSON.stringify({
|
|
hookSpecificOutput: {
|
|
hookEventName: 'SubagentStart',
|
|
additionalContext: instructions
|
|
}
|
|
}));
|
|
}
|
|
main();
|
|
```
|
|
|
|
**Rozdíl oproti SessionStart:** SubagentStart musí vracet **JSON s `hookSpecificOutput`** — jinak Claude kontext zahodí.
|
|
|
|
### 3.5 CommandExecute hook (`ponytail-mode-tracker.js`)
|
|
|
|
```javascript
|
|
const { setMode, clearMode } = require('./ponytail-runtime');
|
|
const { normalizePersistedMode } = require('./ponytail-config');
|
|
|
|
function main() {
|
|
const args = process.argv.slice(2);
|
|
const raw = (args[0] || '').trim().toLowerCase();
|
|
const mode = normalizePersistedMode(raw);
|
|
|
|
if (mode === 'off') {
|
|
clearMode();
|
|
console.log('Ponytail mode deactivated.');
|
|
return;
|
|
}
|
|
if (mode) {
|
|
setMode(mode);
|
|
console.log(`Ponytail mode activated: ${mode}`);
|
|
return;
|
|
}
|
|
console.log('Usage: /ponytail [full|review|off]');
|
|
}
|
|
main();
|
|
```
|
|
|
|
**Příkazy:**
|
|
- `/ponytail full` — aktivuje plný mód (všechna pravidla)
|
|
- `/ponytail review` — aktivuje review mód (kontrola diffů)
|
|
- `/ponytail off` — deaktivuje
|
|
|
|
### 3.6 Runtime (`ponytail-runtime.js`)
|
|
|
|
```javascript
|
|
const STATE_FILE = '.ponytail-active';
|
|
const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA);
|
|
const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA);
|
|
|
|
function setMode(mode) {
|
|
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
fs.writeFileSync(statePath, mode);
|
|
}
|
|
|
|
function readMode() {
|
|
try {
|
|
return fs.readFileSync(statePath, 'utf8').trim() || null;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function writeHookOutput(event, mode, context = '') {
|
|
if (isCodex) {
|
|
// Codex: systemMessage + hookSpecificOutput JSON
|
|
process.stdout.write(JSON.stringify({
|
|
systemMessage: `PONYTAIL:${mode.toUpperCase()}`,
|
|
hookSpecificOutput: { hookEventName: event, additionalContext: context }
|
|
}));
|
|
return;
|
|
}
|
|
// Native Claude: SessionStart = raw stdout, SubagentStart = JSON
|
|
if (event === 'SubagentStart') {
|
|
process.stdout.write(JSON.stringify({
|
|
hookSpecificOutput: { hookEventName: event, additionalContext: context }
|
|
}));
|
|
return;
|
|
}
|
|
process.stdout.write(context);
|
|
}
|
|
```
|
|
|
|
**Klíčové:** Plugin detekuje **runtime prostředí** podle env vars (`PLUGIN_DATA` = Codex, `COPILOT_PLUGIN_DATA` = Copilot) a přizpůsobí output formát.
|
|
|
|
### 3.7 Instrukce (`ponytail-instructions.js`)
|
|
|
|
```javascript
|
|
function getPonytailInstructions(mode = 'full') {
|
|
const base = fs.readFileSync(path.join(__dirname, '..', 'AGENTS.md'), 'utf8');
|
|
if (mode === 'review') {
|
|
return base + '\n\n# REVIEW MODE\nWhen reviewing diffs, flag any over-engineering...';
|
|
}
|
|
return base;
|
|
}
|
|
```
|
|
|
|
- **full mód:** Celý `AGENTS.md` (YAGNI, stdlib first, nejmenší řešení)
|
|
- **review mód:** AGENTS.md + extra sekce pro review diffů
|
|
|
|
### 3.8 Statusline (`ponytail-statusline.sh`)
|
|
|
|
```bash
|
|
flag="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.ponytail-active"
|
|
[ -f "$flag" ] || exit 0
|
|
mode=$(head -n1 "$flag" | tr -d '[:space:]')
|
|
printf '\033[38;5;108m[PONYTAIL:%s]\033[0m' "$mode"
|
|
```
|
|
|
|
Bash skript pro zobrazení aktivního módu ve statusline (např. v promptu nebo tmux).
|
|
|
|
---
|
|
|
|
## 4. Codex CLI integrace
|
|
|
|
### 4.1 Manifest (`.codex-plugin/plugin.json`)
|
|
|
|
Stejný obsah jako Claude manifest, ale umístěný v `.codex-plugin/`. Codex používá **stejné hooks** (`claude-codex-hooks.json`) — jsou kompatibilní.
|
|
|
|
### 4.2 Rozdíly oproti Claude
|
|
|
|
V `ponytail-runtime.js`:
|
|
```javascript
|
|
const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA);
|
|
// Codex dostává JSON output se systemMessage + hookSpecificOutput
|
|
```
|
|
|
|
Codex očekává **strukturovaný JSON output** místo raw textu. Plugin to řeší podmíněným větvením ve `writeHookOutput()`.
|
|
|
|
---
|
|
|
|
## 5. Pi coding agent integrace
|
|
|
|
### 5.1 Plugin (`pi-extension/index.js`)
|
|
|
|
```javascript
|
|
const { getPonytailInstructions } = require('../hooks/ponytail-instructions');
|
|
const { getDefaultMode, normalizePersistedMode } = require('../hooks/ponytail-config');
|
|
|
|
const statePath = path.join(os.homedir(), '.config', 'pi', '.ponytail-active');
|
|
|
|
function readMode() {
|
|
try {
|
|
return normalizePersistedMode(fs.readFileSync(statePath, 'utf8').trim()) || getDefaultMode();
|
|
} catch (e) {
|
|
return getDefaultMode();
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
name: 'ponytail',
|
|
version: '4.8.3',
|
|
|
|
// Hook: před LLM voláním injectuj instrukce do system promptu
|
|
onPreLLM: async (context) => {
|
|
const mode = readMode();
|
|
if (mode === 'off') return context;
|
|
|
|
const instructions = getPonytailInstructions(mode);
|
|
context.systemPrompt = context.systemPrompt
|
|
? `${context.systemPrompt}\n\n${instructions}`
|
|
: instructions;
|
|
return context;
|
|
},
|
|
|
|
// Command: /ponytail <mode>
|
|
commands: {
|
|
ponytail: {
|
|
description: 'Toggle Ponytail mode (full/review/off)',
|
|
handler: async (args) => {
|
|
const mode = normalizePersistedMode(args.trim()) || getDefaultMode();
|
|
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
fs.writeFileSync(statePath, mode);
|
|
return `Ponytail mode: ${mode}`;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### 5.2 Jak Pi plugin funguje
|
|
|
|
1. **Pi agent načte plugin** z `pi-extension/index.js`
|
|
2. **Před každým LLM voláním** spustí `onPreLLM` hook
|
|
3. Hook přečte `~/.config/pi/.ponytail-active`
|
|
4. Pokud je mód aktivní, **připojí AGENTS.md instrukce k system promptu**
|
|
5. **Slash command `/ponytail`** umožňuje uživateli přepínat mód
|
|
|
|
### 5.3 Rozdíly oproti Claude Code
|
|
|
|
| Aspekt | Claude Code | Pi |
|
|
|--------|-------------|-----|
|
|
| Hook mechanism | Lifecycle hooks (SessionStart, SubagentStart, CommandExecute) | `onPreLLM` — před každým LLM call |
|
|
| Output formát | Raw stdout nebo JSON podle eventu | Modifikace `context.systemPrompt` |
|
|
| Subagent support | Explicitní SubagentStart hook | Závisí na Pi interním chování |
|
|
| Flag file | `~/.claude/.ponytail-active` | `~/.config/pi/.ponytail-active` |
|
|
| Command registration | Via CommandExecute hook | Via `commands` export |
|
|
|
|
---
|
|
|
|
## 6. OpenCode integrace
|
|
|
|
### 6.1 Plugin (`.opencode/plugins/ponytail.mjs`)
|
|
|
|
```javascript
|
|
export default async ({ client } = {}) => {
|
|
return {
|
|
// Registrace slash commands + skills directory
|
|
config: async (config) => {
|
|
// Načte command/*.md soubory
|
|
// Přidá skills dir do config.skills.paths
|
|
},
|
|
|
|
// Transform system promptu před každým turnem
|
|
'experimental.chat.system.transform': async (_input, output) => {
|
|
const mode = readMode();
|
|
if (mode === 'off') return;
|
|
output.system.push(getPonytailInstructions(mode));
|
|
},
|
|
|
|
// Před zpracováním /ponytail commandu
|
|
'command.execute.before': async (input) => {
|
|
if (input.command !== 'ponytail') return;
|
|
const mode = normalizePersistedMode((input.arguments || '').trim());
|
|
writeMode(mode);
|
|
}
|
|
};
|
|
};
|
|
```
|
|
|
|
### 6.2 OpenCode specifika
|
|
|
|
- Používá **ES modules** (`.mjs`)
|
|
- `experimental.chat.system.transform` — podobné Pi `onPreLLM`, ale s explicitním `output.system` array
|
|
- `command.execute.before` — pre-hook pro commandy
|
|
- Flag file: `~/.config/opencode/.ponytail-active`
|
|
|
|
---
|
|
|
|
## 7. Gemini CLI integrace
|
|
|
|
### 7.1 Manifest (`gemini-extension.json`)
|
|
|
|
```json
|
|
{
|
|
"name": "ponytail",
|
|
"version": "4.8.3",
|
|
"description": "Lazy senior dev mode...",
|
|
"contextFileName": "AGENTS.md"
|
|
}
|
|
```
|
|
|
|
**Nejjednodušší integrace:** Gemini CLI automaticky načte `AGENTS.md` jako context file. Žádné hooks, žádné runtime skripty — jen **statický ruleset**.
|
|
|
|
---
|
|
|
|
## 8. AGENTS.md — jádro rulesetu
|
|
|
|
```markdown
|
|
# PONYTAIL — Lazy Senior Developer Mode
|
|
|
|
## Core Principles
|
|
|
|
1. **YAGNI** — You Aren't Gonna Need It. Neimplementuj funkci, dokud není explicitně vyžádána.
|
|
2. **Stdlib First** — Použij standardní knihovnu jazyka před externími závislostmi.
|
|
3. **Native Platform Features** — Preferuj nativní API před wrappery.
|
|
4. **Smallest Correct Implementation** — Nejkratší kód který správně řeší problém.
|
|
5. **No Unrequested Abstractions** — Žádné factory patterny, žádné premature generalizace.
|
|
|
|
## Code Style
|
|
|
|
- Imperativní > funkcionální, pokud je imperativní kratší
|
|
- Inline > extrahované funkce, pokud se funkce nepoužívá vícekrát
|
|
- Hardcoded > konfigurovatelné, pokud není důvod konfigurovat
|
|
- Copy-paste > DRY, pokud je abstrakce komplikovanější než duplikace
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Skill definice (`skills/ponytail/SKILL.md`)
|
|
|
|
```markdown
|
|
# Ponytail Skill
|
|
|
|
## Description
|
|
Lazy senior developer mode for coding agents.
|
|
|
|
## Usage
|
|
- Activate: /ponytail full
|
|
- Review mode: /ponytail review
|
|
- Deactivate: /ponytail off
|
|
|
|
## Modes
|
|
- **full**: All ponytail rules active
|
|
- **review**: Focus on flagging over-engineering in diffs
|
|
- **off**: Standard agent behavior
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Shrnutí — jak se zapojuje do každého agenta
|
|
|
|
| Agent | Mechanismus | Hook body | Persistencia módu |
|
|
|-------|-------------|-----------|-------------------|
|
|
| **Claude Code** | Lifecycle hooks (SessionStart, SubagentStart, CommandExecute) | JS skripty co píšou na stdout/JSON | `~/.claude/.ponytail-active` |
|
|
| **Codex CLI** | Stejné hooks jako Claude, ale JSON output | JS skripty s `systemMessage` | `~/.codex/.ponytail-active` (via PLUGIN_DATA) |
|
|
| **Pi** | `onPreLLM` hook + `commands` export | Modifikace `context.systemPrompt` | `~/.config/pi/.ponytail-active` |
|
|
| **OpenCode** | `experimental.chat.system.transform` + `command.execute.before` | Push do `output.system` array | `~/.config/opencode/.ponytail-active` |
|
|
| **Gemini CLI** | `contextFileName` v manifestu | Žádný — statický context | N/A (vždy aktivní) |
|
|
|
|
---
|
|
|
|
## 11. Klíčové technické pozorování
|
|
|
|
1. **Flag-file pattern** — Všechny platformy kromě Gemini používají **souborový flag** (`.ponytail-active`) pro persistenci módu mezi sessiony. Je to jednoduché, robustní, nezávislé na DB.
|
|
|
|
2. **Stdout/JSON dualismus** — Claude/Codex hooks musí vracet buď raw text (SessionStart) nebo JSON (SubagentStart, Codex). Plugin to řeší runtime detekcí.
|
|
|
|
3. **Shared instruction builder** — `ponytail-instructions.js` čte `AGENTS.md` a přidává mód-specifické appendixy. Jedna pravda pro všechny platformy.
|
|
|
|
4. **Pi je nejjednodušší** — Pi plugin má nejmenší kód, protože Pi API (`onPreLLM`, `commands`) je nejvyšší úroveň abstrakce.
|
|
|
|
5. **Gemini je nejprimitivnější** — Jen statický context file, žádná dynamika, žádné přepínání módu.
|
|
|
|
6. **Subagent propagace** — Claude Code explicitně řeší předávání kontextu subagentům (SubagentStart hook). Ostatní platformy to neřeší nebo závisí na interním chování.
|