diff --git a/.github/workflows/skill-docs.yml b/.github/workflows/skill-docs.yml index 700a8222ae..ba4c6d0ab9 100644 --- a/.github/workflows/skill-docs.yml +++ b/.github/workflows/skill-docs.yml @@ -31,3 +31,5 @@ jobs: echo "Generated Factory SKILL.md files are stale. Run: bun run gen:skill-docs --host factory" exit 1 } + - name: Validate generated skill frontmatter + run: bun run skill:frontmatter diff --git a/SKILL.md b/SKILL.md index a35e923c65..278e6317f7 100644 --- a/SKILL.md +++ b/SKILL.md @@ -2,7 +2,7 @@ name: gstack preamble-tier: 1 version: 1.1.0 -description: Fast headless browser for QA testing and site dogfooding. (gstack) +description: "Fast headless browser for QA testing and site dogfooding. (gstack)" allowed-tools: - Bash - Read diff --git a/autoplan/SKILL.md b/autoplan/SKILL.md index f8c20cd592..375d4f7813 100644 --- a/autoplan/SKILL.md +++ b/autoplan/SKILL.md @@ -2,7 +2,7 @@ name: autoplan preamble-tier: 3 version: 1.0.0 -description: Auto-review pipeline — reads the full CEO, design, eng, and DX review skills from disk and runs them sequentially with auto-decisions using 6 decision principles. (gstack) +description: "Auto-review pipeline — reads the full CEO, design, eng, and DX review skills from disk and runs them sequentially with auto-decisions using 6 decision principles. (gstack)" benefits-from: [office-hours] triggers: - run all reviews diff --git a/benchmark-models/SKILL.md b/benchmark-models/SKILL.md index 38baa88519..58f5cc9dce 100644 --- a/benchmark-models/SKILL.md +++ b/benchmark-models/SKILL.md @@ -2,7 +2,7 @@ name: benchmark-models preamble-tier: 1 version: 1.0.0 -description: Cross-model benchmark for gstack skills. (gstack) +description: "Cross-model benchmark for gstack skills. (gstack)" triggers: - cross model benchmark - compare claude gpt gemini diff --git a/benchmark/SKILL.md b/benchmark/SKILL.md index d2b54a2c4e..8315b51032 100644 --- a/benchmark/SKILL.md +++ b/benchmark/SKILL.md @@ -2,7 +2,7 @@ name: benchmark preamble-tier: 1 version: 1.0.0 -description: Performance regression detection using the browse daemon. (gstack) +description: "Performance regression detection using the browse daemon. (gstack)" triggers: - performance benchmark - check page speed diff --git a/browse/SKILL.md b/browse/SKILL.md index 9f73f00053..0e8e721947 100644 --- a/browse/SKILL.md +++ b/browse/SKILL.md @@ -2,7 +2,7 @@ name: browse preamble-tier: 1 version: 1.1.0 -description: Fast headless browser for QA testing and site dogfooding. (gstack) +description: "Fast headless browser for QA testing and site dogfooding. (gstack)" triggers: - browse a page - headless browser diff --git a/canary/SKILL.md b/canary/SKILL.md index e7a1715f8f..af83d34864 100644 --- a/canary/SKILL.md +++ b/canary/SKILL.md @@ -2,7 +2,7 @@ name: canary preamble-tier: 2 version: 1.0.0 -description: Post-deploy canary monitoring. (gstack) +description: "Post-deploy canary monitoring. (gstack)" allowed-tools: - Bash - Read diff --git a/careful/SKILL.md b/careful/SKILL.md index 678d66c16b..61fb0d50e0 100644 --- a/careful/SKILL.md +++ b/careful/SKILL.md @@ -1,7 +1,7 @@ --- name: careful version: 0.1.0 -description: Safety guardrails for destructive commands. (gstack) +description: "Safety guardrails for destructive commands. (gstack)" triggers: - be careful - warn before destructive diff --git a/codex/SKILL.md b/codex/SKILL.md index af351d7f10..011c60a448 100644 --- a/codex/SKILL.md +++ b/codex/SKILL.md @@ -2,7 +2,7 @@ name: codex preamble-tier: 3 version: 1.0.0 -description: OpenAI Codex CLI wrapper — three modes. (gstack) +description: "OpenAI Codex CLI wrapper — three modes. (gstack)" triggers: - codex review - second opinion diff --git a/context-restore/SKILL.md b/context-restore/SKILL.md index 7a272722e7..a3f9fb26f4 100644 --- a/context-restore/SKILL.md +++ b/context-restore/SKILL.md @@ -2,7 +2,7 @@ name: context-restore preamble-tier: 2 version: 1.0.0 -description: Restore working context saved earlier by /context-save. (gstack) +description: "Restore working context saved earlier by /context-save. (gstack)" allowed-tools: - Bash - Read diff --git a/context-save/SKILL.md b/context-save/SKILL.md index 014407fbe4..e560ee44ce 100644 --- a/context-save/SKILL.md +++ b/context-save/SKILL.md @@ -2,7 +2,7 @@ name: context-save preamble-tier: 2 version: 1.0.0 -description: Save working context. (gstack) +description: "Save working context. (gstack)" allowed-tools: - Bash - Read diff --git a/cso/SKILL.md b/cso/SKILL.md index ebacf1ac09..339dd54ad9 100644 --- a/cso/SKILL.md +++ b/cso/SKILL.md @@ -2,7 +2,7 @@ name: cso preamble-tier: 2 version: 2.0.0 -description: Chief Security Officer mode. (gstack) +description: "Chief Security Officer mode. (gstack)" allowed-tools: - Bash - Read diff --git a/devex-review/SKILL.md b/devex-review/SKILL.md index 14ed560d23..80fca1cfcd 100644 --- a/devex-review/SKILL.md +++ b/devex-review/SKILL.md @@ -2,7 +2,7 @@ name: devex-review preamble-tier: 3 version: 1.0.0 -description: Live developer experience audit. (gstack) +description: "Live developer experience audit. (gstack)" triggers: - live dx audit - test developer experience diff --git a/document-generate/SKILL.md b/document-generate/SKILL.md index 2c7e6f0727..43e3f9d3d6 100644 --- a/document-generate/SKILL.md +++ b/document-generate/SKILL.md @@ -2,7 +2,7 @@ name: document-generate preamble-tier: 2 version: 1.0.0 -description: Generate missing documentation from scratch for a feature, module, or entire project. (gstack) +description: "Generate missing documentation from scratch for a feature, module, or entire project. (gstack)" allowed-tools: - Bash - Read diff --git a/document-release/SKILL.md b/document-release/SKILL.md index 43ba9adb12..2934c2c563 100644 --- a/document-release/SKILL.md +++ b/document-release/SKILL.md @@ -2,7 +2,7 @@ name: document-release preamble-tier: 2 version: 1.0.0 -description: Post-ship documentation update. (gstack) +description: "Post-ship documentation update. (gstack)" allowed-tools: - Bash - Read diff --git a/freeze/SKILL.md b/freeze/SKILL.md index fc82b1bea8..3b31ce40bf 100644 --- a/freeze/SKILL.md +++ b/freeze/SKILL.md @@ -1,7 +1,7 @@ --- name: freeze version: 0.1.0 -description: Restrict file edits to a specific directory for the session. (gstack) +description: "Restrict file edits to a specific directory for the session. (gstack)" triggers: - freeze edits to directory - lock editing scope diff --git a/gstack-upgrade/SKILL.md b/gstack-upgrade/SKILL.md index 9f0f2f7ea6..cc0bf19c02 100644 --- a/gstack-upgrade/SKILL.md +++ b/gstack-upgrade/SKILL.md @@ -1,7 +1,7 @@ --- name: gstack-upgrade version: 1.1.0 -description: Upgrade gstack to the latest version. +description: "Upgrade gstack to the latest version." triggers: - upgrade gstack - update gstack version diff --git a/health/SKILL.md b/health/SKILL.md index 921a7b5b4b..3e8af16615 100644 --- a/health/SKILL.md +++ b/health/SKILL.md @@ -2,7 +2,7 @@ name: health preamble-tier: 2 version: 1.0.0 -description: Code quality dashboard. (gstack) +description: "Code quality dashboard. (gstack)" triggers: - code health check - quality dashboard diff --git a/investigate/SKILL.md b/investigate/SKILL.md index daf6be6d81..68407f6b96 100644 --- a/investigate/SKILL.md +++ b/investigate/SKILL.md @@ -2,7 +2,7 @@ name: investigate preamble-tier: 2 version: 1.0.0 -description: Systematic debugging with root cause investigation. (gstack) +description: "Systematic debugging with root cause investigation. (gstack)" allowed-tools: - Bash - Read diff --git a/ios-design-review/SKILL.md b/ios-design-review/SKILL.md index 7bfbdd851a..4b8f894f7e 100644 --- a/ios-design-review/SKILL.md +++ b/ios-design-review/SKILL.md @@ -2,7 +2,7 @@ name: ios-design-review preamble-tier: 3 version: 1.0.0 -description: Visual design audit for iOS apps on real hardware. (gstack) +description: "Visual design audit for iOS apps on real hardware. (gstack)" allowed-tools: - Bash - Read diff --git a/ios-fix/SKILL.md b/ios-fix/SKILL.md index 2d1c3d4b10..6e5ec0487c 100644 --- a/ios-fix/SKILL.md +++ b/ios-fix/SKILL.md @@ -2,7 +2,7 @@ name: ios-fix preamble-tier: 3 version: 1.0.0 -description: Autonomous iOS bug fixer. (gstack) +description: "Autonomous iOS bug fixer. (gstack)" allowed-tools: - Bash - Read diff --git a/ios-qa/SKILL.md b/ios-qa/SKILL.md index 0d40c16e55..eb47f69264 100644 --- a/ios-qa/SKILL.md +++ b/ios-qa/SKILL.md @@ -2,7 +2,7 @@ name: ios-qa preamble-tier: 3 version: 1.0.0 -description: Live-device iOS QA for SwiftUI apps. (gstack) +description: "Live-device iOS QA for SwiftUI apps. (gstack)" allowed-tools: - Bash - Read diff --git a/ios-sync/SKILL.md b/ios-sync/SKILL.md index e7a8039247..f53eb197d3 100644 --- a/ios-sync/SKILL.md +++ b/ios-sync/SKILL.md @@ -2,7 +2,7 @@ name: ios-sync preamble-tier: 3 version: 1.0.0 -description: Regenerate the iOS debug bridge against the latest upstream gstack templates. (gstack) +description: "Regenerate the iOS debug bridge against the latest upstream gstack templates. (gstack)" allowed-tools: - Bash - Read diff --git a/land-and-deploy/SKILL.md b/land-and-deploy/SKILL.md index 2eb9faa6c0..b34ab67499 100644 --- a/land-and-deploy/SKILL.md +++ b/land-and-deploy/SKILL.md @@ -2,7 +2,7 @@ name: land-and-deploy preamble-tier: 4 version: 1.0.0 -description: Land and deploy workflow. (gstack) +description: "Land and deploy workflow. (gstack)" allowed-tools: - Bash - Read diff --git a/landing-report/SKILL.md b/landing-report/SKILL.md index aec9978baf..367d4385b5 100644 --- a/landing-report/SKILL.md +++ b/landing-report/SKILL.md @@ -1,7 +1,7 @@ --- name: landing-report version: 0.1.0 -description: Read-only queue dashboard for workspace-aware ship. (gstack) +description: "Read-only queue dashboard for workspace-aware ship. (gstack)" triggers: - landing report - version queue diff --git a/learn/SKILL.md b/learn/SKILL.md index 08a78b23ca..b0a8742adf 100644 --- a/learn/SKILL.md +++ b/learn/SKILL.md @@ -2,7 +2,7 @@ name: learn preamble-tier: 2 version: 1.0.0 -description: Manage project learnings. +description: "Manage project learnings." triggers: - show learnings - what have we learned diff --git a/make-pdf/SKILL.md b/make-pdf/SKILL.md index 141c60a314..fd55adda77 100644 --- a/make-pdf/SKILL.md +++ b/make-pdf/SKILL.md @@ -2,7 +2,7 @@ name: make-pdf preamble-tier: 1 version: 1.0.0 -description: Turn any markdown file into a publication-quality PDF. (gstack) +description: "Turn any markdown file into a publication-quality PDF. (gstack)" triggers: - markdown to pdf - generate pdf diff --git a/office-hours/SKILL.md b/office-hours/SKILL.md index efa58f7def..a733b945a0 100644 --- a/office-hours/SKILL.md +++ b/office-hours/SKILL.md @@ -2,7 +2,7 @@ name: office-hours preamble-tier: 3 version: 2.0.0 -description: YC Office Hours — two modes. (gstack) +description: "YC Office Hours — two modes. (gstack)" allowed-tools: - Bash - Read diff --git a/open-gstack-browser/SKILL.md b/open-gstack-browser/SKILL.md index 64a93770e7..0f51dfd0de 100644 --- a/open-gstack-browser/SKILL.md +++ b/open-gstack-browser/SKILL.md @@ -1,7 +1,7 @@ --- name: open-gstack-browser version: 0.2.0 -description: Launch GStack Browser — AI-controlled Chromium with the sidebar extension baked in. +description: "Launch GStack Browser — AI-controlled Chromium with the sidebar extension baked in." triggers: - open gstack browser - launch chromium diff --git a/package.json b/package.json index 280299e0c3..b750a5559e 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "test:gemini": "EVALS=1 bun test test/gemini-e2e.test.ts", "test:gemini:all": "EVALS=1 EVALS_ALL=1 bun test test/gemini-e2e.test.ts", "skill:check": "bun run scripts/skill-check.ts", + "skill:frontmatter": "bun run scripts/skill-frontmatter-check.ts", "dev:skill": "bun run scripts/dev-skill.ts", "start": "bun run browse/src/server.ts", "eval:list": "bun run scripts/eval-list.ts", diff --git a/pair-agent/SKILL.md b/pair-agent/SKILL.md index 533a29dc73..3ff48a628c 100644 --- a/pair-agent/SKILL.md +++ b/pair-agent/SKILL.md @@ -1,7 +1,7 @@ --- name: pair-agent version: 0.1.0 -description: Pair a remote AI agent with your browser. (gstack) +description: "Pair a remote AI agent with your browser. (gstack)" triggers: - pair with agent - connect remote agent diff --git a/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md index 57cbf54640..3033d0a1b6 100644 --- a/plan-ceo-review/SKILL.md +++ b/plan-ceo-review/SKILL.md @@ -3,7 +3,7 @@ name: plan-ceo-review preamble-tier: 3 interactive: true version: 1.0.0 -description: CEO/founder-mode plan review. (gstack) +description: "CEO/founder-mode plan review. (gstack)" benefits-from: [office-hours] allowed-tools: - Read diff --git a/plan-design-review/SKILL.md b/plan-design-review/SKILL.md index b1b110ae15..4b931b3207 100644 --- a/plan-design-review/SKILL.md +++ b/plan-design-review/SKILL.md @@ -3,7 +3,7 @@ name: plan-design-review preamble-tier: 3 interactive: true version: 2.0.0 -description: Designer's eye plan review — interactive, like CEO and Eng review. (gstack) +description: "Designer's eye plan review — interactive, like CEO and Eng review. (gstack)" allowed-tools: - Read - Edit diff --git a/plan-devex-review/SKILL.md b/plan-devex-review/SKILL.md index 7336b70a55..e6bda0a1af 100644 --- a/plan-devex-review/SKILL.md +++ b/plan-devex-review/SKILL.md @@ -3,7 +3,7 @@ name: plan-devex-review preamble-tier: 3 interactive: true version: 2.0.0 -description: Interactive developer experience plan review. (gstack) +description: "Interactive developer experience plan review. (gstack)" benefits-from: [office-hours] allowed-tools: - Read diff --git a/plan-eng-review/SKILL.md b/plan-eng-review/SKILL.md index c4ec10bb60..c6304645ef 100644 --- a/plan-eng-review/SKILL.md +++ b/plan-eng-review/SKILL.md @@ -3,7 +3,7 @@ name: plan-eng-review preamble-tier: 3 interactive: true version: 1.0.0 -description: Eng manager-mode plan review. (gstack) +description: "Eng manager-mode plan review. (gstack)" benefits-from: [office-hours] allowed-tools: - Read diff --git a/qa-only/SKILL.md b/qa-only/SKILL.md index db1c3dd081..978bf0926e 100644 --- a/qa-only/SKILL.md +++ b/qa-only/SKILL.md @@ -2,7 +2,7 @@ name: qa-only preamble-tier: 4 version: 1.0.0 -description: Report-only QA testing. (gstack) +description: "Report-only QA testing. (gstack)" allowed-tools: - Bash - Read diff --git a/qa/SKILL.md b/qa/SKILL.md index c5fdf9b565..0250eeed69 100644 --- a/qa/SKILL.md +++ b/qa/SKILL.md @@ -2,7 +2,7 @@ name: qa preamble-tier: 4 version: 2.0.0 -description: Systematically QA test a web application and fix bugs found. (gstack) +description: "Systematically QA test a web application and fix bugs found. (gstack)" allowed-tools: - Bash - Read diff --git a/retro/SKILL.md b/retro/SKILL.md index 287f24e35d..5e16e4dfbf 100644 --- a/retro/SKILL.md +++ b/retro/SKILL.md @@ -2,7 +2,7 @@ name: retro preamble-tier: 2 version: 2.0.0 -description: Weekly engineering retrospective. (gstack) +description: "Weekly engineering retrospective. (gstack)" allowed-tools: - Bash - Read diff --git a/review/SKILL.md b/review/SKILL.md index 4d8049d540..54bde2c729 100644 --- a/review/SKILL.md +++ b/review/SKILL.md @@ -2,7 +2,7 @@ name: review preamble-tier: 4 version: 1.0.0 -description: Pre-landing PR review. (gstack) +description: "Pre-landing PR review. (gstack)" allowed-tools: - Bash - Read diff --git a/scrape/SKILL.md b/scrape/SKILL.md index 0af5db5068..5f45dbfa73 100644 --- a/scrape/SKILL.md +++ b/scrape/SKILL.md @@ -1,7 +1,7 @@ --- name: scrape version: 1.0.0 -description: Pull data from a web page. (gstack) +description: "Pull data from a web page. (gstack)" allowed-tools: - Bash - Read diff --git a/scripts/skill-check.ts b/scripts/skill-check.ts index 9182737ee1..8788695310 100644 --- a/scripts/skill-check.ts +++ b/scripts/skill-check.ts @@ -8,7 +8,7 @@ * - Freshness check (generated files match committed files) */ -import { validateSkill } from '../test/helpers/skill-parser'; +import { validateSkill, validateSkillFrontmatter } from '../test/helpers/skill-parser'; import { discoverTemplates, discoverSkillFiles } from './discover-skills'; import * as fs from 'fs'; import * as path from 'path'; @@ -35,6 +35,16 @@ let hasErrors = false; console.log(' Skills:'); for (const file of SKILL_FILES) { const fullPath = path.join(ROOT, file); + const frontmatterErrors = validateSkillFrontmatter(fullPath); + if (frontmatterErrors.length > 0) { + hasErrors = true; + console.log(` ❌ ${file.padEnd(30)} — invalid frontmatter`); + for (const err of frontmatterErrors) { + console.log(` line ${err.line}: ${err.message}`); + } + continue; + } + const result = validateSkill(fullPath); if (result.warnings.length > 0) { diff --git a/scripts/skill-frontmatter-check.ts b/scripts/skill-frontmatter-check.ts new file mode 100644 index 0000000000..b82065cd59 --- /dev/null +++ b/scripts/skill-frontmatter-check.ts @@ -0,0 +1,54 @@ +#!/usr/bin/env bun +/** + * Validate YAML frontmatter for generated SKILL.md files. + * + * This is intentionally narrower than skill:check so CI can run it after + * regenerating only the host outputs relevant to a workflow. + */ + +import { discoverSkillFiles } from './discover-skills'; +import { validateSkillFrontmatter } from '../test/helpers/skill-parser'; +import * as fs from 'fs'; +import * as path from 'path'; + +const ROOT = path.resolve(import.meta.dir, '..'); + +function walkSkillFiles(dir: string, acc: string[]): void { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === '.git' || entry.name === 'node_modules') continue; + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walkSkillFiles(fullPath, acc); + continue; + } + if (entry.name === 'SKILL.md') { + acc.push(path.relative(ROOT, fullPath)); + } + } +} + +const files = new Set(discoverSkillFiles(ROOT)); +for (const hostDir of ['.agents', '.factory']) { + const fullPath = path.join(ROOT, hostDir); + if (fs.existsSync(fullPath)) { + const hostFiles: string[] = []; + walkSkillFiles(fullPath, hostFiles); + for (const file of hostFiles) files.add(file); + } +} + +const errors: string[] = []; +for (const file of [...files].sort()) { + const fullPath = path.join(ROOT, file); + for (const error of validateSkillFrontmatter(fullPath)) { + errors.push(`${file}:${error.line}: ${error.message}`); + } +} + +if (errors.length > 0) { + console.error('Invalid SKILL.md frontmatter:'); + for (const error of errors) console.error(` ${error}`); + process.exit(1); +} + +console.log(`Validated frontmatter for ${files.size} SKILL.md files.`); diff --git a/setup-browser-cookies/SKILL.md b/setup-browser-cookies/SKILL.md index 2f91f4d687..69580b84e5 100644 --- a/setup-browser-cookies/SKILL.md +++ b/setup-browser-cookies/SKILL.md @@ -2,7 +2,7 @@ name: setup-browser-cookies preamble-tier: 1 version: 1.0.0 -description: Import cookies from your real Chromium browser into the headless browse session. (gstack) +description: "Import cookies from your real Chromium browser into the headless browse session. (gstack)" triggers: - import browser cookies - login to test site diff --git a/setup-deploy/SKILL.md b/setup-deploy/SKILL.md index a35ab97640..d652c1d6ed 100644 --- a/setup-deploy/SKILL.md +++ b/setup-deploy/SKILL.md @@ -2,7 +2,7 @@ name: setup-deploy preamble-tier: 2 version: 1.0.0 -description: Configure deployment settings for /land-and-deploy. +description: "Configure deployment settings for /land-and-deploy." triggers: - configure deploy - setup deployment diff --git a/skillify/SKILL.md b/skillify/SKILL.md index e7911473eb..cda09062ab 100644 --- a/skillify/SKILL.md +++ b/skillify/SKILL.md @@ -1,7 +1,7 @@ --- name: skillify version: 1.0.0 -description: Codify the most recent successful /scrape flow into a permanent browser-skill on disk. (gstack) +description: "Codify the most recent successful /scrape flow into a permanent browser-skill on disk. (gstack)" allowed-tools: - Bash - Read diff --git a/spec/SKILL.md b/spec/SKILL.md index 7279b9c377..dd047f79a8 100644 --- a/spec/SKILL.md +++ b/spec/SKILL.md @@ -1,7 +1,7 @@ --- name: spec version: 0.1.0 -description: Turn vague intent into a precise, executable spec in five phases. (gstack) +description: "Turn vague intent into a precise, executable spec in five phases. (gstack)" allowed-tools: - Bash - Read diff --git a/sync-gbrain/SKILL.md b/sync-gbrain/SKILL.md index 4a3a5bc1d7..307efe8fb4 100644 --- a/sync-gbrain/SKILL.md +++ b/sync-gbrain/SKILL.md @@ -2,7 +2,7 @@ name: sync-gbrain preamble-tier: 2 version: 1.0.0 -description: Keep gbrain current with this repo's code and refresh agent search guidance in CLAUDE.md. Wraps the gstack-gbrain-sync orchestrator with state (gstack) +description: "Keep gbrain current with this repo's code and refresh agent search guidance in CLAUDE.md. Wraps the gstack-gbrain-sync orchestrator with state (gstack)" triggers: - sync gbrain - refresh gbrain diff --git a/test/helpers/skill-parser.ts b/test/helpers/skill-parser.ts index 0e3271ba1b..d4152a6b6e 100644 --- a/test/helpers/skill-parser.ts +++ b/test/helpers/skill-parser.ts @@ -34,6 +34,11 @@ export interface ValidationResult { warnings: string[]; } +export interface FrontmatterError { + line: number; + message: string; +} + /** * Extract all $B invocations from bash code blocks in a SKILL.md file. */ @@ -138,6 +143,50 @@ export function validateSkill(skillPath: string): ValidationResult { return result; } +/** + * Lightweight frontmatter validation for the YAML subset used by SKILL.md. + * + * This intentionally does not try to be a complete YAML parser. It catches the + * class of errors that make agents skip skills: missing delimiters and plain + * scalar values that contain ": " without being quoted or written as a block. + */ +export function validateSkillFrontmatter(skillPath: string): FrontmatterError[] { + const content = fs.readFileSync(skillPath, 'utf-8'); + const errors: FrontmatterError[] = []; + + if (!content.startsWith('---\n')) { + return [{ line: 1, message: 'missing opening frontmatter delimiter' }]; + } + + const fmEnd = content.indexOf('\n---', 4); + if (fmEnd === -1) { + return [{ line: 1, message: 'missing closing frontmatter delimiter' }]; + } + + const frontmatter = content.slice(4, fmEnd); + const lines = frontmatter.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const match = line.match(/^\s*[A-Za-z0-9_-]+:\s+(.+?)\s*$/); + if (!match) continue; + + const value = match[1].trim(); + const isQuoted = value.startsWith('"') || value.startsWith("'"); + const isBlockScalar = value === '|' || value === '>' || value.startsWith('|') || value.startsWith('>'); + const isFlowValue = value.startsWith('[') || value.startsWith('{'); + if (isQuoted || isBlockScalar || isFlowValue) continue; + + if (/:\s/.test(value)) { + errors.push({ + line: i + 2, + message: 'plain YAML scalar contains ": "; quote it or use a block scalar', + }); + } + } + + return errors; +} + /** * Extract all REMOTE_SLUG=$(...) assignment patterns from .md files in given subdirectories. * Returns a Map from filename → array of full assignment lines found. diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts index df5cb79947..82b8385ce6 100644 --- a/test/skill-validation.test.ts +++ b/test/skill-validation.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'bun:test'; -import { validateSkill, extractRemoteSlugPatterns, extractWeightsFromTable } from './helpers/skill-parser'; +import { validateSkill, validateSkillFrontmatter, extractRemoteSlugPatterns, extractWeightsFromTable } from './helpers/skill-parser'; import { ALL_COMMANDS, COMMAND_DESCRIPTIONS, READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from '../browse/src/commands'; import { SNAPSHOT_FLAGS } from '../browse/src/snapshot'; import * as fs from 'fs'; @@ -24,6 +24,30 @@ function readShipUnion(): string { } describe('SKILL.md command validation', () => { + test('all SKILL.md files have loadable YAML frontmatter', () => { + const skillFiles: string[] = []; + const walk = (dir: string) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === '.git') continue; + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(fullPath); + } else if (entry.name === 'SKILL.md') { + skillFiles.push(fullPath); + } + } + }; + walk(ROOT); + + const errors = skillFiles.flatMap((skillPath) => + validateSkillFrontmatter(skillPath).map((error) => + `${path.relative(ROOT, skillPath)}:${error.line}: ${error.message}` + ) + ); + + expect(errors).toEqual([]); + }); + test('all $B commands in SKILL.md are valid browse commands', () => { const result = validateSkill(path.join(ROOT, 'SKILL.md')); expect(result.invalid).toHaveLength(0); diff --git a/unfreeze/SKILL.md b/unfreeze/SKILL.md index a07f1cf5f7..61b5e0aa6f 100644 --- a/unfreeze/SKILL.md +++ b/unfreeze/SKILL.md @@ -1,7 +1,7 @@ --- name: unfreeze version: 0.1.0 -description: Clear the freeze boundary set by /freeze, allowing edits to all directories again. (gstack) +description: "Clear the freeze boundary set by /freeze, allowing edits to all directories again. (gstack)" triggers: - unfreeze edits - unlock all directories