fix(skills): extend CRLF fence handling to stripFrontmatter + derived-manifest name parse

Same root cause as the parent commit, two sibling LF-only fence regexes:
- src/core/skill-brain-first.ts FRONTMATTER_RE: on CRLF skills,
  stripFrontmatter was a no-op, so a frontmatter 'tools: [web_search]'
  leaked into the body scan and false-flagged the skill (the exact F6
  case the code comments guard against). Regression test added.
- src/core/skill-manifest.ts parseSkillName: derived-manifest mode
  (no manifest.json) fell back to the dirname for CRLF skills instead
  of the frontmatter name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-22 11:06:35 -07:00
co-authored by Claude Fable 5
parent 6f52ee8321
commit e45566d0e8
3 changed files with 19 additions and 3 deletions
+4 -2
View File
@@ -162,9 +162,11 @@ export const PHASE_HEADING_RE = /^##+\s*(?:Phase\s*1|Step\s*0)\b[^\n]*brain/im;
/**
* Frontmatter fence regex used by body extraction. Conservative match:
* leading `---\n` through the next `\n---` (greedy stop). Matches the
* shape `parseSkillFrontmatter` already accepts.
* shape `parseSkillFrontmatter` already accepts, including CRLF fences —
* if this stayed LF-only, a CRLF skill's `tools: [web_search]` frontmatter
* would survive into the body scan and false-flag the skill (F6).
*/
const FRONTMATTER_RE = /^---\n[\s\S]*?\n---\n?/;
const FRONTMATTER_RE = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
// ---------------------------------------------------------------------------
// Hardcoded EXEMPT_SKILLS (CMT1 — replaces the dropped upgrade migration)
+1 -1
View File
@@ -47,7 +47,7 @@ export interface ManifestLoadResult {
function parseSkillName(skillMdPath: string): string | null {
try {
const content = readFileSync(skillMdPath, 'utf-8');
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!fmMatch) return null;
const fm = fmMatch[1];
// Match `name: foo` or `name: "foo"` or `name: 'foo'`
+14
View File
@@ -227,6 +227,20 @@ describe('stripFrontmatter', () => {
// Body should NOT contain `web_search` (it was in the stripped frontmatter).
expect(body.includes('web_search')).toBe(false);
});
test('F6 holds for CRLF fences — frontmatter tools do not leak into the body scan', () => {
const content = [
'---',
'name: x',
'tools: [web_search]',
'---',
'',
'# x',
'Body says gbrain search comes first.',
].join('\r\n');
const body = stripFrontmatter(content);
expect(body.includes('web_search')).toBe(false);
});
});
describe('offset helpers', () => {