mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
df3eb3208c
Resolves the 8 self-review findings on PR #865: session-start.sh: - Wire recallTimeframe into a new "Recent sessions" query (--after_date) — it was parsed but unused after the Phase 4 rewrite. The brief now surfaces recent session checkpoints (the resume cursor), which is the one query the recency window legitimately applies to. (#1) - Guard secondaryProjects/teamProjects JSON types — a string value was iterated character-by-character into bogus per-char queries. (#3) - A configured-but-unreachable/misnamed primaryProject now emits a one-line "couldn't read" signal instead of a silent blank brief. (#7) pre-compact.sh: - Filter transcript turns by the `isMeta` and `toolUseResult` flags instead of a `text.startswith("<")` heuristic. This stops dropping legitimate user messages that start with "<" (#4) and stops capturing tool-result/meta frames as human turns (#8) — verified against a real transcript (25 human turns cleanly separated from 8 meta + 288 tool-result frames). - Title now uses second precision so rapid same-minute compactions don't collide and silently drop/overwrite a checkpoint; removed the unused stamp/slug. (#2) - Empty-checkpoint guard now requires at least one real user turn, not just any turn, so an assistant-only transcript can't produce a dangling-title note. (#5) validate_skills.py: - parse_frontmatter now skips indented lines, capturing only top-level keys, so a schema note's nested schema:/settings: children can't overwrite a top-level type/entity via last-write-wins. Documented its single-line-only limitation. (#6) All verified end-to-end; `just package-check-claude-code` and `package-check-skills` pass; ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
73 lines
2.5 KiB
Python
73 lines
2.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate Basic Memory SKILL.md source directories."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
|
|
def parse_frontmatter(path: Path) -> dict[str, str]:
|
|
"""Extract top-level frontmatter keys from a Markdown file.
|
|
|
|
A deliberately minimal parser (no PyYAML — this runs under bare `python3` in
|
|
CI). It only captures **top-level** `key: value` lines. Indented lines are
|
|
skipped, so nested blocks (a schema note's `schema:`/`settings:` children) can't
|
|
overwrite a top-level key like `type` or `entity` via last-write-wins. It does
|
|
not interpret block scalars or multi-line values; callers rely on single-line
|
|
top-level fields (name, description, type, entity).
|
|
"""
|
|
lines = path.read_text().splitlines()
|
|
if not lines or lines[0] != "---":
|
|
raise SystemExit(f"{path}: missing YAML frontmatter")
|
|
|
|
frontmatter: dict[str, str] = {}
|
|
for line in lines[1:]:
|
|
if line == "---":
|
|
break
|
|
if line[:1] in (" ", "\t"): # nested key — not a top-level field
|
|
continue
|
|
if ":" not in line:
|
|
continue
|
|
key, value = line.split(":", 1)
|
|
frontmatter[key.strip()] = value.strip().strip('"')
|
|
else:
|
|
raise SystemExit(f"{path}: unclosed YAML frontmatter")
|
|
|
|
return frontmatter
|
|
|
|
|
|
def validate_skills(skills_root: Path) -> None:
|
|
if not skills_root.exists():
|
|
raise SystemExit(f"Skills directory not found: {skills_root}")
|
|
|
|
skill_dirs = sorted(path for path in skills_root.glob("memory-*") if path.is_dir())
|
|
if not skill_dirs:
|
|
raise SystemExit(f"No memory-* skill directories found in {skills_root}")
|
|
|
|
for skill_dir in skill_dirs:
|
|
skill_file = skill_dir / "SKILL.md"
|
|
if not skill_file.exists():
|
|
raise SystemExit(f"{skill_dir}: missing SKILL.md")
|
|
|
|
frontmatter = parse_frontmatter(skill_file)
|
|
name = frontmatter.get("name")
|
|
description = frontmatter.get("description")
|
|
if name != skill_dir.name:
|
|
raise SystemExit(f"{skill_file}: name {name!r} does not match directory")
|
|
if not description:
|
|
raise SystemExit(f"{skill_file}: missing description")
|
|
|
|
print(f"validated {len(skill_dirs)} skills in {skills_root}")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("skills_root", nargs="?", default="skills")
|
|
args = parser.parse_args()
|
|
validate_skills((Path.cwd() / args.skills_root).resolve())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|