#!/usr/bin/env python3 """List all lore entries in `.lore/` as JSON or human-readable text. Usage: python list_entries.py # human-readable python list_entries.py --json # JSON output python list_entries.py --scope=frontend python list_entries.py --layer=ARCH Walks `.lore/_global/*` and `.lore/scopes/*/*` and parses every Markdown bullet that matches the entry format. Output is one record per entry with these fields: id full ID, e.g. "ARCH-2026-07-09-a3f2" layer prefix, e.g. "ARCH" / "DEC" / "CONV" layer_file source file stem, e.g. "ARCHITECTURE" scope scope name, or "_global" file path relative to .lore/, e.g. "scopes/frontend/ARCHITECTURE.md" text entry body, with tags stripped tags dict of tag name -> value, e.g. {"added": "2026-07-09", "verified": "2026-07-15"} last_verified value of #verified tag, or None Used by: - query / audit / compress workflows (pre-step enumeration) - find_duplicates.py - find_stale.py """ import json import re import sys from pathlib import Path # Schema version this skill understands. Bumped only on breaking # config changes; see references/compatibility.md. KNOWN_SCHEMA_VERSION = 1 def check_schema_version(lore_root: Path) -> None: """Warn if .lore/.config.json is missing or has an unknown schema_version. Output goes to stderr so it does not pollute --json consumers. Idempotent and best-effort: any failure (missing file, malformed JSON, permission error) is silent — config is optional and the user can address it separately. """ cfg_path = lore_root / ".config.json" if not cfg_path.exists(): return try: cfg = json.loads(cfg_path.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError): return version = cfg.get("schema_version") if version is None: print( "[WARN] .lore/.config.json has no schema_version field. " "Add \"schema_version\": 1 so future lore upgrades can detect " "this config and prompt for migrations when they exist.", file=sys.stderr, ) elif isinstance(version, int) and version > KNOWN_SCHEMA_VERSION: print( f"[WARN] .lore/.config.json#schema_version={version} is newer " f"than this lore skill expects (max: {KNOWN_SCHEMA_VERSION}). " "Pull the latest lore from upstream.", file=sys.stderr, ) def find_lore_root(start: Path) -> Path: """Walk up from start to find the project root containing .lore/.""" p = start.resolve() while p != p.parent: if (p / ".lore").is_dir(): return p / ".lore" p = p.parent return None def parse_entry(line: str): """Parse one Markdown bullet line. Returns dict or None if not an entry.""" m = re.match( r"^\s*-\s*\[([A-Z]+)-(\d{4}-\d{2}-\d{2})-([a-f0-9]{4})\]\s+(.*?)\s*$", line, ) if not m: return None layer, date, h, rest = m.group(1), m.group(2), m.group(3), m.group(4) eid = f"{layer}-{date}-{h}" # Extract #tag:value pairs tag_re = re.compile(r"#(added|verified|stale|archived):(\S+)") tags = {name: val for name, val in tag_re.findall(rest)} text = tag_re.sub("", rest).strip() return { "id": eid, "layer": layer, "layer_file": None, # filled in by caller "scope": None, # filled in by caller "file": None, # filled in by caller "text": text, "tags": tags, "last_verified": tags.get("verified"), } def collect_entries(root: Path): entries = [] layers_dirs = [("_global", root / "_global"), ("scopes", root / "scopes")] for section_name, section_path in layers_dirs: if not section_path.exists(): continue for md_file in sorted(section_path.rglob("*.md")): if section_name == "_global": scope = "_global" else: scope = md_file.parent.name layer_file = md_file.stem try: with open(md_file, encoding="utf-8") as f: for line in f: e = parse_entry(line) if e is None: continue e["scope"] = scope e["layer_file"] = layer_file e["file"] = str(md_file.relative_to(root)) entries.append(e) except OSError as exc: print(f"warning: cannot read {md_file}: {exc}", file=sys.stderr) return entries def main(): args = sys.argv[1:] scope_filter = None layer_filter = None json_output = "--json" in args for arg in args: if arg.startswith("--scope="): scope_filter = arg.split("=", 1)[1] elif arg.startswith("--layer="): layer_filter = arg.split("=", 1)[1] root = find_lore_root(Path(".")) if root is None: print("error: .lore/ not found (run from project root or below)", file=sys.stderr) sys.exit(1) check_schema_version(root) entries = collect_entries(root) if scope_filter: entries = [e for e in entries if e["scope"] == scope_filter] if layer_filter: entries = [e for e in entries if e["layer"] == layer_filter] if json_output: print(json.dumps(entries, indent=2, ensure_ascii=False)) return if not entries: print("(no entries)") return for e in entries: verified = ( f" [verified:{e['last_verified']}]" if e["last_verified"] else "" ) stale = " [STALE]" if "stale" in e["tags"] else "" print(f"[{e['file']}] {e['id']} {e['text']}{verified}{stale}") if __name__ == "__main__": main()