From e59e5031c7a004a6ff865d47896153dc27ee7cd8 Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 7 Jul 2026 23:19:03 +0530 Subject: [PATCH] feat(ponytail): playbook skill + review scoping - Add ponytail-playbook skill (TDD-aware, red-green-refactor) - Register playbook in sub_skills, switcher, and config VALID_MODES - Review scope section: default uncommitted, optional branch/range - Closes ponytail PR audit tickets #73, #65 --- crates/ponytail/src/config.rs | 2 +- crates/ponytail/src/skill-playbook.md | 41 +++++++++++++++++++++++++++ crates/ponytail/src/skill-review.md | 7 +++++ crates/ponytail/src/sub_skills.rs | 2 ++ crates/ponytail/src/switcher.rs | 9 ++++-- 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 crates/ponytail/src/skill-playbook.md diff --git a/crates/ponytail/src/config.rs b/crates/ponytail/src/config.rs index f2317f16..72455ea0 100644 --- a/crates/ponytail/src/config.rs +++ b/crates/ponytail/src/config.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; pub const DEFAULT_MODE: &str = "full"; pub const VALID_MODES: &[&str] = &[ - "off", "lite", "full", "ultra", "review", "audit", "debt", "gain", "help", + "off", "lite", "full", "ultra", "review", "audit", "debt", "gain", "help", "playbook", ]; pub const RUNTIME_MODES: &[&str] = &["off", "lite", "full", "ultra"]; diff --git a/crates/ponytail/src/skill-playbook.md b/crates/ponytail/src/skill-playbook.md new file mode 100644 index 00000000..1e4b4a20 --- /dev/null +++ b/crates/ponytail/src/skill-playbook.md @@ -0,0 +1,41 @@ +--- +name: ponytail-playbook +description: > + TDD-aware project companion. Same lazy senior dev persona, but ensures tests + are written first (red-green-refactor), never ships untested code, and treats + the test suite as the spec. Use when the user says "ponytail-playbook", + "/ponytail-playbook", or asks for TDD-style development. +--- + +You are a lazy senior developer on a TDD-aware project. The test suite is the +spec — every behavior change starts with a failing test. + +## Rules + +1. **Red first.** Before writing implementation code, write the failing test. +2. **Green second.** The minimal code that makes the test pass. +3. **Refactor third.** Clean up, simplify, delete what's now unnecessary. +4. **Never ship untested behavior.** If there's no test, it doesn't exist. +5. **Tests are documentation.** Write them so the next developer understands + the contract from the test alone. + +## The ladder (same as ponytail, with TDD priors) + +1. Does this need to exist at all? (YAGNI applies to tests too — don't test + the framework, don't test getters.) +2. Already in this codebase? Reuse test helpers, fixtures, and patterns. +3. Stdlib does it? Use it in both code and tests. +4. Native platform feature? Use it. +5. Already-installed dependency? Use it. +6. Can it be one line? One line of code, one assertion. +7. Only then: the minimum that works — and its test. + +## Output + +After each change: +1. The test that drove it (one assertion minimum) +2. The implementation (shortest working change) +3. The refactored result (if different from step 2) +4. What was skipped, when to add it + +"stop ponytail-playbook" or "normal mode" to revert to standard ponytail. diff --git a/crates/ponytail/src/skill-review.md b/crates/ponytail/src/skill-review.md index 9bbc5cab..03e101b0 100644 --- a/crates/ponytail/src/skill-review.md +++ b/crates/ponytail/src/skill-review.md @@ -14,6 +14,13 @@ Review diffs for unnecessary complexity. Number each finding sequentially. One line per finding: location, what to cut, what replaces it. The diff's best outcome is getting shorter. +## Scope + +Default: tracked changes (`git diff HEAD`) plus untracked files (`git ls-files --cached --others --exclude-standard`). To review a different scope: +- Branch: `git diff main...` +- Range: `git diff ..` +- Entire repo: scan all source files (use `git ls-files`) + ## Format `. :L: . .` diff --git a/crates/ponytail/src/sub_skills.rs b/crates/ponytail/src/sub_skills.rs index 190e386e..17e67e29 100644 --- a/crates/ponytail/src/sub_skills.rs +++ b/crates/ponytail/src/sub_skills.rs @@ -3,6 +3,7 @@ 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 const SKILL_PLAYBOOK: &str = include_str!("skill-playbook.md"); pub fn get(name: &str) -> Option<&'static str> { match name { @@ -11,6 +12,7 @@ pub fn get(name: &str) -> Option<&'static str> { "debt" => Some(SKILL_DEBT), "gain" => Some(SKILL_GAIN), "help" => Some(SKILL_HELP), + "playbook" => Some(SKILL_PLAYBOOK), _ => None, } } diff --git a/crates/ponytail/src/switcher.rs b/crates/ponytail/src/switcher.rs index 3ec15e15..b55e27f1 100644 --- a/crates/ponytail/src/switcher.rs +++ b/crates/ponytail/src/switcher.rs @@ -13,7 +13,7 @@ pub fn detect(input: &str) -> Option { return Some(SwitchAction::Off); } - for skill in &["review", "audit", "debt", "gain", "help"] { + for skill in &["review", "audit", "debt", "gain", "help", "playbook"] { let prefixed = format!("/ponytail-{skill}"); let alt = format!("/ponytail:{skill}"); if prompt == prefixed || prompt.starts_with(&format!("{prefixed} ")) { @@ -43,7 +43,7 @@ pub fn detect(input: &str) -> Option { match sub { "off" => Some(SwitchAction::Off), - "review" | "audit" | "debt" | "gain" | "help" => { + "review" | "audit" | "debt" | "gain" | "help" | "playbook" => { let normalized = config::normalize_config_mode(sub)?; Some(SwitchAction::SetMode(normalized.to_string())) } @@ -104,4 +104,9 @@ mod tests { fn detects_sub_skill_inline() { assert!(matches!(detect("/ponytail review"), Some(SwitchAction::SetMode(m)) if m == "review")); } + + #[test] + fn detects_sub_skill_playbook() { + assert!(matches!(detect("/ponytail-playbook"), Some(SwitchAction::SetMode(m)) if m == "playbook")); + } }