CodeVetter · Repo Unpack · Behavior traces
DietrichGebert/ponytail
Three traced flows: (1) session activation — host fires SessionStart → ponytail-activate.js resolves mode → writes flag → emits filtered ruleset; (2) mid-session mode switch — UserPromptSubmit → ponytail-mode-tracker.js parses stdin → updates flag/config → confirms; (3) subagent spawn — SubagentStart → ponytail-subagent.js reads flag → optionally filters on agent_type → injects. Also traced: OpenCode turn injection and the Cursor hooks install/uninstall path.
- Startup flow (Claude Code): claude-codex-hooks.json SessionStart spawns `node hooks/ponytail-activate.js` → getDefaultMode() resolves env→config→'full' → 'off' exits early after clearMode() → setMode(mode) writes $CLAUDE_CONFIG_DIR/.ponytail-active → getPonytailInstructions(mode) builds the ruleset → statusline check appends a one-time setup nudge → writeHookOutput('SessionStart', mode, output).
- Cursor startup differs: ponytail-activate.js first checks cursorRulePath() for .cursor/rules/ponytail.mdc in CURSOR_PROJECT_DIR; if the always-on rule exists it emits only a cursorRuleNotice (no flag write, no ruleset) so hook and rule can't contradict.
- Mode-switch flow: UserPromptSubmit → ponytail-mode-tracker.js accumulates stdin → finish() JSON-parses (BOM-stripped) → /^[/@$]ponytail/ match → 'default' arg calls writeDefaultMode and returns early; a level arg calls setMode + emits 'PONYTAIL MODE CHANGED'; 'off' calls clearMode + 'PONYTAIL MODE OFF'; bare /ponytail is report-only.
- Deactivation flow: isDeactivationCommand requires the whole trimmed message to be 'stop ponytail' or 'normal mode' (after dropping trailing punctuation), so an incidental phrase like 'add a normal mode toggle' cannot turn ponytail off — regression-tested in tests/hooks.test.js.
- Subagent flow: SubagentStart → ponytail-subagent.js → readMode() returns flag contents; absent/'off' exits 0 silently → optional PONYTAIL_SUBAGENT_MATCHER compiled in try/catch (invalid → inject-everywhere) → if matcher set, stdin agent_type tested; definite mismatch exits, everything else → inject() → writeHookOutput('SubagentStart', mode, ruleset).
- Never-hang contract: both stdin-reading hooks attach stdin 'error' → finish()+exit and a 1000ms unref'd setTimeout fallback, because Windows PowerShell `if {}` wrappers can swallow piped JSON so 'end' never fires (#443); the no-matcher subagent path never touches stdin at all.
- OpenCode turn flow: plugin config() hook scans .opencode/command/*.md via parseCommandFile into config.command and appends skills/ to config.skills.paths → per turn, experimental.chat.system.transform reads ~/.config/opencode/.ponytail-active (falling back to getDefaultMode) and appends getPonytailInstructions(mode) to the system prompt → command.execute.before writes the new mode when the user runs /ponytail.
- pi session flow: session_start → getDefaultMode + getHideStatus + resolveSessionMode(entries) → syncStatus paints '🐴 ponytail: <icon> <MODE>' → before_agent_start returns systemPrompt with ruleset appended (guard against null event/systemPrompt, #439/#440) → agent_start/agent_end toggle the ●/○ active dot.
- Cursor install flow: scripts/cursor-hooks.js install → readConfig (malformed JSON → SyntaxError → refuse) → stripPonytail removes stale ponytail entries → ponytailEntries() renders cursor-hooks.json with PONYTAIL_DIR→forward-slash root after isShellSafe() rejects metachar paths → merged entries appended per event → writeConfig; uninstall reverses and deletes the file if nothing else remains.
- Cleanup flow: scripts/uninstall.js removes .ponytail-active from the Claude and Cursor dirs, the config file, strips ponytail entries from ~/.cursor/hooks.json via cursorHooks.uninstall('user'), and surgically removes only the ponytail segment from a possibly-combined statusLine command in settings.json.
- Agentic benchmark flow: run.py run_cell copies the fixture repo (or writes seed files) → git snapshot → spawns `claude -p <prompt> --model <m> --permission-mode bypassPermissions --disallowedTools Bash --strict-mcp-config` (+ --plugin-dir for skill arms) → stdout→_claude.json file (never a PIPE, so timeouts work) → 300s CELL_TIMEOUT with _tree_kill → score_workspace computes git_diff_stats/code_stats + per-task deterministic scorers.