#!/usr/bin/env python3
"""axones_runner.py — MegaSherpa+ 24/7 loop skeleton for Pascha.
Wire your OpenClaw agents into the three TODO hooks; everything else is done.

Loop contract:
  pull work -> run gates -> emit events (sherpa_events) -> snapshot -> publish -> sleep
Fail-closed everywhere: on any error, emit nothing rather than something wrong,
log locally, heartbeat continues so the console shows STALE honestly instead of lying.
"""
import json, time, subprocess, traceback, os
from collections import Counter
from sherpa_events import Axones, verify

LIVE_DIR   = os.environ.get("AXONES_DIR", "/srv/solonic/live")
PUBLISH    = os.environ.get("AXONES_PUBLISH", "")   # e.g. "rsync -az {src}/ web:/var/www/solonic/live/"
CYCLE_SECS = int(os.environ.get("AXONES_CYCLE", "300"))
ACTOR, FAMILY = os.environ.get("AXONES_ACTOR","megasherpa/pascha"), os.environ.get("AXONES_FAMILY","<declare-honestly>")

def today_log():
    ch=os.environ.get("AXONES_CHANNEL","atlas")
    return os.path.join(LIVE_DIR, f"axones-{ch}-{time.strftime('%Y-%m-%d')}.jsonl")

def pull_work():
    """TODO(OpenClaw): return a list of proposals/tasks.
    Sources: literature scans, edge mining, credence elicitations, challenge submissions."""
    return []

def run_gate(item):
    """TODO(OpenClaw): run the Sherpa/Neurath gates. Return (verdict, evidence, note).
    Must include the semantic-duplicate check — the v0 hole is public."""
    raise NotImplementedError

def gate_actor_for(item):
    """TODO(OpenClaw): Ouroboros rule — the gate family must differ from the proposer family
    for MERGE verdicts. Return (actor, family) of the gating agent."""
    return ACTOR, FAMILY

def snapshot(ax_path):
    events=[json.loads(l) for l in open(ax_path)] if os.path.exists(ax_path) else []
    ok,n,head=verify(ax_path) if events else (True,0,"0"*64)
    fams=sorted({e.get("family","?") for e in events if e["type"]!="GENESIS"}) or ["none"]
    k=len(fams); rho=0.2  # published assumption until measured
    n_eff=round(k/(1+(k-1)*rho),2) if k>1 else float(k)
    tally=Counter(e.get("verdict") for e in events if e.get("verdict"))
    status={"mode":"continuous","continuous_operation":"RUNNING",
      "last_event_ts":events[-1]["ts"] if events else None,
      "events_total":n,"chain_head":head,"chain_ok":ok,
      "families_today":fams,"n_eff_today":n_eff,
      "independence_note":"one family = one vote; machine-juror seats open at /heliaia.html",
      "verdict_tally":dict(tally),
      "corpus_release":{"current":os.environ.get("CORPUS_RELEASE","v1.0.0"),
        "hash_file":"/SHASUMS256.txt",
        "staged_merges":sum(1 for e in events if str(e.get("verdict","")).startswith("AUTO-MERGE")),
        "note":"stream = pending layer; corpus changes only via versioned releases"}}
    json.dump(status,open(os.path.join(LIVE_DIR,"live-status.json"),"w"),indent=1)
    json.dump(events[-100:],open(os.path.join(LIVE_DIR,"live-recent.json"),"w"))

def publish():
    if PUBLISH:
        subprocess.run(PUBLISH.format(src=LIVE_DIR), shell=True, check=False)

def main():
    os.makedirs(LIVE_DIR, exist_ok=True)
    while True:
        ax=Axones(today_log())
        try:
            for item in pull_work():
                actor,family=gate_actor_for(item)
                verdict,evidence,note=run_gate(item)
                ax.emit("GATE",actor,family,target=item.get("id","?"),
                        verdict=verdict,evidence=evidence,note=note)
        except Exception:
            traceback.print_exc()  # fail closed: no fabricated events
        ax.emit("HEARTBEAT",ACTOR,FAMILY,note="cycle complete")
        snapshot(today_log()); publish()
        time.sleep(CYCLE_SECS)

if __name__=="__main__": main()
