From c15db8d3c9bd54e2587a7ed3953fda9ab5cb8ba3 Mon Sep 17 00:00:00 2001 From: Paul Ogier <59232403+PaulOgier@users.noreply.github.com> Date: Sat, 13 Jun 2026 01:43:26 +0200 Subject: [PATCH] fix: stop mode filter stripping rule bullets with a colon filterSkillBodyForMode only filters lines whose label is a real mode (lite/full/ultra). Rule bullets like 'No unrequested abstractions:' and the 'ponytail:' comment convention were being stripped from injected instructions in every mode. Adds regression test. --- hooks/ponytail-instructions.js | 18 ++++++++++++++---- pi-extension/test/helpers.test.js | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/hooks/ponytail-instructions.js b/hooks/ponytail-instructions.js index c68e600..5cf884b 100644 --- a/hooks/ponytail-instructions.js +++ b/hooks/ponytail-instructions.js @@ -12,14 +12,24 @@ function filterSkillBodyForMode(body, mode) { const effectiveMode = normalizeMode(mode) || DEFAULT_MODE; const withoutFrontmatter = String(body || '').replace(/^---[\s\S]*?---\s*/, ''); + // Only the intensity table rows and worked examples are mode-specific, and + // both are keyed by a mode name (lite/full/ultra). A bullet whose label is + // not a mode — e.g. "No unrequested abstractions: ..." — is a normal rule + // and must be kept verbatim. return withoutFrontmatter .split(/\r?\n/) .filter((line) => { - const tableMatch = line.match(/^\|\s*\*\*(.+?)\*\*\s*\|/); - if (tableMatch) return tableMatch[1].trim() === effectiveMode; + const tableLabel = line.match(/^\|\s*\*\*(.+?)\*\*\s*\|/); + if (tableLabel) { + const labelMode = normalizeMode(tableLabel[1].trim()); + if (labelMode) return labelMode === effectiveMode; + } - const exampleMatch = line.match(/^-\s*([^:]+):\s*/); - if (exampleMatch) return exampleMatch[1].trim() === effectiveMode; + const exampleLabel = line.match(/^-\s*([^:]+):\s*/); + if (exampleLabel) { + const labelMode = normalizeMode(exampleLabel[1].trim()); + if (labelMode) return labelMode === effectiveMode; + } return true; }) diff --git a/pi-extension/test/helpers.test.js b/pi-extension/test/helpers.test.js index 8871cce..bbcdfd2 100644 --- a/pi-extension/test/helpers.test.js +++ b/pi-extension/test/helpers.test.js @@ -66,3 +66,20 @@ test("filterSkillBodyForMode keeps only requested intensity examples and rows", assert.ok(filtered.includes("Ultra example")); assert.ok(filtered.includes("Other line")); }); + +test("filterSkillBodyForMode keeps rule bullets that contain a colon", () => { + // Regression: rule bullets outside the Intensity section (e.g. the + // "No unrequested abstractions:" rule or the `ponytail:` comment convention) + // contain a colon and must not be mistaken for mode-example lines. + const skillPath = join(import.meta.dirname, "..", "..", "skills", "ponytail", "SKILL.md"); + const body = readFileSync(skillPath, "utf8"); + + const filtered = filterSkillBodyForMode(body, "full"); + + assert.ok(filtered.includes("No unrequested abstractions")); + assert.ok(filtered.includes("Mark deliberate simplifications")); + // The Intensity examples are still filtered down to the active mode. + assert.ok(filtered.includes('full: "`@lru_cache')); + assert.ok(!filtered.includes('lite: "Done')); + assert.ok(!filtered.includes('ultra: "No cache')); +});