Kind Taxonomy Registry - alexherrero/agentm GitHub Wiki

Kind-taxonomy registry + frontmatter validator reference

Note

Status: implemented โ€” PLAN-v6-15-v6-18-typed-object-moc ships this reference across four sub-deliverables. All are shipped. AgentM Memory System governs this plan. You can confirm this via governs_resolver.py. Note that agentm-memory-index.md is a distinct sibling design. It covers the V6-11 SQLite metadata index. It does not cover this plan. Task 1 ships the registry in kind_registry.py. Task 2 ships the validator in frontmatter_validator.py. Task 3 ships the MOC generator. You can find its own reference page linked here. Task 4 ships the advisory check-kind-taxonomy step in check-all.sh. You can see the per-section status notes below.

The kind_registry.py script formalizes the vault's existing free-form kind: frontmatter taxonomy. It provides a recognized-set catalog. It also provides a read-only audit CLI. The frontmatter_validator.py script adds a check-only validator on top of this registry. It has the same read-only, report-not-mutate shape as vault_lint.py. It scopes narrowly to the kind value and the universal required fields. It does not perform the full nine-check frontmatter sweep.

โšก Quick Reference

Question Answer
What enumerates known kind values? harness/skills/memory/scripts/kind_registry.py โ€” known_kinds() (kind_registry.py:73) + is_known(kind) (kind_registry.py:78).
Is the registry a closed enum? No. It's a recognized-set catalog over a free-form field โ€” an unrecognized kind is flagged, never rejected.
What checks a single note's frontmatter? frontmatter_validator.py's validate(note_path) (frontmatter_validator.py:67) โ€” returns a list of violation strings, empty means clean.
Does either script ever rewrite a file? No โ€” both are read-only by contract. kind_registry.py's audit() (kind_registry.py:101) is confirmed by test_audit_never_writes_to_the_vault in scripts/test_kind_registry.py:68; frontmatter_validator.py's validate() / validate_vault() are confirmed by test_validate_never_writes_to_the_file and test_validate_vault_never_writes_to_any_file in scripts/test_frontmatter_validator.py.
How do I run an audit? python3 harness/skills/memory/scripts/kind_registry.py audit <vault_path> โ€” works from any working directory; the vault argument is passed straight to pathlib.Path() with no cwd-relative resolution (kind_registry.py:111).
How do I check one note or the whole vault? python3 harness/skills/memory/scripts/frontmatter_validator.py --check <path> (one note) / --check-vault <vault_path> (walks personal/ + projects/, excluding the DC-4 dirs) (frontmatter_validator.py:120-149).
Is this wired into CI as a hard gate? No โ€” advisory/report-only. See Advisory kind-taxonomy check below.
Related pages Audit the vault ยท Vault lint checks ยท AgentM Memory System

Registry (task 1)

Note

Status: implemented โ€” You shipped this in harness/skills/memory/scripts/kind_registry.py. You covered it with 15 tests in scripts/test_kind_registry.py. The test classes are TestKnownKinds, TestIsKebab, and TestAudit.

The kind_registry.py script (kind_registry.py:1) is a stdlib-only module. It contains KNOWN_KINDS (kind_registry.py:33-52, a frozenset[str]). You seed this set from two sources. The first source is reserved values that shipped code already references. These are failure-incident, session-cost, and crystallized. The second source is a frequency audit of the real vault's personal/ and projects/ trees. You took this audit at authoring time on 2026-07-10.

The module keeps near-duplicate values as separate, distinct entries deliberately. For example, both convention and conventions are known kinds. You must make an explicit operator judgment call to canonicalize near-synonyms. This module does not decide canonicalization.

The #273 canonicalization pass ran on 2026-07-24 (agentm issue #273, Loose Ends Release 5). A full audit() over the live vault โ€” 3,069 files โ€” found 30 non-canonical values and took them to zero:

  • 15 malformed values carried prose inside the field, like handoff-artifact (verdict memo). Each was stripped to its base kind.
  • arc-index (9 files), dir-index, and pilot-index joined the registry. They belong to the same *-index family as project-index, research-index, and handoff-index.
  • capture joined the registry rather than being remapped. It is the default kind capture.py writes and the memory_append MCP tool documents, so remapping the one live instance would be undone by the next capture.
  • proving-ledger, proving-report, and coordinator-log remapped to handoff-artifact; design-draft and design-brief remapped to design. These were one-off artifact labels rather than recurring kinds.

The REQUIRED_UNIVERSAL_FIELDS tuple (kind_registry.py:59-61, a tuple[str, ...]: kind, status, created, updated, tags, group, slug) also exists in this module. The frontmatter_validator.validate() function now consumes it directly. You can see task 2 below.

Shipped surface:

Function / mode Signature Purpose
is_kebab(value) is_kebab(value: str) -> bool (kind_registry.py:68) True iff value matches the kebab-case shape save.py itself enforces (^[a-z0-9-]+$).
known_kinds() known_kinds() -> frozenset[str] (kind_registry.py:73) Returns KNOWN_KINDS, the recognized-set catalog.
is_known(kind) is_known(kind: str) -> bool (kind_registry.py:78) Exact-match, case-sensitive lookup against KNOWN_KINDS โ€” a differently-cased duplicate (e.g. Fix) is a distinct, unrecognized value by design.
audit(vault_path) audit(vault_path: Path | str, *, vault_root=None, spaces: dict | None = None) -> dict (kind_registry.py:242) Read-only scan of a vault's kind: values. Returns {"by_kind": {kind: count}, "malformed": [(path, raw_kind)], "unrecognized": [(path, raw_kind)], "total_files": int}. Walks the whole vault root the memory root sits in (corpus_root, kind_registry.py:91; corpus_notes, kind_registry.py:134): the vault_root a caller passes, else the memory root's parent when an .obsidian/ there witnesses a vault, else the memory root itself. It skips dot-directories, the contract's recall_exempt_areas (never entered, so a walled file is never opened), _archive/ dirs and PLAN.archive.* files. Paths are keyed from the vault root (agent/memory/โ€ฆ, projects/โ€ฆ), and an optional spaces dict is filled with the notes read per top-level directory. The hand-listed roots it replaced (_WALK_SUBDIRS) had fallen behind the vault's moves; graph_snapshot.py keeps a hand-listed walk of its own, for the reason its _walk_vault_paths gives. "malformed" is a raw value failing is_kebab(); "unrecognized" is valid kebab-case but absent from KNOWN_KINDS. A note with no extractable kind: at all still counts toward total_files but appears in no other bucket.
CLI: audit <vault> python3 harness/skills/memory/scripts/kind_registry.py audit <vault_path> (kind_registry.py:166-183) Runs audit() against the given vault path and prints a human-readable report (_print_report, kind_registry.py:151) โ€” total files scanned, known-kind counts by frequency, then unrecognized and malformed lists.

You reserved two kinds in AgentM Memory Index. These are session-cost and failure-incident. They are recognized values in this same catalog. You do not keep them in a separate list.

Validator (task 2)

Note

Status: implemented โ€” You shipped this in harness/skills/memory/scripts/frontmatter_validator.py. You covered it with 13 tests in scripts/test_frontmatter_validator.py. The test classes are TestValidateSingleNote and TestValidateVault.

The validate(note_path) function (frontmatter_validator.py:67, signature validate(note_path: Path | str) -> list[str]) checks one note's frontmatter. It returns a list of violation strings. An empty list means the note is clean. It never writes to note_path. It runs the following checks:

  • It verifies kind is a known value. It uses kind_registry.is_known (kind_registry.py:78). It flags unrecognized kinds as "kind {kind!r} is not a recognized kind (unrecognized, not rejected)". It does not silently accept or hard-reject them.
  • It verifies kind is valid kebab-case. It uses kind_registry.is_kebab (kind_registry.py:68). It flags malformed kinds as "kind {kind!r} is not valid kebab-case".
  • It verifies the universal required fields are present. It checks against kind_registry.REQUIRED_UNIVERSAL_FIELDS (kind_registry.py:59-61). It imports this directly rather than re-deriving it. It returns one "missing required field \{field_name}`"` violation per absent field.
  • It checks if a note has a frontmatter block. The text must open with a --- delimiter pair. If missing, it returns ["no frontmatter block found"]. It uses a minimal stdlib-only _parse_frontmatter() (frontmatter_validator.py:38-64). This mirrors the parse contract of vault_lint.py. It reads key-value pairs without nested structures or PyYAML.

The validate_vault(vault_path, *, scope_dirs=("personal", "projects")) function (frontmatter_validator.py:95, signature validate_vault(vault_path: Path | str, *, scope_dirs=_DEFAULT_SCOPE_DIRS) -> dict[str, list[str]]) walks every *.md file under the vault's scope dirs. It returns {rel_path: [violations]} for notes with at least one violation. It omits clean notes entirely. It skips _archive/ dirs and PLAN.archive.* files, the same two exclusions the audit() walk from kind_registry.py makes, though that walk covers the whole vault root rather than these scope dirs.

DC-4 exclusion dirs. The _EXCLUDE_DIRS constant in frontmatter_validator.py (a frozenset, matched per path segment: _idea-incubator, _meta, _inbox, scratch, _archive, _opinions, _crystallize-staging, tasks) mirrors the _EXCLUDE_DIRS constant in vault_lint.py exactly. This is the DC-4 convention. These subdirectories carry non-memory-entry content. This includes a project's session-owned plan state and staging areas. You never meant for these to satisfy the universal frontmatter contract. (Several of the named directories โ€” _meta, scratch, _crystallize-staging โ€” were themselves retired from the vault, _meta in two steps: filing-v2 part 2a retired everything under it except the seam-owned repos.json, and the memory-root trims (agentm-vault plan 05) moved that registry out too, to the engine state directory, retiring _meta/ in full. The entries stay in the set so a leftover copy in an unmigrated vault never floods the walk. _harness sat in this set until agentm-vault plan 15 retired the vault's per-project _harness/ directory outright and replaced the entry with tasks โ€” a project's plan, progress and tracker now live under tasks/<slug>/, which carries no frontmatter by design just as the old _harness/PLAN.md didn't.) This exclusion prevents false positives. The first draft's walk lacked it. A real-vault run flooded about 1800 false "no frontmatter block found" violations. These targeted plain harness state files like projects/<repo>/_harness/PLAN.md and progress.md, since renamed projects/<repo>/tasks/<slug>/plan.md and progress.md. These files carry no frontmatter by design. You wrote test_excludes_harness_meta_inbox_dream_staging_dirs in scripts/test_frontmatter_validator.py as the regression test for that bug.

You can run the CLI (frontmatter_validator.py:120-153). The --check <path> flag validates one note. It prints its violations and exits with 1. It prints <path>: clean and exits with 0 on success. The --check-vault <vault_path> flag runs validate_vault() over the whole vault. It prints <rel_path>: and violation lines for each dirty note. It prints clean: no violations found and exits with 0 when it finds no issues. The two flags are mutually exclusive. You must provide one.

How this differs from vault_lint.py

vault_lint.py kind_registry.py + frontmatter_validator.py (both shipped)
Scope Nine checks across the full frontmatter shape (field order, dates, wikilinks, supersede integrity, etc.) kind value + the universal required-field trio only
kind handling Assumes kind is already valid; doesn't catalog known values The catalog itself โ€” known vs. unrecognized, kebab-case malformed (known_kinds(), is_known(), is_kebab()), consumed directly by frontmatter_validator.validate()
Mutation Read-only, report-only Read-only, report-only (same contract; confirmed for kind_registry.py by test_audit_never_writes_to_the_vault, and for frontmatter_validator.py by test_validate_never_writes_to_the_file / test_validate_vault_never_writes_to_any_file)
Schema source Imports save.py's FRONTMATTER_FIELD_ORDER / REQUIRED_FRONTMATTER_FIELDS kind_registry.py keeps its own standalone REQUIRED_UNIVERSAL_FIELDS tuple (no import-time dependency on save.py); frontmatter_validator.py imports it directly
Exclusion dirs N/A _EXCLUDE_DIRS (frontmatter_validator.py) mirrors vault_lint.py's own _EXCLUDE_DIRS exactly (DC-4): _idea-incubator, _meta, _inbox, scratch, _archive, _opinions, _crystallize-staging, tasks (_harness retired from the set under agentm-vault plan 15)

You should point operators to Audit the vault. This page explains which tool answers "is this note's kind legitimate" versus "is this note's frontmatter shape correct."

Downstream consumer

The MOC generator (task 3, V6-18) once used this registry to label each generated per-kind page as a known kind or an unrecognized one. The pages retired at the 2026-08-11 rehoming pass, the functions that wrote them went in agentm-vault plan 07, and moc_generator.py no longer imports this module. vault_lint.py, frontmatter_validator.py, check-vocabulary-membership.py and check-kind-taxonomy (task 4, below) still read it.

Advisory kind-taxonomy check in check-all.sh (task 4)

You wired a check-kind-taxonomy step (scripts/check-kind-taxonomy.sh) into scripts/check-all.sh. This runs the task-1 audit() through check-vocabulary-membership.py --report, over the corpus that gate resolves: $MEMORY_ROOT when exported, else the configured vault. It performs a graceful skip (exit 0) only when nothing resolves. Until 2026-09-20 it read $MEMORY_ROOT alone, and since check-all.sh has never exported either name, every battery run skipped. It acts like the existing check-slop.py --report wiki step. It is report-only, never a failing gate. You cannot use a hard PASS/FAIL on the real vault's 47-value mess yet. You must first make an operator judgment call on normalization. You parked this in agentm issue #273. The script prints violations as a report. The step's own exit code stays 0 regardless of findings. You confirmed this via subprocess-driven tests in scripts/test_check_kind_taxonomy.py. These tests verify an unset path, nonexistent path, clean vault, and a vault with real violations all exit 0. You can read the gate table entry in CI Gates.

Related

  • Audit the vault โ€” This provides the operator recipe for the sibling vault_lint.py tool.
  • Vault lint checks reference โ€” This lists the nine-check catalog. This plan's registry and validator complement it rather than duplicate it.
  • AgentM Memory System โ€” This provides the governing design (V6-15).
  • AgentM Memory Index โ€” This sibling design reserves the session-cost and failure-incident kinds. You can refer to Registry (task 1) above.
  • CI Gates โ€” This documents the advisory check-kind-taxonomy step once shipped.
โš ๏ธ **GitHub.com Fallback** โš ๏ธ