Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion openclaw/skills/gstack-openclaw-ceo-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: gstack-openclaw-ceo-review
description: CEO/founder-mode plan review. Rethink the problem, find the 10-star product, challenge premises, expand scope when it creates a better product. Four modes: SCOPE EXPANSION (dream big), SELECTIVE EXPANSION (hold scope + cherry-pick), HOLD SCOPE (maximum rigor), SCOPE REDUCTION (strip to essentials). Use when asked to review a plan, challenge this, CEO review, poke holes, think bigger, or expand scope.
description: 'CEO/founder-mode plan review. Rethink the problem, find the 10-star product, challenge premises, expand scope when it creates a better product. Four modes: SCOPE EXPANSION (dream big), SELECTIVE EXPANSION (hold scope + cherry-pick), HOLD SCOPE (maximum rigor), SCOPE REDUCTION (strip to essentials). Use when asked to review a plan, challenge this, CEO review, poke holes, think bigger, or expand scope.'
version: 1.0.0
metadata: { "openclaw": { "emoji": "👑" } }
---
Expand Down
2 changes: 1 addition & 1 deletion openclaw/skills/gstack-openclaw-investigate/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: gstack-openclaw-investigate
description: Systematic debugging with root cause investigation. Four phases: investigate, analyze, hypothesize, implement. Iron Law: no fixes without root cause. Use when asked to debug, fix a bug, investigate an error, or root cause analysis. Proactively use when user reports errors, stack traces, unexpected behavior, or says something stopped working.
description: 'Systematic debugging with root cause investigation. Four phases: investigate, analyze, hypothesize, implement. Iron Law: no fixes without root cause. Use when asked to debug, fix a bug, investigate an error, or root cause analysis. Proactively use when user reports errors, stack traces, unexpected behavior, or says something stopped working.'
version: 1.0.0
metadata: { "openclaw": { "emoji": "🔍" } }
---
Expand Down
2 changes: 1 addition & 1 deletion openclaw/skills/gstack-openclaw-office-hours/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: gstack-openclaw-office-hours
description: Product interrogation with six forcing questions. Two modes: startup diagnostic (demand reality, status quo, desperate specificity, narrowest wedge, observation, future-fit) and builder brainstorm. Use when asked to brainstorm, "is this worth building", "I have an idea", "office hours", or "help me think through this". Proactively use when user describes a new product idea or wants to think through design decisions before any code is written.
description: 'Product interrogation with six forcing questions. Two modes: startup diagnostic (demand reality, status quo, desperate specificity, narrowest wedge, observation, future-fit) and builder brainstorm. Use when asked to brainstorm, "is this worth building", "I have an idea", "office hours", or "help me think through this". Proactively use when user describes a new product idea or wants to think through design decisions before any code is written.'
version: 1.0.0
metadata: { "openclaw": { "emoji": "🎯" } }
---
Expand Down
32 changes: 22 additions & 10 deletions scripts/discover-skills.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Shared discovery for SKILL.md and .tmpl files.
* Scans root + one level of subdirs, skipping node_modules/.git/dist.
*/
// Shared discovery for SKILL.md and .tmpl files.
// Template discovery scans root + one level of subdirs.
// SKILL.md discovery walks nested directories so maintenance checks also cover
// host-specific trees such as openclaw/skills/*/SKILL.md.

import * as fs from 'fs';
import * as path from 'path';
Expand All @@ -27,13 +27,25 @@ export function discoverTemplates(root: string): Array<{ tmpl: string; output: s
}

export function discoverSkillFiles(root: string): string[] {
const dirs = ['', ...subdirs(root)];
const results: string[] = [];
for (const dir of dirs) {
const rel = dir ? `${dir}/SKILL.md` : 'SKILL.md';
if (fs.existsSync(path.join(root, rel))) {
results.push(rel);

function walk(currentDir: string, relativeDir = ''): void {
const skillRel = relativeDir ? `${relativeDir}/SKILL.md` : 'SKILL.md';
if (fs.existsSync(path.join(currentDir, 'SKILL.md'))) {
results.push(skillRel);
}

for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith('.')) continue;
if (SKIP.has(entry.name)) continue;

const nextDir = path.join(currentDir, entry.name);
const nextRel = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
walk(nextDir, nextRel);
}
}
return results;

walk(root);
return results.sort();
}
8 changes: 6 additions & 2 deletions scripts/skill-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,20 @@ for (const file of SKILL_FILES) {
const totalValid = result.valid.length;
const totalInvalid = result.invalid.length;
const totalSnapErrors = result.snapshotFlagErrors.length;
const totalFrontmatterErrors = result.frontmatterErrors.length;

if (totalInvalid > 0 || totalSnapErrors > 0) {
if (totalInvalid > 0 || totalSnapErrors > 0 || totalFrontmatterErrors > 0) {
hasErrors = true;
console.log(` \u274c ${file.padEnd(30)} — ${totalValid} valid, ${totalInvalid} invalid, ${totalSnapErrors} snapshot errors`);
console.log(` \u274c ${file.padEnd(30)} — ${totalValid} valid, ${totalInvalid} invalid, ${totalSnapErrors} snapshot errors, ${totalFrontmatterErrors} frontmatter errors`);
for (const inv of result.invalid) {
console.log(` line ${inv.line}: unknown command '${inv.command}'`);
}
for (const se of result.snapshotFlagErrors) {
console.log(` line ${se.command.line}: ${se.error}`);
}
for (const fe of result.frontmatterErrors) {
console.log(` ${fe}`);
}
} else {
console.log(` \u2705 ${file.padEnd(30)} — ${totalValid} commands, all valid`);
}
Expand Down
53 changes: 53 additions & 0 deletions test/helpers/skill-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,61 @@ export interface ValidationResult {
valid: BrowseCommand[];
invalid: BrowseCommand[];
snapshotFlagErrors: Array<{ command: BrowseCommand; error: string }>;
frontmatterErrors: string[];
warnings: string[];
}

function extractFrontmatter(content: string): string | null {
if (!content.startsWith('---\n')) return null;

const fmEnd = content.indexOf('\n---\n', 4);
if (fmEnd === -1) return null;

return content.slice(4, fmEnd);
}

/**
* Lightweight YAML frontmatter validation for SKILL.md files.
*
* We only need to catch the class of errors that breaks host parsers in practice:
* top-level scalar values written without quotes even though they contain `: `,
* which YAML interprets as a nested mapping.
*/
export function validateFrontmatter(skillPath: string): string[] {
const content = fs.readFileSync(skillPath, 'utf-8');
const frontmatter = extractFrontmatter(content);
if (!frontmatter) return [];

const errors: string[] = [];
const lines = frontmatter.split('\n');

for (let index = 0; index < lines.length; index++) {
const line = lines[index];
const trimmed = line.trim();

if (!trimmed || trimmed.startsWith('#')) continue;
if (/^\s/.test(line)) continue;

const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
if (!match) {
errors.push(`line ${index + 2}: invalid frontmatter line`);
continue;
}

const [, key, value] = match;
if (!value) continue;

const firstChar = value[0];
if (['"', '\'', '{', '[', '|', '>'].includes(firstChar)) continue;

if (value.includes(': ')) {
errors.push(`line ${index + 2}: ${key} contains unquoted ": "`);
}
}

return errors;
}

/**
* Extract all $B invocations from bash code blocks in a SKILL.md file.
*/
Expand Down Expand Up @@ -108,6 +160,7 @@ export function validateSkill(skillPath: string): ValidationResult {
valid: [],
invalid: [],
snapshotFlagErrors: [],
frontmatterErrors: validateFrontmatter(skillPath),
warnings: [],
};

Expand Down
33 changes: 32 additions & 1 deletion test/skill-parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, test, expect } from 'bun:test';
import { extractBrowseCommands, validateSkill } from './helpers/skill-parser';
import { extractBrowseCommands, validateFrontmatter, validateSkill } from './helpers/skill-parser';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
Expand Down Expand Up @@ -176,4 +176,35 @@ describe('validateSkill', () => {
expect(result.valid).toHaveLength(4);
expect(result.snapshotFlagErrors).toHaveLength(0);
});

test('invalid frontmatter plain scalars with colon are flagged', () => {
const p = writeFixture('bad-frontmatter.md', [
'---',
'name: bad-skill',
'description: Iron Law: no fixes without root cause.',
'version: 1.0.0',
'---',
'',
'# Skill',
].join('\n'));
const result = validateSkill(p);
expect(result.frontmatterErrors).toEqual([
'line 3: description contains unquoted ": "',
]);
});
});

describe('validateFrontmatter', () => {
test('quoted descriptions with colons pass validation', () => {
const p = writeFixture('quoted-frontmatter.md', [
'---',
'name: good-skill',
"description: 'Iron Law: no fixes without root cause.'",
'version: 1.0.0',
'---',
'',
'# Skill',
].join('\n'));
expect(validateFrontmatter(p)).toEqual([]);
});
});
14 changes: 14 additions & 0 deletions test/skill-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@ import { describe, test, expect } from 'bun:test';
import { validateSkill, 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 { discoverSkillFiles } from '../scripts/discover-skills';
import * as fs from 'fs';
import * as path from 'path';

const ROOT = path.resolve(import.meta.dir, '..');

describe('SKILL.md command validation', () => {
test('all discovered SKILL.md files have valid frontmatter', () => {
const failures: string[] = [];

for (const skillFile of discoverSkillFiles(ROOT)) {
const result = validateSkill(path.join(ROOT, skillFile));
if (result.frontmatterErrors.length > 0) {
failures.push(`${skillFile}: ${result.frontmatterErrors.join(', ')}`);
}
}

expect(failures).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);
Expand Down