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:
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""echo_triage.py — one-tap inbox triage, built on the reflect pipeline.
|
||||
|
||||
Triage used to be pure prose: the agent hand-routed each inbox line with individual
|
||||
PATCH/PUT calls and hand-wrote the processing log — which is why the inbox rots.
|
||||
But the machinery it needs already exists in echo_reflect (validate -> classify
|
||||
against the entity index -> preview -> apply via capture). This module reuses it
|
||||
and adds the two triage-specific pieces:
|
||||
|
||||
* `list_inbox` — parse `inbox/captures/inbox.md` into structured capture lines
|
||||
(date, text, age in days) so the model builds proposals from
|
||||
data, not by re-reading prose;
|
||||
* `apply` — same contract as reflect (dry-run unless confirm), plus an
|
||||
audit line in `inbox/processing-log/YYYY-MM-DD.md` for every
|
||||
routed item. Originals are NEVER deleted (operating contract:
|
||||
the processing log is the audit trail, deletion is explicit).
|
||||
|
||||
Proposals are reflect's PROPOSAL_SCHEMA plus an optional `"line"` — the original
|
||||
inbox line, echoed into the processing log so the audit trail maps 1:1.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import echo # noqa: E402
|
||||
|
||||
INBOX_PATH = "inbox/captures/inbox.md"
|
||||
LOG_DIR = "inbox/processing-log"
|
||||
|
||||
_CAPTURE_LINE = re.compile(r"^\s*-\s*(\d{4}-\d{2}-\d{2})\s*:?\s*(.*\S)\s*$")
|
||||
|
||||
|
||||
def parse_inbox(text: str) -> list[dict]:
|
||||
"""Dated capture bullets -> [{line, date, text, age_days}] (age from ECHO_TODAY)."""
|
||||
import datetime as dt
|
||||
try:
|
||||
today = dt.date.fromisoformat(echo.today())
|
||||
except ValueError:
|
||||
today = dt.date.today()
|
||||
items = []
|
||||
for raw_line in text.splitlines():
|
||||
m = _CAPTURE_LINE.match(raw_line)
|
||||
if not m:
|
||||
continue
|
||||
try:
|
||||
age = (today - dt.date.fromisoformat(m.group(1))).days
|
||||
except ValueError:
|
||||
age = None
|
||||
items.append({"line": raw_line.strip(), "date": m.group(1),
|
||||
"text": m.group(2), "age_days": age})
|
||||
return items
|
||||
|
||||
|
||||
def list_inbox(as_json: bool = False) -> int:
|
||||
status, body = echo.request("GET", echo.vault_url(INBOX_PATH))
|
||||
if status == 404:
|
||||
items = []
|
||||
else:
|
||||
echo.check(status, body, f"triage list {INBOX_PATH}")
|
||||
items = parse_inbox(body.decode(errors="replace"))
|
||||
if as_json:
|
||||
import echo_output
|
||||
env = echo_output.envelope("triage-list", {"path": INBOX_PATH, "items": items,
|
||||
"count": len(items)})
|
||||
print(json.dumps(env, ensure_ascii=False))
|
||||
return 0
|
||||
if not items:
|
||||
print("triage: inbox is empty — nothing to route.")
|
||||
return 0
|
||||
print(f"triage: {len(items)} capture(s) in {INBOX_PATH}")
|
||||
for it in items:
|
||||
age = f"{it['age_days']}d" if it["age_days"] is not None else "?"
|
||||
print(f" [{age:>4}] {it['date']}: {it['text']}")
|
||||
print("\nBuild a proposals JSON (reflect schema + optional \"line\") and run "
|
||||
"`echo.py triage <file>` to preview, `--apply` to route.")
|
||||
return 0
|
||||
|
||||
|
||||
def apply(proposals: list[dict], confirm: bool = False) -> int:
|
||||
"""Validate -> classify -> preview; with confirm, route each via capture AND write
|
||||
the processing-log audit line. Mirrors echo_reflect.apply's contract exactly."""
|
||||
import echo_reflect
|
||||
valid, errors = echo_reflect.validate(proposals)
|
||||
for e in errors:
|
||||
print(f"skip: {e}", file=sys.stderr)
|
||||
if not valid:
|
||||
print("triage: no valid proposals to route.")
|
||||
return 0
|
||||
|
||||
echo_reflect.classify(valid)
|
||||
counts = {a: sum(1 for p in valid if p.get("_action") == a)
|
||||
for a in ("create", "update", "inbox", "error")}
|
||||
print(f"triage: {len(valid)} proposal(s) — "
|
||||
f"{counts['create']} new, {counts['update']} update, {counts['inbox']} stay-in-inbox"
|
||||
+ (f", {counts['error']} error" if counts["error"] else ""))
|
||||
print(echo_reflect.preview(valid))
|
||||
|
||||
if not confirm:
|
||||
print("\ntriage: dry-run — re-run with --apply to route these and log the moves.")
|
||||
return 0
|
||||
|
||||
import echo_ops
|
||||
today_s = echo.today()
|
||||
applied = gated = 0
|
||||
for p in valid:
|
||||
if p.get("_action") == "error":
|
||||
continue
|
||||
if p.get("_action") == "inbox":
|
||||
continue # routing an inbox line back to the inbox is a no-op, not a move
|
||||
bodyfile = echo.temp_file((p.get("body") or "").encode()) if p.get("body") else None
|
||||
rc = echo_ops.capture(
|
||||
p.get("kind"), p["title"], bodyfile,
|
||||
aliases=p.get("aliases") or [], sources=p.get("sources") or [],
|
||||
tags=p.get("tags") or [],
|
||||
date=p.get("date"), domain=p.get("domain", "business"))
|
||||
if rc == 76:
|
||||
gated += 1
|
||||
continue
|
||||
if rc != 0:
|
||||
continue
|
||||
applied += 1
|
||||
original = (p.get("line") or p["title"]).strip()
|
||||
echo.cmd_append(f"{LOG_DIR}/{today_s}.md",
|
||||
f"- {original} → {p.get('_path', '?')}")
|
||||
print(f"triage: routed {applied}/{len(valid)} item(s); audit in {LOG_DIR}/{today_s}.md. "
|
||||
"Originals kept in the inbox (deletion is explicit-only)."
|
||||
+ (f" {gated} stopped at the duplicate gate — re-propose with the existing "
|
||||
f"entity's title or use capture --merge-into." if gated else ""))
|
||||
return 0
|
||||
Reference in New Issue
Block a user