#!/usr/bin/env python3 """Render the pql ticket board as a self-contained interactive HTML snapshot. Companion to clide's two-column board view, for sessions running in the Claude desktop app: the output is deployed as a claude.ai Artifact (stable URL across redeploys), giving always-visible pql board + ticket-detail reference without clide. The page is fully client-side over embedded JSON — artifacts are network-sandboxed, so this is a *snapshot*; regenerate + redeploy to refresh. Data comes from pql's native JSON output only (no output parsing): - `pql ticket list --full` → every ticket, whole rows - `pql ticket show --with-blockers` → dep edges, batched, for non-terminal tickets only (native list output carries no dep edges — if pql ever grows `list --with-blockers`, drop the batching here; candidate upstream feature request per the no-ad-hoc-wrappers rule) - `pql plan status` → header counts Usage: reach pr board-html [--output OUTPUT.html] (default output: /tmp/pql-board.html) The emitted file is an Artifact-ready fragment: no doctype/html/head/body wrapper (the Artifact pipeline adds those); it starts with + <style>. """ import json from datetime import datetime, timezone from tooling.core import config, console, process NON_TERMINAL = ["in_progress", "review", "ready", "backlog"] SHOW_CHUNK = 40 def pql(*args): """Query the planning store. Goes through the guarded exec (D-263). Was a bare subprocess.run with check=True, which raised CalledProcessError — a traceback at someone who wanted to know that pql was not installed. """ out = process.run( ["pql", *args], cwd=config.repo_root(), missing_fix="install pql, or check PATH in a non-interactive shell", fix=f"run `pql {' '.join(args)}` directly to see what it reports", ).stdout return json.loads(out) def fetch(): tickets = pql("ticket", "list", "--full") plan = pql("plan", "status") ids = [t["id"] for t in tickets if t.get("status") in NON_TERMINAL] blockers = {} for i in range(0, len(ids), SHOW_CHUNK): chunk = ids[i : i + SHOW_CHUNK] shown = pql("ticket", "show", ",".join(chunk), "--with-blockers") if isinstance(shown, dict): shown = [shown] for row in shown: edges = row.get("blockers") or row.get("blocked_by") or [] blockers[row["id"]] = [ e["id"] if isinstance(e, dict) else e for e in edges ] return tickets, blockers, plan PAGE = """<title>Settled Reach — pql board

Settled Reach · pql

snapshot __STAMP__
Select a ticket.
""" def run(out_path=None) -> None: """Render the board to `out_path`, defaulting to the repo's scratch dir.""" out_path = str(out_path) if out_path else str(config.path(".cache", "pql-board.html")) tickets, blockers, plan = fetch() stamp = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M") data = json.dumps( {"tickets": tickets, "blockers": blockers, "plan": plan}, ensure_ascii=False, separators=(",", ":"), ).replace("