From 2840734d702da6a594f7594b4580c222fb593d0e Mon Sep 17 00:00:00 2001 From: samporter-31 <88817805+samporter-31@users.noreply.github.com> Date: Thu, 23 Jul 2026 06:43:54 +0930 Subject: [PATCH] fix(doctor): normalize CRLF in extractTriggers so Windows skill triggers parse (#1149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, `core.autocrlf=true` is the default and SKILL.md files are checked out with CRLF line endings. `extractTriggers` used regexes anchored to `\n` (`/^---\n.../` and `/^triggers:\s*\n.../`), which never matched `\r\n`, so the parser returned `[]` for every skill. Result: `gbrain doctor --fast --json` on Windows reported every skill not in `OVERLAP_WHITELIST` (39 of 42) as a false `mece_gap` warning — even though `skill_conformance` in the same run reported "42/42 skills pass". CI runs Ubuntu-only so the divergence never surfaced. Fix: normalize CRLF → LF at the top of `extractTriggers`. Single-line change preserves existing LF behavior. Function is now exported so the test can target it directly. Tests: added `describe("extractTriggers")` block covering LF input, CRLF input (regression case), missing frontmatter, missing triggers field, and quote-stripping. All 30 tests in `check-resolvable.test.ts` pass. Verified locally on Windows: `gbrain doctor --fast --json` now reports `resolver_health: ok, 42 skills, all reachable` (health_score 90 → 95). Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: Time Attakc <89218912+time-attack@users.noreply.github.com> --- src/core/check-resolvable.ts | 16 +++++++++++++--- test/check-resolvable.test.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/core/check-resolvable.ts b/src/core/check-resolvable.ts index a82b4a2c1..9f2f72980 100644 --- a/src/core/check-resolvable.ts +++ b/src/core/check-resolvable.ts @@ -217,9 +217,19 @@ export function parseResolverEntries(resolverContent: string): ResolverEntry[] { // `skillsDir/*/SKILL.md` when manifest.json is missing — the scenario // needed for AGENTS.md-only OpenClaw deployments. See D-CX-12 / F-ENG-1. -/** Simple YAML frontmatter parser — extracts triggers array if present. */ -function extractTriggers(skillContent: string): string[] { - const fmMatch = skillContent.match(/^---\n([\s\S]*?)\n---/); +/** + * Simple YAML frontmatter parser — extracts triggers array if present. + * + * Normalizes CRLF → LF before parsing so Windows checkouts (where + * `core.autocrlf=true` is the default) parse correctly. Without this, + * the `^---\n` and `^triggers:\s*\n` regexes never match because the + * file content is `---\r\n` / `triggers:\r\n`, and every skill on + * Windows is reported as `mece_gap` regardless of its actual content. + * CI runs on Ubuntu-only so the bug only surfaces in user environments. + */ +export function extractTriggers(skillContent: string): string[] { + const content = skillContent.replace(/\r\n/g, '\n'); + const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); if (!fmMatch) return []; const fm = fmMatch[1]; const triggersMatch = fm.match(/^triggers:\s*\n((?:\s+-\s+.+\n?)*)/m); diff --git a/test/check-resolvable.test.ts b/test/check-resolvable.test.ts index 7e0c10e1a..b26040b85 100644 --- a/test/check-resolvable.test.ts +++ b/test/check-resolvable.test.ts @@ -6,6 +6,7 @@ import { checkResolvable, parseResolverEntries, extractDelegationTargets, + extractTriggers, } from "../src/core/check-resolvable.ts"; const SKILLS_DIR = join(import.meta.dir, "..", "skills"); @@ -195,6 +196,39 @@ describe("parseResolverEntries", () => { }); }); +describe("extractTriggers", () => { + const LF_FRONTMATTER = + "---\nname: query\ndescription: Test\ntriggers:\n - \"what do we know\"\n - \"tell me about\"\ntools:\n - search\n---\n\n# Body\n"; + + test("parses triggers from LF-terminated frontmatter", () => { + const triggers = extractTriggers(LF_FRONTMATTER); + expect(triggers).toEqual(["what do we know", "tell me about"]); + }); + + test("parses triggers from CRLF-terminated frontmatter (Windows checkouts)", () => { + // Regression: `core.autocrlf=true` is the Windows default. Without + // CRLF→LF normalization, every Windows skill is reported as a false + // mece_gap warning because the `^---\n` regex never matches `---\r\n`. + const crlf = LF_FRONTMATTER.replace(/\n/g, "\r\n"); + const triggers = extractTriggers(crlf); + expect(triggers).toEqual(["what do we know", "tell me about"]); + }); + + test("returns [] when frontmatter is missing", () => { + expect(extractTriggers("# Just a body, no frontmatter\n")).toEqual([]); + }); + + test("returns [] when triggers field is absent from frontmatter", () => { + const fm = "---\nname: query\ndescription: Test\ntools:\n - search\n---\n"; + expect(extractTriggers(fm)).toEqual([]); + }); + + test("strips surrounding quotes from trigger values", () => { + const fm = "---\nname: x\ntriggers:\n - \"double quoted\"\n - 'single quoted'\n - unquoted\n---\n"; + expect(extractTriggers(fm)).toEqual(["double quoted", "single quoted", "unquoted"]); + }); +}); + describe("checkResolvable — real skills directory", () => { const report = checkResolvable(SKILLS_DIR);