fix(memory): add pre-commit guard to validate skill formatting (#1482)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Sarah Wooders
2026-03-21 14:15:02 -07:00
committed by GitHub
parent 18176c5323
commit 9c677d444e
2 changed files with 68 additions and 1 deletions

View File

@@ -165,6 +165,12 @@ PROTECTED_KEYS="read_only"
ALL_KNOWN_KEYS="description limit read_only"
errors=""
# Skills must always be directories: skills/<name>/SKILL.md
# Reject legacy flat skill files (both current and legacy repo layouts).
for file in $(git diff --cached --name-only --diff-filter=ACMR | grep -E '^(memory/)?skills/[^/]+\\.md$' || true); do
errors="$errors\\n $file: invalid skill path (skills must be folders). Use skills/<name>/SKILL.md"
done
# Helper: extract a frontmatter value from content
get_fm_value() {
local content="$1" key="$2"
@@ -174,7 +180,9 @@ get_fm_value() {
echo "$content" | tail -n +2 | head -n $((closing_line - 1)) | grep "^$key:" | cut -d: -f2- | sed 's/^ *//;s/ *$//'
}
for file in $(git diff --cached --name-only --diff-filter=ACM | grep '^memory/.*\\.md$'); do
# Match .md files under system/ or reference/ (with optional memory/ prefix).
# Skip skill SKILL.md files — they use a different frontmatter format.
for file in $(git diff --cached --name-only --diff-filter=ACM | grep -E '^(memory/)?(system|reference)/.*\\.md$'); do
staged=$(git show ":$file")
# Frontmatter is required

View File

@@ -290,6 +290,65 @@ describe("pre-commit hook: read_only protection", () => {
});
});
describe("pre-commit hook: skill path guard", () => {
test("rejects legacy flat skill file in nested memory layout", () => {
writeAndStage(
"memory/skills/slack-search.md",
`${VALID_FM}Legacy flat skill file.\n`,
);
const result = tryCommit();
expect(result.success).toBe(false);
expect(result.output).toContain("Use skills/<name>/SKILL.md");
});
test("rejects legacy flat skill file in top-level layout", () => {
writeAndStage("skills/slack-search.md", "Legacy flat skill file.\n");
const result = tryCommit();
expect(result.success).toBe(false);
expect(result.output).toContain("Use skills/<name>/SKILL.md");
});
test("allows canonical directory-based skill path", () => {
writeAndStage("skills/slack-search/SKILL.md", "# Slack Search\n");
const result = tryCommit();
expect(result.success).toBe(true);
});
});
describe("pre-commit hook: top-level layout (no memory/ prefix)", () => {
test("validates frontmatter for system files without memory/ prefix", () => {
writeAndStage(
"system/human.md",
"Just plain content\nno frontmatter here\n",
);
const result = tryCommit();
expect(result.success).toBe(false);
expect(result.output).toContain("missing frontmatter");
});
test("allows valid frontmatter in system files without memory/ prefix", () => {
writeAndStage("system/human.md", `${VALID_FM}Block content here.\n`);
const result = tryCommit();
expect(result.success).toBe(true);
});
test("validates frontmatter for reference files without memory/ prefix", () => {
writeAndStage("reference/notes.md", "No frontmatter\n");
const result = tryCommit();
expect(result.success).toBe(false);
expect(result.output).toContain("missing frontmatter");
});
test("skips SKILL.md files inside skill directories", () => {
writeAndStage(
"skills/my-skill/SKILL.md",
"# My Skill\nNo frontmatter needed.\n",
);
const result = tryCommit();
expect(result.success).toBe(true);
});
});
describe("pre-commit hook: non-memory files", () => {
test("ignores non-memory files", () => {
writeAndStage("README.md", "---\nbogus: true\n---\n\nThis is fine.\n");