ver 1.5.0 — six-improvement pass: retention, routing, self-firing memory

1. capture-update keeps the whole body (was truncating to first line)
2. frontmatter completeness: kind-default status + kind-seeded tags at
   capture, fm create-or-replace, incomplete-frontmatter lint, sweep
   backfill (one data source: KIND_STATUS/KIND_REQUIRED_FM)
3. pre-write duplicate gate (exit 76, --merge-into/--force)
4. recall corpus + ranking: sessions/journal indexed (down-weighted),
   BM25 x freshness x status fusion, recall --json, index schema 2
5. session hooks: SessionStart auto-load, Stop reflection nudge
6. one-tap triage verb with processing-log audit; --json on read verbs

+22 mock end-to-end tests; all suites green. Docs current (README,
CHANGELOG, SKILL.md, commands, references, MAINTENANCE, eval README).
Drops tracked .DS_Store (gitignored); retires README-gretchen.md
(moved to gitignored dist/); adds the field report that drove item 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jason Stedwell
2026-07-03 12:57:52 -05:00
parent e76add6192
commit be3fd36ebf
31 changed files with 1209 additions and 174 deletions
@@ -55,6 +55,31 @@ KIND_TYPE = {
DATE_KINDS = {"meeting", "decision"} # filename carries a YYYY-MM-DD prefix
AREA_DOMAINS = ("business", "personal", "learning", "systems")
# kind -> default `status:` stamped by capture when none is given. Every entity note is
# born with a status — a missing key is the #1 source of frontmatter drift (18/71 notes
# in the field audit). Records of past events default to done; living notes to active.
KIND_STATUS = {
"person": "active", "company": "active", "concept": "active",
"reference": "active", "meeting": "done", "project": "active",
"area": "active", "semantic": "active", "episodic": "done",
"working": "active", "skill": "active", "decision": "done",
}
# kind -> frontmatter fields that must be present AND non-empty on that kind's notes.
# Data-driven so vault_lint's incomplete-frontmatter check and sweep's backfill stay in
# lockstep with what capture stamps; add a (kind, field) pair here and all three follow.
KIND_REQUIRED_FM = {
"person": ("status", "tags"),
"company": ("status", "tags"),
"concept": ("status", "tags"),
"reference": ("status", "tags"),
"project": ("status", "tags"),
"area": ("status", "tags"),
"skill": ("status", "tags"),
"meeting": ("tags",),
"decision": ("tags",),
}
def slugify(text: str) -> str:
text = str(text).strip().lower()