#!/usr/bin/env python3 """cc-ci recipe-report launcher — one-shot agent that runs /recipe-report after the weekly upgrade. The report agent's model is configured SEPARATELY from the upgrader (so e.g. the upgrader runs on sonnet while the report is written by opus). Defaults: REPORT_MODEL=opus, REPORT_BACKEND=claude. Usage: launch-report.py start [DATE] use-or-create the session; runs /recipe-report [DATE] launch-report.py fresh [DATE] always start a new session launch-report.py stop kill the session launch-report.py status show session state Env: REPORT_MODEL (default opus), REPORT_BACKEND (default claude), REPORT_SESSION, REPORT_DIR. """ import os, subprocess, sys, time from pathlib import Path from datetime import datetime, timezone SESSION = os.environ.get("REPORT_SESSION", "cc-ci-report") WORKDIR = os.environ.get("REPORT_DIR", "/srv/cc-ci") LOG_DIR = os.environ.get("LOG_DIR", "/srv/cc-ci/.cc-ci-logs") BACKEND = os.environ.get("REPORT_BACKEND", "claude") MODEL = os.environ.get("REPORT_MODEL", "opus") CLAUDE_BIN = os.environ.get("CLAUDE_BIN", "claude") CLAUDE_FLAGS = os.environ.get("CLAUDE_FLAGS", "--dangerously-skip-permissions") OPENCODE_BIN = os.environ.get("OPENCODE_BIN", "/home/loops/.local/bin/opencode") OPENCODE_SERVER = os.environ.get("OPENCODE_SERVER", "http://127.0.0.1:4096") def log(m): print(f"[report {datetime.now(timezone.utc):%H:%M:%S}] {m}", flush=True) def die(m): log(f"ERROR: {m}"); sys.exit(1) def _sh(c): return subprocess.run(c, capture_output=True, text=True) def session_alive(): return _sh(["tmux", "has-session", "-t", SESSION]).returncode == 0 def kill_session(): subprocess.run(["tmux", "kill-session", "-t", SESSION], capture_output=True) def _busy(): return "esc to interrupt" in _sh(["tmux", "capture-pane", "-pt", SESSION]).stdout def build_kickoff(date): arg = f" {date}" if date else "" return ( f"*** cc-ci RECIPE-REPORT — one-shot ***\n" f"You generate the public weekly \"Recipe Report\". Run the /recipe-report skill now:\n" f" invoke /recipe-report{arg}\n" f"Full spec: {WORKDIR}/.claude/skills/recipe-report/SKILL.md. Creds in {WORKDIR}/.testenv; " f"reach the CI server with `ssh cc-ci`.\n" f"You are READ-ONLY: review the latest /upgrade-all run + every recipe's open PRs + CI verdicts, " f"order the wire table by priority-to-address (CVE recipes first), and publish one HTML page per " f"run to report.ci.commoninternet.net (+ regenerate the index). Public page — NO secrets/tokens/raw logs. " f"Never merge/edit/comment on PRs. When done, print the report URL + 'RECIPE REPORT COMPLETE' and " f"go idle (do NOT loop)." ) def start(mode, date): import shutil if not shutil.which("tmux"): die("tmux not found") Path(LOG_DIR).mkdir(parents=True, exist_ok=True) if session_alive(): # The report is a one-shot. Leave the session ONLY if it's actively producing a report; # an idle/leftover session (e.g. last week's, gone idle) is killed so a new run starts. if mode == "use-or-create" and _busy(): log(f"{SESSION} busy with a report — leaving it"); return log(f"{SESSION} exists (idle/leftover) — killing first"); kill_session(); time.sleep(1) kf = Path(LOG_DIR) / f".kickoff-{SESSION}.txt" kf.write_text(build_kickoff(date)) model_flag = f"--model '{MODEL}'" if MODEL else "" if BACKEND == "claude": cmd = f"{CLAUDE_BIN} --remote-control '{SESSION}' {model_flag} {CLAUDE_FLAGS} \"$(cat '{kf}')\"" cwd = WORKDIR elif BACKEND == "opencode": cwd = "/srv/cc-ci-orch/cc-ci" cmd = f"set -a; . /srv/cc-ci/.testenv; set +a; NO_COLOR=1 {OPENCODE_BIN} attach {OPENCODE_SERVER} --dir {cwd}" else: die(f"unknown REPORT_BACKEND '{BACKEND}'") log(f"starting {SESSION} (backend={BACKEND}, model={MODEL}, date={date or 'today'})") subprocess.run(["tmux", "new-session", "-d", "-s", SESSION, "-c", cwd, cmd]) subprocess.run(["tmux", "pipe-pane", "-o", "-t", SESSION, f"cat >> '{LOG_DIR}/{SESSION}.log'"]) if BACKEND == "opencode": time.sleep(12) subprocess.run(["tmux", "send-keys", "-t", SESSION, "-l", "--", f"Read {kf} and follow it."]) time.sleep(0.5); subprocess.run(["tmux", "send-keys", "-t", SESSION, "C-m"]) log(f"started. attach: tmux attach -t {SESSION}") def main(): a = sys.argv[1:] cmd = a[0] if a else "start" date = a[1] if len(a) > 1 else "" if cmd == "start": start("use-or-create", date) elif cmd == "fresh": start("fresh", date) elif cmd == "stop": kill_session(); log("stopped.") elif cmd == "status": log(f"{SESSION}: {'RUNNING' if session_alive() else 'stopped'} (backend={BACKEND} model={MODEL})") else: print(__doc__); sys.exit(2) if __name__ == "__main__": main()