Files
cc-ci-orchestrator/cc-ci-plan/launch-report.py
autonomic-bot c7301a9e39 feat(recipe-report): /recipe-report skill + helper + launcher (default opus); wire into upgrade-all
- recipe-report.py: survey (run + per-recipe PRs + CI verdicts) / render (spec->HTML) / publish
  (copy to cc-ci:/var/lib/cc-ci-reports + regen index).
- skill .claude/skills/recipe-report: review the weekly run, classify needs-attention vs routine,
  publish one public HTML page per week + index at report.ci.commoninternet.net. Read-only.
- launch-report.py: one-shot cc-ci-report agent, REPORT_MODEL default opus (separate from the
  sonnet upgrader), REPORT_BACKEND default claude.
- upgrade-all SKILL: closing step launches the report agent.
Serving (nix/modules/reports.nix) already deployed + live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:02:22 +00:00

99 lines
4.5 KiB
Python
Executable File

#!/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 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"classify needs-attention vs routine, and publish one HTML page per run to "
f"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():
if mode == "use-or-create":
log(f"{SESSION} already running — leaving it"); return
log(f"{SESSION} exists — killing (fresh)"); 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()