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
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""echo_hook_stop.py — Stop hook: one-shot session-end reflection nudge.
Reflection (/echo-reflect) only captures memory if someone remembers to run it.
This hook watches the first Stop of a *substantive* session and, when nothing has
been reflected or session-logged yet, blocks once with a reminder so the model
runs the reflect flow (which still previews and asks the operator before writing
— the safety gate lives in reflect, not here).
Guards (a hook must NEVER trap a session in a loop or nag):
* `stop_hook_active` -> exit immediately (loop guard);
* fires at most ONCE per session (marker file under ECHO_STATE_DIR/hook-state);
* skips quiet sessions (< ECHO_REFLECT_MIN_TURNS user turns, default 5);
* skips when the transcript shows reflection / a session log already happened;
* skips when ECHO isn't configured on this machine;
* always exits 0.
"""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
MIN_TURNS = int(os.environ.get("ECHO_REFLECT_MIN_TURNS", "5"))
REASON = (
"[echo-memory] Session-end reflection has not run yet. If this session produced "
"durable facts, decisions, commitments, or artifacts worth remembering: run the "
"/echo-reflect flow (extract -> preview -> apply only with the operator's confirm) "
"and write the session log + heartbeat per the echo-memory skill. If nothing "
"durable emerged, simply finish — do not invent memories. "
"(This reminder fires once per session.)"
)
def state_marker(session_id: str) -> Path:
base = Path(os.environ.get("ECHO_STATE_DIR") or (Path.home() / ".echo-memory"))
return base / "hook-state" / f"{session_id}.reflect-nudged"
def count_user_turns(raw: str) -> int:
turns = 0
for line in raw.splitlines():
try:
rec = json.loads(line)
except Exception: # noqa: BLE001
continue
if rec.get("type") != "user":
continue
content = (rec.get("message") or {}).get("content")
if isinstance(content, str) and content.strip():
turns += 1
elif isinstance(content, list) and any(
isinstance(b, dict) and b.get("type") == "text" for b in content):
turns += 1
return turns
def already_reflected(raw: str) -> bool:
"""Transcript evidence that reflection or session logging already happened."""
return ("/echo-reflect" in raw
or re.search(r'echo\.py"?\s+reflect\b', raw) is not None
or "put _agent/sessions/" in raw
or "put _agent/heartbeat/last-session.md" in raw)
def main() -> int:
try:
payload = json.load(sys.stdin)
except Exception: # noqa: BLE001
return 0
if payload.get("stop_hook_active"):
return 0
session_id = str(payload.get("session_id") or "unknown")
marker = state_marker(session_id)
if marker.exists():
return 0
try:
import echo_config
if not echo_config.is_configured(echo_config.load()):
return 0 # nothing to reflect into — stay silent
except Exception: # noqa: BLE001
return 0
raw = ""
tp = payload.get("transcript_path")
if tp:
try:
raw = Path(tp).read_text(encoding="utf-8", errors="replace")
except Exception: # noqa: BLE001
raw = ""
if already_reflected(raw):
try: # done for this session — never fire
marker.parent.mkdir(parents=True, exist_ok=True)
marker.write_text("reflected\n", encoding="utf-8")
except Exception: # noqa: BLE001
pass
return 0
if count_user_turns(raw) < MIN_TURNS:
return 0 # quiet so far; a later stop may qualify (no marker written)
try:
marker.parent.mkdir(parents=True, exist_ok=True)
marker.write_text("nudged\n", encoding="utf-8")
except Exception: # noqa: BLE001
pass # if the marker can't be written, still nudge; stop_hook_active guards loops
print(json.dumps({"decision": "block", "reason": REASON}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())