Add Canvas command surface

This commit is contained in:
Svrnty
2026-05-28 21:44:02 -04:00
parent cf723141a4
commit 8b6c810f4a
17 changed files with 2874 additions and 8 deletions
+18 -3
View File
@@ -28,10 +28,19 @@ PLUGIN_REPO = Path(__file__).resolve().parent.parent
SMOKE = [
{"path": "/health", "expect": [200], "kind": "vanilla"},
{"path": "/api/vault/status", "expect": [200, 401, 403], "kind": "plugin"},
{"path": "/api/canvas/status", "expect": [200, 503, 401, 403], "kind": "plugin"},
{"path": "/api/canvas/tools", "expect": [200, 401, 403], "kind": "plugin"},
{"path": "/api/canvas/proxy?path=/api/v1/capabilities", "expect": [200, 401, 403, 502], "kind": "plugin"},
{"path": "/api/canvas/command", "method": "POST", "body": b"{}", "expect": [400, 401, 403], "kind": "plugin"},
{"path": "/api/canvas/design-context", "expect": [200, 401, 403], "kind": "plugin"},
{"path": "/api/canvas/events", "expect": [200, 401, 403], "kind": "plugin"},
{"path": "/api/umbrella", "expect": [200, 401, 403], "kind": "plugin"},
{"path": "/api/umbrella/doc?path=sot/README.md", "expect": [200, 401, 403], "kind": "plugin"},
{"path": "/plugins/svrnty/app.css", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
{"path": "/plugins/svrnty/app.js", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
{"path": "/plugins/svrnty/canvas.css", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
{"path": "/plugins/svrnty/canvas.js", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
{"path": "/plugins/svrnty/canvas.html", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
{"path": "/plugins/svrnty/umbrella.html", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
{"path": "/plugins/svrnty/umbrella.css", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
{"path": "/plugins/svrnty/umbrella.js", "expect": [200, 302, 401, 403], "kind": "plugin-static"},
@@ -61,10 +70,16 @@ def _wait_for(url, timeout=20):
return False
def _hit(base, path):
def _hit(base, spec):
path = spec["path"]
url = base.rstrip("/") + path
method = spec.get("method", "GET")
body = spec.get("body")
try:
with urlopen(url, timeout=5) as r:
req = Request(url, data=body, method=method)
if body is not None:
req.add_header("Content-Type", "application/json")
with urlopen(req, timeout=5) as r:
return r.status, r.read()[:200]
except URLError as e:
if hasattr(e, "code"):
@@ -78,7 +93,7 @@ def smoke(base):
rows = []
failed = 0
for s in SMOKE:
status, _body = _hit(base, s["path"])
status, _body = _hit(base, s)
ok = status in s["expect"]
rows.append({"path": s["path"], "status": status, "kind": s["kind"], "ok": ok})
if not ok:
+339
View File
@@ -0,0 +1,339 @@
#!/usr/bin/env python3
"""Browser smoke for the Hermes Canvas command-center loop.
Starts canva-editor in mock mode, starts hermes-webui with the Svrnty plugin,
opens the real WebUI shell, switches to Canvas, generates variants, captures a
screenshot, and writes a compact JSON proof.
"""
from __future__ import annotations
import argparse
import json
import os
import signal
import socket
import subprocess
import sys
import tempfile
import time
import warnings
from datetime import datetime
from pathlib import Path
from urllib.error import URLError
from urllib.request import urlopen
from PIL import Image
from playwright.sync_api import sync_playwright
PLUGIN_REPO = Path(__file__).resolve().parent.parent
HERMES_REPO = PLUGIN_REPO.parent
WEBUI_REPO = HERMES_REPO / "hermes-webui"
AGENT_REPO = HERMES_REPO / "hermes-agent"
CANVA_REPO = HERMES_REPO.parent / "cortex" / "L2-svrnty.tool-canva-editor"
CMO_REPO = HERMES_REPO / "cmo"
DEFAULT_OUT = HERMES_REPO / ".scratch" / "canvas-visual-proof.json"
def _free_port() -> int:
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
return port
def _wait_for(url: str, timeout: int = 30) -> bool:
deadline = time.time() + timeout
while time.time() < deadline:
try:
with urlopen(url, timeout=1) as res:
if res.status < 500:
return True
except URLError:
pass
except Exception:
pass
time.sleep(0.25)
return False
def _pixel_stats(path: Path) -> dict:
img = Image.open(path).convert("RGB")
width, height = img.size
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
pixels = list(img.getdata())
total = len(pixels)
bright = sum(1 for r, g, b in pixels if max(r, g, b) > 150)
distinct = len(set(pixels[:: max(1, total // 20000)]))
return {
"path": str(path),
"width": width,
"height": height,
"bright_ratio": round(bright / total, 4),
"sampled_distinct_colors": distinct,
"nonblank": bright > 1000 and distinct > 20,
}
def _start_canva(port: int, db_path: Path) -> subprocess.Popen:
env = os.environ.copy()
env["PATH"] = f"/home/svrnty/sdk/go1.26.0/bin{os.pathsep}{env.get('PATH', '')}"
env["PORT"] = str(port)
env["DB_PATH"] = str(db_path)
return subprocess.Popen(
["go", "run", "./cmd/server", "--mock"],
cwd=CANVA_REPO,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
preexec_fn=os.setsid,
)
def _start_webui(port: int, state_dir: Path, canva_base: str, plugin_module: str) -> subprocess.Popen:
env = os.environ.copy()
env.pop("HERMES_WEBUI_PASSWORD", None)
env["CANVA_EDITOR_BASE_URL"] = canva_base
env["HERMES_WEBUI_PYTHON_PLUGIN"] = plugin_module
if plugin_module == "svrnty_hermes_webui_refactor_plugin":
env["SVRNTY_WEBUI_DEVELOPMENT_INCLUDE_PROD"] = "1"
env["HERMES_WEBUI_PORT"] = str(port)
env["HERMES_WEBUI_STATE_DIR"] = str(state_dir)
env["HERMES_WEBUI_AGENT_DIR"] = str(AGENT_REPO)
env["HERMES_REPO_ROOT"] = str(HERMES_REPO)
env["PYTHONPATH"] = (
f"{PLUGIN_REPO}{os.pathsep}{HERMES_REPO / 'webui-plugin-development'}{os.pathsep}{AGENT_REPO}"
if not env.get("PYTHONPATH")
else f"{PLUGIN_REPO}{os.pathsep}{HERMES_REPO / 'webui-plugin-development'}{os.pathsep}{AGENT_REPO}{os.pathsep}{env['PYTHONPATH']}"
)
py = WEBUI_REPO / "venv" / "bin" / "python"
agent_py = AGENT_REPO / "venv" / "bin" / "python"
python_exe = str(py) if py.exists() else str(agent_py) if agent_py.exists() else "python3"
return subprocess.Popen(
[python_exe, str(WEBUI_REPO / "server.py")],
cwd=AGENT_REPO if AGENT_REPO.exists() else WEBUI_REPO,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
preexec_fn=os.setsid,
)
def _stop(proc: subprocess.Popen | None) -> str:
if proc is None:
return ""
output = ""
try:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
proc.wait(timeout=5)
except Exception:
try:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
except Exception:
pass
if proc.stdout:
try:
output = proc.stdout.read(4000)
except Exception:
output = ""
return output
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--output-json", default=str(DEFAULT_OUT))
parser.add_argument("--screenshot-dir", default=str(HERMES_REPO / ".scratch" / "screenshots" / "canvas"))
parser.add_argument("--plugin-module", default="svrnty_hermes_webui_plugin")
args = parser.parse_args()
if not CANVA_REPO.exists():
raise SystemExit(f"missing canva-editor repo: {CANVA_REPO}")
out_json = Path(args.output_json)
screenshot_dir = Path(args.screenshot_dir)
screenshot_dir.mkdir(parents=True, exist_ok=True)
canva_port = _free_port()
webui_port = _free_port()
canva_base = f"http://127.0.0.1:{canva_port}"
webui_base = f"http://127.0.0.1:{webui_port}"
canva_proc: subprocess.Popen | None = None
webui_proc: subprocess.Popen | None = None
with tempfile.TemporaryDirectory(prefix="hermes-canvas-webui-") as tmp:
tmp_path = Path(tmp)
canva_proc = _start_canva(canva_port, tmp_path / "canva-editor.db")
try:
if not _wait_for(f"{canva_base}/health", timeout=45):
raise RuntimeError(f"canva-editor did not become healthy at {canva_base}/health")
webui_proc = _start_webui(webui_port, tmp_path / "webui-state", canva_base, args.plugin_module)
if not _wait_for(f"{webui_base}/health", timeout=45):
raise RuntimeError(f"webui did not become healthy at {webui_base}/health")
proof = {
"generated_at": datetime.now().isoformat(timespec="seconds"),
"webui_base": webui_base,
"canva_base": canva_base,
"plugin_module": args.plugin_module,
}
cmo_helper = subprocess.run(
[
sys.executable,
str(CMO_REPO / "canvas_command.py"),
"command",
"Create a CMO-triggered draft promo card for Plan B",
"--variants",
"1",
"--brand",
"planb",
"--project",
"Hermes CMO Canvas Smoke",
"--wait",
],
env={**os.environ, "HERMES_WEBUI_URL": webui_base},
capture_output=True,
text=True,
timeout=60,
)
try:
cmo_helper_payload = json.loads(cmo_helper.stdout or "{}")
except ValueError:
cmo_helper_payload = {"parse_error": cmo_helper.stdout}
proof["cmo_helper"] = {
"returncode": cmo_helper.returncode,
"ok": cmo_helper.returncode == 0 and bool(cmo_helper_payload.get("ok")),
"command_id": cmo_helper_payload.get("command_id"),
"screens": len(cmo_helper_payload.get("screens") or []),
"event_wait_ok": bool((cmo_helper_payload.get("event_wait") or {}).get("ok")),
"design_context_status": (cmo_helper_payload.get("design_context") or {}).get("status"),
"secondbrain_status": (cmo_helper_payload.get("design_context") or {}).get("secondbrain_status"),
"stderr": cmo_helper.stderr[-1000:],
}
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
executable_path="/usr/bin/google-chrome" if Path("/usr/bin/google-chrome").exists() else None,
args=["--no-sandbox"],
)
page = browser.new_page(viewport={"width": 1440, "height": 1000})
page.goto(webui_base, wait_until="domcontentloaded", timeout=30000)
page.wait_for_function(
"() => window.SvrntyCanvas && window.SvrntyNav && typeof window.switchPanel === 'function'",
timeout=30000,
)
slash_registered = page.evaluate(
"() => typeof COMMANDS !== 'undefined' && COMMANDS.some((cmd) => cmd && cmd.name === 'canvas')"
)
page.evaluate("() => window.switchPanel('canvas')")
page.wait_for_selector("#svrntyCanvasOverlay:not([hidden])", timeout=10000)
page.wait_for_function(
"() => document.querySelector('#svrntyCanvasStatus')?.textContent.includes('online')",
timeout=30000,
)
page.evaluate(
"""() => window.SvrntyCanvas.request({
prompt: 'Create a polished CMO launch dashboard with campaign KPIs',
variants: 2,
brand_id: 'planb',
source: 'visual-smoke'
})"""
)
page.wait_for_function(
"() => document.querySelectorAll('.svrnty-canvas-artboard').length >= 2",
timeout=60000,
)
editable = page.locator('.svrnty-canvas-artboard:first-child .svrnty-canvas-node[contenteditable="true"]').first
editable.fill("Edited KPI panel")
page.click('.svrnty-canvas-artboard:first-child [data-canvas-action="save"]')
try:
page.wait_for_function(
"() => !!window.SvrntyCanvas.state.lastEditPersisted",
timeout=30000,
)
except Exception:
debug_state = page.evaluate(
"""() => ({
artboards: window.SvrntyCanvas.state.artboards.map((b) => ({id:b.id,title:b.title,dirty:b.dirty,badge:b.badge})),
logs: window.SvrntyCanvas.state.log.map((row) => row.message),
editableCount: document.querySelectorAll('.svrnty-canvas-node[contenteditable="true"]').length,
saveButtonCount: document.querySelectorAll('[data-canvas-action="save"]').length
})"""
)
print(json.dumps({"canvas_edit_debug": debug_state}, indent=2), file=sys.stderr)
raise
page.click('.svrnty-canvas-artboard:first-child [data-canvas-action="prototype-source"]')
page.click('.svrnty-canvas-artboard:nth-child(2) [data-canvas-action="prototype-link"]')
page.wait_for_function(
"() => !!window.SvrntyCanvas.state.lastPrototypeEdge",
timeout=30000,
)
page.click('.svrnty-canvas-artboard:first-child [data-canvas-action="export"]')
page.wait_for_function(
"() => window.SvrntyCanvas.state.exportSummary && window.SvrntyCanvas.state.exportSummary.screens.length >= 2",
timeout=30000,
)
state = page.evaluate(
"""(slashRegistered) => ({
connected: window.SvrntyCanvas.state.connected,
artboards: window.SvrntyCanvas.state.artboards.length,
slashCommandRegistered: slashRegistered,
editPersisted: !!window.SvrntyCanvas.state.lastEditPersisted,
prototypeLinked: !!window.SvrntyCanvas.state.lastPrototypeEdge,
exportScreens: window.SvrntyCanvas.state.exportSummary ? window.SvrntyCanvas.state.exportSummary.screens.length : 0,
exportPrototypeEdges: window.SvrntyCanvas.state.exportSummary ? window.SvrntyCanvas.state.exportSummary.prototypeEdges.length : 0,
designContextReady: window.SvrntyCanvas.state.designContext ? window.SvrntyCanvas.state.designContext.status === 'ready' : false,
designContextSource: window.SvrntyCanvas.state.designContext ? window.SvrntyCanvas.state.designContext.source : '',
designContextVersion: window.SvrntyCanvas.state.designContext ? window.SvrntyCanvas.state.designContext.source_version : '',
secondbrainStatus: window.SvrntyCanvas.state.designContext ? window.SvrntyCanvas.state.designContext.secondbrain_status : '',
memoryHintCount: window.SvrntyCanvas.state.designContext && window.SvrntyCanvas.state.designContext.memory_hints ? window.SvrntyCanvas.state.designContext.memory_hints.length : 0,
logs: window.SvrntyCanvas.state.log.map((row) => row.message).slice(0, 8),
status: document.querySelector('#svrntyCanvasStatus')?.textContent || '',
mainClass: document.querySelector('main.main')?.className || ''
})""",
slash_registered,
)
shot = screenshot_dir / "canvas-desktop-proof.png"
page.screenshot(path=str(shot), full_page=True)
browser.close()
proof["state"] = state
proof["pixel_stats"] = _pixel_stats(shot)
proof["pass"] = bool(
state["connected"]
and state["artboards"] >= 2
and state["slashCommandRegistered"]
and state["editPersisted"]
and state["prototypeLinked"]
and state["exportScreens"] >= 2
and state["exportPrototypeEdges"] >= 1
and state["designContextReady"]
and state["designContextSource"] == "cmo.brand_profile_cache"
and state["secondbrainStatus"] in ("ready", "unconfigured")
and state["memoryHintCount"] >= 1
and proof["cmo_helper"]["ok"]
and proof["cmo_helper"]["screens"] >= 1
and proof["cmo_helper"]["event_wait_ok"]
and proof["cmo_helper"]["design_context_status"] == "ready"
and "svrnty-showing-canvas" in state["mainClass"]
and proof["pixel_stats"]["nonblank"]
)
out_json.parent.mkdir(parents=True, exist_ok=True)
out_json.write_text(json.dumps(proof, indent=2), encoding="utf-8")
print(json.dumps(proof, indent=2))
return 0 if proof["pass"] else 1
finally:
webui_log = _stop(webui_proc)
canva_log = _stop(canva_proc)
if webui_log:
print(webui_log, file=sys.stderr)
if canva_log:
print(canva_log, file=sys.stderr)
if __name__ == "__main__":
raise SystemExit(main())
+247
View File
@@ -0,0 +1,247 @@
#!/usr/bin/env python3
"""Smoke a natural CMO Hermes turn into the WebUI Canvas bridge.
This proves the runtime handoff JP cares about:
CMO profile -> terminal helper -> Hermes WebUI plugin -> canva-editor -> replayable
Canvas events. The canva-editor is mocked here so the proof isolates routing; the
real Spark qwen3.6 generator is covered by canva-editor's real-model smoke.
"""
from __future__ import annotations
import argparse
import json
import os
import signal
import socket
import subprocess
import sys
import tempfile
import time
from datetime import datetime
from pathlib import Path
from urllib.error import URLError
from urllib.request import urlopen
PLUGIN_REPO = Path(__file__).resolve().parent.parent
HERMES_REPO = PLUGIN_REPO.parent
WEBUI_REPO = HERMES_REPO / "hermes-webui"
AGENT_REPO = HERMES_REPO / "hermes-agent"
CANVA_REPO = HERMES_REPO.parent / "cortex" / "L2-svrnty.tool-canva-editor"
CMO_REPO = HERMES_REPO / "cmo"
DEFAULT_OUT = HERMES_REPO / ".scratch" / "cmo-natural-canvas-proof.json"
def _free_port() -> int:
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
return port
def _wait_for(url: str, timeout: int = 45) -> bool:
deadline = time.time() + timeout
while time.time() < deadline:
try:
with urlopen(url, timeout=1) as res:
if res.status < 500:
return True
except URLError:
pass
except Exception:
pass
time.sleep(0.25)
return False
def _fetch_json(url: str, timeout: float = 8.0) -> dict:
with urlopen(url, timeout=timeout) as res:
return json.loads(res.read().decode("utf-8"))
def _start_canva(port: int, db_path: Path) -> subprocess.Popen:
env = os.environ.copy()
env["PATH"] = f"/home/svrnty/sdk/go1.26.0/bin{os.pathsep}{env.get('PATH', '')}"
env["PORT"] = str(port)
env["DB_PATH"] = str(db_path)
return subprocess.Popen(
["go", "run", "./cmd/server", "--mock"],
cwd=CANVA_REPO,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
preexec_fn=os.setsid,
)
def _start_webui(port: int, state_dir: Path, canva_base: str) -> subprocess.Popen:
env = os.environ.copy()
env.pop("HERMES_WEBUI_PASSWORD", None)
env["CANVA_EDITOR_BASE_URL"] = canva_base
env["HERMES_WEBUI_PYTHON_PLUGIN"] = "svrnty_hermes_webui_plugin"
env["HERMES_WEBUI_PORT"] = str(port)
env["HERMES_WEBUI_STATE_DIR"] = str(state_dir)
env["HERMES_WEBUI_AGENT_DIR"] = str(AGENT_REPO)
env["HERMES_REPO_ROOT"] = str(HERMES_REPO)
env["PYTHONPATH"] = (
f"{PLUGIN_REPO}{os.pathsep}{AGENT_REPO}"
if not env.get("PYTHONPATH")
else f"{PLUGIN_REPO}{os.pathsep}{AGENT_REPO}{os.pathsep}{env['PYTHONPATH']}"
)
py = WEBUI_REPO / "venv" / "bin" / "python"
agent_py = AGENT_REPO / "venv" / "bin" / "python"
python_exe = str(py) if py.exists() else str(agent_py) if agent_py.exists() else "python3"
return subprocess.Popen(
[python_exe, str(WEBUI_REPO / "server.py")],
cwd=AGENT_REPO if AGENT_REPO.exists() else WEBUI_REPO,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
preexec_fn=os.setsid,
)
def _stop(proc: subprocess.Popen | None) -> str:
if proc is None:
return ""
output = ""
try:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
proc.wait(timeout=5)
except Exception:
try:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
except Exception:
pass
if proc.stdout:
try:
output = proc.stdout.read(4000)
except Exception:
output = ""
return output
def _run_cmo_turn(webui_base: str, timeout_s: int) -> subprocess.CompletedProcess:
prompt = f"""
You are CMO for Plan B. This is a routing smoke for the Canvas design command center.
Use the terminal tool to run exactly this command:
python3 "{CMO_REPO / "canvas_command.py"}" command "Create one CMO-routed Plan B promo card for a family meal campaign" --variants 1 --brand planb --project "Hermes CMO Natural Smoke" --wait
After the terminal command returns, reply with a compact summary containing command_id, project_id, number of screens, design_context.status, and secondbrain_status. Do not call canva-editor directly.
""".strip()
env = os.environ.copy()
env["HERMES_WEBUI_URL"] = webui_base
env["CMO_LIB"] = str(CMO_REPO)
env["HERMES_INFERENCE_PROVIDER"] = "vllm"
env["HERMES_INFERENCE_MODEL"] = "qwen3.6-35b-a3b"
return subprocess.run(
[
"hermes",
"-p",
"cmo-planb",
"--provider",
"vllm",
"--model",
"qwen3.6-35b-a3b",
"-z",
prompt,
"--toolsets",
"terminal",
"--accept-hooks",
"--yolo",
],
cwd=HERMES_REPO,
env=env,
capture_output=True,
text=True,
timeout=timeout_s,
)
def _canvas_event_summary(webui_base: str) -> dict:
payload = _fetch_json(f"{webui_base}/api/canvas/events?format=json&since=0")
events = payload.get("events") or []
completed = [event for event in events if event.get("type") == "canvas.command.completed"]
persisted = [event for event in events if event.get("type") == "canvas.screen.persisted"]
failed = [event for event in events if event.get("type") == "canvas.command.failed"]
command_ids = sorted({event.get("command_id") for event in completed if event.get("command_id")})
return {
"cursor": payload.get("cursor"),
"event_count": len(events),
"completed_count": len(completed),
"persisted_count": len(persisted),
"failed_count": len(failed),
"command_ids": command_ids,
"last_completed": completed[-1] if completed else None,
"last_persisted": persisted[-1] if persisted else None,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--output-json", default=str(DEFAULT_OUT))
parser.add_argument("--timeout", type=int, default=180)
args = parser.parse_args()
out_json = Path(args.output_json)
canva_port = _free_port()
webui_port = _free_port()
canva_base = f"http://127.0.0.1:{canva_port}"
webui_base = f"http://127.0.0.1:{webui_port}"
canva_proc: subprocess.Popen | None = None
webui_proc: subprocess.Popen | None = None
with tempfile.TemporaryDirectory(prefix="hermes-cmo-canvas-") as tmp:
tmp_path = Path(tmp)
proof = {
"generated_at": datetime.now().isoformat(timespec="seconds"),
"webui_base": webui_base,
"canva_base": canva_base,
"profile": "cmo-planb",
"provider": "vllm",
"model": "qwen3.6-35b-a3b",
}
try:
canva_proc = _start_canva(canva_port, tmp_path / "canva-editor.db")
if not _wait_for(f"{canva_base}/health"):
raise RuntimeError(f"canva-editor did not become healthy at {canva_base}/health")
webui_proc = _start_webui(webui_port, tmp_path / "webui-state", canva_base)
if not _wait_for(f"{webui_base}/health"):
raise RuntimeError(f"webui did not become healthy at {webui_base}/health")
result = _run_cmo_turn(webui_base, args.timeout)
events = _canvas_event_summary(webui_base)
proof["cmo_turn"] = {
"returncode": result.returncode,
"stdout": result.stdout[-4000:],
"stderr": result.stderr[-2000:],
}
proof["events"] = events
proof["pass"] = bool(
result.returncode == 0
and events["completed_count"] >= 1
and events["persisted_count"] >= 1
and events["failed_count"] == 0
and events["command_ids"]
)
out_json.parent.mkdir(parents=True, exist_ok=True)
out_json.write_text(json.dumps(proof, indent=2), encoding="utf-8")
print(json.dumps(proof, indent=2))
return 0 if proof["pass"] else 1
finally:
webui_log = _stop(webui_proc)
canva_log = _stop(canva_proc)
if webui_log:
print(webui_log, file=sys.stderr)
if canva_log:
print(canva_log, file=sys.stderr)
if __name__ == "__main__":
raise SystemExit(main())