fix: stop filterSkillBodyForMode from swallowing rule bullets that start with a mode word (#571)

filterSkillBodyForMode is the shared filter every host adapter (Claude
hooks, Pi, OpenCode, the MCP server, Hermes) uses to trim the ponytail
skill body down to the active intensity. Its worked-example detection
matched any bullet shaped "- label: text" and treated it as a
mode-specific example whenever the label happened to normalize to a
mode name (lite/full/ultra/off) -- silently dropping it in every other
mode.

The current SKILL.md doesn't trip this today (its only such bullets are
the three real worked examples), but it's a landmine: an ordinary rule
bullet added later that happens to start with "Full:" or "Lite:" would
vanish from every other mode's instructions with no warning, in the one
code path every adapter shares. Confirmed with a real bullet:

  filterSkillBodyForMode('- Full: real rule text.', 'ultra')

silently drops the line before this fix.

All three real worked examples are quoted (`- lite: "..."`); ordinary
rule bullets aren't. Required the quote so only genuine examples are
mode-filtered, and added a regression test plus fixed an existing test
whose synthetic fixture used unquoted examples that don't match the
real file's format.
This commit is contained in:
Dhruv Garg 2026-07-10 06:34:06 +05:30 committed by GitHub
parent b6c04480c0
commit 0cdd11fe0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 22 additions and 2 deletions

View File

@ -25,7 +25,11 @@ function filterSkillBodyForMode(body, mode) {
if (labelMode) return labelMode === effectiveMode;
}
const exampleLabel = line.match(/^-\s*([^:]+):\s*/);
// Require a quoted value: every worked example is `- lite: "..."`. Without
// this, an ordinary rule bullet that happens to start with a mode word
// (e.g. "- Full: ...") is silently dropped in every other mode — it looks
// like a worked example but is really prose meant to survive verbatim.
const exampleLabel = line.match(/^-\s*([^:]+):\s*"/);
if (exampleLabel) {
const labelMode = normalizeMode(exampleLabel[1].trim());
if (labelMode) return labelMode === effectiveMode;

View File

@ -104,7 +104,9 @@ test("readQuietStartup resolves env var, config file, and default in that order"
});
test("filterSkillBodyForMode keeps only requested intensity examples and rows", () => {
const body = `---\nname: ponytail\n---\n| **lite** | keep lite |\n| **full** | keep full |\n| **ultra** | keep ultra |\n- lite: Lite example\n- full: Full example\n- ultra: Ultra example\nOther line`;
// Examples are quoted in the real SKILL.md (`- lite: "..."`) — match that
// shape here too; see the next test for why the quote is load-bearing.
const body = `---\nname: ponytail\n---\n| **lite** | keep lite |\n| **full** | keep full |\n| **ultra** | keep ultra |\n- lite: "Lite example"\n- full: "Full example"\n- ultra: "Ultra example"\nOther line`;
const filtered = filterSkillBodyForMode(body, "ultra");
@ -116,6 +118,20 @@ test("filterSkillBodyForMode keeps only requested intensity examples and rows",
assert.ok(filtered.includes("Other line"));
});
test("filterSkillBodyForMode does not drop a rule bullet whose label matches a mode name", () => {
// A rule bullet like "- Full: ..." has the same "label: text" shape as a
// worked example, but isn't one — it must survive in every mode. Only the
// quoted, `- lite: "..."`-style bullets are real per-mode examples.
const body = `- Full: do not confuse this rule label with the mode name.\n- Lite: same risk, this is a real rule bullet.\n- lite: "real worked example"\n- ultra: "real worked example"`;
const filtered = filterSkillBodyForMode(body, "ultra");
assert.ok(filtered.includes("Full: do not confuse"), "an unquoted rule bullet must not be treated as a mode example");
assert.ok(filtered.includes("Lite: same risk"), "an unquoted rule bullet must not be treated as a mode example");
assert.ok(!filtered.includes("- lite:"), "the real quoted lite example must still be filtered out in ultra mode");
assert.ok(filtered.includes('ultra: "real worked example"'));
});
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)