Files
nanobot-runtime/results/2026-06-28_ponytail-claude-pi-deepdive.md
2026-06-30 05:32:46 +02:00

18 KiB

Ponytail Plugin — Deep-dive: Claude Code a Pi Coding Agent

Zdroj: https://github.com/DietrichGebert/ponytail
Verze: 4.8.3 | Licence: MIT
Analýza vyčtená přímo ze zdrojových kódů


1. Claude Code — jak se plugin zapojuje

1.1 Manifest (.claude-plugin/plugin.json)

{
  "name": "ponytail",
  "version": "4.8.3",
  "description": "Lazy senior dev mode...",
  "author": { "name": "Dietrich Gebert", "url": "..." },
  "hooks": "./hooks/claude-codex-hooks.json"
}

Jediné klíčové pole: hooks → ukazuje na JSON soubor s definicemi lifecycle hook bindings. Claude Code při načtení pluginu přečte tento manifest a zaregistruje hooky.

1.2 Hook bindings (hooks/claude-codex-hooks.json)

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume|clear|compact",
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-activate.js\"; exit 0",
            "timeout": 5,
            "statusMessage": "Loading ponytail mode..."
          }
        ]
      }
    ],
    "SubagentStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-subagent.js\"; exit 0",
            "timeout": 5
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ponytail-mode-tracker.js\"; exit 0",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

Tři hook body, každý spouští Node.js skript:

Hook Kdy se spouští Co dělá
SessionStart Při startup, resume, clear, compact Aktivuje mód, zapíše flag file, emituje instrukce do system promptu
SubagentStart Při spuštění subagentu (Task tool) Předá ponytail kontext subagentu
UserPromptSubmit Při každém user promptu Detekuje /ponytail commandy, přepíná mód

Důležité: exit 0 na konci každého commandu — hook musí skončit úspěšně, jinak Claude považuje celý hook za failed.


1.3 SessionStart hook (ponytail-activate.js) — 91 řádků

Flow:

  1. Načte default mód z ponytail-config.js (env var > config file > 'full')
  2. Pokud off: vymaže flag file, vypíše 'OK' (nebo prázdný string pro Codex), exit
  3. Zapíše flag file ~/.claude/.ponytail-active s aktuálním módem
  4. Načte instrukce z SKILL.md přes ponytail-instructions.js, filtruje podle módu
  5. Detekuje statusline — kontroluje ~/.claude/settings.json zda existuje statusLine config; pokud ne, připojí do outputu nudge pro uživatele
  6. Vypíše instrukce na stdout — Claude Code stdout zachytí a připojí k system promptu

Klíčový kód (zkráceně):

const mode = getDefaultMode();  // 'full' default

if (mode === 'off') {
  clearMode();
  writeHookOutput('SessionStart', 'off', isCodex ? '' : 'OK');
  process.exit(0);
}

setMode(mode);  // zapíše ~/.claude/.ponytail-active
let output = getPonytailInstructions(mode);  // načte SKILL.md, filtruje

// Detekce chybějícího statusline
if (!hasStatusline) {
  output += "\n\nSTATUSLINE SETUP NEEDED: ...";
}

writeHookOutput('SessionStart', mode, output);  // → stdout → Claude system prompt

Stdout capture: Claude spustí skript jako shell command, přečte stdout a připojí obsah k system promptu jako hidden context. Uživatel to nevidí, ale LLM to vidí jako instrukce.


1.4 SubagentStart hook (ponytail-subagent.js) — 22 řádků

Proč existuje: SessionStart context je parent-thread only a nikdy nedosáhne subagentů (issue #252 v ponytail repo). Bez tohoto hooku by každý Task-spawned agent běžel bez ponytail pravidel.

Flow:

  1. Přečte mód z flag file (readMode())
  2. Pokud off nebo chybí flag — exit, nic neinjectuje
  3. Načte instrukce pro daný mód
  4. Vypíše JSON s hookSpecificOutput — Claude SubagentStart vyžaduje tento formát
const mode = readMode();
if (!mode || mode === 'off') process.exit(0);

writeHookOutput('SubagentStart', mode, getPonytailInstructions(mode));

Rozdíl oproti SessionStart: SubagentStart musí vracet JSON:

{"hookSpecificOutput": {"hookEventName": "SubagentStart", "additionalContext": "...instrukce..."}}

Pokud by vrátil raw text (jako SessionStart), Claude by kontext zahodil.


1.5 UserPromptSubmit hook (ponytail-mode-tracker.js) — 55 řádků

Flow:

  1. Čte stdin jako JSON ({"prompt": "..."}) — Claude předává user prompt hookům přes stdin
  2. Parsuje prompt, hledá /ponytail [lite|full|ultra|off] nebo /ponytail-review
  3. Při matchi zapíše/maže flag file a emituje potvrzení
  4. Detekuje deaktivační commandy: "stop ponytail" nebo "normal mode" (celá zpráva, ne substring)
process.stdin.on('end', () => {
  const data = JSON.parse(input.replace(/^\uFEFF/, ''));  // strip BOM
  const prompt = (data.prompt || '').trim().toLowerCase();

  if (/^[/@$]ponytail/.test(prompt)) {
    const parts = prompt.split(/\s+/);
    const cmd = parts[0].replace(/^[@$]/, '/');
    const arg = parts[1] || '';

    if (cmd === '/ponytail-review') mode = 'review';
    else if (cmd === '/ponytail') {
      if (arg === 'lite') mode = 'lite';
      else if (arg === 'full') mode = 'full';
      else if (arg === 'ultra') mode = 'ultra';
      else if (arg === 'off') mode = 'off';
      else mode = getDefaultMode();
    }

    if (mode && mode !== 'off') {
      setMode(mode);
      writeHookOutput('UserPromptSubmit', mode, 'PONYTAIL MODE CHANGED — level: ' + mode);
    } else if (mode === 'off') {
      clearMode();
      writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF');
    }
  }
});

Důležité: UserPromptSubmit hook běží před zpracováním promptu LLM. Pokud detekuje /ponytail command, zapíše flag file a emituje potvrzení — ale samotný /ponytail text pak jde do LLM jako normální prompt. LLM vidí potvrzení změny módu a reaguje na něj.


1.6 Runtime (ponytail-runtime.js) — 68 řádků

Flag file pattern:

  • Cesta: ~/.claude/.ponytail-active (nebo $CLAUDE_CONFIG_DIR/.ponytail-active)
  • Obsah: prostý text — 'lite', 'full', 'ultra', 'review', nebo chybí = off

Output formáty podle runtime:

const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA);
const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA);

function writeHookOutput(event, mode, context) {
  if (isCopilot) {
    // Copilot: JSON s additionalContext na SessionStart
    process.stdout.write(JSON.stringify(
      event === 'SessionStart' && context ? { additionalContext: context } : {}));
    return;
  }
  if (isCodex) {
    // Codex: JSON se systemMessage + hookSpecificOutput
    const output = { systemMessage: `PONYTAIL:${mode.toUpperCase()}` };
    if (context) output.hookSpecificOutput = { hookEventName: event, additionalContext: context };
    process.stdout.write(JSON.stringify(output));
    return;
  }
  // Native Claude:
  if (event === 'SubagentStart') {
    // SubagentStart vyžaduje JSON formát
    process.stdout.write(JSON.stringify(
      { hookSpecificOutput: { hookEventName: event, additionalContext: context } }));
    return;
  }
  // SessionStart / UserPromptSubmit: raw stdout
  process.stdout.write(context);
}

Detekce runtime:

  • PLUGIN_DATA env var = Codex CLI
  • COPILOT_PLUGIN_DATA env var = GitHub Copilot
  • Nic z toho = native Claude Code

1.7 Instrukce builder (ponytail-instructions.js) — 94 řádků

Jedna pravda pro všechny platformy:

  1. Čte skills/ponytail/SKILL.md (relativní cesta ../skills/ponytail/SKILL.md)
  2. Odstraní YAML frontmatter (---...---)
  3. Filtruje podle módu:
    • Tabulky: řádky začínající | **mode** | se ponechají jen pro aktivní mód
    • Worked examples: řádky - mode: ... se ponechají jen pro aktivní mód
    • Ostatní řádky (obecná pravidla) se ponechají vždy
  4. Pro review mód vrací jiný text: 'PONYTAIL MODE ACTIVE — level: review. Behavior defined by /ponytail-review skill.'
  5. Fallback: pokud SKILL.md neexistuje, generuje inline instrukce (getFallbackInstructions())
function getPonytailInstructions(mode) {
  const configuredMode = normalizePersistedMode(mode) || DEFAULT_MODE;

  if (INDEPENDENT_MODES.has(configuredMode)) {  // 'review'
    return 'PONYTAIL MODE ACTIVE — level: ' + configuredMode + '. Behavior defined by /ponytail-' + configuredMode + ' skill.';
  }

  try {
    return 'PONYTAIL MODE ACTIVE — level: ' + effectiveMode + '\n\n' +
      filterSkillBodyForMode(fs.readFileSync(SKILL_PATH, 'utf8'), effectiveMode);
  } catch (e) {
    return getFallbackInstructions(effectiveMode);  // fallback když SKILL.md chybí
  }
}

1.8 Konfigurace (ponytail-config.js) — 122 řádků

Resolution order default módu:

  1. PONYTAIL_DEFAULT_MODE env var
  2. $XDG_CONFIG_HOME/ponytail/config.json nebo ~/.config/ponytail/config.json
  3. 'full'

Validní módy: off, lite, full, ultra, review

Deaktivace: isDeactivationCommand(text) matchuje celou zprávu "stop ponytail" nebo "normal mode" (ignoruje case a trailing punctuation). Důvod: dřívější verze matchovaly substring, což deaktivovalo mód u běžných requestů jako "add a normal mode toggle".


2. Pi Coding Agent — jak se plugin zapojuje

2.1 Plugin entry point (pi-extension/index.js) — 189 řádků, ESM

import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
// Import sdílených modulů z hooks/
const { getDefaultMode, normalizePersistedMode, ... } = require("../hooks/ponytail-config.js");
const { getPonytailInstructions, filterSkillBodyForMode } = require("../hooks/ponytail-instructions.js");

export default function ponytailExtension(pi) {
  let currentMode = DEFAULT_MODE;
  let configuredDefaultMode = getDefaultMode();
  let isActive = false;
  let lastCtx = null;
  // ...
}

Pi používá stejné sdílené moduly jako Claude (ponytail-config.js, ponytail-instructions.js) — jedna pravda pro obě platformy.


2.2 Command registrace

Pi má explicitní API pro commandy — nikoli hooky jako Claude:

pi.registerCommand("ponytail", {
  description: "Set or report Ponytail mode",
  handler: async (args, ctx) => {
    const parsed = parsePonytailCommand(args, configuredDefaultMode);

    if (parsed.type === "status") {
      ctx?.ui?.notify?.(`Ponytail: current ${currentMode} • default ${configuredDefaultMode}`, "info");
      return;
    }
    if (parsed.type === "set-default") {
      writeDefaultMode(parsed.mode);  // zapíše do ~/.config/ponytail/config.json
      configuredDefaultMode = getDefaultMode();
      return;
    }
    if (parsed.type === "set-mode") {
      setMode(parsed.mode, ctx);  // změní currentMode, zapíše do session entries
      return;
    }
  }
});

Podporované argumenty:

  • /ponytail → toggle (off → full, jinak default)
  • /ponytail lite|full|ultra|off → set mód
  • /ponytail status → zobrazí current + default
  • /ponytail default <mode> → nastaví default mód do config file

Skill aliasy:

pi.registerCommand("ponytail-review", {
  handler: (_args, ctx) => sendAlias("/skill:ponytail-review", "", ctx)
});
// + ponytail-audit, ponytail-gain, ponytail-debt, ponytail-help

sendAlias() posílá zprávu jako user message — buď okamžitě (pokud agent idle) nebo jako follow-up (pokud agent běží).


2.3 Event hooks (pi.on())

Pi používá event-driven API místo Claude lifecycle hooks:

Event Kdy se spouští Co dělá
input Při každém user inputu Detekuje "stop ponytail" / "normal mode", deaktivuje
session_start Při startu session Načte mód z session historie nebo defaultu, sync status bar
agent_start Když agent začne pracovat isActive = true, sync status bar
agent_end Když agent skončí isActive = false, sync status bar
before_agent_start Před LLM voláním Injectuje instrukce do systemPrompt

Klíčový hook — before_agent_start:

pi.on("before_agent_start", async (event) => {
  if (!currentMode || currentMode === "off") return;
  return { systemPrompt: `${event.systemPrompt}\n\n${getPonytailInstructions(currentMode)}` };
});

Rozdíl oproti Claude: Pi explicitně modifikuje systemPrompt a vrací ho jako return value. Claude zachytává stdout a připojuje ho interně.


2.4 Persistencia módu v Pi

Pi nepoužívá flag file jako Claude. Místo toho:

  1. Session entries: pi.appendEntry("ponytail-mode", { mode: normalized }) — zapisuje mód do session logu
  2. Session restore: resolveSessionMode(entries, fallback) — při session_start čte historii entries typu "ponytail-mode" od konce a najde poslední uložený mód
  3. Default mód: getDefaultMode() ze sdíleného configu (env var > ~/.config/ponytail/config.json > 'full')
export function resolveSessionMode(entries, fallbackMode = DEFAULT_MODE) {
  if (!Array.isArray(entries)) return fallback;
  for (let i = entries.length - 1; i >= 0; i -= 1) {
    const entry = entries[i];
    if (entry?.type !== "custom" || entry?.customType !== "ponytail-mode") continue;
    const mode = normalizePersistedMode(entry?.data?.mode);
    if (mode) return mode;
  }
  return fallback;
}

Proč ne flag file? Pi agent má vlastní session management (sessionManager.getBranch(), sessionManager.getEntries()). Plugin využívá nativní Pi API místo externího souboru.


2.5 Status bar

function syncStatus(ctx) {
  if (!c?.ui?.setStatus || !c.ui.theme?.fg) return;
  const levelIcons = { lite: "🌿", full: "⚡", ultra: "🔥" };
  const icon = levelIcons[currentMode] || "";
  const label = currentMode.toUpperCase();
  const indicator = isActive ? theme.fg("accent", "●") : theme.fg("dim", "○");
  c.ui.setStatus("ponytail", indicator + " 🐴 " + theme.fg("muted", "ponytail: ") + theme.fg("text", icon + " " + label));
}

Pi má nativní status bar API (ui.setStatus). Claude nemá — proto potřebuje externí bash skript (ponytail-statusline.sh) který se musí ručně nakonfigurovat v settings.json.


3. Srovnání: Claude Code vs Pi

Aspekt Claude Code Pi Coding Agent
Plugin API Manifest JSON + lifecycle hooks pi.registerCommand() + pi.on() events
Prompt injection Stdout capture — skript píše na stdout, Claude připojí k system promptu before_agent_start event — explicitní modifikace systemPrompt
Subagent support SubagentStart hook — explicitní předání kontextu Závisí na Pi interním chování (není explicitní SubagentStart)
Command parsing UserPromptSubmit hook parsuje stdin JSON, detekuje /ponytail pi.registerCommand("ponytail", ...) — nativní command API
Persistencia módu Flag file ~/.claude/.ponytail-active Session entries (pi.appendEntry) + config file
Status indikace Externí bash skript (ponytail-statusline.sh) konfigurovaný v settings.json Nativní ui.setStatus() API
Deaktivace "stop ponytail" / "normal mode" via UserPromptSubmit hook pi.on("input", ...) detekuje stejné fráze
Output formát Raw stdout (SessionStart) nebo JSON (SubagentStart) Return value { systemPrompt: "..." }
Runtime detekce Env vars (PLUGIN_DATA, COPILOT_PLUGIN_DATA) Není potřeba — Pi má vlastní API
Sdílené kódy ponytail-config.js, ponytail-instructions.js Stejné + ponytail-config.js, ponytail-instructions.js

4. Klíčové technické detaily

4.1 Stdout vs explicitní API

Claude: Hook skripty jsou externí procesy. Claude spustí node script.js, přečte stdout a interně připojí k promptu. Skript nemá přímý přístup k Claude interním strukturám.

Pi: Plugin běží v rámci Pi procesu jako ESM modul. Má přímý přístup k pi objektu a může volat pi.on(), pi.registerCommand(), modifikovat systemPrompt.

4.2 Flag file vs session entries

Claude: Používá souborový flag (~/.claude/.ponytail-active) protože:

  • Hook skripty běží jako samostatné procesy — nemají přístup ke Claude session state
  • Flag file je jediný způsob, jak předat stav mezi SessionStart a SubagentStart hooky
  • Jednoduché, robustní, nezávislé na DB

Pi: Používá session entries (pi.appendEntry) protože:

  • Pi má nativní session management API
  • Entries se persistují v rámci session a jsou dostupné při restore
  • Lepší integrace s Pi architekturou

4.3 BOM handling

Oba systémy řeší UTF-8 BOM (Byte Order Mark) který některé editory přidávají na začátek souborů:

// Claude: v mode-tracker.js (stdin JSON)
const data = JSON.parse(input.replace(/^\uFEFF/, ''));

// Claude: v activate.js (settings.json)
const raw = fs.readFileSync(settingsPath, 'utf8').replace(/^\uFEFF/, '');

BOM breakuje JSON.parse() — proto se explicitně stripuje.

4.4 Silent fail philosophy

Celý plugin je navržený s silent fail přístupem:

try {
  setMode(mode);
} catch (e) {
  // Silent fail -- flag is best-effort, don't block the hook
}

try {
  writeHookOutput('SessionStart', mode, output);
} catch (e) {
  // Silent fail — stdout closed/EPIPE at hook exit must not surface as a hook failure
}

Důvod: hook failure by zastavila celou session. Plugin je best-effort — když selže, Claude běží normálně bez ponytail.

4.5 Shell safety

function isShellSafe(p) {
  return typeof p === 'string' && /^[A-Za-z0-9 _.\-:/\\~]+$/.test(p);
}

Před vložením cesty pluginu do shell commandu (statusline config) se kontroluje, že cesta neobsahuje shell metacharacters. Pokud ano, nudge se změní na manuální setup místo embeddovaného command snippetu.