@@ -283,6 +283,41 @@ const AGENTS_TARGET_DIR = "${agentsTargetDir.replace(/\\/g, "/")}"
283283
284284const MIN_CONTENT_LENGTH = 100
285285const REQUIRED_KEYWORDS = ["agent", "task"]
286+ const REQUIRED_FRONTMATTER_FIELDS = ["version", "requires"]
287+
288+ function parseFrontmatter(content) {
289+ if (!content.startsWith("---")) {
290+ return { found: false, fields: {}, endIndex: 0 }
291+ }
292+
293+ const endMatch = content.indexOf("\\n---", 3)
294+ if (endMatch === -1) {
295+ return { found: false, fields: {}, endIndex: 0 }
296+ }
297+
298+ const frontmatterContent = content.slice(4, endMatch)
299+ const fields = {}
300+
301+ for (const line of frontmatterContent.split("\\n")) {
302+ const trimmed = line.trim()
303+ if (!trimmed || trimmed.startsWith("#")) continue
304+
305+ const colonIndex = trimmed.indexOf(":")
306+ if (colonIndex === -1) continue
307+
308+ const key = trimmed.slice(0, colonIndex).trim()
309+ let value = trimmed.slice(colonIndex + 1).trim()
310+
311+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
312+ value = value.slice(1, -1)
313+ }
314+
315+ fields[key] = value
316+ }
317+
318+ const endIndex = endMatch + 4
319+ return { found: true, fields, endIndex }
320+ }
286321
287322function validateAgentContent(filePath) {
288323 const content = readFileSync(filePath, "utf-8")
@@ -294,10 +329,28 @@ function validateAgentContent(filePath) {
294329 }
295330 }
296331
297- if (!content.startsWith("# ")) {
332+ const frontmatter = parseFrontmatter(content)
333+ if (!frontmatter.found) {
334+ return {
335+ valid: false,
336+ error: "File missing YAML frontmatter (must start with ---)",
337+ }
338+ }
339+
340+ const missingFields = REQUIRED_FRONTMATTER_FIELDS.filter((field) => !frontmatter.fields[field])
341+ if (missingFields.length > 0) {
298342 return {
299343 valid: false,
300- error: "File does not start with a markdown header (# )",
344+ error: \`Frontmatter missing required fields: \${missingFields.join(", ")}\`,
345+ }
346+ }
347+
348+ const contentAfterFrontmatter = content.slice(frontmatter.endIndex).trimStart()
349+
350+ if (!contentAfterFrontmatter.startsWith("# ")) {
351+ return {
352+ valid: false,
353+ error: "File does not have a markdown header (# ) after frontmatter",
301354 }
302355 }
303356
@@ -387,8 +440,13 @@ main()
387440`
388441 }
389442
390- // Valid agent content for tests (meets all requirements)
391- const validAgentContent = `# Test Agent
443+ // Valid agent content for tests (meets all requirements including frontmatter)
444+ const validAgentContent = `---
445+ version: 0.1.0
446+ requires: ">=0.1.0"
447+ ---
448+
449+ # Test Agent
392450
393451This is a valid agent file that contains enough content to pass the minimum length requirement.
394452
@@ -426,11 +484,101 @@ The agent handles various tasks and operations in the system.
426484 expect ( stderr ) . toContain ( "File too short" )
427485 } )
428486
429- it ( "should reject files without markdown header" , async ( ) => {
430- // Create a file without markdown header
431- const contentWithoutHeader = `This file does not start with a markdown header.
487+ it ( "should reject files without YAML frontmatter" , async ( ) => {
488+ // Create a file without frontmatter (starts with header directly)
489+ const contentWithoutFrontmatter = `# Test Agent Without Frontmatter
490+
491+ This file does not have YAML frontmatter at the start.
432492It has enough content and contains the word agent and task.
433- This should fail the validation because it doesn't start with # symbol.`
493+ This should fail the validation because it doesn't start with ---.`
494+ writeFileSync ( join ( agentsSourceDir , "no-frontmatter.md" ) , contentWithoutFrontmatter )
495+
496+ const scriptPath = join ( mockProjectDir , "test-postinstall.mjs" )
497+ writeFileSync ( scriptPath , createPostinstallWithIntegrity ( ) )
498+
499+ const proc = Bun . spawn ( [ "node" , scriptPath ] , {
500+ cwd : mockProjectDir ,
501+ stdout : "pipe" ,
502+ stderr : "pipe" ,
503+ } )
504+
505+ await proc . exited
506+
507+ const stderr = await new Response ( proc . stderr ) . text ( )
508+ expect ( stderr ) . toContain ( "no-frontmatter.md" )
509+ expect ( stderr ) . toContain ( "missing YAML frontmatter" )
510+ } )
511+
512+ it ( "should reject files with frontmatter missing required fields" , async ( ) => {
513+ // Create a file with frontmatter but missing 'requires' field
514+ const contentMissingRequires = `---
515+ version: 0.1.0
516+ ---
517+
518+ # Test Agent
519+
520+ This file has frontmatter but is missing the requires field.
521+ It contains the word agent and task to pass keyword validation.
522+ This should fail because requires is a required frontmatter field.`
523+ writeFileSync ( join ( agentsSourceDir , "missing-requires.md" ) , contentMissingRequires )
524+
525+ const scriptPath = join ( mockProjectDir , "test-postinstall.mjs" )
526+ writeFileSync ( scriptPath , createPostinstallWithIntegrity ( ) )
527+
528+ const proc = Bun . spawn ( [ "node" , scriptPath ] , {
529+ cwd : mockProjectDir ,
530+ stdout : "pipe" ,
531+ stderr : "pipe" ,
532+ } )
533+
534+ await proc . exited
535+
536+ const stderr = await new Response ( proc . stderr ) . text ( )
537+ expect ( stderr ) . toContain ( "missing-requires.md" )
538+ expect ( stderr ) . toContain ( "Frontmatter missing required fields" )
539+ expect ( stderr ) . toContain ( "requires" )
540+ } )
541+
542+ it ( "should reject files with frontmatter missing version field" , async ( ) => {
543+ // Create a file with frontmatter but missing 'version' field
544+ const contentMissingVersion = `---
545+ requires: ">=0.1.0"
546+ ---
547+
548+ # Test Agent
549+
550+ This file has frontmatter but is missing the version field.
551+ It contains the word agent and task to pass keyword validation.
552+ This should fail because version is a required frontmatter field.`
553+ writeFileSync ( join ( agentsSourceDir , "missing-version.md" ) , contentMissingVersion )
554+
555+ const scriptPath = join ( mockProjectDir , "test-postinstall.mjs" )
556+ writeFileSync ( scriptPath , createPostinstallWithIntegrity ( ) )
557+
558+ const proc = Bun . spawn ( [ "node" , scriptPath ] , {
559+ cwd : mockProjectDir ,
560+ stdout : "pipe" ,
561+ stderr : "pipe" ,
562+ } )
563+
564+ await proc . exited
565+
566+ const stderr = await new Response ( proc . stderr ) . text ( )
567+ expect ( stderr ) . toContain ( "missing-version.md" )
568+ expect ( stderr ) . toContain ( "Frontmatter missing required fields" )
569+ expect ( stderr ) . toContain ( "version" )
570+ } )
571+
572+ it ( "should reject files without markdown header after frontmatter" , async ( ) => {
573+ // Create a file with frontmatter but no header after it
574+ const contentWithoutHeader = `---
575+ version: 0.1.0
576+ requires: ">=0.1.0"
577+ ---
578+
579+ This file has valid frontmatter but no markdown header after it.
580+ It has enough content and contains the word agent and task.
581+ This should fail the validation because it needs a # header.`
434582 writeFileSync ( join ( agentsSourceDir , "no-header.md" ) , contentWithoutHeader )
435583
436584 const scriptPath = join ( mockProjectDir , "test-postinstall.mjs" )
@@ -446,12 +594,17 @@ This should fail the validation because it doesn't start with # symbol.`
446594
447595 const stderr = await new Response ( proc . stderr ) . text ( )
448596 expect ( stderr ) . toContain ( "no-header.md" )
449- expect ( stderr ) . toContain ( "does not start with a markdown header" )
597+ expect ( stderr ) . toContain ( "does not have a markdown header" )
450598 } )
451599
452600 it ( "should reject files missing required keywords" , async ( ) => {
453- // Create a file with header and length but missing keywords
454- const contentWithoutKeywords = `# Valid Header
601+ // Create a file with frontmatter, header, and length but missing keywords
602+ const contentWithoutKeywords = `---
603+ version: 0.1.0
604+ requires: ">=0.1.0"
605+ ---
606+
607+ # Valid Header
455608
456609This file has a valid markdown header and enough content length.
457610However, it does not contain any of the required keywords.
@@ -565,7 +718,12 @@ This should fail the validation check for missing keywords.`
565718 } )
566719
567720 it ( "should accept keyword 'agent' case-insensitively" , async ( ) => {
568- const contentWithUpperAgent = `# Test File
721+ const contentWithUpperAgent = `---
722+ version: 0.1.0
723+ requires: ">=0.1.0"
724+ ---
725+
726+ # Test File
569727
570728This file contains the word AGENT in uppercase and has enough content.
571729The validation should accept this because keyword matching is case-insensitive.
@@ -589,7 +747,12 @@ Adding more text to ensure minimum length requirement is satisfied here.`
589747 } )
590748
591749 it ( "should accept keyword 'task' as alternative to 'agent'" , async ( ) => {
592- const contentWithTask = `# Test File
750+ const contentWithTask = `---
751+ version: 0.1.0
752+ requires: ">=0.1.0"
753+ ---
754+
755+ # Test File
593756
594757This file contains the word TASK but not the other keyword.
595758The validation should accept this because either keyword is sufficient.
0 commit comments