YAML Frontmatter Parsing & Debug Parsing

- Add skill_parsing.hpp and skill_parsing.cpp for YAML frontmatter parsing
- Extract name (required) and description (optional) from SKILL.md files
- Add -y flag to print parsed metadata and exit
- Add test_skill_parsing.sh to verify the -y flag functionality
- Update CMakeLists.txt to include the new source and test
This commit is contained in:
2026-05-20 15:19:27 -05:00
parent 244ecc1d32
commit c9461f2e88
6 changed files with 222 additions and 0 deletions
+1
View File
@@ -30,6 +30,7 @@ add_library(drharness_lib
src/debug_client.cpp
src/editor.cpp
src/skill_scanner.cpp
src/skill_parsing.cpp
${TOOL_SOURCES}
)
target_include_directories(drharness_lib PUBLIC include tools)
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <string>
#include <optional>
#include <stdexcept>
// Represents parsed YAML frontmatter metadata from a SKILL.md file
struct SkillMetadata {
std::string name;
std::optional<std::string> description;
};
// Parse YAML frontmatter from a file.
// Throws std::runtime_error if the file is malformed or name is missing.
SkillMetadata parse_skill_frontmatter(const std::string& filepath);
+22
View File
@@ -13,6 +13,7 @@
#include "file_read_tool.hpp"
#include "grep_tool.hpp"
#include "skill_scanner.hpp"
#include "skill_parsing.hpp"
#include <nlohmann/json.hpp>
#include <cstdlib>
#include <iostream>
@@ -47,6 +48,7 @@ int main(int argc, char** argv) {
bool use_debug = false;
bool debug_paths = false;
char const* debug_frontmatter = nullptr;
ToolRegistry master_registery;
ToolRegistry agent_tools;
@@ -127,6 +129,11 @@ int main(int argc, char** argv) {
consume_arg();
debug_paths = true;
}
else if (argv[0] == std::string("-y"))
{
consume_arg();
debug_frontmatter = consume_arg();
}
else if (argv[0] == std::string("-L"))
{
consume_arg();
@@ -159,6 +166,7 @@ int main(int argc, char** argv) {
<< " -l List available tools\n"
<< " -L List available models on server side\n"
<< " -D Debug: print all discovered skill paths and exit\n"
<< " -y <file> Debug: print YAML frontmatter from a SKILL.md file and exit\n"
<< " -d Use debug client (mock API via stdin)\n"
<< " -h Display this help menu\n";
exit(0);
@@ -185,6 +193,20 @@ int main(int argc, char** argv) {
return 0;
}
if (debug_frontmatter) {
try {
auto metadata = parse_skill_frontmatter(debug_frontmatter);
std::cout << "name: " << metadata.name << "\n";
if (metadata.description.has_value()) {
std::cout << "description: " << metadata.description.value() << "\n";
}
} catch (const std::runtime_error& e) {
std::cerr << "Error parsing frontmatter: " << e.what() << "\n";
return 1;
}
return 0;
}
if (list_models)
{
auto models = create_client(use_debug)->list_models();
+77
View File
@@ -0,0 +1,77 @@
#include "skill_parsing.hpp"
#include <fstream>
#include <sstream>
#include <iostream>
SkillMetadata parse_skill_frontmatter(const std::string& filepath) {
std::ifstream file(filepath);
if (!file.is_open()) {
throw std::runtime_error("Cannot open file: " + filepath);
}
std::string line;
bool in_frontmatter = false;
std::string name;
std::optional<std::string> description;
while (std::getline(file, line)) {
// Remove trailing carriage return if present
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
if (!in_frontmatter) {
// Look for opening ---
if (line == "---") {
in_frontmatter = true;
continue;
}
// If we hit a non-empty line before ---, it's not valid frontmatter
if (!line.empty()) {
throw std::runtime_error("File does not start with YAML frontmatter (---)");
}
} else {
// Look for closing ---
if (line == "---") {
if (name.empty()) {
throw std::runtime_error("YAML frontmatter is missing required 'name' field");
}
// Successfully parsed frontmatter
SkillMetadata metadata;
metadata.name = name;
metadata.description = description;
return metadata;
}
// Parse YAML key-value pair (simple key: value format)
if (line.find(':') != std::string::npos) {
size_t colon_pos = line.find(':');
std::string key = line.substr(0, colon_pos);
std::string value = line.substr(colon_pos + 1);
// Trim leading space from value
if (!value.empty() && value[0] == ' ') {
value = value.substr(1);
}
// Remove surrounding quotes from value if present
if (value.size() >= 2 && value.front() == '"' && value.back() == '"') {
value = value.substr(1, value.size() - 2);
} else if (value.size() >= 2 && value.front() == '\'' && value.back() == '\'') {
value = value.substr(1, value.size() - 2);
}
if (key == "name") {
if (name.empty()) {
name = value;
}
} else if (key == "description") {
description = value;
}
// Ignore unknown keys
}
}
}
throw std::runtime_error("YAML frontmatter is not properly closed (missing closing ---)");
}
+1
View File
@@ -27,6 +27,7 @@ add_dr_test(test_load_chat)
add_dr_test(process_reasoning)
add_dr_test(test_skill_scanner)
add_dr_test(test_nested_skills)
add_dr_test(test_skill_parsing)
# Add editor tests
add_dr_test(test_editor)
+107
View File
@@ -0,0 +1,107 @@
#!/bin/bash
# test_skill_parsing.sh - Verifies the -y flag correctly parses YAML frontmatter
# Create test skill files
echo '# Test Skill' > "$TEST_TEMP_DIR/test_skill.md"
# Test 1: Skill with name and description
cat > "$TEST_TEMP_DIR/full_skill.md" << 'SKILL'
---
name: full-skill
description: A full skill with description
---
# Full Skill Content
This is the full skill.
SKILL
OUTPUT=$("${DRHARNESS}" -y "$TEST_TEMP_DIR/full_skill.md" -F dummy.chat)
if ! echo "$OUTPUT" | grep -q "name: full-skill"; then
echo "FAIL: Expected 'name: full-skill' in output"
exit 1
fi
if ! echo "$OUTPUT" | grep -q "description: A full skill with description"; then
echo "FAIL: Expected description in output"
exit 1
fi
# Test 2: Skill with only name (no description)
cat > "$TEST_TEMP_DIR/name_only_skill.md" << 'SKILL'
---
name: name-only-skill
---
# Name Only Skill Content
SKILL
OUTPUT=$("${DRHARNESS}" -y "$TEST_TEMP_DIR/name_only_skill.md" -F dummy.chat)
if ! echo "$OUTPUT" | grep -q "name: name-only-skill"; then
echo "FAIL: Expected 'name: name-only-skill' in output"
exit 1
fi
if echo "$OUTPUT" | grep -q "description:"; then
echo "FAIL: Unexpected description in output"
exit 1
fi
# Test 3: Missing name should fail
cat > "$TEST_TEMP_DIR/missing_name_skill.md" << 'SKILL'
---
description: No name here
---
# Missing Name Skill
SKILL
set +e
"${DRHARNESS}" -y "$TEST_TEMP_DIR/missing_name_skill.md" -F dummy.chat 2>/dev/null
MISSING_NAME_EXIT=$?
set -e
if [ $MISSING_NAME_EXIT -eq 0 ]; then
echo "FAIL: Expected failure when name is missing"
exit 1
fi
# Test 4: Invalid frontmatter (no closing ---) should fail
cat > "$TEST_TEMP_DIR/invalid_skill.md" << 'SKILL'
---
name: invalid-skill
description: No closing
# Invalid Skill
SKILL
set +e
"${DRHARNESS}" -y "$TEST_TEMP_DIR/invalid_skill.md" -F dummy.chat 2>/dev/null
INVALID_EXIT=$?
set -e
if [ $INVALID_EXIT -eq 0 ]; then
echo "FAIL: Expected failure for invalid frontmatter"
exit 1
fi
# Test 5: File without frontmatter should fail
cat > "$TEST_TEMP_DIR/no_frontmatter_skill.md" << 'SKILL'
# No Frontmatter Skill
This skill has no frontmatter.
SKILL
set +e
"${DRHARNESS}" -y "$TEST_TEMP_DIR/no_frontmatter_skill.md" -F dummy.chat 2>/dev/null
NO_FRONTMATTER_EXIT=$?
set -e
if [ $NO_FRONTMATTER_EXIT -eq 0 ]; then
echo "FAIL: Expected failure for file without frontmatter"
exit 1
fi
echo "All tests passed!"