#!/usr/bin/env python3
"""session-mail-run — run the numbered items under an MHS PATH, in order, verified (`memory run /BOOT/WAKE` is the wake).

Ported from Pip's wake script (CID-164, 2026-08-24); agent + root from the
environment, one implementation for every agent. MAIL-TO-REALIZED (Mike's term, voice
2026-08-23): the commands live IN memory as numbered items under
/BOOT/WAKE. Each item's subject after the number is one shell line; a body
(more than a placeholder) is the script instead; a trailing `# budget=N`
caps that section's tokens (default 600).

Provides nothing itself. For each item, in number order: VERIFY it (MHS 170
membership: From and To the agent, and the sibling session-mail-verify passes — an
unverified item is not an edge and is NEVER executed), run it, print it under
a header carrying the exact command and its cost, truncate at the budget and
say so. Ends with the total.

  memory run PATH [--json] [--only N,...] [--list] [--toml FILE]

  --list   materialize only: print the script that would run, do not run it
  --toml   fallback config (~/.<agent>/wake.toml) used ONLY if /BOOT/WAKE is
           unreachable — the shipwreck path, reported loudly when taken.
"""
import importlib.util, json, os, re, subprocess, sys

HOME = os.path.expanduser("~")
# Identity from the environment (same resolution as `session mail arm`, CA-209).
AGENT = (os.environ.get("CLAUDE_CODE_AGENT") or os.environ.get("SESSION_AGENT") or "").strip()
if not AGENT:
    sys.exit("memory run: no agent — set CLAUDE_CODE_AGENT (or SESSION_AGENT)")
# One implementation of the reader: the sibling `session thread` script.
PT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mhs-thread")
import shutil
TOKENS = shutil.which("tokens")   # the skillbox `tokens` command; bytes/4 only if it is missing

def count(text):
    """Tokens via `tokens -` (Mike, 2026-08-23: "we have a tokens command");
    bytes/4 is the fallback and is ~30% LOW on tree output (measured 2666 vs 2081)."""
    if TOKENS and text:
        try:
            r = subprocess.run([TOKENS, "-"], input=text, capture_output=True, text=True, timeout=10)
            m = re.search(r"tokens:\s*(\d+)", r.stdout)
            if m: return int(m.group(1))
        except Exception:
            pass
    return len(text) // 4

def load_pt():
    from importlib.machinery import SourceFileLoader
    loader = SourceFileLoader("pt", PT)           # no .py suffix on print-thread
    spec = importlib.util.spec_from_loader("pt", loader)
    m = importlib.util.module_from_spec(spec); loader.exec_module(m); return m

def kids_of(flat, i, d):
    """The direct children of flat[i] (depth d), as (depth, message)."""
    out = []
    for d2, m2 in flat[i + 1:]:
        if d2 <= d:
            break
        if d2 == d + 1:
            out.append((d2, m2))
    return out

def items_from_mail(pt, path):
    """[(num, command, file, verified?)] for the surviving children of PATH (e.g. /BOOT/WAKE)."""
    r = subprocess.run([PT, "--path", path, "--resolve"], capture_output=True, text=True)
    if r.returncode != 0:
        raise RuntimeError(r.stderr.strip() or r.stdout.strip())
    label = r.stdout.strip().strip("<>")
    tid = subprocess.run(["notmuch", "search", "--output=threads", "--limit=1", f"id:{label}"],
                         capture_output=True, text=True).stdout.strip()
    raw = subprocess.run(["notmuch", "show", "--format=json", "--body=true", tid],
                         capture_output=True, text=True).stdout
    flats = []
    for thread in json.loads(raw):
        flat = []
        for top in thread:
            pt.flatten(top, 0, flat)
        flats.append(flat)
    # Only a MEMBER's Supersedes: counts (MHS 170): an unsigned reply naming a
    # wake item dropped that step from the wake — never executed in its place
    # (each item is verified below), but gone (TARS / Pip, 2026-09-28).
    member = pt.Members(AGENT)
    pairs = pt.member_pairs(flats, member)
    dead = pairs
    out = []
    for flat in flats:
        for i, (d, m) in enumerate(flat):
            if m.get("id", "").strip("<>") != label:
                continue
            pairs.prefetch([x.get("id", "") for _d2, x in kids_of(flat, i, d)])  # chains, in parallel
            for j in pt.effective_children(flat, i, dead, pairs):
                m2 = flat[j][1]
                s = m2["headers"].get("Subject", "").strip()
                mm = re.match(r"^(\d+(?:\.\d+)*)\s+(.*)$", s)
                if not mm: continue          # labels ([CHARTER]) and unnumbered suggestions are not commands
                num, rest = mm.group(1), mm.group(2)
                budget = 600
                bm = re.search(r"#\s*budget=(\d+)\s*$", rest)
                if bm:
                    budget = int(bm.group(1)); rest = rest[: bm.start()].rstrip()
                body = pt.body_text(m2)
                cmd = body if body and body.strip() not in ("", "x") else rest
                fn = next((f for f in m2.get("filename", []) if os.path.exists(f)), "")
                frm = m2["headers"].get("From", "")
                # The reader's own membership test (MHS 170): From AND To the
                # agent AND signed. From + signature alone let a signed item
                # addressed to someone else run (review, 2026-09-28).
                ok = member(m2)
                why = "" if ok else ("verify timed out" if member.unknown(m2)
                                     else f"failed verification (From {frm})")
                out.append({"num": num, "command": cmd, "title": rest, "budget": budget, "file": fn,
                            "verified": ok, "from": frm, "why": why})
            # A superseder whose verify timed out retires its item and runs
            # neither version (MemberPairs.blocked). List it, SKIPPED, with the
            # reason — a step that silently went back to v1 is what this ends
            # (review, 20:26 Mon 2026-09-28).
            by = {x.get("id", "").strip("<>"): x for _d, x in flat}
            todo = [x.get("id", "").strip("<>") for d2, x in kids_of(flat, i, d)]
            seen = set(todo)
            while todo:
                tid_ = todo.pop()
                for sid in pairs.blocked(tid_):
                    ms = by[sid]
                    s2 = ms["headers"].get("Subject", "").strip()
                    mm = re.match(r"^(\d+(?:\.\d+)*)\s+(.*)$", s2)
                    if mm and sid not in seen:
                        body = pt.body_text(ms)
                        rest2 = mm.group(2)
                        tnum = re.match(r"^(\d+(?:\.\d+)*)", by[tid_]["headers"].get("Subject", "").strip())
                        # Say "does not run either" only when it is true: a
                        # non-member target retires nothing, so what it
                        # names may still run (review, 22:03).
                        why = ("verify timed out; it supersedes item %s, which does not run either"
                               % (tnum.group(1) if tnum else "?")) if member(by[tid_]) else \
                              "verify timed out (what it supersedes is not a member, so it retires nothing)"
                        out.append({"num": mm.group(1),
                                    "command": body if body and body.strip() not in ("", "x") else rest2,
                                    "title": rest2, "budget": 600,
                                    "file": next((f for f in ms.get("filename", []) if os.path.exists(f)), ""),
                                    "verified": False, "from": ms["headers"].get("From", ""),
                                    "why": why})
                for sid in pairs.raw.get(tid_, []):
                    if sid in by and sid not in seen:
                        seen.add(sid)
                        todo.append(sid)
            break
    out.sort(key=lambda x: [int(p) for p in x["num"].split(".")])
    return out

def items_from_toml(path):
    import tomllib
    with open(path, "rb") as f:
        cfg = tomllib.load(f)
    secs = sorted(cfg.get("section", {}).items(), key=lambda kv: kv[1].get("order", 50))
    return [{"num": str(s.get("order", 50)), "command": s["command"], "budget": int(s.get("budget", 600)),
             "title": s["command"], "file": "", "verified": True, "from": "toml:" + name} for name, s in secs]

def main():
    argv = sys.argv[1:]
    if "-h" in argv or "--help" in argv:
        sys.exit(__doc__)
    as_json = "--json" in argv; list_only = "--list" in argv
    only = set(argv[argv.index("--only") + 1].split(",")) if "--only" in argv else None
    toml = argv[argv.index("--toml") + 1] if "--toml" in argv else f"{HOME}/.{AGENT}/wake.toml"
    paths = [a for a in argv if a.startswith("/")]
    if len(paths) != 1:
        sys.exit("memory run: exactly one PATH (e.g. /BOOT/WAKE)\n" + __doc__)
    path = paths[0]
    source = path
    try:
        items = items_from_mail(load_pt(), path)
        if not items:
            raise RuntimeError(f"{path} has no numbered items")
    except Exception as e:
        if os.path.exists(toml):
            source = f"SHIPWRECK fallback {toml} — {path} unreachable: {e}"
            items = items_from_toml(toml)
        else:
            # No items in memory and no fallback file: run the MHS minimum —
            # `ls /` + the verified [BOOT] item — and SAY so, so a fresh agent
            # (a root, no wake items yet) boots on something true rather than
            # a traceback or a silent empty wake. Mint /BOOT/WAKE items to extend.
            source = f"DEFAULT (ls / + [BOOT] only) — {path} unreachable: {e}; no {toml}"
            items = [{"num": "10", "command": f"{PT} --boot", "title": f"{PT} --boot", "budget": 800,
                      "file": "", "verified": True, "from": "default"}]
    if only:
        items = [i for i in items if i["num"] in only]
    if list_only:
        print(f"# materialized from {source}")
        for i in items:
            flag = "" if i["verified"] else "   # NOT VERIFIED — would be skipped: %s" % i.get("why", "")
            print(f"# {i['num']}  budget={i['budget']}{flag}\n{i['command']}\n")
        return
    out, total = [], 0
    for i in items:
        if not i["verified"]:
            out.append({**i, "rc": -1, "tokens": 0, "truncated": False,
                        "output": f"SKIPPED: item {i['num']} {i.get('why') or 'failed verification'} — not an edge, not run"})
            continue
        try:
            r = subprocess.run(["sh", "-c", i["command"]], capture_output=True, text=True, timeout=20, stdin=subprocess.DEVNULL)
            text, rc, err = (r.stdout or "").rstrip("\n"), r.returncode, (r.stderr or "").strip()
        except subprocess.TimeoutExpired:
            text, rc, err = "", 124, "timeout"
        if rc != 0 and not text:
            text = "(failed rc=%d) %s" % (rc, err.splitlines()[-1] if err else "")
        tok = count(text); trunc = False
        if tok > i["budget"]:
            keep = int(len(text) * i["budget"] / tok)       # proportional cut at the measured rate
            text = text[:keep].rstrip() + "\n… truncated at %d tok (of %d) — re-run the command for all" % (i["budget"], tok)
            trunc, tok = True, i["budget"]
        total += tok
        out.append({**i, "rc": rc, "tokens": tok, "truncated": trunc, "output": text})
    if as_json:
        print(json.dumps({"source": source, "sections": out, "total_tokens": total}, ensure_ascii=False, indent=1)); return
    for s in out:
        print("═══ %s  $ %s  (≈%d tok%s) ═══" % (s["num"], s.get("title") or s["command"].splitlines()[0], s["tokens"], ", truncated" if s["truncated"] else ""))
        print(s["output"]); print()
    print("═══ run total ≈%d tok (%s) · %d items · materialized from %s ═══" % (total, "tokens" if TOKENS else "bytes/4", len(out), source))

if __name__ == "__main__":
    main()
