#!/usr/bin/env python3 """Build the livepass harnesses — render real hatch dialogs/shelves headlessly. The livepass is how converted modal surfaces get verified without booting a server: a minimal page that symlinks the REAL stylesheets and scripts, embeds the REAL markup (extracted fresh from the index files at build time), stubs ``window.authFetch`` with canned fixtures, and drives surfaces via ``?open=`` query params — including click-driving submits so dead buttons can't hide (the model-Save bug class). Usage: python3 scripts/livepass.py # build into /tmp/livepass/ python3 scripts/livepass.py --out DIR # build elsewhere python3 scripts/livepass.py --serve 8950 # build + serve (Ctrl+C stops) Then screenshot states (file:// blocks ES modules — always serve over http; the reduced-motion flag is REQUIRED, entrance animations race the capture): google-chrome --headless --disable-gpu --hide-scrollbars \\ --force-prefers-reduced-motion --window-size=1440,900 \\ --virtual-time-budget=9000 --screenshot=out.png \\ "http://localhost:8950/ui/livepass.html?open=new-ws&theme=light" UI harness (?open=): new-ws · new-ws-fork · edit-title · delete-ws · revoke-mcp · ws-delete · ws-delete-results (+ &theme=light, &busy=1) Console harness (?open=): schedule-create · schedule-edit · model-create · model-edit · model-save (drives a Save click; document.title becomes PUT-OK- on success) · policy · confirm · token Plus &tall=1 (90-row users panel — the .admin-content scroll state; the synthetic rows wrap to two lines, so judge overflow geometry, not row cadence) · &scrolled=1 lands mid-list, &scrolled=bottom shows the 24px scroll tail · &focuslast=1 focuses the last shelf-body control (the displaced-dock regression probe: only .sh-body may scroll; head/foot stay pinned). All combinable with ?open=. The console page wraps the fragment in the REAL L-shell chain — pane-pinned height, interior scroller — so scroll/dock geometry matches production; keep it that way. Body-level dialogs (confirm/install/coord-delete) are injected as riders; a driven ?open= that ends with no open dialog stamps OPEN-FAILED- into the title instead of passing silently. Governance surfaces (roles/HR/OGP/memory/skill) need fixtures that are not canned yet — add a fixture + driver branch below when you need one. Shell harness (?split=): right (default) · down · three · none — boots the REAL shell.js + pane.js split-view engine over stubbed seams (two demo conversational panes; ?split=three adds the Dashboard cell). + &theme=light. document.title stamps SPLIT-READY- on success and SPLIT-FAILED- when a driven split was denied — judge the focused cell's top accent bar, the separators, and the .shown tab marker. Rebuild after ANY markup change: the dialog blocks are embedded at build time. Assets are symlinked, so CSS/JS edits are live on refresh. """ from __future__ import annotations import argparse import re from pathlib import Path ROOT = Path(__file__).resolve().parent.parent UI_INDEX = ROOT / "turnstone/ui/static/index.html" CONSOLE_INDEX = ROOT / "turnstone/console/static/index.html" def extract_dialogs(index: Path, only_id: str | None = None) -> list[str]: """Every block, verbatim from the tree.""" html = index.read_text(encoding="utf-8") blocks = [] for m in re.finditer(r"[ \t]*]*class=\"[^\"]*\bhatch\b[^\"]*\"", html): end = html.index("", m.start()) + len("") block = html[m.start() : end] if only_id and f'id="{only_id}"' not in block: continue blocks.append(block) if not blocks: raise SystemExit(f"no dialog.hatch blocks found in {index}") return blocks def extract_admin_fragment() -> str: """The console admin pane — the hatch-host all shelves live inside.""" html = CONSOLE_INDEX.read_text(encoding="utf-8") start = html.index('
") + len("") return html[start:end] def inject(template: str, marker: str, payload: str) -> str: begin = template.index(f"") + len(f"") end = template.index(f"") return template[:begin] + "\n" + payload + "\n" + template[end:] def symlink(link: Path, target: Path) -> None: if link.is_symlink() or link.exists(): link.unlink() link.symlink_to(target) # -------------------------------------------------------------------------- # UI harness — the standalone app's dialog tier. Drives the REAL cards.js # controller for the batch surfaces so the production code path renders. # -------------------------------------------------------------------------- UI_TEMPLATE = """ ui livepass
""" # -------------------------------------------------------------------------- # Console harness — the admin pane fragment hosts the shelves (token-created # included); dialog-tier markup outside the fragment (confirm/install/ # coord-delete) is injected via the RIDERS marker in build(). # model-save click-drives the submit: document.title flips to PUT-OK-. # -------------------------------------------------------------------------- CONSOLE_TEMPLATE = """ console livepass
""" # -------------------------------------------------------------------------- # Shell harness — the SPLIT-VIEW surface. Unlike the ui/console pages (which # embed extracted markup), this one boots the REAL shell.js + pane.js over # stubbed classic seams and drives the split engine via ?split=. Two demo # conversational panes give the cells plausible content; the Dashboard pane # (registered by the shell itself) fills the third cell in ?split=three. # Loud-failure rule: the title stamps SPLIT-READY- only when the built # state matches the request — a denied/failed split stamps SPLIT-FAILED-. # -------------------------------------------------------------------------- SHELL_TEMPLATE = """ shell livepass

Dashboard

Launcher + workstreams table live here (livepass stub).

""" def build(out: Path) -> None: ui = out / "ui" con = out / "console" ui.mkdir(parents=True, exist_ok=True) con.mkdir(parents=True, exist_ok=True) symlink(ui / "shared", ROOT / "turnstone/shared_static") symlink(ui / "static", ROOT / "turnstone/ui/static") blocks = extract_dialogs(UI_INDEX) # the coordinator batch dialog shares the cards.js builder — ride along blocks += extract_dialogs(CONSOLE_INDEX, only_id="coord-delete-dialog") (ui / "livepass.html").write_text( inject(UI_TEMPLATE, "DIALOGS", "\n".join(blocks)), encoding="utf-8" ) print(f"{ui}/livepass.html — {len(blocks)} dialogs") symlink(con / "shared", ROOT / "turnstone/shared_static") symlink(con / "console-static", ROOT / "turnstone/console/static") frag = extract_admin_fragment() # Dialog-tier markup living OUTSIDE #admin-layout (confirm, install, # coord-delete) would otherwise be silently absent — and ?open=confirm # would screenshot a dialog-less page while the gate stayed green. riders = [b for b in extract_dialogs(CONSOLE_INDEX) if b not in frag] page = inject(CONSOLE_TEMPLATE, "FRAGMENT", frag) page = inject(page, "RIDERS", "\n".join(riders)) (con / "livepass.html").write_text(page, encoding="utf-8") print(f"{con}/livepass.html — admin fragment + {len(riders)} rider dialogs") sh = out / "shell" sh.mkdir(parents=True, exist_ok=True) symlink(sh / "shared", ROOT / "turnstone/shared_static") symlink(sh / "static", ROOT / "turnstone/console/static") (sh / "livepass.html").write_text(SHELL_TEMPLATE, encoding="utf-8") print(f"{sh}/livepass.html — split-view shell surface") def main() -> None: ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument("--out", type=Path, default=Path("/tmp/livepass")) ap.add_argument("--serve", type=int, metavar="PORT") args = ap.parse_args() build(args.out) if args.serve: import functools import http.server handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(args.out)) print(f"serving {args.out} on http://localhost:{args.serve}/ — Ctrl+C stops") http.server.ThreadingHTTPServer(("127.0.0.1", args.serve), handler).serve_forever() if __name__ == "__main__": main()