Files

122 lines
3.5 KiB
JavaScript
Raw Permalink Normal View History

2026-01-29 08:05:43 +01:00
#!/usr/bin/env node
/**
* Validate agent markdown files have required frontmatter
*/
const fs = require('fs');
const path = require('path');
const AGENTS_DIR = path.join(__dirname, '../../agents');
const REQUIRED_FIELDS = ['model', 'tools'];
const VALID_MODELS = ['haiku', 'sonnet', 'opus'];
2026-01-29 08:05:43 +01:00
function extractFrontmatter(content) {
// Strip BOM if present (UTF-8 BOM: \uFEFF)
const cleanContent = content.replace(/^\uFEFF/, '');
// Support both LF and CRLF line endings
const match = cleanContent.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match) return null;
const frontmatter = {};
const duplicates = [];
2026-07-26 03:20:15 -07:00
const sequenceFields = [];
let currentTopLevelKey = null;
const lines = match[1].split(/\r?\n/);
2026-01-29 08:05:43 +01:00
for (const line of lines) {
2026-07-26 03:20:15 -07:00
if (/^\s*-\s+/.test(line)) {
if (currentTopLevelKey) {
sequenceFields.push(currentTopLevelKey);
}
continue;
}
// Only top-level keys are unique. Indented YAML belongs to nested values.
if (/^\s/.test(line)) continue;
2026-07-26 03:20:15 -07:00
if (!line.trim() || line.trim().startsWith('#')) continue;
currentTopLevelKey = null;
2026-01-29 08:05:43 +01:00
const colonIdx = line.indexOf(':');
if (colonIdx > 0) {
const key = line.slice(0, colonIdx).trim();
const value = line.slice(colonIdx + 1).trim();
2026-07-26 03:20:15 -07:00
currentTopLevelKey = key;
if (Object.prototype.hasOwnProperty.call(frontmatter, key)) {
duplicates.push(key);
}
2026-01-29 08:05:43 +01:00
frontmatter[key] = value;
2026-07-26 03:20:15 -07:00
if (value && '[!&*{|>'.includes(value[0])) {
sequenceFields.push(key);
}
2026-01-29 08:05:43 +01:00
}
}
Object.defineProperty(frontmatter, '__duplicates__', {
value: duplicates,
enumerable: false,
});
2026-07-26 03:20:15 -07:00
Object.defineProperty(frontmatter, '__sequenceFields__', {
value: sequenceFields,
enumerable: false,
});
2026-01-29 08:05:43 +01:00
return frontmatter;
}
function validateAgents() {
if (!fs.existsSync(AGENTS_DIR)) {
console.log('No agents directory found, skipping validation');
process.exit(0);
}
const files = fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md'));
let hasErrors = false;
for (const file of files) {
const filePath = path.join(AGENTS_DIR, file);
let content;
try {
content = fs.readFileSync(filePath, 'utf-8');
} catch (err) {
console.error(`ERROR: ${file} - ${err.message}`);
hasErrors = true;
continue;
}
2026-01-29 08:05:43 +01:00
const frontmatter = extractFrontmatter(content);
if (!frontmatter) {
console.error(`ERROR: ${file} - Missing frontmatter`);
hasErrors = true;
continue;
}
if (frontmatter.__duplicates__.length > 0) {
console.error(`ERROR: ${file} - Duplicate frontmatter keys: ${[...new Set(frontmatter.__duplicates__)].join(', ')}`);
hasErrors = true;
}
2026-01-29 08:05:43 +01:00
for (const field of REQUIRED_FIELDS) {
if (!frontmatter[field] || (typeof frontmatter[field] === 'string' && !frontmatter[field].trim())) {
2026-01-29 08:05:43 +01:00
console.error(`ERROR: ${file} - Missing required field: ${field}`);
hasErrors = true;
}
}
2026-07-26 03:20:15 -07:00
if (frontmatter.__sequenceFields__.includes('tools')) {
console.error(`ERROR: ${file} - Agent tools must be a comma-separated scalar, not a YAML sequence`);
hasErrors = true;
}
// Validate model is a known value
if (frontmatter.model && !VALID_MODELS.includes(frontmatter.model)) {
console.error(`ERROR: ${file} - Invalid model '${frontmatter.model}'. Must be one of: ${VALID_MODELS.join(', ')}`);
hasErrors = true;
}
2026-01-29 08:05:43 +01:00
}
if (hasErrors) {
process.exit(1);
}
console.log(`Validated ${files.length} agent files`);
}
validateAgents();