2.1.1 — MCP Phase 0: every high-level op returns an envelope (return-not-print)
Internal refactor per docs/MCP-SERVER-SPEC.md §4; CLI output and exit codes unchanged (wrappers reproduce them from the envelopes): - echo_ops: capture_op/resolve_op/link_op — duplicate gate and offline queueing are data (action: duplicate-gate + candidates; queued: true); the --json stdout-swallow hack is deleted (helper chatter -> stderr inside the cores); capture_op takes body_text= directly (the MCP path, no temp file). - echo_recall.recall_op — one structured result backs prose and --json. - echo_triage.list_op/route_op, echo_reflect.apply_op, echo_session.session_end_op — call capture_op internally, count gate outcomes from envelopes. - echo.scope_show_op/scope_set_op/load_op (+ shared _load_gather), and echo_doctor.run_op (checks as data). New eval/test_ops_api.py asserts the contract for every core: envelope shape AND stdout purity (a stray print would corrupt an MCP response stream). All six suites green. The 2.2 server build now starts directly at the app layer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -99,43 +99,69 @@ def preview(proposals: list[dict]) -> str:
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def apply(proposals: list[dict], confirm: bool = False) -> int:
|
||||
"""Validate -> classify -> preview; with confirm=True, apply each via echo_ops.capture
|
||||
(which routes/indexes/links/logs, each self-lock-guarded). Without confirm it is a
|
||||
dry-run that writes nothing — the preview IS the confirmation step."""
|
||||
def apply_op(proposals: list[dict], confirm: bool = False) -> dict:
|
||||
"""Core reflect (Phase 0): validate -> classify -> (apply via capture_op). Returns
|
||||
an envelope — rows carry the classified preview; with confirm, `results` carries
|
||||
each proposal's capture outcome. Never prints to stdout."""
|
||||
import echo_output
|
||||
valid, errors = validate(proposals)
|
||||
for e in errors:
|
||||
print(f"skip: {e}", file=sys.stderr)
|
||||
if not valid:
|
||||
print("reflect: no valid proposals to apply.")
|
||||
return 0
|
||||
|
||||
classify(valid)
|
||||
counts = {a: sum(1 for p in valid if p.get("_action") == a) for a in ("create", "update", "inbox", "error")}
|
||||
print(f"reflect: {len(valid)} proposal(s) — "
|
||||
f"{counts['create']} new, {counts['update']} update, {counts['inbox']} inbox"
|
||||
+ (f", {counts['error']} error" if counts["error"] else ""))
|
||||
print(preview(valid))
|
||||
|
||||
if not confirm:
|
||||
print("\nreflect: dry-run — re-run with --apply to write these to memory.")
|
||||
return 0
|
||||
rows: list[dict] = []
|
||||
counts: dict[str, int] = {}
|
||||
if valid:
|
||||
classify(valid)
|
||||
counts = {a: sum(1 for p in valid if p.get("_action") == a)
|
||||
for a in ("create", "update", "inbox", "error")}
|
||||
rows = [{"action": p.get("_action"), "kind": p.get("kind"),
|
||||
"title": p["title"], "path": p.get("_path")} for p in valid]
|
||||
data = {"proposals": len(proposals or []), "valid": len(valid), "errors": errors,
|
||||
"counts": counts, "rows": rows, "dry_run": not confirm,
|
||||
"applied": 0, "gated": 0, "results": []}
|
||||
if not valid or not confirm:
|
||||
return echo_output.envelope("reflect", data)
|
||||
|
||||
import echo_ops # lazy: the apply path pulls in the capture/index/link stack
|
||||
applied = gated = 0
|
||||
results = []
|
||||
for p in valid:
|
||||
if p.get("_action") == "error":
|
||||
continue
|
||||
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,
|
||||
env = echo_ops.capture_op(
|
||||
p.get("kind"), p["title"], body_text=p.get("body") or "",
|
||||
aliases=p.get("aliases") or [], sources=p.get("sources") or [],
|
||||
tags=p.get("tags") or [],
|
||||
date=p.get("date"), domain=p.get("domain", "business"),
|
||||
inbox=bool(p.get("inbox")) or not p.get("kind"))
|
||||
applied += 1 if rc == 0 else 0
|
||||
gated += 1 if rc == 76 else 0
|
||||
print(f"reflect: applied {applied}/{len(valid)} proposal(s)."
|
||||
act = env.get("action", "")
|
||||
if act == "duplicate-gate":
|
||||
gated += 1
|
||||
elif env.get("ok"):
|
||||
applied += 1
|
||||
results.append({"title": p["title"], "action": act,
|
||||
"path": env.get("path"), "ok": bool(env.get("ok"))})
|
||||
data.update(applied=applied, gated=gated, results=results)
|
||||
return echo_output.envelope("reflect", data)
|
||||
|
||||
|
||||
def apply(proposals: list[dict], confirm: bool = False) -> int:
|
||||
"""CLI wrapper: same preview/summary text as pre-Phase-0. Without confirm it is a
|
||||
dry-run that writes nothing — the preview IS the confirmation step."""
|
||||
env = apply_op(proposals, confirm=confirm)
|
||||
for e in env["errors"]:
|
||||
print(f"skip: {e}", file=sys.stderr)
|
||||
if not env["valid"]:
|
||||
print("reflect: no valid proposals to apply.")
|
||||
return 0
|
||||
counts = env["counts"]
|
||||
print(f"reflect: {env['valid']} proposal(s) — "
|
||||
f"{counts['create']} new, {counts['update']} update, {counts['inbox']} inbox"
|
||||
+ (f", {counts['error']} error" if counts["error"] else ""))
|
||||
print("\n".join(f" {r['action'] or '?':7} | {('-' if r['action'] == 'inbox' else r['kind'] or '?'):9} "
|
||||
f"| {r['title']} -> {r['path'] or '?'}" for r in env["rows"]))
|
||||
if env["dry_run"]:
|
||||
print("\nreflect: dry-run — re-run with --apply to write these to memory.")
|
||||
return 0
|
||||
gated = env["gated"]
|
||||
print(f"reflect: applied {env['applied']}/{env['valid']} proposal(s)."
|
||||
+ (f" {gated} stopped at the duplicate gate — re-propose with the existing "
|
||||
f"entity's title (or capture --merge-into <slug>)." if gated else ""))
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user