{"record":{"id":"bf567603fd77fe3b","repo":"sipeed/picoclaw","slug":"skill-heading-is-required","errorCode":null,"errorMessage":"skill heading is required","messagePattern":"skill heading is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/evolution/apply.go","lineNumber":152,"sourceCode":"\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc isDirNotEmptyError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn strings.Contains(strings.ToLower(err.Error()), \"directory not empty\")\n}\n\nfunc validateAppliedSkillBody(body, targetSkillName string, allowExtraFrontmatterFields bool) error {\n\tbody = strings.TrimSpace(body)\n\tif !strings.HasPrefix(body, \"---\\n\") {\n\t\treturn fmt.Errorf(\"skill frontmatter is required\")\n\t}\n\tif !strings.Contains(body, \"\\n# \") {\n\t\treturn fmt.Errorf(\"skill heading is required\")\n\t}\n\tfrontmatter, _ := splitSkillFrontmatter(body)\n\tfields, err := parseSkillFrontmatterFields(frontmatter, allowExtraFrontmatterFields)\n\tif err != nil {\n\t\treturn err\n\t}\n\tname := strings.TrimSpace(fields[\"name\"])\n\tif name == \"\" {\n\t\treturn fmt.Errorf(\"skill frontmatter name is required\")\n\t}\n\tif name != targetSkillName {\n\t\treturn fmt.Errorf(\"skill frontmatter name %q does not match target skill %q\", name, targetSkillName)\n\t}\n\tif strings.TrimSpace(fields[\"description\"]) == \"\" {\n\t\treturn fmt.Errorf(\"skill frontmatter description is required\")\n\t}\n\treturn nil\n}","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/evolution/apply.go#L134-L170","documentation":"validateAppliedSkillBody requires the skill body to contain a line-anchored H1: the literal substring \"\\n# \". Frontmatter alone is not enough - the body must also carry an ATX H1 heading (which becomes the skill document's title). Note the check: '## Subheading' does not match, '#Heading' without a space does not match, and a heading is found only when preceded by a newline (the frontmatter's closing '---' line supplies one).","triggerScenarios":"Applying a skill whose body is only frontmatter plus paragraphs or bullet lists; a body that uses only '##' subheadings; a heading written without a space after the hash ('#Title'); content where the only H1 was deleted during editing.","commonSituations":"Authors structuring skill docs purely with H2s; diff/patch tooling that strips what looks like a duplicate title; generated content from templates that omit the H1 when a title field exists in frontmatter.","solutions":["Add an H1 line after the frontmatter, e.g. '# Skill Name' with a space after the hash","If you only have '##' headings, promote the first one to '#'","Verify locally: strings.Contains(body, \"\\\\n# \") before applying"],"exampleFix":"# before: frontmatter + only H2 headings\n---\nname: deploy\ndescription: Deploy the app\n---\n## Build\n...\n## Ship\n...\n\n# after: H1 present\n---\nname: deploy\ndescription: Deploy the app\n---\n# Deploy\n## Build\n...\n## Ship\n...","handlingStrategy":"validation","validationCode":"func hasH1(body string) bool {\n    return strings.Contains(strings.ReplaceAll(body, \"\\r\\n\", \"\\n\"), \"\\n# \")\n}","typeGuard":null,"tryCatchPattern":"if err := apply.Change(body); err != nil {\n    if strings.Contains(err.Error(), \"skill heading is required\") {\n        return fmt.Errorf(\"skill body needs a top-level '# ' heading: %w\", err)\n    }\n    return err\n}","preventionTips":["Give every skill doc exactly one H1 title line following the frontmatter","Remember '## ' and '#NoSpace' do not satisfy the check - use '# ' with a space","Include an H1 in skill templates so generated content inherits it"],"tags":["evolution","skills","validation","markdown","structure"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}