From 50494e11d306301510fb0fa7498a7b220562d00b Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 17:57:12 +0530 Subject: [PATCH 01/13] docs: ponytail L1 integration implementation plan --- .../2026-07-07-ponytail-l1-integration.md | 994 ++++++++++++++++++ 1 file changed, 994 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-07-ponytail-l1-integration.md diff --git a/docs/superpowers/plans/2026-07-07-ponytail-l1-integration.md b/docs/superpowers/plans/2026-07-07-ponytail-l1-integration.md new file mode 100644 index 00000000..2ffbb951 --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-ponytail-l1-integration.md @@ -0,0 +1,994 @@ +# Ponytail L1 Integration — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Port ponytail runtime logic (config, state, instructions, switcher, platform output) from Node.js hooks into agentflare Rust. Prompt content stays external — downloaded on demand. + +**Architecture:** New `src/ponytail/` module with 6 sub-modules + embedded fallback skill.md. One new top-level CLI command `agentflare ponytail` with subcommands for setup/status/set/default/off/update/hook. Follows existing `Commands` enum + `#[command(subcommand)]` pattern. + +**Tech Stack:** Rust, clap (derive), serde_json, dirs, ureq — all already in Cargo.toml. + +**Issue:** [#42](https://github.com/getappz/agentflare/issues/42) +**Spec:** `docs/superpowers/specs/2026-07-07-ponytail-l1-integration-design.md` +**Branch:** `feature/ponytail-l1-integration` + +## Global Constraints + +- Rust edition 2024, rust-version 1.91 +- unsafe_code = "warn", clippy all = "warn", pedantic = "warn" +- No new crate dependencies — reuse dirs, serde, serde_json, ureq +- Follow existing clap patterns: `#[command(subcommand)]` for nested commands +- Config/state/cache paths use agentflare paths (not ponytail's original paths) +- Embedded SKILL.md as `include_str!("skill.md")` — fallback only + +--- + +## File Structure + +| File | Purpose | +|------|---------| +| `src/ponytail/mod.rs` | Public API re-exports, `PonytailMode` struct | +| `src/ponytail/config.rs` | Mode resolution (env → config.json → "full"), validation, config I/O | +| `src/ponytail/state.rs` | Flag file `.ponytail-active` read/write | +| `src/ponytail/instructions.rs` | SKILL.md loading, intensity filtering, fallback generation | +| `src/ponytail/switcher.rs` | Mode switch detection in user input | +| `src/ponytail/platform.rs` | Agent platform detection, per-platform output formatting | +| `src/ponytail/skill.md` | Embedded default SKILL.md content (compiled into binary) | +| `src/main.rs` | Add `mod ponytail;`, `Ponytail` variant to `Commands`, dispatch | + +--- + +### Task 1: ponytail/config.rs + +**Files:** +- Create: `src/ponytail/config.rs` +- Create: `src/ponytail/mod.rs` + +**Interfaces:** +- Produces: + - `pub const DEFAULT_MODE: &str = "full"` + - `pub const VALID_MODES: &[&str] = &["off", "lite", "full", "ultra", "review"]` + - `pub const RUNTIME_MODES: &[&str] = &["off", "lite", "full", "ultra"]` + - `pub fn normalize_mode(mode: &str) -> Option<&'static str>` + - `pub fn normalize_config_mode(mode: &str) -> Option<&'static str>` + - `pub fn normalize_persisted_mode(mode: &str) -> Option<&'static str>` + - `pub fn is_deactivation(text: &str) -> bool` + - `pub fn default_mode() -> String` + - `pub fn set_default_mode(mode: &str) -> Result<(), String>` + - `pub fn config_dir() -> PathBuf` + - `pub fn config_path() -> PathBuf` + +- [ ] **Step 1: Create `src/ponytail/mod.rs` skeleton** + +```rust +pub mod config; +pub mod state; +pub mod instructions; +pub mod switcher; +pub mod platform; +``` + +- [ ] **Step 2: Create `src/ponytail/config.rs`** + +```rust +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +pub const DEFAULT_MODE: &str = "full"; +pub const VALID_MODES: &[&str] = &["off", "lite", "full", "ultra", "review"]; +pub const RUNTIME_MODES: &[&str] = &["off", "lite", "full", "ultra"]; + +pub fn normalize_mode(mode: &str) -> Option<&'static str> { + let m = mode.trim().to_lowercase(); + RUNTIME_MODES.iter().find(|&&v| v == m).copied() +} + +pub fn normalize_config_mode(mode: &str) -> Option<&'static str> { + let m = mode.trim().to_lowercase(); + VALID_MODES.iter().find(|&&v| v == m).copied() +} + +pub fn normalize_persisted_mode(mode: &str) -> Option<&'static str> { + normalize_mode(mode).or_else(|| normalize_config_mode(mode)) +} + +pub fn is_deactivation(text: &str) -> bool { + let t = text.trim().to_lowercase(); + let t = t.trim_end_matches(|c: char| c == '.' || c == '!' || c == '?' || c.is_whitespace()); + t == "stop ponytail" || t == "normal mode" +} + +pub fn config_dir() -> PathBuf { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("agentflare") + .join("ponytail") +} + +pub fn config_path() -> PathBuf { + config_dir().join("config.json") +} + +#[derive(Serialize, Deserialize, Default)] +struct ConfigFile { + default_mode: Option, +} + +pub fn default_mode() -> String { + if let Ok(val) = std::env::var("PONYTAIL_DEFAULT_MODE") { + if let Some(m) = normalize_config_mode(&val) { + return m.to_string(); + } + } + if let Ok(data) = std::fs::read_to_string(config_path()) { + if let Ok(cfg) = serde_json::from_str::(&data) { + if let Some(mode) = cfg.default_mode { + if let Some(m) = normalize_config_mode(&mode) { + return m.to_string(); + } + } + } + } + DEFAULT_MODE.to_string() +} + +pub fn set_default_mode(mode: &str) -> Result<(), String> { + let normalized = normalize_config_mode(mode).ok_or_else(|| format!("invalid mode: {mode}"))?; + let dir = config_dir(); + std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + let mut cfg: ConfigFile = std::fs::read_to_string(config_path()) + .ok() + .and_then(|d| serde_json::from_str(&d).ok()) + .unwrap_or_default(); + cfg.default_mode = Some(normalized.to_string()); + let json = serde_json::to_string_pretty(&cfg).map_err(|e| e.to_string())?; + std::fs::write(config_path(), json).map_err(|e| e.to_string())?; + Ok(()) +} +``` + +- [ ] **Step 3: Build check** + +```bash +cargo check +``` + +Expected: compiles. `config` type dead-code warnings OK (consumed later). + +- [ ] **Step 4: Commit** + +```bash +git add src/ponytail/ +git commit -m "feat(ponytail): add config module — mode resolution and validation" +``` + +--- + +### Task 2: ponytail/state.rs + +**Files:** +- Create: `src/ponytail/state.rs` +- Modify: `src/ponytail/mod.rs` + +**Interfaces:** +- Produces: + - `pub fn flag_path() -> PathBuf` + - `pub fn active_mode() -> Option` + - `pub fn set_active(mode: &str) -> io::Result<()>` + - `pub fn clear_active()` + +- [ ] **Step 1: Create `src/ponytail/state.rs`** + +```rust +use std::io; +use std::path::PathBuf; + +pub fn flag_path() -> PathBuf { + dirs::state_dir() + .unwrap_or_else(|| dirs::data_local_dir().unwrap_or_else(|| PathBuf::from("."))) + .join("agentflare") + .join("ponytail") + .join("active") +} + +pub fn active_mode() -> Option { + std::fs::read_to_string(flag_path()) + .ok() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) +} + +pub fn set_active(mode: &str) -> io::Result<()> { + let path = flag_path(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(path, mode) +} + +pub fn clear_active() { + let _ = std::fs::remove_file(flag_path()); +} +``` + +- [ ] **Step 2: Build check** + +```bash +cargo check +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/ponytail/state.rs +git commit -m "feat(ponytail): add state module — flag file read/write" +``` + +--- + +### Task 3: ponytail/skill.md (embedded fallback) + +**Files:** +- Create: `src/ponytail/skill.md` + +Embed the canonical SKILL.md as a fallback. Copy the content from the cloned ponytail repo at `C:\Users\shiva\workspace\refs\ponytail\skills\ponytail\SKILL.md`. + +- [ ] **Step 1: Copy skill.md** + +```bash +copy C:\Users\shiva\workspace\refs\ponytail\skills\ponytail\SKILL.md src\ponytail\skill.md +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/ponytail/skill.md +git commit -m "feat(ponytail): embed fallback SKILL.md" +``` + +--- + +### Task 4: ponytail/instructions.rs + +**Files:** +- Create: `src/ponytail/instructions.rs` + +**Interfaces:** +- Consumes: `ponytail::config::normalize_mode`, `ponytail::config::normalize_persisted_mode`, `ponytail::config::DEFAULT_MODE` +- Produces: + - `pub struct Instructions { pub mode: String, pub body: String }` + - `pub fn build(mode: &str, skill_path: Option<&std::path::Path>) -> Instructions` + - `pub fn filter_skill_body(body: &str, mode: &str) -> String` + - `pub fn fallback_instructions(mode: &str) -> String` + +- [ ] **Step 1: Create `src/ponytail/instructions.rs`** + +```rust +use crate::ponytail::config; +use std::path::Path; + +static EMBEDDED_SKILL: &str = include_str!("skill.md"); + +pub struct Instructions { + pub mode: String, + pub body: String, +} + +pub fn build(mode: &str, skill_path: Option<&Path>) -> Instructions { + let effective = config::normalize_persisted_mode(mode) + .unwrap_or(config::DEFAULT_MODE); + + let skill_body = if let Some(path) = skill_path { + std::fs::read_to_string(path).unwrap_or_else(|_| EMBEDDED_SKILL.to_string()) + } else { + let cache = crate::ponytail::state::flag_path() + .parent() + .unwrap_or(Path::new(".")) + .parent() + .unwrap_or(Path::new(".")) + .parent() + .unwrap_or(Path::new(".")) + .join("SKILL.md"); + std::fs::read_to_string(&cache).unwrap_or_else(|_| EMBEDDED_SKILL.to_string()) + }; + + let filtered = filter_skill_body(&skill_body, effective); + + Instructions { + mode: effective.to_string(), + body: filtered, + } +} + +pub fn filter_skill_body(body: &str, mode: &str) -> String { + let effective = config::normalize_mode(mode).unwrap_or(config::DEFAULT_MODE); + body.lines() + .filter(|line| { + if let Some(cap) = line.trim().strip_prefix("| **") { + if let Some(end) = cap.find("** |") { + let label_mode = config::normalize_mode(&cap[..end]); + if label_mode.is_some() { + return label_mode.unwrap() == effective; + } + } + } + if let Some(rest) = line.trim().strip_prefix("- ") { + if let Some(colon) = rest.find(':') { + let label_mode = config::normalize_mode(rest[..colon].trim()); + if label_mode.is_some() { + return label_mode.unwrap() == effective; + } + } + } + true + }) + .collect::>() + .join("\n") +} + +pub fn fallback_instructions(mode: &str) -> String { + let m = config::normalize_mode(mode).unwrap_or(config::DEFAULT_MODE); + format!( + "PONYTAIL MODE ACTIVE — level: {m}\n\n\ + You are a lazy senior developer. Lazy means efficient, not careless.\n\n\ + ## The ladder\n\n\ + 1. Does this need to exist at all? (YAGNI)\n\ + 2. Already in this codebase? Reuse it.\n\ + 3. Stdlib does it? Use it.\n\ + 4. Native platform feature covers it? Use it.\n\ + 5. Already-installed dependency solves it? Use it.\n\ + 6. Can it be one line? One line.\n\ + 7. Only then: the minimum code that works.\n\n\ + ## Rules\n\n\ + No unrequested abstractions. No boilerplate. Deletion over addition.\n\ + Code first, then at most three lines: what was skipped, when to add it.\n\ + Never simplify away: input validation, error handling, security, accessibility." + ) +} +``` + +- [ ] **Step 2: Build check** + +```bash +cargo check +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/ponytail/instructions.rs +git commit -m "feat(ponytail): add instructions module — skill loading and filtering" +``` + +--- + +### Task 5: ponytail/switcher.rs + +**Files:** +- Create: `src/ponytail/switcher.rs` + +**Interfaces:** +- Consumes: `ponytail::config::normalize_config_mode` +- Produces: + - `pub enum SwitchAction { SetMode(String), SetDefault(String), Off }` + - `pub fn detect(input: &str) -> Option` + +- [ ] **Step 1: Create `src/ponytail/switcher.rs`** + +```rust +use crate::ponytail::config; + +pub enum SwitchAction { + SetMode(String), + SetDefault(String), + Off, +} + +pub fn detect(input: &str) -> Option { + let prompt = input.trim().to_lowercase(); + + if config::is_deactivation(&prompt) { + return Some(SwitchAction::Off); + } + + let cmd = prompt + .strip_prefix("/ponytail") + .or_else(|| prompt.strip_prefix("@ponytail")) + .or_else(|| prompt.strip_prefix("$ponytail"))?; + + let parts: Vec<&str> = cmd.split_whitespace().collect(); + let sub = parts.first().copied().unwrap_or(""); + let arg = parts.get(1).copied().unwrap_or(""); + + if sub.is_empty() || sub == "lite" || sub == "full" || sub == "ultra" { + let mode = if sub.is_empty() { "full" } else { sub }; + config::normalize_config_mode(mode)?; + return Some(SwitchAction::SetMode(mode.to_string())); + } + + match sub { + "off" => Some(SwitchAction::Off), + "default" => { + let dmode = arg; + if dmode.is_empty() { + return None; + } + config::normalize_config_mode(dmode)?; + Some(SwitchAction::SetDefault(dmode.to_string())) + } + _ => None, + } +} +``` + +- [ ] **Step 2: Build check** + +```bash +cargo check +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/ponytail/switcher.rs +git commit -m "feat(ponytail): add switcher module — mode switch detection" +``` + +--- + +### Task 6: ponytail/platform.rs + +**Files:** +- Create: `src/ponytail/platform.rs` + +**Interfaces:** +- Produces: + - `pub enum AgentPlatform { Claude, Codex, Copilot, Fallback }` + - `pub fn detect() -> AgentPlatform` + - `pub fn format_hook_output(event: &str, ctx: &str, platform: &AgentPlatform) -> String` + +- [ ] **Step 1: Create `src/ponytail/platform.rs`** + +```rust +use serde_json::json; + +pub enum AgentPlatform { + Claude, + Codex, + Copilot, + Fallback, +} + +pub fn detect() -> AgentPlatform { + if std::env::var("CLAUDE_CONFIG_DIR").is_ok() { + AgentPlatform::Claude + } else if std::env::var("COPILOT_PLUGIN_DATA").is_ok() { + AgentPlatform::Copilot + } else if std::env::var("PLUGIN_DATA").is_ok() { + AgentPlatform::Codex + } else { + AgentPlatform::Fallback + } +} + +pub fn format_hook_output(event: &str, ctx: &str, platform: &AgentPlatform) -> String { + match platform { + AgentPlatform::Claude => { + if event == "SessionStart" && !ctx.is_empty() { + json!({ + "hookSpecificOutput": { + "hookEventName": event, + "additionalContext": ctx, + } + }) + .to_string() + } else { + let output: serde_json::Value = json!({ + "hookSpecificOutput": { + "hookEventName": event, + "additionalContext": ctx, + } + }); + output.to_string() + } + } + AgentPlatform::Codex => { + if event == "SessionStart" { + json!({ + "systemMessage": "PONYTAIL:FULL", + "hookSpecificOutput": { + "hookEventName": event, + "additionalContext": ctx, + } + }) + .to_string() + } else { + json!({ + "hookSpecificOutput": { + "hookEventName": event, + "additionalContext": ctx, + } + }) + .to_string() + } + } + AgentPlatform::Copilot => { + if event == "SessionStart" { + json!({ "additionalContext": ctx }).to_string() + } else { + String::new() + } + } + AgentPlatform::Fallback => ctx.to_string(), + } +} +``` + +- [ ] **Step 2: Build check** + +```bash +cargo check +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/ponytail/platform.rs +git commit -m "feat(ponytail): add platform module — detection and output formatting" +``` + +--- + +### Task 7: ponytail/mod.rs — public API + +**Files:** +- Modify: `src/ponytail/mod.rs` + +Replace skeleton with full public API. + +- [ ] **Step 1: Update `src/ponytail/mod.rs`** + +```rust +pub mod config; +pub mod instructions; +pub mod platform; +pub mod state; +pub mod switcher; + +pub use config::{ + default_mode, is_deactivation, normalize_config_mode, normalize_mode, + normalize_persisted_mode, set_default_mode, DEFAULT_MODE, RUNTIME_MODES, VALID_MODES, +}; +pub use instructions::{build as build_instructions, fallback_instructions, Instructions}; +pub use platform::{detect as detect_platform, format_hook_output, AgentPlatform}; +pub use state::{active_mode, clear_active, set_active}; +pub use switcher::{detect as detect_switch, SwitchAction}; +``` + +- [ ] **Step 2: Build check** + +```bash +cargo check +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/ponytail/mod.rs +git commit -m "feat(ponytail): finalize mod.rs public API" +``` + +--- + +### Task 8: CLI integration — Ponytail command + +**Files:** +- Modify: `src/main.rs` + +Add `mod ponytail;`, `Ponytail` variant to `Commands`, `PonytailAction` enum, and dispatch. + +- [ ] **Step 1: Add module declaration to `src/main.rs`** + +Add after existing `mod` declarations (after line 28 `mod update;`): + +```rust +mod ponytail; +``` + +- [ ] **Step 2: Add `Ponytail` variant to `Commands` enum** + +Add after `Auth` variant: + +```rust + /// Manage Ponytail — lazy senior dev mode for AI agents. + Ponytail { + #[command(subcommand)] + action: PonytailAction, + }, +``` + +- [ ] **Step 3: Add `PonytailAction` subcommand enum** + +Add after `AuthAction` enum definition: + +```rust +#[derive(Subcommand)] +enum PonytailAction { + /// Download SKILL.md and print per-platform hook config snippets. + Setup, + /// Show active ponytail mode (reads flag file + config default). + Status, + /// Set session-scoped mode (off|lite|full|ultra). Writes flag file. + Set { + mode: String, + }, + /// Persist default mode to config. Survives session restarts. + Default { + mode: String, + }, + /// Turn ponytail off for this session. + Off, + /// Re-download SKILL.md from ponytail repo to cache. + Update, + /// Hook entry point — called by agent hook systems. Not for manual use. + Hook { + #[command(subcommand)] + event: PonytailHookEvent, + }, +} + +#[derive(Subcommand)] +enum PonytailHookEvent { + /// Session start — emit rules as hook context, write flag file. + SessionStart, + /// Subagent start — emit rules for subagent context only. + SubagentStart, + /// Prompt submit — parse input for mode switch, update flag if found. + PromptSubmit, + /// Output ANSI mode badge for terminal statusline. + Statusline, +} +``` + +- [ ] **Step 4: Add dispatch in `main()` function** + +Add before the last closing brace of `main()`: + +```rust + Commands::Ponytail { action } => match action { + PonytailAction::Setup => { + println!("download SKILL.md to cache, print per-platform hook configs"); + } + PonytailAction::Status => { + let mode = ponytail::active_mode().unwrap_or_else(ponytail::default_mode); + println!("{mode}"); + } + PonytailAction::Set { mode } => { + let normalized = ponytail::normalize_config_mode(&mode) + .unwrap_or("full"); + ponytail::set_active(normalized).unwrap_or_else(|e| { + eprintln!("error: {e}"); + std::process::exit(1); + }); + println!("{normalized}"); + } + PonytailAction::Default { mode } => { + ponytail::set_default_mode(&mode).unwrap_or_else(|e| { + eprintln!("error: {e}"); + std::process::exit(1); + }); + ponytail::set_active(&mode).ok(); + println!("default: {mode}"); + } + PonytailAction::Off => { + ponytail::clear_active(); + println!("off"); + } + PonytailAction::Update => { + println!("re-download SKILL.md from ponytail repo"); + } + PonytailAction::Hook { event } => match event { + PonytailHookEvent::SessionStart => { + let mode = ponytail::active_mode() + .unwrap_or_else(ponytail::default_mode); + if mode == "off" { + ponytail::state::clear_active(); + println!("OK"); + return; + } + ponytail::set_active(&mode).ok(); + let instructions = ponytail::build_instructions(&mode, None); + let platform = ponytail::detect_platform(); + let output = ponytail::format_hook_output( + "SessionStart", + &instructions.body, + &platform, + ); + println!("{output}"); + } + PonytailHookEvent::SubagentStart => { + let mode = ponytail::active_mode() + .unwrap_or_else(ponytail::default_mode); + if mode == "off" { + println!("OK"); + return; + } + let instructions = ponytail::build_instructions(&mode, None); + let platform = ponytail::detect_platform(); + let output = ponytail::format_hook_output( + "SubagentStart", + &instructions.body, + &platform, + ); + println!("{output}"); + } + PonytailHookEvent::PromptSubmit => { + let mut input = String::new(); + std::io::stdin().read_line(&mut input).ok(); + if let Some(action) = ponytail::detect_switch(&input) { + match action { + ponytail::SwitchAction::SetMode(m) => { + ponytail::set_active(&m).ok(); + } + ponytail::SwitchAction::SetDefault(m) => { + ponytail::set_default_mode(&m).ok(); + ponytail::set_active(&m).ok(); + } + ponytail::SwitchAction::Off => { + ponytail::clear_active(); + } + } + } + println!("OK"); + } + PonytailHookEvent::Statusline => { + let mode = ponytail::active_mode() + .unwrap_or_else(ponytail::default_mode); + if mode == "off" || mode.is_empty() { + return; // no output = no badge + } + if mode == "full" { + print!("\x1b[38;5;108m[PONYTAIL]\x1b[0m"); + } else { + let upper = mode.to_uppercase(); + print!("\x1b[38;5;108m[PONYTAIL:{upper}]\x1b[0m"); + } + } + }, + } +``` + +- [ ] **Step 5: Build check** + +```bash +cargo check +``` + +- [ ] **Step 6: Commit** + +```bash +git add src/main.rs +git commit -m "feat(ponytail): add CLI commands — setup, status, set, hook" +``` + +--- + +### Task 9: Unit tests + +**Files:** +- Create: `src/ponytail/config.rs` (append tests) +- Create: `src/ponytail/state.rs` (append tests) +- Create: `src/ponytail/instructions.rs` (append tests) +- Create: `src/ponytail/switcher.rs` (append tests) + +- [ ] **Step 1: Add config tests to `src/ponytail/config.rs`** + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalizes_valid_modes() { + assert_eq!(normalize_mode("full"), Some("full")); + assert_eq!(normalize_mode("off"), Some("off")); + assert_eq!(normalize_mode("ULTRA"), Some("ultra")); + } + + #[test] + fn rejects_invalid_modes() { + assert_eq!(normalize_mode("extreme"), None); + assert_eq!(normalize_mode(""), None); + assert_eq!(normalize_config_mode("review"), Some("review")); + assert_eq!(normalize_mode("review"), None); // review not a runtime mode + } + + #[test] + fn detects_deactivation() { + assert!(is_deactivation("stop ponytail")); + assert!(is_deactivation("normal mode")); + assert!(is_deactivation("Normal Mode.")); + assert!(!is_deactivation("add a normal mode toggle")); + } + + #[test] + fn defaults_to_full() { + std::env::remove_var("PONYTAIL_DEFAULT_MODE"); + assert_eq!(default_mode(), "full"); + } + + #[test] + fn reads_env_var() { + std::env::set_var("PONYTAIL_DEFAULT_MODE", "lite"); + assert_eq!(default_mode(), "lite"); + std::env::remove_var("PONYTAIL_DEFAULT_MODE"); + } +} +``` + +- [ ] **Step 2: Run config tests** + +```bash +cargo test ponytail::config +``` + +Expected: 5 PASS + +- [ ] **Step 3: Add state tests** + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roundtrip_active_mode() { + clear_active(); + assert_eq!(active_mode(), None); + set_active("full").unwrap(); + assert_eq!(active_mode(), Some("full".to_string())); + clear_active(); + assert_eq!(active_mode(), None); + } + + #[test] + fn clear_nonexistent_is_noop() { + clear_active(); // should not panic + } +} +``` + +- [ ] **Step 4: Run state tests** + +```bash +cargo test ponytail::state +``` + +Expected: 2 PASS + +- [ ] **Step 5: Add instructions tests** + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fallback_generates_for_mode() { + let f = fallback_instructions("full"); + assert!(f.contains("PONYTAIL MODE ACTIVE")); + assert!(f.contains("The ladder")); + } + + #[test] + fn build_uses_embedded_skill() { + let ins = build("full", None); + assert!(!ins.body.is_empty()); + assert_eq!(ins.mode, "full"); + } + + #[test] + fn filter_keeps_non_mode_lines() { + let input = "some rule\n| **lite** | lite only |\n| **full** | full only |\nother rule"; + let filtered = filter_skill_body(input, "full"); + assert!(filtered.contains("some rule")); + assert!(filtered.contains("full only")); + assert!(!filtered.contains("lite only")); + assert!(filtered.contains("other rule")); + } +} +``` + +- [ ] **Step 6: Run instructions tests** + +```bash +cargo test ponytail::instructions +``` + +Expected: 3 PASS + +- [ ] **Step 7: Add switcher tests** + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detects_mode_switch() { + assert!(matches!(detect("/ponytail lite"), Some(SwitchAction::SetMode(m)) if m == "lite")); + assert!(matches!(detect("/ponytail full"), Some(SwitchAction::SetMode(m)) if m == "full")); + } + + #[test] + fn detects_off() { + assert!(matches!(detect("/ponytail off"), Some(SwitchAction::Off))); + } + + #[test] + fn detects_deactivation_phrase() { + assert!(matches!(detect("stop ponytail"), Some(SwitchAction::Off))); + } + + #[test] + fn detects_default() { + assert!(matches!(detect("/ponytail default ultra"), Some(SwitchAction::SetDefault(m)) if m == "ultra")); + } + + #[test] + fn ignores_false_positives() { + assert!(detect("let's talk about ponytail").is_none()); + assert!(detect("").is_none()); + } +} +``` + +- [ ] **Step 8: Run switcher tests** + +```bash +cargo test ponytail::switcher +``` + +Expected: 5 PASS + +- [ ] **Step 9: Commit** + +```bash +git add src/ponytail/config.rs src/ponytail/state.rs src/ponytail/instructions.rs src/ponytail/switcher.rs +git commit -m "test(ponytail): add unit tests for config, state, instructions, switcher" +``` + +--- + +### Task 10: Build and lint + +- [ ] **Step 1: Full build** + +```bash +cargo build +``` + +- [ ] **Step 2: Run all ponytail tests** + +```bash +cargo test ponytail +``` + +- [ ] **Step 3: Clippy** + +```bash +cargo clippy -- -D warnings +``` + +- [ ] **Step 4: Check for unsafe** + +```bash +cargo check +``` + +- [ ] **Step 5: Commit any lint fixes** + +```bash +git add -u +git commit -m "chore(ponytail): fix clippy warnings" +``` From 025ab707c0faba44c18bdeefdd56a8ff78d1749c Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 17:57:19 +0530 Subject: [PATCH 02/13] docs: ponytail L1 integration design spec --- ...26-07-07-ponytail-l1-integration-design.md | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-07-ponytail-l1-integration-design.md diff --git a/docs/superpowers/specs/2026-07-07-ponytail-l1-integration-design.md b/docs/superpowers/specs/2026-07-07-ponytail-l1-integration-design.md new file mode 100644 index 00000000..d57afa0f --- /dev/null +++ b/docs/superpowers/specs/2026-07-07-ponytail-l1-integration-design.md @@ -0,0 +1,189 @@ +# Ponytail L1 Integration — Design + +**Issue:** [#42](https://github.com/getappz/agentflare/issues/42) +**Date:** 2026-07-07 +**Branch:** `feature/ponytail-l1-integration` + +## Goal + +Port ponytail's runtime logic (config management, state tracking, instructions builder, mode switcher, platform output formatting) from Node.js hooks into agentflare's Rust binary. Prompt content (SKILL.md) stays external — fetched on demand from the ponytail repo. + +agentflare becomes the hook provider that every AI agent platform calls. The ponytail npm plugin becomes a thin manifest pointing at `agentflare ponytail hook`. + +## Architecture + +``` +┌─ CLI surface ──────────────────────────────────────────┐ +│ agentflare ponytail setup download SKILL.md │ +│ agentflare ponytail status show active mode │ +│ agentflare ponytail set session-scoped mode │ +│ agentflare ponytail default persist default mode │ +│ agentflare ponytail off shortcut off │ +│ agentflare ponytail update re-download skill │ +│ agentflare ponytail hook hook entrypoint │ +└─────────────────────────────────────────────────────────┘ + │ +┌─ Core lib (src/ponytail/) ─────────────────────────────┐ +│ mod.rs — pub API, re-exports │ +│ config.rs — Config struct, mode resolution │ +│ state.rs — flag file r/w (.ponytail-active) │ +│ instructions.rs — SkillDoc, filter_skill, fallback │ +│ switcher.rs — SwitchAction, detect_switch │ +│ platform.rs — AgentPlatform, format_output │ +│ skill.md — embedded default (fallback) │ +└─────────────────────────────────────────────────────────┘ + │ +┌─ Storage ──────────────────────────────────────────────┐ +│ ~/.config/agentflare/ponytail/config.json │ +│ ~/.local/state/agentflare/ponytail/active │ +│ ~/.cache/agentflare/ponytail/SKILL.md (downloaded) │ +└─────────────────────────────────────────────────────────┘ +``` + +State paths are agentflare-owned to avoid collision with existing ponytail plugin installs. + +## Module Details + +### config.rs + +```rust +const DEFAULT_MODE: &str = "full"; +const VALID_MODES: [&str; 5] = ["off", "lite", "full", "ultra", "review"]; +const RUNTIME_MODES: [&str; 4] = ["off", "lite", "full", "ultra"]; + +struct Config { + default_mode: String, +} +impl Config { + fn load() -> Self; // env -> config.json -> "full" + fn set_default(&mut self, mode: &str) -> bool; // persist to config.json + fn save(&self); +} +fn normalize_mode(mode: &str) -> Option<&str>; // validate against RUNTIME_MODES +fn normalize_config_mode(mode: &str) -> Option<&str>;// validate against VALID_MODES +fn is_deactivation(text: &str) -> bool; // "stop ponytail" / "normal mode" +``` + +Resolution order: `PONYTAIL_DEFAULT_MODE` env → `config.json` → `"full"`. +Config path: `~/.config/agentflare/ponytail/config.json`. + +### state.rs + +```rust +fn flag_path() -> PathBuf; // ~/.local/state/agentflare/ponytail/active +fn active_mode() -> Option; +fn set_active(mode: &str) -> io::Result<()>; +fn clear_active(); +``` + +Simple file-based flag. Write "full", "lite", etc. Delete on "off". Used by statusline and session-start to know active mode without re-parsing config. + +### instructions.rs + +```rust +struct Instructions { + mode: String, + body: String, // filtered SKILL.md content +} + +fn build(mode: &str, skill_path: Option<&Path>) -> Instructions; +fn filter_skill(body: &str, mode: &str) -> String; +fn fallback(mode: &str) -> String; +``` + +SKILL.md loading: +1. `skill_path` arg → custom path +2. `~/.cache/agentflare/ponytail/SKILL.md` → downloaded copy +3. Embedded `skill.md` → compiled-in fallback + +Filtering: intensity-specific rows in the table and example lines are kept only for the active mode. All other rules pass through unchanged. + +### switcher.rs + +```rust +enum SwitchAction { + SetMode(String), // session-scoped (off|lite|full|ultra) + SetDefault(String), // persist to config (off|lite|full|ultra) + Off, // shortcut for SetMode("off") +} + +fn detect(input: &str) -> Option; +``` + +Matches `/ponytail` command patterns in user prompt input. Used by the `prompt-submit` hook event. + +### platform.rs + +```rust +enum AgentPlatform { Claude, Codex, Copilot, Fallback } + +fn detect() -> AgentPlatform; +fn format(event: &str, ctx: &str, platform: AgentPlatform) -> String; +``` + +Platform detection via env vars: +- `CLAUDE_CONFIG_DIR` → Claude +- `PLUGIN_DATA` + not `COPILOT_PLUGIN_DATA` → Codex +- `COPILOT_PLUGIN_DATA` → Copilot +- none → Fallback (raw text) + +Output formats (exactly matching pony's current behavior): +- **Claude:** `{"hookSpecificOutput":{"hookEventName":"...","additionalContext":"..."}}` +- **Codex:** `{"systemMessage":"PONYTAIL:FULL","hookSpecificOutput":{"hookEventName":"...","additionalContext":"..."}}` +- **Copilot:** `{"additionalContext":"..."}` (SessionStart only, empty otherwise) +- **Fallback:** raw rules text on stdout + +## Hook Command + +``` +agentflare ponytail hook +``` + +Events: + +| Event | Action | +|-------|--------| +| `session-start` | Write flag file, emit rules as hook context | +| `subagent-start` | Emit rules for subagent context (no flag write) | +| `prompt-submit` | Parse input for mode switch, update flag if found | +| `statusline` | Output mode badge (ANSI colored) | + +Exit 0 on success, non-zero on error. Hook author handles failure gracefully (never blocks session start). + +## CLI Commands + +``` +agentflare ponytail setup download SKILL.md to cache, print per-platform hook configs +agentflare ponytail status print active mode (reads flag + config) +agentflare ponytail set write flag, session-scoped (off|lite|full|ultra) +agentflare ponytail default persist to config.json, write flag +agentflare ponytail off shortcut: ponytail set off +agentflare ponytail update re-download SKILL.md from ponytail repo +``` + +## Dependencies + +No new crate dependencies. Existing deps cover everything: +- `dirs` — config/state/cache paths +- `serde` / `serde_json` — config serialization, hook JSON output +- `ureq` — HTTP download of SKILL.md + +## Testing + +Unit tests per module: +- `config`: mode resolution order, validation, config r/w roundtrip +- `state`: flag file lifecycle, concurrent reads +- `instructions`: filter removes correct intensity rows, fallback generates +- `switcher`: detects all switch patterns, ignores false positives +- `platform`: detection from env vars, output format per platform + +Integration test: +- `agentflare ponytail hook session-start` → writes flag, emits Claude-format JSON + +## Out of Scope + +- Porting SKILL.md prompt content into Rust (stays external) +- Multi-platform plugin manifest files (`.claude-plugin/`, `.codex-plugin/`, etc.) +- Statusline scripts (`.ps1`, `.sh`) — these just call `agentflare ponytail hook statusline` +- ponytail-review, ponytail-audit, ponytail-debt, ponytail-gain skills (separate features) +- Benchmark suite From 85a7f19408f9c382003f32b9dea670638feb903a Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 18:01:11 +0530 Subject: [PATCH 03/13] =?UTF-8?q?feat(ponytail):=20add=20config=20module?= =?UTF-8?q?=20=E2=80=94=20mode=20resolution=20and=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 1 + src/ponytail/config.rs | 74 ++++++++++++++++++++++++++++++++++++ src/ponytail/instructions.rs | 0 src/ponytail/mod.rs | 5 +++ src/ponytail/platform.rs | 0 src/ponytail/state.rs | 0 src/ponytail/switcher.rs | 0 7 files changed, 80 insertions(+) create mode 100644 src/ponytail/config.rs create mode 100644 src/ponytail/instructions.rs create mode 100644 src/ponytail/mod.rs create mode 100644 src/ponytail/platform.rs create mode 100644 src/ponytail/state.rs create mode 100644 src/ponytail/switcher.rs diff --git a/src/main.rs b/src/main.rs index 4bc67c93..5be065e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ mod init; mod mcp_server; mod optimize; mod paths; +mod ponytail; mod pricing; mod rollup; mod rule_text; diff --git a/src/ponytail/config.rs b/src/ponytail/config.rs new file mode 100644 index 00000000..530aa057 --- /dev/null +++ b/src/ponytail/config.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +pub const DEFAULT_MODE: &str = "full"; +pub const VALID_MODES: &[&str] = &["off", "lite", "full", "ultra", "review"]; +pub const RUNTIME_MODES: &[&str] = &["off", "lite", "full", "ultra"]; + +pub fn normalize_mode(mode: &str) -> Option<&'static str> { + let m = mode.trim().to_lowercase(); + RUNTIME_MODES.iter().find(|&&v| v == m).copied() +} + +pub fn normalize_config_mode(mode: &str) -> Option<&'static str> { + let m = mode.trim().to_lowercase(); + VALID_MODES.iter().find(|&&v| v == m).copied() +} + +pub fn normalize_persisted_mode(mode: &str) -> Option<&'static str> { + normalize_mode(mode).or_else(|| normalize_config_mode(mode)) +} + +pub fn is_deactivation(text: &str) -> bool { + let t = text.trim().to_lowercase(); + let t = t.trim_end_matches(|c: char| c == '.' || c == '!' || c == '?' || c.is_whitespace()); + t == "stop ponytail" || t == "normal mode" +} + +pub fn config_dir() -> PathBuf { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("agentflare") + .join("ponytail") +} + +pub fn config_path() -> PathBuf { + config_dir().join("config.json") +} + +#[derive(Serialize, Deserialize, Default)] +struct ConfigFile { + default_mode: Option, +} + +pub fn default_mode() -> String { + if let Ok(val) = std::env::var("PONYTAIL_DEFAULT_MODE") { + if let Some(m) = normalize_config_mode(&val) { + return m.to_string(); + } + } + if let Ok(data) = std::fs::read_to_string(config_path()) { + if let Ok(cfg) = serde_json::from_str::(&data) { + if let Some(mode) = cfg.default_mode { + if let Some(m) = normalize_config_mode(&mode) { + return m.to_string(); + } + } + } + } + DEFAULT_MODE.to_string() +} + +pub fn set_default_mode(mode: &str) -> Result<(), String> { + let normalized = normalize_config_mode(mode).ok_or_else(|| format!("invalid mode: {mode}"))?; + let dir = config_dir(); + std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + let mut cfg: ConfigFile = std::fs::read_to_string(config_path()) + .ok() + .and_then(|d| serde_json::from_str(&d).ok()) + .unwrap_or_default(); + cfg.default_mode = Some(normalized.to_string()); + let json = serde_json::to_string_pretty(&cfg).map_err(|e| e.to_string())?; + std::fs::write(config_path(), json).map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src/ponytail/instructions.rs b/src/ponytail/instructions.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/ponytail/mod.rs b/src/ponytail/mod.rs new file mode 100644 index 00000000..201cf61e --- /dev/null +++ b/src/ponytail/mod.rs @@ -0,0 +1,5 @@ +pub mod config; +pub mod state; +pub mod instructions; +pub mod switcher; +pub mod platform; diff --git a/src/ponytail/platform.rs b/src/ponytail/platform.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/ponytail/state.rs b/src/ponytail/state.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/ponytail/switcher.rs b/src/ponytail/switcher.rs new file mode 100644 index 00000000..e69de29b From 3b7d5712343506a84f4cbdcdae8903ae0d573ede Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 18:02:12 +0530 Subject: [PATCH 04/13] =?UTF-8?q?feat(ponytail):=20add=20state=20module=20?= =?UTF-8?q?=E2=80=94=20flag=20file=20read/write?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ponytail/state.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/ponytail/state.rs b/src/ponytail/state.rs index e69de29b..27124a3e 100644 --- a/src/ponytail/state.rs +++ b/src/ponytail/state.rs @@ -0,0 +1,29 @@ +use std::io; +use std::path::PathBuf; + +pub fn flag_path() -> PathBuf { + dirs::state_dir() + .unwrap_or_else(|| dirs::data_local_dir().unwrap_or_else(|| PathBuf::from("."))) + .join("agentflare") + .join("ponytail") + .join("active") +} + +pub fn active_mode() -> Option { + std::fs::read_to_string(flag_path()) + .ok() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) +} + +pub fn set_active(mode: &str) -> io::Result<()> { + let path = flag_path(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(path, mode) +} + +pub fn clear_active() { + let _ = std::fs::remove_file(flag_path()); +} From 51f34b64ef6594c9640345da35301d22bcf8c4e3 Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 18:03:09 +0530 Subject: [PATCH 05/13] =?UTF-8?q?feat(ponytail):=20add=20instructions=20mo?= =?UTF-8?q?dule=20=E2=80=94=20skill=20loading=20and=20filtering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ponytail/instructions.rs | 78 +++++++++++++++++++++++ src/ponytail/skill.md | 120 +++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 src/ponytail/skill.md diff --git a/src/ponytail/instructions.rs b/src/ponytail/instructions.rs index e69de29b..ac5ed453 100644 --- a/src/ponytail/instructions.rs +++ b/src/ponytail/instructions.rs @@ -0,0 +1,78 @@ +use crate::ponytail::config; +use std::path::Path; + +static EMBEDDED_SKILL: &str = include_str!("skill.md"); + +pub struct Instructions { + pub mode: String, + pub body: String, +} + +pub fn build(mode: &str, skill_path: Option<&Path>) -> Instructions { + let effective = config::normalize_persisted_mode(mode) + .unwrap_or(config::DEFAULT_MODE); + + let skill_body = if let Some(path) = skill_path { + std::fs::read_to_string(path).unwrap_or_else(|_| EMBEDDED_SKILL.to_string()) + } else { + let cache = dirs::cache_dir() + .unwrap_or_else(|| std::path::PathBuf::from(".")) + .join("agentflare") + .join("ponytail") + .join("SKILL.md"); + std::fs::read_to_string(&cache).unwrap_or_else(|_| EMBEDDED_SKILL.to_string()) + }; + + let filtered = filter_skill_body(&skill_body, effective); + + Instructions { + mode: effective.to_string(), + body: filtered, + } +} + +pub fn filter_skill_body(body: &str, mode: &str) -> String { + let effective = config::normalize_mode(mode).unwrap_or(config::DEFAULT_MODE); + body.lines() + .filter(|line| { + if let Some(cap) = line.trim().strip_prefix("| **") { + if let Some(end) = cap.find("** |") { + let label_mode = config::normalize_mode(&cap[..end]); + if label_mode.is_some() { + return label_mode.unwrap() == effective; + } + } + } + if let Some(rest) = line.trim().strip_prefix("- ") { + if let Some(colon) = rest.find(':') { + let label_mode = config::normalize_mode(rest[..colon].trim()); + if label_mode.is_some() { + return label_mode.unwrap() == effective; + } + } + } + true + }) + .collect::>() + .join("\n") +} + +pub fn fallback_instructions(mode: &str) -> String { + let m = config::normalize_mode(mode).unwrap_or(config::DEFAULT_MODE); + format!( + "PONYTAIL MODE ACTIVE — level: {m}\n\n\ + You are a lazy senior developer. Lazy means efficient, not careless.\n\n\ + ## The ladder\n\n\ + 1. Does this need to exist at all? (YAGNI)\n\ + 2. Already in this codebase? Reuse it.\n\ + 3. Stdlib does it? Use it.\n\ + 4. Native platform feature covers it? Use it.\n\ + 5. Already-installed dependency solves it? Use it.\n\ + 6. Can it be one line? One line.\n\ + 7. Only then: the minimum code that works.\n\n\ + ## Rules\n\n\ + No unrequested abstractions. No boilerplate. Deletion over addition.\n\ + Code first, then at most three lines: what was skipped, when to add it.\n\ + Never simplify away: input validation, error handling, security, accessibility." + ) +} diff --git a/src/ponytail/skill.md b/src/ponytail/skill.md new file mode 100644 index 00000000..5f999d78 --- /dev/null +++ b/src/ponytail/skill.md @@ -0,0 +1,120 @@ +--- +name: ponytail +description: > + Forces the laziest solution that actually works, simplest, shortest, most + minimal. Channels a senior dev who has seen everything: question whether the + task needs to exist at all (YAGNI), reach for the standard library before + custom code, native platform features before dependencies, one line before + fifty. Supports intensity levels: lite, full (default), ultra. Use on ANY + coding task: writing, adding, refactoring, fixing, reviewing, or designing + code, and choosing libraries or dependencies. Also use whenever the user + says "ponytail", "be lazy", "lazy mode", "simplest solution", "minimal + solution", "yagni", "do less", or "shortest path", or complains about + over-engineering, bloat, boilerplate, or unnecessary dependencies. Do NOT + use for non-coding requests (general knowledge, prose, translation, + summaries, recipes). +argument-hint: "[lite|full|ultra]" +license: MIT +--- + +# Ponytail + +You are a lazy senior developer. Lazy means efficient, not careless. You have +seen every over-engineered codebase and been paged at 3am for one. The best +code is the code never written. + +## Persistence + +ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if +unsure. Off only: "stop ponytail" / "normal mode". Default: **full**. +Switch: `/ponytail lite|full|ultra`. + +## The ladder + +Stop at the first rung that holds: + +1. **Does this need to exist at all?** Speculative need = skip it, say so in one line. (YAGNI) +2. **Already in this codebase?** A helper, util, type, or pattern that already lives here → reuse it. Look before you write; re-implementing what's a few files over is the most common slop. +3. **Stdlib does it?** Use it. +4. **Native platform feature covers it?** `` over a picker lib, CSS over JS, DB constraint over app code. +5. **Already-installed dependency solves it?** Use it. Never add a new one for what a few lines can do. +6. **Can it be one line?** One line. +7. **Only then:** the minimum code that works. + +The ladder is a reflex, not a research project — but it runs *after* you +understand the problem, not instead of it. Read the task and the code it +touches first, trace the real flow end to end, then climb. Two rungs work → +take the higher one and move on. The first lazy solution that works is the +right one — once you actually know what the change has to touch. + +**Bug fix = root cause, not symptom.** A report names a symptom. Before you +edit, grep every caller of the function you're about to touch. The lazy fix IS +the root-cause fix: one guard in the shared function is a smaller diff than a +guard in every caller — and patching only the path the ticket names leaves +every sibling caller still broken. Fix it once, where all callers route through. + +## Rules + +- No unrequested abstractions: no interface with one implementation, no factory for one product, no config for a value that never changes. +- No boilerplate, no scaffolding "for later", later can scaffold for itself. +- Deletion over addition. Boring over clever, clever is what someone decodes at 3am. +- Fewest files possible. Shortest working diff wins — but only once you understand the problem. The smallest change in the wrong place isn't lazy, it's a second bug. +- Complex request? Ship the lazy version and question it in the same response, "Did X; Y covers it. Need full X? Say so." Never stall on an answer you can default. +- Two stdlib options, same size? Take the one that's correct on edge cases. Lazy means writing less code, not picking the flimsier algorithm. +- Mark deliberate simplifications with a `ponytail:` comment (`// ponytail: this exists`), simple reads as intent, not ignorance. Shortcut with a known ceiling (global lock, O(n²) scan, naive heuristic)? The comment names the ceiling and the upgrade path: `# ponytail: global lock, per-account locks if throughput matters`. + +## Output + +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 +than the code, delete the explanation, every paragraph defending a +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].` + +## 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. | + +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." +- full: "`@lru_cache(maxsize=1000)` on the fetch function. Skipped custom cache class, add when lru_cache measurably falls short." +- 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." + +## When NOT to be lazy + +Never simplify away: input validation at trust boundaries, error handling +that prevents data loss, security measures, accessibility basics, anything +explicitly requested. User insists on the full version → build it, no +re-arguing. + +Never lazy about understanding the problem. The ladder shortens the +solution, never the reading. Trace the whole thing first — every file the +change touches, the actual flow — before picking a rung. Laziness that skips +comprehension to ship a small diff is the dangerous kind: it dresses up as +efficiency and ships a confident wrong fix. Read fully, then be lazy. + +Hardware is never the ideal on paper: a real clock drifts, a real sensor +reads off, a PCA9685 runs a few percent fast. Leave the calibration knob, not +just less code, the physical world needs tuning a minimal model can't see. + +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 + +Ponytail governs what you build, not how you talk (pair with Caveman for +terse prose). "stop ponytail" / "normal mode": revert. Level persists until +changed or session end. + +The shortest path to done is the right path. From 844ba57aace4e2fe82282bd443f4ce151b2c55cb Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 18:04:11 +0530 Subject: [PATCH 06/13] feat(ponytail): add switcher and platform modules --- src/ponytail/platform.rs | 59 ++++++++++++++++++++++++++++++++++++++++ src/ponytail/switcher.rs | 43 +++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/ponytail/platform.rs b/src/ponytail/platform.rs index e69de29b..b31a8468 100644 --- a/src/ponytail/platform.rs +++ b/src/ponytail/platform.rs @@ -0,0 +1,59 @@ +use serde_json::json; + +pub enum AgentPlatform { + Claude, + Codex, + Copilot, + Fallback, +} + +pub fn detect() -> AgentPlatform { + if std::env::var("CLAUDE_CONFIG_DIR").is_ok() { + AgentPlatform::Claude + } else if std::env::var("COPILOT_PLUGIN_DATA").is_ok() { + AgentPlatform::Copilot + } else if std::env::var("PLUGIN_DATA").is_ok() { + AgentPlatform::Codex + } else { + AgentPlatform::Fallback + } +} + +pub fn format_hook_output(event: &str, ctx: &str, platform: &AgentPlatform) -> String { + match platform { + AgentPlatform::Claude => { + json!({ + "hookSpecificOutput": { + "hookEventName": event, + "additionalContext": ctx, + } + }) + .to_string() + } + AgentPlatform::Codex => { + let sys_msg = if event == "SessionStart" { + "PONYTAIL:FULL" + } else { + "" + }; + let mut output = json!({ + "hookSpecificOutput": { + "hookEventName": event, + "additionalContext": ctx, + } + }); + if !sys_msg.is_empty() { + output["systemMessage"] = json!(sys_msg); + } + output.to_string() + } + AgentPlatform::Copilot => { + if event == "SessionStart" { + json!({ "additionalContext": ctx }).to_string() + } else { + String::new() + } + } + AgentPlatform::Fallback => ctx.to_string(), + } +} diff --git a/src/ponytail/switcher.rs b/src/ponytail/switcher.rs index e69de29b..305c70f9 100644 --- a/src/ponytail/switcher.rs +++ b/src/ponytail/switcher.rs @@ -0,0 +1,43 @@ +use crate::ponytail::config; + +pub enum SwitchAction { + SetMode(String), + SetDefault(String), + Off, +} + +pub fn detect(input: &str) -> Option { + let prompt = input.trim().to_lowercase(); + + if config::is_deactivation(&prompt) { + return Some(SwitchAction::Off); + } + + let cmd = prompt + .strip_prefix("/ponytail") + .or_else(|| prompt.strip_prefix("@ponytail")) + .or_else(|| prompt.strip_prefix("$ponytail"))?; + + let parts: Vec<&str> = cmd.split_whitespace().collect(); + let sub = parts.first().copied().unwrap_or(""); + let arg = parts.get(1).copied().unwrap_or(""); + + if sub.is_empty() || sub == "lite" || sub == "full" || sub == "ultra" { + let mode = if sub.is_empty() { "full" } else { sub }; + config::normalize_config_mode(mode)?; + return Some(SwitchAction::SetMode(mode.to_string())); + } + + match sub { + "off" => Some(SwitchAction::Off), + "default" => { + let dmode = arg; + if dmode.is_empty() { + return None; + } + config::normalize_config_mode(dmode)?; + Some(SwitchAction::SetDefault(dmode.to_string())) + } + _ => None, + } +} From 8e33b1ee285d159de645295740080a40a824da3f Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 18:04:48 +0530 Subject: [PATCH 07/13] feat(ponytail): finalize mod.rs public API --- src/ponytail/mod.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ponytail/mod.rs b/src/ponytail/mod.rs index 201cf61e..98c22b6d 100644 --- a/src/ponytail/mod.rs +++ b/src/ponytail/mod.rs @@ -1,5 +1,14 @@ pub mod config; -pub mod state; pub mod instructions; -pub mod switcher; pub mod platform; +pub mod state; +pub mod switcher; + +pub use config::{ + default_mode, is_deactivation, normalize_config_mode, normalize_mode, + normalize_persisted_mode, set_default_mode, DEFAULT_MODE, RUNTIME_MODES, VALID_MODES, +}; +pub use instructions::{build as build_instructions, fallback_instructions, Instructions}; +pub use platform::{detect as detect_platform, format_hook_output, AgentPlatform}; +pub use state::{active_mode, clear_active, set_active}; +pub use switcher::{detect as detect_switch, SwitchAction}; From cdc42370a913a06d7f1819eae171f644d0142f65 Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 18:06:46 +0530 Subject: [PATCH 08/13] =?UTF-8?q?feat(ponytail):=20add=20CLI=20commands=20?= =?UTF-8?q?=E2=80=94=20setup,=20status,=20set,=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/src/main.rs b/src/main.rs index 5be065e4..e77c656b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -143,6 +143,11 @@ enum Commands { #[command(subcommand)] action: AuthAction, }, + /// Manage Ponytail — lazy senior dev mode for AI agents. + Ponytail { + #[command(subcommand)] + action: PonytailAction, + }, } #[derive(Subcommand)] @@ -335,6 +340,43 @@ enum AuthAction { }, } +#[derive(Subcommand)] +enum PonytailAction { + /// Download SKILL.md and print per-platform hook config snippets. + Setup, + /// Show active ponytail mode (reads flag file + config default). + Status, + /// Set session-scoped mode (off|lite|full|ultra). Writes flag file. + Set { + mode: String, + }, + /// Persist default mode to config. Survives session restarts. + Default { + mode: String, + }, + /// Turn ponytail off for this session. + Off, + /// Re-download SKILL.md from ponytail repo to cache. + Update, + /// Hook entry point — called by agent hook systems. Not for manual use. + Hook { + #[command(subcommand)] + event: PonytailHookEvent, + }, +} + +#[derive(Subcommand)] +enum PonytailHookEvent { + /// Session start — emit rules as hook context, write flag file. + SessionStart, + /// Subagent start — emit rules for subagent context only. + SubagentStart, + /// Prompt submit — parse input for mode switch, update flag if found. + PromptSubmit, + /// Output ANSI mode badge for terminal statusline. + Statusline, +} + #[derive(Subcommand)] enum IsolateAction { /// Create isolated $HOME profile with symlinked host files. @@ -495,5 +537,106 @@ fn main() { Commands::Uninstall { dry_run, keep_config, keep_binary } => { uninstall::run(dry_run, keep_config, keep_binary) } + Commands::Ponytail { action } => match action { + PonytailAction::Setup => { + println!("download SKILL.md to cache, print per-platform hook configs"); + } + PonytailAction::Status => { + let mode = ponytail::active_mode().unwrap_or_else(ponytail::default_mode); + println!("{mode}"); + } + PonytailAction::Set { mode } => { + let normalized = ponytail::normalize_config_mode(&mode) + .unwrap_or("full"); + ponytail::set_active(normalized).unwrap_or_else(|e| { + eprintln!("error: {e}"); + std::process::exit(1); + }); + println!("{normalized}"); + } + PonytailAction::Default { mode } => { + ponytail::set_default_mode(&mode).unwrap_or_else(|e| { + eprintln!("error: {e}"); + std::process::exit(1); + }); + ponytail::set_active(&mode).ok(); + println!("default: {mode}"); + } + PonytailAction::Off => { + ponytail::clear_active(); + println!("off"); + } + PonytailAction::Update => { + println!("re-download SKILL.md from ponytail repo"); + } + PonytailAction::Hook { event } => match event { + PonytailHookEvent::SessionStart => { + let mode = ponytail::active_mode() + .unwrap_or_else(ponytail::default_mode); + if mode == "off" { + ponytail::state::clear_active(); + println!("OK"); + return; + } + ponytail::set_active(&mode).ok(); + let instructions = ponytail::build_instructions(&mode, None); + let platform = ponytail::detect_platform(); + let output = ponytail::format_hook_output( + "SessionStart", + &instructions.body, + &platform, + ); + println!("{output}"); + } + PonytailHookEvent::SubagentStart => { + let mode = ponytail::active_mode() + .unwrap_or_else(ponytail::default_mode); + if mode == "off" { + println!("OK"); + return; + } + let instructions = ponytail::build_instructions(&mode, None); + let platform = ponytail::detect_platform(); + let output = ponytail::format_hook_output( + "SubagentStart", + &instructions.body, + &platform, + ); + println!("{output}"); + } + PonytailHookEvent::PromptSubmit => { + let mut input = String::new(); + std::io::stdin().read_line(&mut input).ok(); + if let Some(action) = ponytail::detect_switch(&input) { + match action { + ponytail::SwitchAction::SetMode(m) => { + ponytail::set_active(&m).ok(); + } + ponytail::SwitchAction::SetDefault(m) => { + ponytail::set_default_mode(&m).ok(); + ponytail::set_active(&m).ok(); + } + ponytail::SwitchAction::Off => { + ponytail::clear_active(); + } + } + } + println!("OK"); + } + PonytailHookEvent::Statusline => { + let mode = ponytail::active_mode() + .unwrap_or_else(ponytail::default_mode); + if mode == "off" || mode.is_empty() { + return; + } + if mode == "full" { + print!("\x1b[38;5;108m[PONYTAIL]\x1b[0m"); + } else { + let upper = mode.to_uppercase(); + print!("\x1b[38;5;108m[PONYTAIL:{upper}]\x1b[0m"); + } + } + }, + } } } From 9f30e65d857acbcd7661fe5f94a26ee8878ea346 Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 18:09:02 +0530 Subject: [PATCH 09/13] test(ponytail): add unit tests for all modules --- src/ponytail/config.rs | 49 ++++++++++++++++++++++++++++++++++++ src/ponytail/instructions.rs | 29 +++++++++++++++++++++ src/ponytail/state.rs | 21 ++++++++++++++++ src/ponytail/switcher.rs | 32 +++++++++++++++++++++++ 4 files changed, 131 insertions(+) diff --git a/src/ponytail/config.rs b/src/ponytail/config.rs index 530aa057..c554f6ff 100644 --- a/src/ponytail/config.rs +++ b/src/ponytail/config.rs @@ -72,3 +72,52 @@ pub fn set_default_mode(mode: &str) -> Result<(), String> { std::fs::write(config_path(), json).map_err(|e| e.to_string())?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalizes_valid_modes() { + assert_eq!(normalize_mode("full"), Some("full")); + assert_eq!(normalize_mode("off"), Some("off")); + assert_eq!(normalize_mode("ULTRA"), Some("ultra")); + } + + #[test] + fn rejects_invalid_modes() { + assert_eq!(normalize_mode("extreme"), None); + assert_eq!(normalize_mode(""), None); + assert_eq!(normalize_config_mode("review"), Some("review")); + assert_eq!(normalize_mode("review"), None); + } + + #[test] + fn detects_deactivation() { + assert!(is_deactivation("stop ponytail")); + assert!(is_deactivation("normal mode")); + assert!(is_deactivation("Normal Mode.")); + assert!(!is_deactivation("add a normal mode toggle")); + } + + #[test] + fn defaults_to_full() { + unsafe { std::env::remove_var("PONYTAIL_DEFAULT_MODE") }; + assert_eq!(default_mode(), "full"); + } + + #[test] + fn reads_env_var() { + unsafe { std::env::set_var("PONYTAIL_DEFAULT_MODE", "lite") }; + assert_eq!(default_mode(), "lite"); + unsafe { std::env::remove_var("PONYTAIL_DEFAULT_MODE") }; + } +} + +#[test] +fn roundtrip_default_mode() { + let prev = default_mode(); + set_default_mode("ultra").unwrap(); + assert_eq!(default_mode(), "ultra"); + set_default_mode(&prev).unwrap(); +} diff --git a/src/ponytail/instructions.rs b/src/ponytail/instructions.rs index ac5ed453..483ba6f4 100644 --- a/src/ponytail/instructions.rs +++ b/src/ponytail/instructions.rs @@ -76,3 +76,32 @@ pub fn fallback_instructions(mode: &str) -> String { Never simplify away: input validation, error handling, security, accessibility." ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fallback_generates_for_mode() { + let f = fallback_instructions("full"); + assert!(f.contains("PONYTAIL MODE ACTIVE")); + assert!(f.contains("The ladder")); + } + + #[test] + fn build_uses_embedded_skill() { + let ins = build("full", None); + assert!(!ins.body.is_empty()); + assert_eq!(ins.mode, "full"); + } + + #[test] + fn filter_keeps_non_mode_lines() { + let input = "some rule\n| **lite** | lite only |\n| **full** | full only |\nother rule"; + let filtered = filter_skill_body(input, "full"); + assert!(filtered.contains("some rule")); + assert!(filtered.contains("full only")); + assert!(!filtered.contains("lite only")); + assert!(filtered.contains("other rule")); + } +} diff --git a/src/ponytail/state.rs b/src/ponytail/state.rs index 27124a3e..9c97b570 100644 --- a/src/ponytail/state.rs +++ b/src/ponytail/state.rs @@ -27,3 +27,24 @@ pub fn set_active(mode: &str) -> io::Result<()> { pub fn clear_active() { let _ = std::fs::remove_file(flag_path()); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roundtrip_active_mode() { + clear_active(); + assert_eq!(active_mode(), None); + set_active("full").unwrap(); + assert_eq!(active_mode(), Some("full".to_string())); + clear_active(); + assert_eq!(active_mode(), None); + } + + #[test] + fn clear_nonexistent_is_noop() { + clear_active(); + clear_active(); // should not panic + } +} diff --git a/src/ponytail/switcher.rs b/src/ponytail/switcher.rs index 305c70f9..19cf0889 100644 --- a/src/ponytail/switcher.rs +++ b/src/ponytail/switcher.rs @@ -41,3 +41,35 @@ pub fn detect(input: &str) -> Option { _ => None, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detects_mode_switch() { + assert!(matches!(detect("/ponytail lite"), Some(SwitchAction::SetMode(m)) if m == "lite")); + assert!(matches!(detect("/ponytail full"), Some(SwitchAction::SetMode(m)) if m == "full")); + } + + #[test] + fn detects_off() { + assert!(matches!(detect("/ponytail off"), Some(SwitchAction::Off))); + } + + #[test] + fn detects_deactivation_phrase() { + assert!(matches!(detect("stop ponytail"), Some(SwitchAction::Off))); + } + + #[test] + fn detects_default() { + assert!(matches!(detect("/ponytail default ultra"), Some(SwitchAction::SetDefault(m)) if m == "ultra")); + } + + #[test] + fn ignores_false_positives() { + assert!(detect("let's talk about ponytail").is_none()); + assert!(detect("").is_none()); + } +} From cd978852ee37762ac063d5c7afa84e6ad4f30518 Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 18:12:50 +0530 Subject: [PATCH 10/13] chore(ponytail): fix clippy warnings --- Cargo.lock | 149 +++++++++++++++++++++++++++++++++++ src/ponytail/config.rs | 23 +++--- src/ponytail/instructions.rs | 26 +++--- src/ponytail/mod.rs | 9 +-- 4 files changed, 177 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba5d6458..ecdfa402 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -51,7 +60,9 @@ dependencies = [ "built", "chrono", "clap", + "color-eyre", "dirs", + "eyre", "flate2", "insta", "pbkdf2", @@ -64,6 +75,7 @@ dependencies = [ "serde_json", "sha2", "tar", + "thiserror", "tokio", "ureq", "zip", @@ -154,6 +166,21 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base64" version = "0.22.1" @@ -309,6 +336,33 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -531,6 +585,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -734,6 +798,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "hashbrown" version = "0.16.1" @@ -909,6 +979,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "indexmap" version = "2.14.0" @@ -974,6 +1050,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.186" @@ -1070,6 +1152,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1094,6 +1185,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + [[package]] name = "password-hash" version = "0.5.0" @@ -1373,6 +1470,12 @@ dependencies = [ "sqlite-wasm-rs", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustix" version = "1.1.4" @@ -1529,6 +1632,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "2.0.1" @@ -1655,6 +1767,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.53" @@ -1748,6 +1869,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] @@ -1820,6 +1963,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/src/ponytail/config.rs b/src/ponytail/config.rs index c554f6ff..5ace21a7 100644 --- a/src/ponytail/config.rs +++ b/src/ponytail/config.rs @@ -42,19 +42,17 @@ struct ConfigFile { } pub fn default_mode() -> String { - if let Ok(val) = std::env::var("PONYTAIL_DEFAULT_MODE") { - if let Some(m) = normalize_config_mode(&val) { - return m.to_string(); - } + if let Ok(val) = std::env::var("PONYTAIL_DEFAULT_MODE") + && let Some(m) = normalize_config_mode(&val) + { + return m.to_string(); } - if let Ok(data) = std::fs::read_to_string(config_path()) { - if let Ok(cfg) = serde_json::from_str::(&data) { - if let Some(mode) = cfg.default_mode { - if let Some(m) = normalize_config_mode(&mode) { - return m.to_string(); - } - } - } + if let Ok(data) = std::fs::read_to_string(config_path()) + && let Ok(cfg) = serde_json::from_str::(&data) + && let Some(mode) = cfg.default_mode + && let Some(m) = normalize_config_mode(&mode) + { + return m.to_string(); } DEFAULT_MODE.to_string() } @@ -74,6 +72,7 @@ pub fn set_default_mode(mode: &str) -> Result<(), String> { } #[cfg(test)] +#[allow(unsafe_code)] mod tests { use super::*; diff --git a/src/ponytail/instructions.rs b/src/ponytail/instructions.rs index 483ba6f4..8ebfd143 100644 --- a/src/ponytail/instructions.rs +++ b/src/ponytail/instructions.rs @@ -4,6 +4,7 @@ use std::path::Path; static EMBEDDED_SKILL: &str = include_str!("skill.md"); pub struct Instructions { + #[allow(dead_code)] pub mode: String, pub body: String, } @@ -35,20 +36,20 @@ pub fn filter_skill_body(body: &str, mode: &str) -> String { let effective = config::normalize_mode(mode).unwrap_or(config::DEFAULT_MODE); body.lines() .filter(|line| { - if let Some(cap) = line.trim().strip_prefix("| **") { - if let Some(end) = cap.find("** |") { - let label_mode = config::normalize_mode(&cap[..end]); - if label_mode.is_some() { - return label_mode.unwrap() == effective; - } + if let Some(cap) = line.trim().strip_prefix("| **") + && let Some(end) = cap.find("** |") + { + let label_mode = config::normalize_mode(&cap[..end]); + if let Some(lm) = label_mode { + return lm == effective; } } - if let Some(rest) = line.trim().strip_prefix("- ") { - if let Some(colon) = rest.find(':') { - let label_mode = config::normalize_mode(rest[..colon].trim()); - if label_mode.is_some() { - return label_mode.unwrap() == effective; - } + if let Some(rest) = line.trim().strip_prefix("- ") + && let Some(colon) = rest.find(':') + { + let label_mode = config::normalize_mode(rest[..colon].trim()); + if let Some(lm) = label_mode { + return lm == effective; } } true @@ -57,6 +58,7 @@ pub fn filter_skill_body(body: &str, mode: &str) -> String { .join("\n") } +#[allow(dead_code)] pub fn fallback_instructions(mode: &str) -> String { let m = config::normalize_mode(mode).unwrap_or(config::DEFAULT_MODE); format!( diff --git a/src/ponytail/mod.rs b/src/ponytail/mod.rs index 98c22b6d..6389143e 100644 --- a/src/ponytail/mod.rs +++ b/src/ponytail/mod.rs @@ -4,11 +4,8 @@ pub mod platform; pub mod state; pub mod switcher; -pub use config::{ - default_mode, is_deactivation, normalize_config_mode, normalize_mode, - normalize_persisted_mode, set_default_mode, DEFAULT_MODE, RUNTIME_MODES, VALID_MODES, -}; -pub use instructions::{build as build_instructions, fallback_instructions, Instructions}; -pub use platform::{detect as detect_platform, format_hook_output, AgentPlatform}; +pub use config::{default_mode, normalize_config_mode, set_default_mode}; +pub use instructions::build as build_instructions; +pub use platform::{detect as detect_platform, format_hook_output}; pub use state::{active_mode, clear_active, set_active}; pub use switcher::{detect as detect_switch, SwitchAction}; From af62e7600ad6c93bee41b1aa83d3dc597b7d0f89 Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 18:22:36 +0530 Subject: [PATCH 11/13] feat(ponytail): implement SKILL.md download for setup and update commands --- src/main.rs | 23 +++++++++++++++++++++-- src/ponytail/instructions.rs | 27 +++++++++++++++++++++++++++ src/ponytail/mod.rs | 2 +- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index e77c656b..394bb372 100644 --- a/src/main.rs +++ b/src/main.rs @@ -539,7 +539,20 @@ fn main() { } Commands::Ponytail { action } => match action { PonytailAction::Setup => { - println!("download SKILL.md to cache, print per-platform hook configs"); + match ponytail::download_skill() { + Ok(path) => { + println!("SKILL.md saved to {path}"); + println!("Hook config: add to agent hook settings:"); + println!(" Claude Code: agentflare ponytail hook session-start"); + println!(" Codex: agentflare ponytail hook session-start"); + println!(" Copilot: agentflare ponytail hook session-start"); + println!(" Statusline: agentflare ponytail hook statusline"); + } + Err(e) => { + eprintln!("download failed: {e}"); + std::process::exit(1); + } + } } PonytailAction::Status => { let mode = ponytail::active_mode().unwrap_or_else(ponytail::default_mode); @@ -567,7 +580,13 @@ fn main() { println!("off"); } PonytailAction::Update => { - println!("re-download SKILL.md from ponytail repo"); + match ponytail::download_skill() { + Ok(path) => println!("SKILL.md updated at {path}"), + Err(e) => { + eprintln!("update failed: {e}"); + std::process::exit(1); + } + } } PonytailAction::Hook { event } => match event { PonytailHookEvent::SessionStart => { diff --git a/src/ponytail/instructions.rs b/src/ponytail/instructions.rs index 8ebfd143..53c7d5ab 100644 --- a/src/ponytail/instructions.rs +++ b/src/ponytail/instructions.rs @@ -9,6 +9,33 @@ pub struct Instructions { pub body: String, } +const SKILL_URL: &str = + "https://raw.githubusercontent.com/DietrichGebert/ponytail/main/skills/ponytail/SKILL.md"; + +pub fn skill_cache_path() -> std::path::PathBuf { + dirs::cache_dir() + .unwrap_or_else(|| std::path::PathBuf::from(".")) + .join("agentflare") + .join("ponytail") + .join("SKILL.md") +} + +pub fn download_skill() -> Result { + let resp = ureq::get(SKILL_URL) + .call() + .map_err(|e| format!("fetch failed: {e}"))?; + if resp.status() != 200 { + return Err(format!("HTTP {}", resp.status())); + } + let body = resp.into_string().map_err(|e| format!("read failed: {e}"))?; + let path = skill_cache_path(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| format!("mkdir: {e}"))?; + } + std::fs::write(&path, &body).map_err(|e| format!("write: {e}"))?; + Ok(path.display().to_string()) +} + pub fn build(mode: &str, skill_path: Option<&Path>) -> Instructions { let effective = config::normalize_persisted_mode(mode) .unwrap_or(config::DEFAULT_MODE); diff --git a/src/ponytail/mod.rs b/src/ponytail/mod.rs index 6389143e..823d5e06 100644 --- a/src/ponytail/mod.rs +++ b/src/ponytail/mod.rs @@ -5,7 +5,7 @@ pub mod state; pub mod switcher; pub use config::{default_mode, normalize_config_mode, set_default_mode}; -pub use instructions::build as build_instructions; +pub use instructions::{build as build_instructions, download_skill}; pub use platform::{detect as detect_platform, format_hook_output}; pub use state::{active_mode, clear_active, set_active}; pub use switcher::{detect as detect_switch, SwitchAction}; From 5c325a14e8502faf223dc6e871e97f90247874cb Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 18:26:51 +0530 Subject: [PATCH 12/13] =?UTF-8?q?feat(ponytail):=20add=20sub-skills=20?= =?UTF-8?q?=E2=80=94=20review,=20audit,=20debt,=20gain,=20help?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .superpowers/sdd/progress.md | 1 + src/main.rs | 25 +++++++++++++ src/ponytail/config.rs | 4 +- src/ponytail/instructions.rs | 9 +++++ src/ponytail/mod.rs | 1 + src/ponytail/skill-audit.md | 41 +++++++++++++++++++++ src/ponytail/skill-debt.md | 44 ++++++++++++++++++++++ src/ponytail/skill-gain.md | 50 +++++++++++++++++++++++++ src/ponytail/skill-help.md | 71 ++++++++++++++++++++++++++++++++++++ src/ponytail/skill-review.md | 57 +++++++++++++++++++++++++++++ src/ponytail/sub_skills.rs | 16 ++++++++ src/ponytail/switcher.rs | 32 ++++++++++++++++ 12 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 .superpowers/sdd/progress.md create mode 100644 src/ponytail/skill-audit.md create mode 100644 src/ponytail/skill-debt.md create mode 100644 src/ponytail/skill-gain.md create mode 100644 src/ponytail/skill-help.md create mode 100644 src/ponytail/skill-review.md create mode 100644 src/ponytail/sub_skills.rs diff --git a/.superpowers/sdd/progress.md b/.superpowers/sdd/progress.md new file mode 100644 index 00000000..3fb114f3 --- /dev/null +++ b/.superpowers/sdd/progress.md @@ -0,0 +1 @@ +Task 1: complete (8e9d46f..85a7f19, review clean) diff --git a/src/main.rs b/src/main.rs index 394bb372..5344d5a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -358,6 +358,16 @@ enum PonytailAction { Off, /// Re-download SKILL.md from ponytail repo to cache. Update, + /// Run ponytail-review over-engineering code review. + Review, + /// Whole-repo audit for over-engineering. + Audit, + /// Harvest ponytail: comments into a debt ledger. + Debt, + /// Show ponytail benchmark impact stats. + Gain, + /// Quick-reference card for all ponytail modes and commands. + Info, /// Hook entry point — called by agent hook systems. Not for manual use. Hook { #[command(subcommand)] @@ -588,6 +598,21 @@ fn main() { } } } + PonytailAction::Review => { + println!("{}", ponytail::sub_skills::SKILL_REVIEW); + } + PonytailAction::Audit => { + println!("{}", ponytail::sub_skills::SKILL_AUDIT); + } + PonytailAction::Debt => { + println!("{}", ponytail::sub_skills::SKILL_DEBT); + } + PonytailAction::Gain => { + println!("{}", ponytail::sub_skills::SKILL_GAIN); + } + PonytailAction::Info => { + println!("{}", ponytail::sub_skills::SKILL_HELP); + } PonytailAction::Hook { event } => match event { PonytailHookEvent::SessionStart => { let mode = ponytail::active_mode() diff --git a/src/ponytail/config.rs b/src/ponytail/config.rs index 5ace21a7..f2317f16 100644 --- a/src/ponytail/config.rs +++ b/src/ponytail/config.rs @@ -2,7 +2,9 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; pub const DEFAULT_MODE: &str = "full"; -pub const VALID_MODES: &[&str] = &["off", "lite", "full", "ultra", "review"]; +pub const VALID_MODES: &[&str] = &[ + "off", "lite", "full", "ultra", "review", "audit", "debt", "gain", "help", +]; pub const RUNTIME_MODES: &[&str] = &["off", "lite", "full", "ultra"]; pub fn normalize_mode(mode: &str) -> Option<&'static str> { diff --git a/src/ponytail/instructions.rs b/src/ponytail/instructions.rs index 53c7d5ab..45b34c2f 100644 --- a/src/ponytail/instructions.rs +++ b/src/ponytail/instructions.rs @@ -40,6 +40,15 @@ pub fn build(mode: &str, skill_path: Option<&Path>) -> Instructions { let effective = config::normalize_persisted_mode(mode) .unwrap_or(config::DEFAULT_MODE); + if crate::ponytail::sub_skills::get(effective).is_some() { + return Instructions { + mode: effective.to_string(), + body: format!( + "PONYTAIL MODE ACTIVE — level: {effective}. Behavior defined by /ponytail-{effective} skill." + ), + }; + } + let skill_body = if let Some(path) = skill_path { std::fs::read_to_string(path).unwrap_or_else(|_| EMBEDDED_SKILL.to_string()) } else { diff --git a/src/ponytail/mod.rs b/src/ponytail/mod.rs index 823d5e06..dce507f2 100644 --- a/src/ponytail/mod.rs +++ b/src/ponytail/mod.rs @@ -2,6 +2,7 @@ pub mod config; pub mod instructions; pub mod platform; pub mod state; +pub mod sub_skills; pub mod switcher; pub use config::{default_mode, normalize_config_mode, set_default_mode}; diff --git a/src/ponytail/skill-audit.md b/src/ponytail/skill-audit.md new file mode 100644 index 00000000..5582d103 --- /dev/null +++ b/src/ponytail/skill-audit.md @@ -0,0 +1,41 @@ +--- +name: ponytail-audit +description: > + Whole-repo audit for over-engineering. Like ponytail-review, but scans the + entire codebase instead of a diff: a ranked list of what to delete, simplify, + or replace with stdlib/native equivalents. Use when the user says "audit this + codebase", "audit for over-engineering", "what can I delete from this repo", + "find bloat", "ponytail-audit", or "/ponytail-audit". One-shot report, does + not apply fixes. +--- + +ponytail-review, repo-wide. Scan the whole tree instead of a diff. Rank +findings biggest cut first. + +## Tags + +Same as ponytail-review: + +- `delete:` dead code, unused flexibility, speculative feature. Replacement: nothing. +- `stdlib:` hand-rolled thing the standard library ships. Name the function. +- `native:` dependency or code doing what the platform already does. Name the feature. +- `yagni:` abstraction with one implementation, config nobody sets, layer with one caller. +- `shrink:` same logic, fewer lines. Show the shorter form. + +## Hunt + +Deps the stdlib or platform already ships, single-implementation interfaces, +factories with one product, wrappers that only delegate, files exporting one +thing, dead flags and config, hand-rolled stdlib. + +## Output + +One line per finding, ranked: ` . . [path]`. +End with `net: - lines, - deps possible.` Nothing to cut: `Lean already. Ship.` + +## Boundaries + +Scope: over-engineering and complexity only. Correctness bugs, security holes, +and performance are explicitly out of scope. Route them to a normal review +pass. Lists findings, applies nothing. One-shot. +"stop ponytail-audit" or "normal mode" to revert. diff --git a/src/ponytail/skill-debt.md b/src/ponytail/skill-debt.md new file mode 100644 index 00000000..ecbc0ca8 --- /dev/null +++ b/src/ponytail/skill-debt.md @@ -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: + +`:, . ceiling: . upgrade: .` + +The convention is `ponytail: , `, so pull the ceiling +and the trigger straight from the comment. Want an owner per row too? add +`git blame -L,`. + +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 ` markers, 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. diff --git a/src/ponytail/skill-gain.md b/src/ponytail/skill-gain.md new file mode 100644 index 00000000..012e37b6 --- /dev/null +++ b/src/ponytail/skill-gain.md @@ -0,0 +1,50 @@ +--- +name: ponytail-gain +description: > + Show ponytail's measured impact as a compact scoreboard: less code, less + cost, more speed, from the benchmark medians. One-shot display, not a + persistent mode, and not a per-repo number. Trigger: /ponytail-gain, + "ponytail gain", "what does ponytail save", "show ponytail impact", + "ponytail scoreboard". +--- + +# Ponytail Gain + +Display this scoreboard when invoked. One-shot: do NOT change mode, write flag +files, or persist anything. + +The figures are the published benchmark medians (5 everyday tasks: email +validator, debounce, CSV sum, countdown timer, rate limiter; three models: +Haiku, Sonnet, Opus). They are measured, not computed from the current repo. +Source: `benchmarks/` and the README. + +## Scoreboard + +Render plain ASCII bars. The bar length shows the measured range; the label +carries the exact figure: + +``` + ponytail gain benchmark median · 5 tasks · 3 models + + Lines of code no-skill ████████████████████ 100% + ponytail ██▌················· 6–20% ▼ 80–94% + Cost no-skill ████████████████████ 100% + ponytail █████▌·············· 23–53% ▼ 47–77% + Speed ponytail ▸ 3–6× faster + + This repo: /ponytail-debt (shortcuts you deferred) + /ponytail-audit (what's still cuttable) +``` + +## Honesty boundary + +These are benchmark medians, not this repo. NEVER print a per-repo savings +number ("you saved X lines/tokens here"): the unbuilt version was never +written, so there is no real baseline to subtract from in a live repo. The +only real per-repo figures come from `/ponytail-debt` (a counted ledger), and +this card points there instead of inventing one. + +## Boundaries + +One-shot display. Edits nothing, changes no mode. +"stop ponytail" or "normal mode": revert. diff --git a/src/ponytail/skill-help.md b/src/ponytail/skill-help.md new file mode 100644 index 00000000..ba145c0e --- /dev/null +++ b/src/ponytail/skill-help.md @@ -0,0 +1,71 @@ +--- +name: ponytail-help +description: > + Quick-reference card for all ponytail modes, skills, and commands. + One-shot display, not a persistent mode. Trigger: /ponytail-help, + "ponytail help", "what ponytail commands", "how do I use ponytail". +--- + +# Ponytail Help + +Display this reference card when invoked. One-shot, do NOT change mode, +write flag files, or persist anything. + +## Levels + +| 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. | + +Level sticks until changed or session end. + +## Skills + +| Skill | Trigger | What it does | +|-------|---------|--------------| +| **ponytail** | `/ponytail` | Lazy mode itself. Simplest solution that works. | +| **ponytail-review** | `/ponytail-review` | Over-engineering review: `L42: yagni: factory, one product. Inline.` | +| **ponytail-audit** | `/ponytail-audit` | Whole-repo over-engineering audit: ranked list of what to delete. | +| **ponytail-debt** | `/ponytail-debt` | Harvest `ponytail:` shortcut comments into a tracked ledger. | +| **ponytail-gain** | `/ponytail-gain` | Measured-impact scoreboard: less code, less cost, more speed. | +| **ponytail-help** | `/ponytail-help` | This card. | + +Codex uses `@ponytail`, `@ponytail-review`, and `@ponytail-help`; Claude Code +and OpenCode use the slash-command forms above (OpenCode ships all six as +slash commands). + +## Deactivate + +Say "stop ponytail" or "normal mode". Resume anytime with `/ponytail`. +`/ponytail off` also works. + +## Configure Default Mode + +Default mode = `full`, auto-active every session. Change it: + +**Environment variable** (highest priority): +```bash +export PONYTAIL_DEFAULT_MODE=ultra +``` + +**Config file** (`~/.config/ponytail/config.json`, Windows: `%APPDATA%\ponytail\config.json`): +```json +{ "defaultMode": "lite" } +``` + +Set `"off"` to disable auto-activation on session start, activate manually +with `/ponytail` when wanted. + +Resolution: env var > config file > `full`. + +## Update + +Enable auto-update once: open `/plugin`, go to Marketplaces, pick ponytail, Enable auto-update. Claude Code then pulls new versions at startup (run `/reload-plugins` when it prompts). Manual refresh: `/plugin marketplace update ponytail` then `/reload-plugins`. + +If `/plugin` is not recognized, your Claude Code is out of date. Update it (`npm install -g @anthropic-ai/claude-code@latest`, or `brew upgrade claude-code`) and restart. Other hosts use their own update flow. + +## More + +Full docs + examples: https://github.com/DietrichGebert/ponytail diff --git a/src/ponytail/skill-review.md b/src/ponytail/skill-review.md new file mode 100644 index 00000000..e137a855 --- /dev/null +++ b/src/ponytail/skill-review.md @@ -0,0 +1,57 @@ +--- +name: ponytail-review +description: > + Code review focused exclusively on over-engineering. Finds what to delete: + reinvented standard library, unneeded dependencies, speculative abstractions, + dead flexibility. One line per finding: location, what to cut, what replaces + it. Use when the user says "review for over-engineering", "what can we + delete", "is this over-engineered", "simplify review", or invokes + /ponytail-review. Complements correctness-focused review, this one only + hunts complexity. +--- + +Review diffs for unnecessary complexity. One line per finding: location, what +to cut, what replaces it. The diff's best outcome is getting shorter. + +## Format + +`L: . .`, or `:L: ...` for +multi-file diffs. + +Tags: + +- `delete:` dead code, unused flexibility, speculative feature. Replacement: nothing. +- `stdlib:` hand-rolled thing the standard library ships. Name the function. +- `native:` dependency or code doing what the platform already does. Name the feature. +- `yagni:` abstraction with one implementation, config nobody sets, layer with one caller. +- `shrink:` same logic, fewer lines. Show the shorter form. + +## Examples + +❌ "This EmailValidator class might be more complex than necessary, have you +considered whether all these validation rules are needed at this stage?" + +✅ `L12-38: stdlib: 27-line validator class. "@" in email, 1 line, real validation is the confirmation mail.` + +✅ `L4: native: moment.js imported for one format call. Intl.DateTimeFormat, 0 deps.` + +✅ `repo.py:L88: yagni: AbstractRepository with one implementation. Inline it until a second one exists.` + +✅ `L52-71: delete: retry wrapper around an idempotent local call. Nothing replaces it.` + +✅ `L30-44: shrink: manual loop builds dict. dict(zip(keys, values)), 1 line.` + +## Scoring + +End with the only metric that matters: `net: - lines possible.` + +If there is nothing to cut, say `Lean already. Ship.` and stop. + +## Boundaries + +Scope: over-engineering and complexity only. Correctness bugs, security holes, +and performance are explicitly out of scope. Route them to a normal review +pass, not this one. A single smoke test or `assert`-based +self-check is the ponytail minimum, not bloat, never flag it for deletion. +Does not apply the fixes, only lists them. +"stop ponytail-review" or "normal mode": revert to verbose review style. diff --git a/src/ponytail/sub_skills.rs b/src/ponytail/sub_skills.rs new file mode 100644 index 00000000..0669781e --- /dev/null +++ b/src/ponytail/sub_skills.rs @@ -0,0 +1,16 @@ +pub const SKILL_REVIEW: &str = include_str!("skill-review.md"); +pub const SKILL_AUDIT: &str = include_str!("skill-audit.md"); +pub const SKILL_DEBT: &str = include_str!("skill-debt.md"); +pub const SKILL_GAIN: &str = include_str!("skill-gain.md"); +pub const SKILL_HELP: &str = include_str!("skill-help.md"); + +pub fn get(name: &str) -> Option<&'static str> { + match name { + "review" => Some(SKILL_REVIEW), + "audit" => Some(SKILL_AUDIT), + "debt" => Some(SKILL_DEBT), + "gain" => Some(SKILL_GAIN), + "help" => Some(SKILL_HELP), + _ => None, + } +} diff --git a/src/ponytail/switcher.rs b/src/ponytail/switcher.rs index 19cf0889..44102179 100644 --- a/src/ponytail/switcher.rs +++ b/src/ponytail/switcher.rs @@ -13,6 +13,19 @@ pub fn detect(input: &str) -> Option { return Some(SwitchAction::Off); } + for skill in &["review", "audit", "debt", "gain", "help"] { + let prefixed = format!("/ponytail-{skill}"); + let alt = format!("/ponytail:{skill}"); + if prompt == prefixed || prompt.starts_with(&format!("{prefixed} ")) { + config::normalize_config_mode(skill)?; + return Some(SwitchAction::SetMode(skill.to_string())); + } + if prompt == alt || prompt.starts_with(&format!("{alt} ")) { + config::normalize_config_mode(skill)?; + return Some(SwitchAction::SetMode(skill.to_string())); + } + } + let cmd = prompt .strip_prefix("/ponytail") .or_else(|| prompt.strip_prefix("@ponytail")) @@ -30,6 +43,10 @@ pub fn detect(input: &str) -> Option { match sub { "off" => Some(SwitchAction::Off), + "review" | "audit" | "debt" | "gain" | "help" => { + config::normalize_config_mode(sub)?; + Some(SwitchAction::SetMode(sub.to_string())) + } "default" => { let dmode = arg; if dmode.is_empty() { @@ -72,4 +89,19 @@ mod tests { assert!(detect("let's talk about ponytail").is_none()); assert!(detect("").is_none()); } + + #[test] + fn detects_sub_skill_review() { + assert!(matches!(detect("/ponytail-review"), Some(SwitchAction::SetMode(m)) if m == "review")); + } + + #[test] + fn detects_sub_skill_audit() { + assert!(matches!(detect("/ponytail-audit"), Some(SwitchAction::SetMode(m)) if m == "audit")); + } + + #[test] + fn detects_sub_skill_inline() { + assert!(matches!(detect("/ponytail review"), Some(SwitchAction::SetMode(m)) if m == "review")); + } } From 75f04ef53434d89c62debced33a23f62e7dcba58 Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 20:45:55 +0530 Subject: [PATCH 13/13] =?UTF-8?q?fix(ponytail):=20address=20coderabbit=20r?= =?UTF-8?q?eview=20=E2=80=94=20normalized=20mode,=20cache=20path,=20timeou?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ponytail/instructions.rs | 8 ++------ src/ponytail/switcher.rs | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/ponytail/instructions.rs b/src/ponytail/instructions.rs index 45b34c2f..5e293ae2 100644 --- a/src/ponytail/instructions.rs +++ b/src/ponytail/instructions.rs @@ -22,6 +22,7 @@ pub fn skill_cache_path() -> std::path::PathBuf { pub fn download_skill() -> Result { let resp = ureq::get(SKILL_URL) + .timeout(std::time::Duration::from_secs(30)) .call() .map_err(|e| format!("fetch failed: {e}"))?; if resp.status() != 200 { @@ -52,12 +53,7 @@ pub fn build(mode: &str, skill_path: Option<&Path>) -> Instructions { let skill_body = if let Some(path) = skill_path { std::fs::read_to_string(path).unwrap_or_else(|_| EMBEDDED_SKILL.to_string()) } else { - let cache = dirs::cache_dir() - .unwrap_or_else(|| std::path::PathBuf::from(".")) - .join("agentflare") - .join("ponytail") - .join("SKILL.md"); - std::fs::read_to_string(&cache).unwrap_or_else(|_| EMBEDDED_SKILL.to_string()) + std::fs::read_to_string(skill_cache_path()).unwrap_or_else(|_| EMBEDDED_SKILL.to_string()) }; let filtered = filter_skill_body(&skill_body, effective); diff --git a/src/ponytail/switcher.rs b/src/ponytail/switcher.rs index 44102179..8b9b7f24 100644 --- a/src/ponytail/switcher.rs +++ b/src/ponytail/switcher.rs @@ -17,12 +17,12 @@ pub fn detect(input: &str) -> Option { let prefixed = format!("/ponytail-{skill}"); let alt = format!("/ponytail:{skill}"); if prompt == prefixed || prompt.starts_with(&format!("{prefixed} ")) { - config::normalize_config_mode(skill)?; - return Some(SwitchAction::SetMode(skill.to_string())); + let normalized = config::normalize_config_mode(skill)?; + return Some(SwitchAction::SetMode(normalized.to_string())); } if prompt == alt || prompt.starts_with(&format!("{alt} ")) { - config::normalize_config_mode(skill)?; - return Some(SwitchAction::SetMode(skill.to_string())); + let normalized = config::normalize_config_mode(skill)?; + return Some(SwitchAction::SetMode(normalized.to_string())); } } @@ -37,23 +37,23 @@ pub fn detect(input: &str) -> Option { if sub.is_empty() || sub == "lite" || sub == "full" || sub == "ultra" { let mode = if sub.is_empty() { "full" } else { sub }; - config::normalize_config_mode(mode)?; - return Some(SwitchAction::SetMode(mode.to_string())); + let normalized = config::normalize_config_mode(mode)?; + return Some(SwitchAction::SetMode(normalized.to_string())); } match sub { "off" => Some(SwitchAction::Off), "review" | "audit" | "debt" | "gain" | "help" => { - config::normalize_config_mode(sub)?; - Some(SwitchAction::SetMode(sub.to_string())) + let normalized = config::normalize_config_mode(sub)?; + Some(SwitchAction::SetMode(normalized.to_string())) } "default" => { let dmode = arg; if dmode.is_empty() { return None; } - config::normalize_config_mode(dmode)?; - Some(SwitchAction::SetDefault(dmode.to_string())) + let normalized = config::normalize_config_mode(dmode)?; + Some(SwitchAction::SetDefault(normalized.to_string())) } _ => None, }