mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 17:02:19 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e45566d0e8 | ||
|
|
6f52ee8321 |
@@ -1411,6 +1411,7 @@ This is the dispatcher. Skills are the implementation. **Read the skill file bef
|
||||
| "get more out of gbrain", "is my brain set up right", "weekly brain checkup", "advise me on my brain", "gbrain advisor" | `skills/gbrain-advisor/SKILL.md` |
|
||||
| Save or load reports | `skills/reports/SKILL.md` |
|
||||
| "Create a skill", "improve this skill" | `skills/skill-creator/SKILL.md` |
|
||||
| "optimize this skill", "tune the skill against the benchmark", "make the skill better", "run skillopt" | `skills/skill-optimizer/SKILL.md` |
|
||||
| "Skillify this", "is this a skill?", "make this proper" | `skills/skillify/SKILL.md` |
|
||||
| "Compress my resolver", "AGENTS.md too large", "RESOLVER.md too big", "functional area dispatcher", "shrink routing table" | `skills/functional-area-resolver/SKILL.md` |
|
||||
| "Is gbrain healthy?", morning health check, skillpack-check | `skills/skillpack-check/SKILL.md` |
|
||||
|
||||
@@ -60,6 +60,7 @@ This is the dispatcher. Skills are the implementation. **Read the skill file bef
|
||||
| "get more out of gbrain", "is my brain set up right", "weekly brain checkup", "advise me on my brain", "gbrain advisor" | `skills/gbrain-advisor/SKILL.md` |
|
||||
| Save or load reports | `skills/reports/SKILL.md` |
|
||||
| "Create a skill", "improve this skill" | `skills/skill-creator/SKILL.md` |
|
||||
| "optimize this skill", "tune the skill against the benchmark", "make the skill better", "run skillopt" | `skills/skill-optimizer/SKILL.md` |
|
||||
| "Skillify this", "is this a skill?", "make this proper" | `skills/skillify/SKILL.md` |
|
||||
| "Compress my resolver", "AGENTS.md too large", "RESOLVER.md too big", "functional area dispatcher", "shrink routing table" | `skills/functional-area-resolver/SKILL.md` |
|
||||
| "Is gbrain healthy?", morning health check, skillpack-check | `skills/skillpack-check/SKILL.md` |
|
||||
|
||||
@@ -219,13 +219,13 @@ export function parseResolverEntries(resolverContent: string): ResolverEntry[] {
|
||||
|
||||
/** Simple YAML frontmatter parser — extracts triggers array if present. */
|
||||
function extractTriggers(skillContent: string): string[] {
|
||||
const fmMatch = skillContent.match(/^---\n([\s\S]*?)\n---/);
|
||||
const fmMatch = skillContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!fmMatch) return [];
|
||||
const fm = fmMatch[1];
|
||||
const triggersMatch = fm.match(/^triggers:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
||||
const triggersMatch = fm.match(/^triggers:\s*\r?\n((?:\s+-\s+.+\r?\n?)*)/m);
|
||||
if (!triggersMatch) return [];
|
||||
return triggersMatch[1]
|
||||
.split('\n')
|
||||
.split(/\r?\n/)
|
||||
.map(l => l.replace(/^\s+-\s+/, '').replace(/^["']|["']$/g, '').trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -82,7 +82,7 @@ export interface ParsedFrontmatter {
|
||||
* `readFileSync(path, 'utf-8')` at the boundary.
|
||||
*/
|
||||
export function parseSkillFrontmatter(content: string): ParsedFrontmatter | null {
|
||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!fmMatch) return null;
|
||||
const raw = fmMatch[1];
|
||||
const out: ParsedFrontmatter = { raw };
|
||||
@@ -139,11 +139,11 @@ function parseArrayField(raw: string, field: string): string[] | undefined {
|
||||
.filter(Boolean);
|
||||
}
|
||||
// Block form: `field:` + indented `- value` lines on subsequent lines.
|
||||
const blockRe = new RegExp(`^${field}:\\s*\\n((?:[ \\t]+-[ \\t]+[^\\n]+\\n?)+)`, 'm');
|
||||
const blockRe = new RegExp(`^${field}:\\s*\\r?\\n((?:[ \\t]+-[ \\t]+[^\\r\\n]+\\r?\\n?)+)`, 'm');
|
||||
const blockMatch = raw.match(blockRe);
|
||||
if (blockMatch) {
|
||||
return blockMatch[1]
|
||||
.split('\n')
|
||||
.split(/\r?\n/)
|
||||
.map(l => l.replace(/^[ \t]+-[ \t]+/, '').replace(/^["']|["']$/g, '').trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -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'`
|
||||
|
||||
@@ -382,6 +382,49 @@ describe("DRY detection — checkResolvable", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("CRLF frontmatter — checkResolvable", () => {
|
||||
let dir: string;
|
||||
afterEachCleanup(() => dir && rmSync(dir, { recursive: true, force: true }));
|
||||
|
||||
test("extracts triggers from Windows CRLF skill frontmatter", () => {
|
||||
dir = mkdtempSync(join(tmpdir(), "gbrain-crlf-"));
|
||||
writeFileSync(
|
||||
join(dir, "RESOLVER.md"),
|
||||
'## Test\n| Trigger | Skill |\n|-----|-----|\n| "one" | `skills/one/SKILL.md` |\n| "two" | `skills/two/SKILL.md` |\n',
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, "manifest.json"),
|
||||
JSON.stringify({
|
||||
skills: [
|
||||
{ name: "one", path: "one/SKILL.md" },
|
||||
{ name: "two", path: "two/SKILL.md" },
|
||||
],
|
||||
}, null, 2),
|
||||
);
|
||||
for (const name of ["one", "two"]) {
|
||||
mkdirSync(join(dir, name), { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, name, "SKILL.md"),
|
||||
[
|
||||
"---",
|
||||
`name: ${name}`,
|
||||
"description: test",
|
||||
"triggers:",
|
||||
" - shared windows trigger",
|
||||
"---",
|
||||
`# ${name}`,
|
||||
].join("\r\n"),
|
||||
);
|
||||
}
|
||||
|
||||
const report = checkResolvable(dir);
|
||||
// Both skills declare the same frontmatter trigger; with LF-only
|
||||
// parsing the CRLF triggers were invisible and the overlap went
|
||||
// undetected.
|
||||
expect(report.issues.filter(i => i.type === "mece_overlap")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("v0.22.4 regression — actual repo skills/ has 0 errors", () => {
|
||||
test("repo skills/ pass check-resolvable cleanly (zero errors AND zero warnings)", () => {
|
||||
// The v0.22.4 (Part A) contract was zero warnings AND zero errors.
|
||||
|
||||
@@ -103,6 +103,36 @@ describe('parseSkillFrontmatter', () => {
|
||||
expect(fm!.triggers).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
test('parses frontmatter fences and block arrays with CRLF line endings', () => {
|
||||
const content = [
|
||||
'---',
|
||||
'name: windows-skill',
|
||||
'writes_to:',
|
||||
' - people/',
|
||||
'tools:',
|
||||
' - search',
|
||||
'triggers:',
|
||||
' - windows trigger',
|
||||
'---',
|
||||
'# Windows Skill',
|
||||
].join('\r\n');
|
||||
const fm = parseSkillFrontmatter(content);
|
||||
expect(fm).not.toBeNull();
|
||||
expect(fm!.name).toBe('windows-skill');
|
||||
expect(fm!.writes_to).toEqual(['people/']);
|
||||
expect(fm!.tools).toEqual(['search']);
|
||||
expect(fm!.triggers).toEqual(['windows trigger']);
|
||||
});
|
||||
|
||||
test('block arrays still tolerate trailing whitespace after the field colon', () => {
|
||||
// Regression guard: the CRLF blockRe must keep `\\s*` as a real
|
||||
// whitespace class inside the template literal (a raw `\s` would turn
|
||||
// it into a literal "s" and silently break `triggers: ` + spaces).
|
||||
const content = '---\nname: x\ntriggers: \n - foo\n---\n';
|
||||
const fm = parseSkillFrontmatter(content);
|
||||
expect(fm!.triggers).toEqual(['foo']);
|
||||
});
|
||||
|
||||
test('canonical brain_first: exempt populates the typed field', () => {
|
||||
const content = '---\nname: x\nbrain_first: exempt\n---\n';
|
||||
const fm = parseSkillFrontmatter(content);
|
||||
@@ -197,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', () => {
|
||||
|
||||
Reference in New Issue
Block a user