1
0
forked from jason/echo

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
@@ -120,6 +120,22 @@ def as_list(value: object) -> list[object]:
return value if isinstance(value, list) else [value]
def fm_field_populated(raw: str, fields: dict[str, object], field: str) -> bool:
"""True when `field` exists AND holds a real value. parse_frontmatter is line-based,
so a block-style list (`tags:\n - x`) parses as "" — check the raw text for items
before calling the field empty."""
value = fields.get(field)
if isinstance(value, list):
return bool(value)
if str(value or "").strip():
return True
lines = raw.splitlines()
for index, line in enumerate(lines):
if re.match(rf"^{re.escape(field)}:\s*$", line):
return index + 1 < len(lines) and bool(re.match(r"^\s+-\s*\S", lines[index + 1]))
return False
def route_matchers():
routing = json.loads((SCRIPT_DIR / "routing.json").read_text(encoding="utf-8"))
routes = [(item["id"], re.compile(item["pattern"])) for item in routing.get("routes", [])]
@@ -171,6 +187,15 @@ def main() -> int:
missing = [k for k in REQUIRED_FM if not str(fm.get(k, "")).strip()]
if fm and missing:
flag("missing-frontmatter", f"{path}: missing {', '.join(missing)}")
# Kind-scoped completeness: entity notes must classify (status + tags). The
# per-kind requirement map lives in echo_index (KIND_REQUIRED_FM) so capture's
# defaults, this check, and sweep's backfill all read the same source of truth.
kind = idx_mod.kind_for_path(path)
if fm and kind:
for field in idx_mod.KIND_REQUIRED_FM.get(kind, ()):
if not fm_field_populated(raw, fm, field):
what = f"missing {field}" if field not in fm else f"empty {field}"
flag("incomplete-frontmatter", f"{path}: {what}")
created, updated = parse_date(fm.get("created")), parse_date(fm.get("updated"))
if created and updated and updated < created:
flag("date-order", f"{path}: updated {updated} is before created {created}")
@@ -307,6 +332,7 @@ def main() -> int:
"unknown-path": "Path matches no route in routing.json",
"retired-path": "Write to a retired/dead path",
"missing-frontmatter": "Missing required frontmatter field",
"incomplete-frontmatter": "Incomplete entity frontmatter (status/tags) — sweep.py --apply backfills",
"date-order": "updated earlier than created",
"future-date": "updated date is in the future",
"source-notes-wikilink": "Wikilink in source_notes (must be plain paths)",