fix(preferences): warn on silent parse failure for non-frontmatter files (#3310)

* fix(preferences): warn when preferences file lacks YAML frontmatter delimiters

parsePreferencesMarkdown() silently returned null for non-frontmatter
content, causing all preferences (including git.isolation: none) to be
ignored. The system fell back to default worktree isolation with no
indication to the user.

Now emits a stderr warning when a non-empty preferences file cannot be
parsed due to missing --- fences, so users know their file was skipped.

Fixes #2036

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: retrigger after perf test fix

* fix(test): update preferences test to expect heading+list parser result

The heading+list fallback parser (parseHeadingListFormat) now handles
non-frontmatter markdown content like "## Git\n- isolation: none",
so the test should expect a parsed object instead of null.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: retrigger CI

* fix: add missing closing brace for test block in preferences.test.ts

The 'unrecognized format warning' test block was missing its closing });
after the finally clause, causing TS1005 syntax error at line 535.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(preferences): include 'unrecognized format' in warn-once message (#2373)

The test filters for "unrecognized format" but the message only said
"does not use YAML frontmatter delimiters". Add the phrase so the
warn-once regression test can find its expected output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: trek-e <trek-e@users.noreply.github.com>
This commit is contained in:
Tom Boucher 2026-04-05 01:04:46 -04:00 committed by GitHub
parent 5c2d8988bb
commit 1fe316477d
2 changed files with 16 additions and 2 deletions

View file

@ -224,9 +224,13 @@ export function parsePreferencesMarkdown(content: string): GSDPreferences | null
return parseHeadingListFormat(content);
}
if (!_warnedUnrecognizedFormat) {
// Warn when a non-empty file exists but lacks frontmatter delimiters (#2036).
if (content.trim().length > 0 && !_warnedUnrecognizedFormat) {
_warnedUnrecognizedFormat = true;
console.warn("[parsePreferencesMarkdown] preferences.md exists but uses an unrecognized format — skipping.");
console.warn(
"[GSD] Warning: preferences file has unrecognized format — content does not use YAML frontmatter delimiters (---). " +
"Wrap your preferences in --- fences. See https://github.com/gsd-build/gsd-2/issues/2036",
);
}
return null;
}

View file

@ -412,6 +412,16 @@ test("unrecognized format warning is emitted at most once (#2373)", () => {
}
});
test("parsePreferencesMarkdown parses heading+list format without frontmatter (#2036)", () => {
// A GSD agent recovery session wrote preferences in markdown heading+list
// format instead of YAML frontmatter. Since the heading+list fallback parser
// was added, this format is now handled gracefully.
const content = "## Git\n\n- isolation: none\n";
const result = parsePreferencesMarkdown(content);
assert.notEqual(result, null, "heading+list content should be parsed");
assert.deepStrictEqual(result!.git, { isolation: "none" });
});
// ── Experimental preferences ─────────────────────────────────────────────────
test("experimental.rtk: true is accepted and stored", () => {