agentic-dev-loop 0.12.2__py3-none-any.whl → 0.12.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,7 @@ from .hostboundary import (check_orchestrator_install,
27
27
  from .isolation import startup_sandbox
28
28
  from .llm import build_llm
29
29
  from .loaders import resolve_docs
30
+ from .provider_wire import provider_name
30
31
  from .secrets import resolve_secrets, scan_env_files
31
32
  from .registry import (Registry, default_registry, load_extensions,
32
33
  validate_action_mounts_egress, validate_action_network)
@@ -291,7 +292,7 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
291
292
  httpd, _thread = viz.serve_viz(
292
293
  bus, topology["mermaid"], start_state["subject"],
293
294
  args.viz_port, topology["nodes"], topology["flow"],
294
- topology["edges"])
295
+ topology["edges"], provider_name(cfg.llm.base_url))
295
296
  bound = httpd.server_address[1]
296
297
  url = f"http://127.0.0.1:{bound}"
297
298
  if args.viz_port and bound != args.viz_port:
@@ -40,6 +40,13 @@ def adapter_for(base_url: str | None) -> WireAdapter | None:
40
40
  return WIRE_ADAPTERS.get(base_url.rstrip("/"))
41
41
 
42
42
 
43
+ def provider_name(base_url: str | None) -> str:
44
+ """Display name of the LLM provider behind a base URL."""
45
+ if base_url is not None and base_url.rstrip("/") == COPILOT_BASE_URL:
46
+ return "Copilot"
47
+ return "OpenAI"
48
+
49
+
43
50
  class WireShapeError(RuntimeError):
44
51
  """A surfaced or outgoing wire shape that cannot carry a tool call.
45
52
 
@@ -6,4 +6,4 @@ assets, not this file, and rebuild.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- PAGE = "<!doctype html>\n<html lang=\"en\" data-theme=\"dark\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>agentic-dev-loop viz</title>\n<style>\n/* agentic-dev-loop viz - Carbon re-skin. Tokens are the exact values from\n planning-docs/design/tokens.css (data-theme scoped). */\n\n:root,\n:root[data-theme=\"dark\"] {\n color-scheme: dark;\n --cds-background: #0F1214;\n --cds-layer-01: #171B1F;\n --cds-layer-02: #20262B;\n --cds-layer-03: #2A3137;\n --cds-layer-hover-01: #20262B;\n --cds-border-subtle-01: #2E353B;\n --cds-border-strong-01: #4D565E;\n --cds-text-primary: #EEF1F3;\n --cds-text-secondary: #A3ACB4;\n --cds-text-helper: #8A949C;\n --cds-icon-primary: #EEF1F3;\n --cds-icon-secondary: #A3ACB4;\n --cds-interactive: #08BDBA;\n --cds-focus: #3DDBD9;\n --cds-link-primary: #3DDBD9;\n --cds-support-success: #42BE65;\n --cds-support-warning: #F1C21B;\n --cds-support-error: #FF6B72;\n --cds-support-info: #08BDBA;\n --adl-support-unverifiable: #BE95FF;\n --adl-tint-run: rgba(8, 189, 186, .14);\n --adl-tint-ok: rgba(66, 190, 101, .11);\n --adl-tint-warn: rgba(241, 194, 27, .12);\n --adl-tint-err: rgba(250, 77, 86, .14);\n --adl-tint-unv: rgba(165, 110, 255, .14);\n --adl-console: #0B0E10;\n}\n\n:root[data-theme=\"light\"] {\n color-scheme: light;\n --cds-background: #F2F4F5;\n --cds-layer-01: #FFFFFF;\n --cds-layer-02: #F4F6F7;\n --cds-layer-03: #E6E9EB;\n --cds-layer-hover-01: #F4F6F7;\n --cds-border-subtle-01: #DDE1E4;\n --cds-border-strong-01: #8D969E;\n --cds-text-primary: #15191C;\n --cds-text-secondary: #4C555D;\n --cds-text-helper: #5F6870;\n --cds-icon-primary: #15191C;\n --cds-icon-secondary: #4C555D;\n --cds-interactive: #007D79;\n --cds-focus: #007D79;\n --cds-link-primary: #007D79;\n --cds-support-success: #198038;\n --cds-support-warning: #8E6A00;\n --cds-support-error: #DA1E28;\n --cds-support-info: #007D79;\n --adl-support-unverifiable: #8A3FFC;\n --adl-tint-run: rgba(0, 125, 121, .09);\n --adl-tint-ok: rgba(25, 128, 56, .08);\n --adl-tint-warn: rgba(241, 194, 27, .18);\n --adl-tint-err: rgba(218, 30, 40, .08);\n --adl-tint-unv: rgba(138, 63, 252, .08);\n --adl-console: #121619;\n}\n\n:root {\n --adl-console-text: #D6DCE0;\n --adl-console-muted: #8A949C;\n --adl-console-control: #20262B;\n --adl-console-control-hover: #2A3137;\n --adl-console-scroll-thumb: #4D565E;\n --adl-font-sans: \"IBM Plex Sans\", system-ui, sans-serif;\n --adl-font-mono: \"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, monospace;\n --adl-radius: 0;\n --adl-motion-productive: cubic-bezier(.2, 0, .38, .9);\n}\n\n* { box-sizing: border-box; }\nhtml, body { height: 100%; }\nbody {\n margin: 0; background: var(--cds-background); color: var(--cds-text-primary);\n font-family: var(--adl-font-sans); font-size: 13px;\n display: flex; flex-direction: column; overflow: hidden;\n}\nbutton { font-family: inherit; border-radius: var(--adl-radius); }\n:focus-visible { outline: 2px solid var(--cds-focus); outline-offset: -2px; }\n.mono, .big, .badge, .elabel, .stage-index, .stage-time, .stage-status,\n.stage-verdict, .nidx, .ntime, .nlabel, .stream, #run-log, .stage-console {\n font-family: var(--adl-font-mono); font-variant-numeric: tabular-nums;\n}\n.skip { position: absolute; left: -9999px; }\n.skip:focus { left: 8px; top: 8px; z-index: 100; background: var(--cds-layer-01);\n padding: 8px; }\n\n/* Header */\n.hdr {\n flex: 0 0 48px; display: flex; align-items: center; gap: 16px;\n padding: 0 16px; background: var(--cds-layer-01);\n border-bottom: 1px solid var(--cds-border-subtle-01);\n}\n.hdr-brand { display: flex; align-items: center; gap: 10px; font-weight: 600; }\n.mark { width: 10px; height: 10px; background: var(--cds-interactive); display: block; }\n.hdr-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }\n.stream { display: inline-flex; align-items: center; gap: 6px; font-size: 11px;\n letter-spacing: .08em; text-transform: uppercase; color: var(--cds-text-secondary); }\n.led { width: 8px; height: 8px; display: inline-block; background: currentColor; }\n.stream .led { background: var(--cds-interactive); }\n.stream.reconnecting { color: var(--cds-support-warning); }\n.stream.reconnecting .led { background: var(--cds-support-warning); }\n.hdr-action {\n min-width: 44px; min-height: 44px; display: inline-flex; align-items: center;\n justify-content: center; background: transparent; color: var(--cds-icon-primary);\n border: 1px solid transparent; cursor: pointer;\n}\n.hdr-action:hover { background: var(--cds-layer-hover-01); }\n.ic { width: 16px; height: 16px; display: block; position: relative; }\n.ic-sun, .ic-moon { position: absolute; top: 0; left: 0; display: block; }\n/* dark theme shows the sun (click to switch to light); light shows the moon */\n:root[data-theme=\"light\"] .ic-sun { display: none; }\n:root[data-theme=\"dark\"] .ic-moon { display: none; }\n\n/* Banners */\n#stream-banner {\n display: none; padding: 8px 16px; background: var(--adl-tint-warn);\n color: var(--cds-text-primary);\n border-bottom: 1px solid var(--cds-support-warning); font-size: 12px;\n}\n.outcome { display: none; padding: 10px 14px; border: 1px solid; font-size: 12px; }\n.outcome-success { background: var(--adl-tint-ok); border-color: var(--cds-support-success); }\n.outcome-failure { background: var(--adl-tint-err); border-color: var(--cds-support-error); }\n.outcome-title { font-weight: 700; letter-spacing: .08em; }\n.outcome-sub { color: var(--cds-text-secondary); margin-top: 2px; }\n\n/* Main column */\n.shell {\n flex: 1 1 auto; min-height: 0; width: 100%; max-width: 1600px; margin: 0 auto;\n padding: 12px 24px 16px; display: flex; flex-direction: column; gap: 12px;\n}\n.tiles { display: flex; gap: 1px; background: var(--cds-border-subtle-01);\n border: 1px solid var(--cds-border-subtle-01); }\n.tile {\n display: block; flex: 1 1 0; min-width: 0; padding: 8px 10px;\n background: var(--cds-layer-01);\n}\n.lbl { font-size: 11px; text-transform: uppercase; letter-spacing: .08em;\n color: var(--cds-text-helper); margin-bottom: 4px; }\n.big { font-size: 16px; color: var(--cds-text-primary); overflow-wrap: break-word; }\n#thread-id { cursor: pointer; user-select: all; background: var(--cds-layer-02);\n padding: 1px 4px; border: 1px solid var(--cds-border-subtle-01); font-size: 12px; }\n#thread-id.copied { border-color: var(--cds-support-success);\n color: var(--cds-support-success); }\n#total { display: none; }\n.total-note { font-size: 11px; color: var(--cds-text-helper); margin-left: 6px; }\n.meter { display: flex; gap: 2px; margin-top: 5px; }\n.meter span { width: 8px; height: 6px; background: var(--cds-border-strong-01); }\n.meter span.on { background: var(--cds-interactive); }\n.meter.exhausted span.on { background: var(--cds-support-error); }\n\n.verdicts { display: flex; flex-wrap: wrap; gap: 6px; }\n.badge { font-size: 11px; padding: 2px 6px; border: 1px solid var(--cds-border-strong-01);\n color: var(--cds-text-secondary); background: var(--cds-layer-01); }\n.badge-pass { border-color: var(--cds-support-success); color: var(--cds-support-success); }\n.badge-fail { border-color: var(--cds-support-warning); color: var(--cds-support-warning); }\n.badge-unverifiable { border-color: var(--adl-support-unverifiable);\n color: var(--adl-support-unverifiable); }\n.badge-unclear { border-color: var(--cds-border-strong-01); }\n\n/* Panels and pipeline */\n.panel { border: 1px solid var(--cds-border-subtle-01); background: var(--cds-layer-01);\n display: flex; flex-direction: column; min-height: 0; }\n.panel-h { display: flex; align-items: center; justify-content: space-between;\n padding: 6px 10px; border-bottom: 1px solid var(--cds-border-subtle-01);\n font-size: 11px; text-transform: uppercase; letter-spacing: .08em;\n color: var(--cds-text-helper); }\n#pipeline-panel { flex: 0 0 auto; min-height: 96px; max-height: 60vh; overflow: auto; }\n.graph-wrap { padding: 12px; display: flex; justify-content: center; }\n#graph { position: relative; margin: 0 auto; min-width: 112px; min-height: 80px; }\n.graph-msg { color: var(--cds-text-helper); font-size: 12px; margin: 8px; }\n\n.lower { flex: 1 1 auto; min-height: 0; display: flex; gap: 12px; }\n.panel-stages { flex: 999 1 640px; min-width: 0; }\n.panel-log { flex: 1 1 360px; min-width: 0; }\n#stages { overflow-y: auto; min-height: 0; }\n#run-log { list-style: none; margin: 0; padding: 6px 10px; overflow-y: auto;\n min-height: 0; font-size: 12px; }\n#run-log li { padding: 2px 0; border-bottom: 1px solid var(--cds-border-subtle-01);\n color: var(--cds-text-secondary); }\n#run-log li.started { color: var(--cds-text-primary); }\n#run-log li.interrupted { color: var(--cds-support-warning); }\n#run-log li.failed { color: var(--cds-support-error); }\n#run-log li.success { color: var(--cds-support-success); }\n\n/* Stations */\n.node {\n position: absolute; width: 112px; height: 76px; padding: 7px 9px;\n background: var(--cds-layer-01); border: 1px solid var(--cds-border-strong-01);\n display: flex; flex-direction: column; justify-content: space-between;\n overflow: hidden;\n}\n.nrow { display: flex; justify-content: space-between; font-size: 11px; color: var(--cds-text-helper); }\n.ntime { color: var(--cds-text-secondary); }\n.nname { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden;\n text-overflow: ellipsis; }\n.nstat { display: flex; align-items: center; gap: 5px; font-size: 11px;\n text-transform: uppercase; letter-spacing: .06em; }\n.nlabel { font-size: 11px; }\n.st-pass { background: var(--adl-tint-ok); border-color: var(--cds-support-success); }\n.st-pass .led, .st-pass .nlabel { background: var(--cds-support-success); }\n.st-pass .nlabel { background: transparent; color: var(--cds-support-success); }\n.st-running { background: var(--adl-tint-run); border: 2px solid var(--cds-interactive); padding: 6px 8px; }\n.st-running .led { background: var(--cds-interactive); animation: blink 1s steps(2) infinite; }\n.st-running .nlabel { color: var(--cds-interactive); }\n.st-running::after { content: \"\"; position: absolute; left: 0; bottom: 0; height: 3px;\n width: 100%; background: var(--cds-interactive); transform-origin: left;\n animation: sweep 1.4s var(--adl-motion-productive) infinite; }\n.st-fail { background: var(--adl-tint-warn); border-color: var(--cds-support-warning); }\n.st-fail .led { background: var(--cds-support-warning); }\n.st-fail .nlabel { color: var(--cds-support-warning); }\n.st-fatal { background: var(--adl-tint-err); border: 2px solid var(--cds-support-error); padding: 6px 8px; }\n.st-fatal .led { background: var(--cds-support-error); }\n.st-fatal .nlabel { color: var(--cds-support-error); }\n.st-unverifiable { background: var(--adl-tint-unv); border-color: var(--adl-support-unverifiable); }\n.st-unverifiable .led { background: var(--adl-support-unverifiable); }\n.st-unverifiable .nlabel { color: var(--adl-support-unverifiable); }\n.st-pending { background: var(--cds-layer-01); border: 1px dashed var(--cds-border-strong-01); }\n.st-pending .led { background: transparent; border: 1px solid var(--cds-border-strong-01); }\n.st-pending .nlabel { color: var(--cds-text-secondary); }\n\n.ln { position: absolute; }\n.ln-flow { background: var(--cds-border-strong-01); }\n.ln-flow.on { background: var(--cds-support-success); }\n.ln-back { color: var(--cds-border-strong-01); }\n.ln-back.on { color: var(--cds-support-warning); }\n.ln-back[data-dir=\"h\"] { border-top: 1px dashed currentColor; }\n.ln-back[data-dir=\"v\"] { border-left: 1px dashed currentColor; }\n.arr { position: absolute; width: 0; height: 0; }\n.arr-right { border-top: 5px solid transparent; border-bottom: 5px solid transparent;\n border-left: 7px solid var(--cds-border-strong-01); }\n.arr-flow.on.arr-right { border-left-color: var(--cds-support-success); }\n.arr-back.on.arr-up { border-bottom-color: var(--cds-support-warning); }\n.arr-up { border-left: 5px solid transparent; border-right: 5px solid transparent;\n border-bottom: 7px solid var(--cds-border-strong-01); }\n.elabel { position: absolute; transform: translateX(-100%); background: var(--cds-layer-02);\n border: 1px solid var(--cds-border-subtle-01); color: var(--cds-text-secondary);\n font-size: 11px; padding: 0 4px; white-space: nowrap; }\n\n/* Stages accordion */\n.stage { border-bottom: 1px solid var(--cds-border-subtle-01); }\n.stage-header { display: flex; align-items: center; gap: 6px; width: 100%;\n min-height: 44px; padding: 4px 10px; background: var(--cds-layer-01);\n color: var(--cds-text-primary); border: 0; text-align: left; cursor: pointer;\n border-left: 2px solid transparent; }\n.stage-header:hover { background: var(--cds-layer-hover-01); }\n.stage-header.status-running { border-left-color: var(--cds-interactive); }\n.stage-header.status-pass { border-left-color: var(--cds-support-success); }\n.stage-header.status-fail { border-left-color: var(--cds-support-warning); }\n.stage-header.status-fatal { border-left-color: var(--cds-support-error); }\n.stage-header.status-unverifiable { border-left-color: var(--adl-support-unverifiable); }\n.stage-glyph { width: 10px; color: var(--cds-text-helper); }\n.stage-index { font-size: 11px; color: var(--cds-text-helper); }\n.stage-header .led { color: var(--cds-border-strong-01); }\n.status-running .led { background: var(--cds-interactive); }\n.status-pass .led { background: var(--cds-support-success); }\n.status-fail .led { background: var(--cds-support-warning); }\n.status-fatal .led { background: var(--cds-support-error); }\n.status-unverifiable .led { background: var(--adl-support-unverifiable); }\n.stage-name { font-weight: 600; }\n.stage-status { font-size: 11px; text-transform: uppercase; color: var(--cds-text-secondary); }\n.stage-verdict { font-size: 11px; color: var(--cds-text-helper); }\n.stage-time { margin-left: auto; font-size: 11px; color: var(--cds-text-secondary); }\n.console-wrap { position: relative; max-height: 320px; overflow-y: auto;\n background: var(--adl-console); display: none; }\n.stage:not(.collapsed) .console-wrap { display: block; }\n.stage-console { margin: 0; padding: 8px 10px; color: var(--adl-console-text);\n font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; }\n.stage-console .cline { display: block; min-height: 1em; }\n.stage-console .omitted, .stage-console .stage-placeholder {\n color: var(--adl-console-muted); font-style: italic; }\n.copy-btn { position: sticky; top: 6px; float: right; margin: 6px 6px 0 0;\n width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;\n background: var(--adl-console-control); color: var(--adl-console-text);\n border: 0; cursor: pointer; }\n.copy-btn:hover { background: var(--adl-console-control-hover); }\n\n/* Scrollbars */\n* { scrollbar-width: thin; scrollbar-color: var(--cds-border-strong-01) transparent; }\n*::-webkit-scrollbar { width: 8px; height: 8px; }\n*::-webkit-scrollbar-thumb { background: var(--cds-border-strong-01); }\n*::-webkit-scrollbar-thumb:hover { background: var(--cds-text-helper); }\n.console-wrap { scrollbar-color: var(--adl-console-scroll-thumb) var(--adl-console); }\n\n@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }\n@keyframes sweep { 0% { transform: scaleX(0); } 50% { transform: scaleX(1); }\n 100% { transform: scaleX(0); } }\n\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after { animation: none !important; transition: none !important; }\n}\n\n@media (max-width: 760px) {\n body { overflow: auto; height: auto; }\n .shell { padding: 12px; }\n .tiles { flex-wrap: wrap; }\n .tiles .tile { flex: 1 1 45%; }\n .lower { flex-direction: column; }\n #pipeline-panel { overflow-x: auto; }\n}\n</style>\n<script>\n/*\n * pipeline-layout.js - reference implementation of the viz pipeline layout.\n *\n * Pure function, no DOM access, ES5 (matches the inline-script style of\n * src/agentic_dev_loop/viz/server.py PAGE). Usable as a <script> (exposes\n * window.layoutPipeline) or via require()/import in tests.\n *\n * Contract\n * layoutPipeline({\n * nodes: [\"brief\", \"deps\", \"dev\", ...], // display order (= /topology nodes)\n * flow: [{src, dst}], // on_pass chain (solid)\n * edges: [{src, dst, label|null}], // on_fail routes (dashed)\n * status: {nodeId: \"pass\"|\"running\"|\"fail\"|\"fatal\"|\"unverifiable\"|\"pending\"},\n * availWidth: 1200 // px available to the graph\n * }) -> {\n * width, height, rows, perRow,\n * nodes: [{id, index, x, y, w, h}],\n * lines: [{x, y, w, h, kind: \"flow\"|\"back\", on: bool, dir: \"h\"|\"v\"}],\n * arrows: [{x, y, dir: \"right\"|\"up\", kind, on}], // x,y = top-left of a CSS-border triangle box\n * labels: [{x, y, text}] // x = right edge (translateX(-100%)), y = top\n * }\n *\n * Algorithm\n * 1. Stations keep natural size (NW x NH). Choose the FEWEST rows R such that\n * the row (perRow = ceil(N/R)) plus routing margins fits availWidth.\n * perRow never drops below 2; if 2 still doesn't fit, the caller scrolls.\n * 2. Consecutive flow edges in a row = rail with arrow. Row change = wrap\n * connector: right stub -> down to the row's deepest gutter lane -> left\n * to the left channel -> down -> right into the next row's first station.\n * 3. Conditional edges route orthogonally:\n * U same row (back or forward): drop into gutter, run, rise into target.\n * C backward across rows: drop into source gutter, run left to a\n * left-hand channel, rise to the target row's gutter, run right,\n * rise into the target.\n * F forward across rows (e.g. -> failure terminal): mirrored through\n * a right-hand channel, descending.\n * 4. Crossing-free ordering (proved for U and C; F is best-effort):\n * gutter lanes, top -> bottom: U (shortest span first), C-into-row\n * (outer channel first), F-into-row, C-leaving-row (leftmost source\n * first), F-leaving-row, wrap connector last;\n * C channels: inner -> outer by (source row asc, source index asc);\n * target entry points: U rightmost, then C inner -> outer, then F,\n * stepping 14px leftwards across the target's bottom edge.\n */\n(function (root) {\n \"use strict\";\n\n var NW = 112, NH = 76, GAP = 24, STEP = NW + GAP;\n var LANE = 18, LANE_TOP = 14, LANE_PAD = 16, CH = 12;\n\n function layoutPipeline(opts) {\n var names = opts.nodes || [];\n var N = names.length;\n var status = opts.status || {};\n var avail = Math.max(0, opts.availWidth || 0);\n var flow = opts.flow || [];\n var condEdges = opts.edges || [];\n var idx = {};\n names.forEach(function (n, i) { idx[n] = i; });\n var reached = function (n) { return (status[n] || \"pending\") !== \"pending\"; };\n var failed = function (n) { var s = status[n] || \"pending\"; return s === \"fail\" || s === \"fatal\"; };\n\n // flow edges that are NOT to the next station are routed like conditional edges (solid)\n var rails = [], extraFlow = [];\n flow.forEach(function (f) {\n if (!(f.src in idx) || !(f.dst in idx)) { return; }\n if (idx[f.dst] === idx[f.src] + 1) { rails.push(f); }\n else { extraFlow.push({ src: f.src, dst: f.dst, label: null, kind: \"flow\" }); }\n });\n var routed = condEdges.filter(function (e) { return (e.src in idx) && (e.dst in idx); })\n .map(function (e) { return { src: e.src, dst: e.dst, label: e.label || null, kind: \"back\" }; })\n .concat(extraFlow);\n\n function plan(R) {\n var per = Math.max(1, Math.ceil(N / R));\n var rows = Math.max(1, Math.ceil(N / per));\n var pos = names.map(function (n, i) { return { r: Math.floor(i / per), c: i % per }; });\n var es = routed.map(function (e) {\n var si = idx[e.src], ti = idx[e.dst];\n return { label: e.label, kind: e.kind, dst: e.dst, si: si, ti: ti, rs: pos[si].r, rt: pos[ti].r };\n });\n var U = es.filter(function (e) { return e.rs === e.rt; })\n .sort(function (a, b) { return Math.abs(a.si - a.ti) - Math.abs(b.si - b.ti); });\n var C = es.filter(function (e) { return e.rs > e.rt; })\n .sort(function (a, b) { return a.rs - b.rs || a.si - b.si; });\n var F = es.filter(function (e) { return e.rs < e.rt; })\n .sort(function (a, b) { return a.rt - b.rt || b.si - a.si; });\n C.forEach(function (e, j) { e.j = j; });\n F.forEach(function (e, k) { e.k = k; });\n var multi = rows > 1;\n var O = multi ? CH + CH * C.length + 8 : 0;\n var core = per * STEP - GAP;\n var right = (multi ? 14 : 0) + (F.length ? 8 + CH * F.length : 0);\n return { per: per, rows: rows, pos: pos, U: U, C: C, F: F, O: O, core: core, width: O + core + right };\n }\n\n var L = plan(1);\n for (var R = 2; L.width > avail && L.per > 2; R++) { L = plan(R); }\n var O = L.O;\n\n // gutter lane assignment\n var gut = [];\n for (var g = 0; g < L.rows; g++) {\n var items = 0;\n var take = function () { return items++; };\n L.U.filter(function (e) { return e.rt === g; }).forEach(function (e) { e.laneS = take(); });\n L.C.filter(function (e) { return e.rt === g; }).sort(function (a, b) { return b.j - a.j; })\n .forEach(function (e) { e.laneT = take(); });\n L.F.filter(function (e) { return e.rt === g; }).forEach(function (e) { e.laneT = take(); });\n L.C.filter(function (e) { return e.rs === g; }).sort(function (a, b) { return a.si - b.si; })\n .forEach(function (e) { e.laneS = take(); });\n L.F.filter(function (e) { return e.rs === g; }).forEach(function (e) { e.laneS = take(); });\n var wrap = g < L.rows - 1 ? take() : -1;\n gut.push({ n: items, wrapLane: wrap });\n }\n var rowY = [0];\n gut.forEach(function (gt, g) {\n gt.h = gt.n ? LANE_TOP + (gt.n - 1) * LANE + LANE_PAD : 4;\n rowY.push(rowY[g] + NH + gt.h);\n });\n var laneY = function (g, l) { return rowY[g] + NH + LANE_TOP + l * LANE; };\n var nx = function (i) { return O + L.pos[i].c * STEP; };\n var ny = function (i) { return rowY[L.pos[i].r]; };\n\n var entryCount = {};\n L.U.concat(L.C, L.F).forEach(function (e) {\n var m = entryCount[e.ti] || 0;\n entryCount[e.ti] = m + 1;\n e.xe = Math.max(nx(e.ti) + 8, nx(e.ti) + NW - 14 - m * 14);\n });\n\n var lines = [], arrows = [], labels = [];\n function H(x1, x2, y, kind, on) { lines.push({ x: Math.min(x1, x2), y: y, w: Math.abs(x2 - x1), h: 0, dir: \"h\", kind: kind, on: !!on }); }\n function V(x, y1, y2, kind, on) { lines.push({ x: x, y: Math.min(y1, y2), w: 0, h: Math.abs(y2 - y1), dir: \"v\", kind: kind, on: !!on }); }\n\n rails.forEach(function (f) {\n var i = idx[f.src], on = reached(f.dst);\n if (L.pos[i + 1].r === L.pos[i].r) {\n H(nx(i) + NW, nx(i + 1), ny(i) + 37, \"flow\", on);\n arrows.push({ x: nx(i + 1) - 6, y: ny(i) + 33, dir: \"right\", kind: \"flow\", on: on });\n } else {\n var r = L.pos[i].r, xr = nx(i) + NW + 12, yw = laneY(r, gut[r].wrapLane), yn = ny(i + 1) + 37;\n H(nx(i) + NW, xr + 2, ny(i) + 37, \"flow\", on);\n V(xr, ny(i) + 37, yw, \"flow\", on);\n H(O - 12, xr + 2, yw - 1, \"flow\", on);\n V(O - 12, yw - 1, yn + 2, \"flow\", on);\n H(O - 12, O, yn, \"flow\", on);\n arrows.push({ x: O - 6, y: yn - 4, dir: \"right\", kind: \"flow\", on: on });\n }\n });\n\n L.U.concat(L.C, L.F).forEach(function (e) {\n var xs = nx(e.si) + NW / 2, top = ny(e.ti) + NH, y1 = laneY(e.rs, e.laneS);\n var on = e.kind === \"back\" && failed(names[e.si]);\n V(xs, ny(e.si) + NH, y1, e.kind, on);\n if (e.rs === e.rt) {\n H(e.xe, xs, y1, e.kind, on);\n V(e.xe, top + 6, y1, e.kind, on);\n } else {\n var xc = e.rs > e.rt\n ? O - CH - CH * (e.j + 1)\n : O + L.core + 14 + 8 + CH * e.k;\n var y2 = laneY(e.rt, e.laneT);\n H(xc, xs, y1, e.kind, on);\n V(xc, y2, y1, e.kind, on);\n H(xc, e.xe, y2, e.kind, on);\n V(e.xe, top + 6, y2, e.kind, on);\n }\n arrows.push({ x: e.xe - 5, y: top, dir: \"up\", kind: e.kind, on: on });\n if (e.label) { labels.push({ x: xs - 6, y: y1 - 9, text: e.label }); }\n });\n\n return {\n width: L.width,\n height: rowY[L.rows],\n rows: L.rows,\n perRow: L.per,\n nodes: names.map(function (n, i) { return { id: n, index: i, x: nx(i), y: ny(i), w: NW, h: NH }; }),\n lines: lines, arrows: arrows, labels: labels\n };\n }\n\n layoutPipeline.metrics = { NW: NW, NH: NH, GAP: GAP, STEP: STEP, LANE: LANE };\n root.layoutPipeline = layoutPipeline;\n if (typeof module !== \"undefined\" && module.exports) { module.exports = layoutPipeline; }\n})(typeof window !== \"undefined\" ? window : this);\n</script>\n</head>\n<body>\n<a class=\"skip\" href=\"#stages\">Skip to stages</a>\n<header class=\"hdr\">\n <div class=\"hdr-brand\"><span class=\"mark\" aria-hidden=\"true\"></span>\n <span class=\"hdr-name\">agentic-dev-loop viz</span></div>\n <div class=\"hdr-right\">\n <span id=\"stream\" class=\"stream\" role=\"status\"><span class=\"led\" aria-hidden=\"true\"></span>\n <span id=\"stream-label\">STREAM LIVE</span></span>\n <button id=\"theme-toggle\" class=\"hdr-action\" type=\"button\"\n data-testid=\"theme-toggle\" aria-label=\"Switch to light theme\">\n <span id=\"theme-icon\" class=\"ic\" aria-hidden=\"true\">\n <svg class=\"ic-sun\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"3.33\" fill=\"currentColor\"/>\n <path stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"\n d=\"M8 .67v2M8 13.33v2M.67 8h2M13.33 8h2M2.81 2.81l.95.95M12.24 12.24l.95.95M13.19 2.81l-.95.95M3.76 12.24l-.95.95\"/>\n </svg>\n <svg class=\"ic-moon\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\">\n <path d=\"M14 8.53A6 6 0 1 1 7.47 2 4.67 4.67 0 0 0 14 8.53Z\" fill=\"currentColor\"/>\n </svg>\n </span>\n </button>\n </div>\n</header>\n<div id=\"stream-banner\" role=\"status\" aria-live=\"polite\" data-testid=\"stream-banner\"></div>\n<main class=\"shell\">\n <section class=\"tiles\" aria-label=\"Run summary\">\n <div class=\"tile\"><div class=\"lbl\">Run</div>\n <div id=\"subject\" class=\"big\">agentic-dev-loop pipeline</div></div>\n <div class=\"tile\"><div class=\"lbl\">Attempt</div>\n <div id=\"attempt\" class=\"big\">?/?</div><div id=\"meter\" class=\"meter\"></div></div>\n <div class=\"tile\"><div class=\"lbl\">Thread ID</div>\n <div id=\"thread\" class=\"big\"><code id=\"thread-id\" tabindex=\"0\" role=\"button\" title=\"click to copy\">--</code></div></div>\n <div class=\"tile\"><div class=\"lbl\">Total</div>\n <div id=\"total\" class=\"big\"></div></div>\n </section>\n <div id=\"verdicts\" class=\"verdicts\" aria-label=\"Step verdicts\"></div>\n <div id=\"run-banner\" class=\"outcome\" role=\"status\" aria-live=\"polite\" data-testid=\"run-banner\"></div>\n <section id=\"pipeline-panel\" class=\"panel\" aria-label=\"Pipeline\">\n <div class=\"panel-h\"><span>Pipeline</span>\n <span class=\"legend mono\">solid = on_pass / dashed = on_fail</span></div>\n <div class=\"graph-wrap\"><div id=\"graph\" role=\"img\" aria-label=\"Pipeline graph\"></div></div>\n </section>\n <div class=\"lower\">\n <section class=\"panel panel-stages\" aria-label=\"Stages\">\n <div class=\"panel-h\"><span>Stages</span></div>\n <div id=\"stages\"></div>\n </section>\n <section class=\"panel panel-log\" aria-label=\"Event log\">\n <div class=\"panel-h\"><span>Event log</span></div>\n <ol id=\"run-log\" data-testid=\"run-log\"></ol>\n </section>\n </div>\n</main>\n<script>\n(function () {\n \"use strict\";\n\n var nodeStatus = {};\n var stageWord = {};\n var verdictSteps = new Set();\n var nodeIds = new Set();\n var currentVerdicts = {};\n var verdictFail = new Set();\n var verdictPass = new Set();\n var verdictUnverifiable = new Set();\n var verdictEnv = new Set();\n var fatalNodes = new Set();\n var maxAttempts = null;\n var attempt = null;\n var topology = null;\n var stations = {};\n var pendingCompletion = null;\n var stepTimes = {};\n var liveElapsed = {};\n var runStartWall = null;\n var totalTimer = null;\n var lastEventAt = 0;\n var reconnectAttempts = 0;\n var runActive = false;\n var runFailed = false;\n var topologyReady = false;\n var topologyFailed = false;\n var pendingEvents = [];\n var MAX_CONSOLE = 500;\n var PALETTE = [\"#0c0c0c\", \"#c50f1f\", \"#13a10e\", \"#c19c00\", \"#0037da\",\n \"#881798\", \"#3a96dd\", \"#cccccc\"];\n var BRIGHT = [\"#767676\", \"#e74856\", \"#16c60c\", \"#f9f1a5\", \"#3b78ff\",\n \"#b4009e\", \"#61d6d6\", \"#f2f2f2\"];\n var COPY_ICON = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\"'\n + ' height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"'\n + ' stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"'\n + ' aria-hidden=\"true\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\"'\n + ' rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1'\n + ' 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>';\n var CHECK_ICON = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\"'\n + ' height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"'\n + ' stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"'\n + ' aria-hidden=\"true\"><polyline points=\"20 6 9 17 4 12\"></polyline></svg>';\n\n function el(id) { return document.getElementById(id); }\n\n function layoutOf(opts) {\n if (typeof layoutPipeline === \"function\") { return layoutPipeline(opts); }\n if (typeof window !== \"undefined\" && typeof window.layoutPipeline === \"function\") {\n return window.layoutPipeline(opts);\n }\n if (typeof globalThis !== \"undefined\"\n && typeof globalThis.layoutPipeline === \"function\") {\n return globalThis.layoutPipeline(opts);\n }\n return null;\n }\n\n function zeroPad(n) { return n < 10 ? \"0\" + n : String(n); }\n function mmss(totalSeconds) {\n var s = totalSeconds % 60;\n var m = Math.floor(totalSeconds / 60);\n return m + \":\" + (s < 10 ? \"0\" : \"\") + s;\n }\n\n /* Theme ------------------------------------------------------------- */\n function applyTheme(theme) {\n var root = document.documentElement;\n if (root && root.setAttribute) { root.setAttribute(\"data-theme\", theme); }\n var btn = el(\"theme-toggle\");\n if (btn) {\n btn.setAttribute(\"aria-label\", theme === \"dark\"\n ? \"Switch to light theme\" : \"Switch to dark theme\");\n }\n }\n\n function initialTheme() {\n try {\n var stored = window.localStorage && window.localStorage.getItem(\"adl-theme\");\n if (stored === \"dark\" || stored === \"light\") { return stored; }\n } catch (err) { /* session-only */ }\n try {\n if (window.matchMedia\n && window.matchMedia(\"(prefers-color-scheme: light)\").matches) {\n return \"light\";\n }\n } catch (err) { /* default dark */ }\n return \"dark\";\n }\n\n var theme = initialTheme();\n applyTheme(theme);\n (function initThemeToggle() {\n var btn = el(\"theme-toggle\");\n if (!btn) { return; }\n btn.addEventListener(\"click\", function () {\n theme = theme === \"dark\" ? \"light\" : \"dark\";\n applyTheme(theme);\n try { window.localStorage.setItem(\"adl-theme\", theme); }\n catch (err) { /* session-only */ }\n });\n })();\n\n /* Run log ----------------------------------------------------------- */\n function logLine(text, cls) {\n var li = document.createElement(\"li\");\n if (cls) { li.className = cls; }\n li.textContent = text;\n var log = el(\"run-log\");\n if (!log) { return; }\n log.appendChild(li);\n log.scrollTop = log.scrollHeight;\n }\n\n function logConsole(html) {\n var li = document.createElement(\"li\");\n li.innerHTML = html;\n var log = el(\"run-log\");\n if (!log) { return; }\n log.appendChild(li);\n log.scrollTop = log.scrollHeight;\n }\n\n /* Summary tiles ----------------------------------------------------- */\n function renderMeter() {\n var box = el(\"meter\");\n if (!box) { return; }\n box.innerHTML = \"\";\n box.className = \"meter\";\n if (maxAttempts === null) { return; }\n var used = attempt || 0;\n for (var i = 0; i < maxAttempts; i++) {\n var seg = document.createElement(\"span\");\n seg.className = i < used ? \"on\" : \"\";\n box.appendChild(seg);\n }\n if (runFailed && used >= maxAttempts) { box.className = \"meter exhausted\"; }\n }\n\n function updateAttempt() {\n var line = el(\"attempt\");\n if (line) {\n line.textContent = attempt === null\n ? \"?/\" + (maxAttempts === null ? \"?\" : maxAttempts)\n : attempt + \"/\" + (maxAttempts === null ? \"?\" : maxAttempts);\n }\n renderMeter();\n }\n\n function renderTotal() {\n var line = el(\"total\");\n if (!line) { return; }\n line.innerHTML = \"\";\n if (runStartWall === null) {\n line.textContent = \"\";\n line.style.display = \"none\";\n } else {\n line.textContent = \"total \" + mmss(Math.max(0,\n Math.round(Date.now() / 1000 - runStartWall)));\n line.style.display = \"block\";\n var note = document.createElement(\"span\");\n note.className = \"total-note\";\n note.textContent = runActive ? \"LIVE\" : \"FINAL\";\n line.appendChild(note);\n }\n if (runActive && Date.now() - lastEventAt > 10000) { recover(); }\n }\n\n /* Status ------------------------------------------------------------ */\n function statusOf(n) {\n if (nodeStatus[n] === \"running\") { return \"running\"; }\n if (fatalNodes.has(n)) { return \"fatal\"; }\n if (nodeStatus[n] === \"fatal\") { return \"fatal\"; }\n if (verdictFail.has(n)) { return \"fail\"; }\n if (verdictPass.has(n)) { return \"pass\"; }\n if (verdictUnverifiable.has(n)) { return \"unverifiable\"; }\n if (verdictEnv.has(n)) { return \"env\"; }\n return nodeStatus[n] || \"pending\";\n }\n\n function renderStatusOf(n) {\n var s = statusOf(n);\n return s === \"env\" ? \"fatal\" : s;\n }\n\n function statusWord(n) {\n var s = renderStatusOf(n);\n if (s === \"pass\") { return currentVerdicts[n] ? \"PASS\" : \"DONE\"; }\n if (s === \"running\") { return \"RUNNING\"; }\n if (s === \"fail\") { return \"RETRY\"; }\n if (s === \"fatal\") { return currentVerdicts[n] === \"fail\" ? \"FAIL\" : \"FATAL\"; }\n if (s === \"unverifiable\") { return \"UNVERIF\"; }\n return \"PENDING\";\n }\n\n function updateBadges() {\n var box = el(\"verdicts\");\n if (!box) { return; }\n box.innerHTML = \"\";\n Array.from(verdictSteps).forEach(function (step) {\n var b = document.createElement(\"span\");\n var v = currentVerdicts[step];\n b.className = \"badge\";\n if (v === \"pass\") { b.className += \" badge-pass\"; }\n else if (v === \"fail\") { b.className += \" badge-fail\"; }\n else if (v === \"unverifiable\") { b.className += \" badge-unverifiable\"; }\n else if (v === \"env\") { b.className += \" badge-fail\"; }\n else { b.className += \" badge-unclear\"; }\n b.textContent = step + \": \"\n + (v === null || v === undefined ? \"no verdict\" : v);\n box.appendChild(b);\n });\n }\n\n function resetVerdicts() {\n verdictSteps.clear();\n verdictPass.clear();\n verdictFail.clear();\n verdictUnverifiable.clear();\n verdictEnv.clear();\n fatalNodes.clear();\n currentVerdicts = {};\n nodeStatus = {};\n stageWord = {};\n pendingCompletion = null;\n updateBadges();\n Array.from(nodeIds).forEach(function (n) { renderStageHeader(n); });\n renderGraph();\n }\n\n /* Pipeline graph ---------------------------------------------------- */\n function ensureStation(id) {\n if (stations[id]) { return stations[id]; }\n var s = document.createElement(\"div\");\n s.className = \"node st-pending\";\n s.id = \"station-\" + id;\n s.setAttribute(\"data-testid\", \"station-\" + id);\n var row = document.createElement(\"div\");\n row.className = \"nrow\";\n var idx = document.createElement(\"span\");\n idx.className = \"nidx\";\n var time = document.createElement(\"span\");\n time.className = \"ntime\";\n row.appendChild(idx);\n row.appendChild(time);\n var name = document.createElement(\"div\");\n name.className = \"nname\";\n var stat = document.createElement(\"div\");\n stat.className = \"nstat\";\n var led = document.createElement(\"span\");\n led.className = \"led\";\n led.setAttribute(\"aria-hidden\", \"true\");\n var label = document.createElement(\"span\");\n label.className = \"nlabel\";\n stat.appendChild(led);\n stat.appendChild(label);\n s.appendChild(row);\n s.appendChild(name);\n s.appendChild(stat);\n stations[id] = s;\n return s;\n }\n\n function nodeIndex(id) {\n if (!topology) { return 0; }\n var i = topology.nodes.indexOf(id);\n return i < 0 ? 0 : i;\n }\n\n function nodeTime(id) {\n if (nodeStatus[id] === \"running\" && liveElapsed[id] !== undefined) {\n return mmss(liveElapsed[id]);\n }\n if (stepTimes[id] !== undefined) { return mmss(stepTimes[id]); }\n return \"-\";\n }\n\n function updateStation(id) {\n var s = ensureStation(id);\n var status = renderStatusOf(id);\n s.className = \"node st-\" + status;\n var name = s.querySelector(\".nname\");\n var idx = s.querySelector(\".nidx\");\n var time = s.querySelector(\".ntime\");\n var label = s.querySelector(\".nlabel\");\n if (name) { name.textContent = id; }\n if (idx) { idx.textContent = zeroPad(nodeIndex(id) + 1); }\n if (time) { time.textContent = nodeTime(id); }\n if (label) { label.textContent = statusWord(id); }\n }\n\n function graphLabel() {\n if (!topology) { return \"Pipeline graph\"; }\n var parts = topology.nodes.map(function (n, i) {\n return zeroPad(i + 1) + \" \" + n + \" \" + statusWord(n);\n });\n var rows = stations.__rows || 1;\n return \"Pipeline graph: \" + parts.join(\", \") + \". \" + rows + \" row\"\n + (rows === 1 ? \"\" : \"s\") + \".\";\n }\n\n function makeLine(l) {\n var d = document.createElement(\"div\");\n d.className = \"ln ln-\" + l.kind + (l.on ? \" on\" : \"\");\n d.setAttribute(\"data-dir\", l.dir);\n d.style.left = l.x + \"px\";\n d.style.top = l.y + \"px\";\n d.style.width = (l.dir === \"h\" ? l.w : 2) + \"px\";\n d.style.height = (l.dir === \"v\" ? l.h : 2) + \"px\";\n return d;\n }\n\n function makeArrow(a) {\n var d = document.createElement(\"div\");\n d.className = \"arr arr-\" + a.dir + \" arr-\" + a.kind + (a.on ? \" on\" : \"\");\n d.style.left = a.x + \"px\";\n d.style.top = a.y + \"px\";\n return d;\n }\n\n function makeLabel(l) {\n var d = document.createElement(\"div\");\n d.className = \"elabel\";\n d.style.left = l.x + \"px\";\n d.style.top = l.y + \"px\";\n d.textContent = l.text;\n return d;\n }\n\n function renderGraph() {\n var box = el(\"graph\");\n if (!box) { return; }\n box.innerHTML = \"\";\n if (topologyFailed) {\n box.style.width = \"\";\n box.style.height = \"\";\n var msg = document.createElement(\"p\");\n msg.className = \"graph-msg\";\n msg.textContent = \"Graph unavailable - the event log below is still live\";\n box.appendChild(msg);\n box.setAttribute(\"aria-label\", \"Pipeline graph unavailable\");\n return;\n }\n if (!topology) {\n var loading = document.createElement(\"p\");\n loading.className = \"graph-msg\";\n loading.textContent = \"Loading graph...\";\n box.appendChild(loading);\n box.setAttribute(\"aria-label\", \"Loading pipeline graph\");\n return;\n }\n var status = {};\n topology.nodes.forEach(function (n) { status[n] = renderStatusOf(n); });\n var avail = (typeof window !== \"undefined\" && window.innerWidth)\n ? window.innerWidth : 1200;\n var res = layoutOf({\n nodes: topology.nodes,\n flow: topology.flow || [],\n edges: topology.edges || [],\n status: status,\n availWidth: avail\n });\n if (!res) { return; }\n box.style.width = res.width + \"px\";\n box.style.height = res.height + \"px\";\n res.lines.forEach(function (l) { box.appendChild(makeLine(l)); });\n res.arrows.forEach(function (a) { box.appendChild(makeArrow(a)); });\n res.labels.forEach(function (l) { box.appendChild(makeLabel(l)); });\n res.nodes.forEach(function (nd) {\n updateStation(nd.id);\n var s = stations[nd.id];\n s.style.left = nd.x + \"px\";\n s.style.top = nd.y + \"px\";\n box.appendChild(s);\n });\n stations.__rows = res.rows;\n box.setAttribute(\"aria-label\", graphLabel());\n }\n\n /* Stages ------------------------------------------------------------ */\n function addPlaceholder(pre) {\n var ph = document.createElement(\"div\");\n ph.className = \"stage-placeholder\";\n ph.textContent = \"No output\";\n pre.appendChild(ph);\n }\n\n function ensureSection(node) {\n var existing = document.getElementById(\"stage-\" + node);\n if (existing) { return existing; }\n var stages = el(\"stages\");\n var section = document.createElement(\"section\");\n section.className = \"stage collapsed\";\n section.id = \"stage-\" + node;\n section.setAttribute(\"data-testid\", \"stage-\" + node);\n\n var header = document.createElement(\"button\");\n header.className = \"stage-header\";\n header.type = \"button\";\n header.setAttribute(\"aria-expanded\", \"false\");\n header.setAttribute(\"aria-controls\", \"console-\" + node);\n header.addEventListener(\"click\", function () {\n var willCollapse = !section.classList.contains(\"collapsed\");\n section.classList.toggle(\"collapsed\");\n header.setAttribute(\"aria-expanded\", willCollapse ? \"false\" : \"true\");\n renderStageHeader(node);\n if (!willCollapse) {\n requestAnimationFrame(function () { tailSection(section); });\n }\n });\n\n var glyph = document.createElement(\"span\");\n glyph.className = \"stage-glyph\";\n glyph.setAttribute(\"aria-hidden\", \"true\");\n var index = document.createElement(\"span\");\n index.className = \"stage-index\";\n var led = document.createElement(\"span\");\n led.className = \"led\";\n led.setAttribute(\"aria-hidden\", \"true\");\n var name = document.createElement(\"span\");\n name.className = \"stage-name\";\n var status = document.createElement(\"span\");\n status.className = \"stage-status\";\n var verdict = document.createElement(\"span\");\n verdict.className = \"stage-verdict\";\n var time = document.createElement(\"span\");\n time.className = \"stage-time\";\n\n header.appendChild(glyph);\n header.appendChild(index);\n header.appendChild(led);\n header.appendChild(name);\n header.appendChild(status);\n header.appendChild(verdict);\n header.appendChild(time);\n\n var console = document.createElement(\"pre\");\n console.className = \"stage-console\";\n addPlaceholder(console);\n\n var wrap = document.createElement(\"div\");\n wrap.className = \"console-wrap\";\n wrap.id = \"console-\" + node;\n var copyBtn = document.createElement(\"button\");\n copyBtn.className = \"copy-btn\";\n copyBtn.type = \"button\";\n copyBtn.setAttribute(\"title\", \"Copy output\");\n copyBtn.setAttribute(\"aria-label\", \"Copy \" + node + \" console output\");\n copyBtn.setAttribute(\"data-testid\", \"copy-console-\" + node);\n copyBtn.innerHTML = COPY_ICON;\n copyBtn.addEventListener(\"click\", function () {\n copyConsole(console, copyBtn);\n });\n wrap.appendChild(copyBtn);\n wrap.appendChild(console);\n\n section.appendChild(header);\n section.appendChild(wrap);\n stages.appendChild(section);\n renderStageHeader(node);\n return section;\n }\n\n function renderStageHeader(node) {\n var section = ensureSection(node);\n var header = section.querySelector(\".stage-header\");\n if (!header) { return; }\n var glyph = header.querySelector(\".stage-glyph\");\n var index = header.querySelector(\".stage-index\");\n var name = header.querySelector(\".stage-name\");\n var status = header.querySelector(\".stage-status\");\n var verdict = header.querySelector(\".stage-verdict\");\n var time = header.querySelector(\".stage-time\");\n var collapsed = section.classList.contains(\"collapsed\");\n if (glyph) { glyph.textContent = collapsed ? \">\" : \"v\"; }\n if (index) { index.textContent = zeroPad(nodeIndex(node) + 1); }\n if (name) { name.textContent = node; }\n var word = stageWord[node] || \"\";\n if (status) { status.textContent = word; }\n var v = currentVerdicts[node];\n if (verdict) {\n verdict.textContent = (word === \"completed\" && v)\n ? \"verdict: \" + String(v).toUpperCase() : \"\";\n }\n if (time) {\n time.textContent = (word === \"completed\" && stepTimes[node])\n ? mmss(stepTimes[node])\n : (word === \"running\" && liveElapsed[node] !== undefined)\n ? mmss(liveElapsed[node]) : \"\";\n }\n header.className = \"stage-header status-\" + renderStatusOf(node);\n header.setAttribute(\"aria-expanded\", collapsed ? \"false\" : \"true\");\n }\n\n function setStageStatus(node, word) {\n stageWord[node] = word;\n renderStageHeader(node);\n }\n\n function flushPending() {\n if (pendingCompletion === null) { return; }\n var p = pendingCompletion;\n pendingCompletion = null;\n setStageStatus(p.node, p.word);\n }\n\n function clearConsoles() {\n stageWord = {};\n Array.from(nodeIds).forEach(function (n) {\n var section = ensureSection(n);\n var pre = consoleOf(section);\n if (pre) {\n pre.innerHTML = \"\";\n addPlaceholder(pre);\n }\n section.classList.add(\"collapsed\");\n renderStageHeader(n);\n });\n var log = el(\"run-log\");\n if (log) { log.innerHTML = \"\"; }\n }\n\n function consoleOf(section) {\n var wrap = section.querySelector(\".console-wrap\");\n return wrap ? wrap.querySelector(\".stage-console\") : null;\n }\n\n function scrollPortOf(section) { return section.querySelector(\".console-wrap\"); }\n\n function copyConsole(pre, btn) {\n var lines = Array.prototype.map.call(pre.querySelectorAll(\".cline\"),\n function (c) { return c.textContent; });\n if (lines.length === 0) {\n btn.setAttribute(\"title\", \"Nothing to copy\");\n return;\n }\n var text = lines.join(\"\\n\");\n function done(ok) {\n btn.innerHTML = ok ? CHECK_ICON : COPY_ICON;\n btn.setAttribute(\"title\", ok ? \"Copied!\" : \"Copy failed\");\n if (ok) {\n setTimeout(function () {\n btn.innerHTML = COPY_ICON;\n btn.setAttribute(\"title\", \"Copy output\");\n }, 1500);\n }\n }\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(function () { done(true); },\n function () { done(false); });\n } else {\n var ta = document.createElement(\"textarea\");\n ta.value = text;\n ta.style.position = \"fixed\";\n ta.style.top = \"-1000px\";\n document.body.appendChild(ta);\n ta.select();\n var ok = false;\n try { ok = document.execCommand(\"copy\"); } catch (err) { ok = false; }\n document.body.removeChild(ta);\n done(ok);\n }\n }\n\n function nearBottom(pre) {\n return pre.scrollHeight - pre.scrollTop - pre.clientHeight < 24;\n }\n\n function tailSection(section) {\n if (section.classList.contains(\"collapsed\")) { return; }\n var sp = scrollPortOf(section);\n if (sp) { sp.scrollTop = sp.scrollHeight; }\n }\n\n function appendConsole(section, html) {\n var pre = consoleOf(section);\n var sp = scrollPortOf(section);\n if (!pre) { return; }\n var stick = !section.classList.contains(\"collapsed\") && nearBottom(sp);\n var ph = pre.querySelector(\".stage-placeholder\");\n if (ph) { pre.removeChild(ph); }\n if (pre.querySelectorAll(\".cline\").length >= MAX_CONSOLE) {\n var first = pre.querySelector(\".cline\");\n if (first) { pre.removeChild(first); }\n var omitted = pre.querySelector(\".omitted\");\n if (!omitted) {\n omitted = document.createElement(\"div\");\n omitted.className = \"omitted\";\n pre.insertBefore(omitted, pre.firstChild);\n }\n var count = parseInt(omitted.getAttribute(\"data-n\") || \"0\", 10) + 1;\n omitted.setAttribute(\"data-n\", String(count));\n omitted.textContent = \"...\" + count + \" line\"\n + (count === 1 ? \"\" : \"s\") + \" omitted\";\n }\n var line = document.createElement(\"span\");\n line.className = \"cline\";\n line.innerHTML = html;\n pre.appendChild(line);\n if (stick && sp) { sp.scrollTop = sp.scrollHeight; }\n }\n\n function handleConsole(ev) {\n var html = ansiToHtml(ev.text);\n if (ev.node && nodeIds.has(ev.node)) {\n appendConsole(ensureSection(ev.node), html);\n } else {\n logConsole(html);\n }\n }\n\n function ansiToHtml(text) {\n var out = \"\";\n var i = 0;\n var ESC = \"\\x1b\";\n var cur = { fg: null, bg: null, bold: false, italic: false, underline: false };\n var open = false;\n\n function currentStyle() {\n var parts = [];\n if (cur.bold) { parts.push(\"font-weight:bold\"); }\n if (cur.italic) { parts.push(\"font-style:italic\"); }\n if (cur.underline) { parts.push(\"text-decoration:underline\"); }\n if (cur.fg !== null) { parts.push(\"color:\" + cur.fg); }\n if (cur.bg !== null) { parts.push(\"background-color:\" + cur.bg); }\n return parts.join(\";\");\n }\n function closeSpan() {\n if (open) { out += \"</span>\"; open = false; }\n }\n function pushSpan() {\n closeSpan();\n var s = currentStyle();\n if (s) { out += '<span style=\"' + s + '\">'; open = true; }\n }\n\n var html = text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n var n = html.length;\n while (i < n) {\n var idx = html.indexOf(ESC, i);\n if (idx === -1) { out += html.slice(i); break; }\n out += html.slice(i, idx);\n var j = idx + 1;\n if (j >= n) { i = idx + 1; continue; }\n var ch = html.charAt(j);\n if (ch === \"[\") {\n var k = j + 1;\n while (k < n) {\n var cc = html.charCodeAt(k);\n if (cc >= 0x40 && cc <= 0x7e) { break; }\n k++;\n }\n if (k >= n) { i = n; break; }\n var final = html.charAt(k);\n var params = html.slice(j + 1, k);\n if (final === \"m\") {\n var parts = params.split(\";\");\n var codes = [];\n for (var p = 0; p < parts.length; p++) {\n codes.push(parts[p] === \"\" ? 0 : parseInt(parts[p], 10));\n }\n var changed = false;\n for (var c = 0; c < codes.length; c++) {\n var code = codes[c];\n if (code === 0) {\n cur = { fg: null, bg: null, bold: false, italic: false, underline: false };\n changed = true;\n } else if (code === 1) { cur.bold = true; changed = true; }\n else if (code === 22) { cur.bold = false; changed = true; }\n else if (code === 3) { cur.italic = true; changed = true; }\n else if (code === 23) { cur.italic = false; changed = true; }\n else if (code === 4) { cur.underline = true; changed = true; }\n else if (code === 24) { cur.underline = false; changed = true; }\n else if (code >= 30 && code <= 37) { cur.fg = PALETTE[code - 30]; changed = true; }\n else if (code >= 90 && code <= 97) { cur.fg = BRIGHT[code - 90]; changed = true; }\n else if (code >= 40 && code <= 47) { cur.bg = PALETTE[code - 40]; changed = true; }\n else if (code >= 100 && code <= 107) { cur.bg = BRIGHT[code - 100]; changed = true; }\n else if (code === 39) { cur.fg = null; changed = true; }\n else if (code === 49) { cur.bg = null; changed = true; }\n else if (code === 38 || code === 48) {\n if (c + 2 < codes.length && codes[c + 1] === 5) {\n var val = codes[c + 2];\n var col;\n if (val < 8) { col = PALETTE[val]; }\n else if (val < 16) { col = BRIGHT[val - 8]; }\n else if (val < 232) {\n var cube = val - 16;\n var levels = [0, 95, 135, 175, 215, 255];\n col = \"rgb(\" + levels[Math.floor(cube / 36)] + \",\"\n + levels[Math.floor((cube % 36) / 6)] + \",\"\n + levels[cube % 6] + \")\";\n } else {\n var g = 8 + 10 * (val - 232);\n col = \"rgb(\" + g + \",\" + g + \",\" + g + \")\";\n }\n if (code === 38) { cur.fg = col; } else { cur.bg = col; }\n c += 2;\n changed = true;\n } else if (c + 4 < codes.length && codes[c + 1] === 2) {\n var rgb = \"rgb(\" + codes[c + 2] + \",\" + codes[c + 3] + \",\"\n + codes[c + 4] + \")\";\n if (code === 38) { cur.fg = rgb; } else { cur.bg = rgb; }\n c += 4;\n changed = true;\n }\n }\n }\n if (changed) { pushSpan(); }\n }\n i = k + 1;\n continue;\n }\n if (ch === \"]\") {\n var bel = html.indexOf(String.fromCharCode(7), j);\n var st = html.indexOf(ESC + String.fromCharCode(92), j);\n var end = -1;\n if (bel !== -1) { end = bel + 1; }\n if (st !== -1 && (end === -1 || st + 2 < end)) { end = st + 2; }\n if (end === -1) { i = n; break; }\n i = end;\n continue;\n }\n i = idx + 1;\n }\n closeSpan();\n return out;\n }\n\n /* Events ------------------------------------------------------------ */\n function handle(ev) {\n switch (ev.type) {\n case \"run_started\":\n flushPending();\n stepTimes = {};\n liveElapsed = {};\n runActive = true;\n runFailed = false;\n fatalNodes.clear();\n if (totalTimer !== null) { clearInterval(totalTimer); totalTimer = null; }\n runStartWall = (typeof ev.started_at === \"number\") ? ev.started_at : null;\n totalTimer = setInterval(renderTotal, 1000);\n renderTotal();\n clearConsoles();\n resetVerdicts();\n if (ev.attempt !== undefined) { attempt = ev.attempt; }\n maxAttempts = ev.max_attempts;\n var subject = el(\"subject\");\n if (subject) { subject.textContent = ev.subject; }\n if (ev.thread_id !== undefined) {\n var tid = el(\"thread-id\");\n if (tid) { tid.textContent = ev.thread_id; }\n }\n logLine(\"run started: \" + ev.subject + \" (thread \" + ev.thread_id + \")\",\n \"started\");\n updateAttempt();\n break;\n case \"node_started\":\n flushPending();\n if (!nodeIds.has(ev.node)) { break; }\n nodeStatus[ev.node] = \"running\";\n stageWord[ev.node] = \"running\";\n liveElapsed = {};\n liveElapsed[ev.node] = 0;\n renderGraph();\n break;\n case \"node_finished\":\n if (!nodeIds.has(ev.node)) { break; }\n if (ev.elapsed_seconds !== undefined) {\n stepTimes[ev.node] = (stepTimes[ev.node] || 0) + ev.elapsed_seconds;\n }\n if (!fatalNodes.has(ev.node)) {\n nodeStatus[ev.node] = ev.ok ? \"pass\" : \"fail\";\n }\n pendingCompletion = { node: ev.node, word: ev.ok ? \"completed\" : \"errored\" };\n setStageStatus(ev.node, ev.ok ? \"completed\" : \"errored\");\n renderGraph();\n break;\n case \"heartbeat\":\n if (nodeIds.has(ev.node) && nodeStatus[ev.node] === \"running\") {\n liveElapsed[ev.node] = ev.elapsed_seconds;\n renderStageHeader(ev.node);\n renderGraph();\n }\n break;\n case \"state\":\n if (ev.attempt !== undefined) { attempt = ev.attempt; }\n if (ev.step_verdicts !== undefined) {\n Object.keys(ev.step_verdicts).forEach(function (k) {\n verdictSteps.add(k);\n var v = ev.step_verdicts[k];\n if (v === \"pass\") { verdictPass.add(k); verdictFail.delete(k); verdictUnverifiable.delete(k); verdictEnv.delete(k); }\n else if (v === \"fail\") { verdictFail.add(k); verdictPass.delete(k); verdictUnverifiable.delete(k); verdictEnv.delete(k); }\n else if (v === \"unverifiable\") { verdictUnverifiable.add(k); verdictPass.delete(k); verdictFail.delete(k); verdictEnv.delete(k); }\n else if (v === \"env\") { verdictEnv.add(k); verdictPass.delete(k); verdictFail.delete(k); verdictUnverifiable.delete(k); }\n else { verdictPass.delete(k); verdictFail.delete(k); verdictUnverifiable.delete(k); verdictEnv.delete(k); }\n currentVerdicts[k] = v;\n });\n updateBadges();\n if (pendingCompletion !== null\n && (pendingCompletion.node in ev.step_verdicts)) {\n flushPending();\n }\n renderGraph();\n }\n if (ev.failed) {\n Object.keys(nodeStatus).forEach(function (n) {\n if (nodeStatus[n] === \"running\") { fatalNodes.add(n); }\n });\n logLine(\"state: failed\", \"failed\");\n renderGraph();\n }\n updateAttempt();\n break;\n case \"run_finished\":\n runActive = false;\n if (totalTimer !== null) { clearInterval(totalTimer); totalTimer = null; }\n var total = el(\"total\");\n if (ev.elapsed_seconds !== undefined) {\n runStartWall = null;\n if (total) {\n total.innerHTML = \"\";\n total.textContent = \"total \" + mmss(ev.elapsed_seconds);\n total.style.display = \"block\";\n var finalNote = document.createElement(\"span\");\n finalNote.className = \"total-note\";\n finalNote.textContent = \"FINAL\";\n total.appendChild(finalNote);\n }\n }\n flushPending();\n if (!ev.success) {\n runFailed = true;\n Object.keys(nodeStatus).forEach(function (n) {\n var s = statusOf(n);\n if (s === \"fail\" || s === \"running\") { nodeStatus[n] = \"fatal\"; }\n });\n renderGraph();\n }\n renderMeter();\n logLine(\"run finished: \" + (ev.success ? \"success\" : \"failure\"),\n ev.success ? \"success\" : \"failed\");\n var banner = el(\"run-banner\");\n if (banner) {\n banner.innerHTML = \"\";\n var title = document.createElement(\"div\");\n title.className = \"outcome-title\";\n title.textContent = ev.success ? \"SUCCESS\" : \"FAILURE\";\n var sub = document.createElement(\"div\");\n sub.className = \"outcome-sub\";\n sub.textContent = \"Run finished - \" + (el(\"subject\")\n ? el(\"subject\").textContent : \"\") + \" - attempt \"\n + (attempt === null ? \"?\" : attempt) + \"/\"\n + (maxAttempts === null ? \"?\" : maxAttempts)\n + \" - total \" + (ev.elapsed_seconds !== undefined\n ? mmss(ev.elapsed_seconds) : \"-\");\n banner.appendChild(title);\n banner.appendChild(sub);\n banner.className = \"outcome \"\n + (ev.success ? \"outcome-success\" : \"outcome-failure\");\n banner.style.display = \"block\";\n }\n break;\n case \"console\":\n handleConsole(ev);\n break;\n }\n }\n\n function loadTopology() {\n fetch(\"/topology\").then(function (r) { return r.json(); }).then(function (d) {\n var subject = el(\"subject\");\n if (subject) { subject.textContent = d.subject; }\n topology = {\n nodes: d.nodes || [],\n flow: d.flow || [],\n edges: d.edges || []\n };\n topology.nodes.forEach(function (n) {\n nodeIds.add(n);\n ensureSection(n);\n ensureStation(n);\n });\n topologyReady = true;\n renderGraph();\n pendingEvents.forEach(handle);\n pendingEvents = [];\n }).catch(function () {\n topologyFailed = true;\n topologyReady = true;\n logLine(\"failed to load topology\");\n renderGraph();\n pendingEvents.forEach(handle);\n pendingEvents = [];\n });\n }\n\n /* Stream ------------------------------------------------------------ */\n var es = null;\n\n function setStreamLive() {\n var stream = el(\"stream\");\n if (stream) { stream.className = \"stream\"; }\n var label = el(\"stream-label\");\n if (label) { label.textContent = \"STREAM LIVE\"; }\n }\n\n function setStreamReconnecting() {\n var stream = el(\"stream\");\n if (stream) { stream.className = \"stream reconnecting\"; }\n var label = el(\"stream-label\");\n if (label) { label.textContent = \"RECONNECTING\"; }\n }\n\n function connectStream() {\n var stream = new EventSource(\"/events\");\n stream.onmessage = function (e) {\n lastEventAt = Date.now();\n reconnectAttempts = 0;\n var banner = el(\"stream-banner\");\n if (banner) { banner.style.display = \"none\"; }\n setStreamLive();\n var ev;\n try { ev = JSON.parse(e.data); }\n catch (err) { logLine(\"bad event: \" + e.data); return; }\n if (!topologyReady) { pendingEvents.push(ev); return; }\n try { handle(ev); }\n catch (err) { logLine(\"bad event: \" + e.data); }\n };\n stream.onerror = function () { logLine(\"event stream interrupted\", \"interrupted\"); };\n return stream;\n }\n\n function recover() {\n var banner = el(\"stream-banner\");\n reconnectAttempts++;\n setStreamReconnecting();\n if (banner) {\n banner.textContent = \"Connection lost, reconnecting... attempt \"\n + reconnectAttempts + \"/3 (no event for 10 s)\";\n banner.style.display = \"block\";\n }\n if (reconnectAttempts > 3) {\n location.reload();\n return;\n }\n if (es) { es.close(); }\n es = connectStream();\n lastEventAt = Date.now();\n }\n\n loadTopology();\n\n setTimeout(function () {\n if (!topologyReady) {\n topologyReady = true;\n logLine(\"topology load timed out - event log only\");\n pendingEvents.forEach(handle);\n pendingEvents = [];\n }\n }, 5000);\n\n es = connectStream();\n\n document.addEventListener(\"visibilitychange\", function () {\n if (runActive && Date.now() - lastEventAt > 10000) { recover(); }\n });\n\n (function initThreadCopy() {\n var tid = el(\"thread-id\");\n if (!tid) { return; }\n\n function selectFallback() {\n // Explicit degradation: if the clipboard API is unavailable, select\n // the text so the user can copy it manually.\n try {\n var range = document.createRange();\n range.selectNodeContents(tid);\n var sel = window.getSelection();\n sel.removeAllRanges();\n sel.addRange(range);\n } catch (err) { /* selection is best-effort */ }\n }\n\n function showFeedback(value) {\n tid.textContent = \"Copied\";\n tid.classList.add(\"copied\");\n setTimeout(function () {\n tid.classList.remove(\"copied\");\n // Only restore when the feedback is still showing; a newer thread\n // id from run_started must not be clobbered.\n if (tid.textContent === \"Copied\") { tid.textContent = value; }\n }, 1200);\n }\n\n function copyThreadId() {\n var value = tid.textContent;\n if (!value || value === \"--\" || value === \"Copied\") { return; }\n if (!navigator.clipboard || !navigator.clipboard.writeText) {\n selectFallback();\n return;\n }\n navigator.clipboard.writeText(value).then(\n function () { showFeedback(value); }, selectFallback);\n }\n\n tid.addEventListener(\"click\", copyThreadId);\n tid.addEventListener(\"keydown\", function (ev) {\n if (ev.key === \"Enter\" || ev.key === \" \") {\n ev.preventDefault();\n copyThreadId();\n }\n });\n })();\n\n (function initRelayout() {\n if (typeof ResizeObserver !== \"undefined\") {\n var target = el(\"pipeline-panel\") || document.body;\n if (target) {\n new ResizeObserver(function () { renderGraph(); }).observe(target);\n }\n }\n window.addEventListener(\"resize\", function () { renderGraph(); });\n })();\n})();\n</script>\n</body>\n</html>\n"
9
+ PAGE = "<!doctype html>\n<html lang=\"en\" data-theme=\"dark\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>agentic-dev-loop viz</title>\n<style>\n/* agentic-dev-loop viz - Carbon re-skin. Tokens are the exact values from\n planning-docs/design/tokens.css (data-theme scoped). */\n\n:root,\n:root[data-theme=\"dark\"] {\n color-scheme: dark;\n --cds-background: #0F1214;\n --cds-layer-01: #171B1F;\n --cds-layer-02: #20262B;\n --cds-layer-03: #2A3137;\n --cds-layer-hover-01: #20262B;\n --cds-border-subtle-01: #2E353B;\n --cds-border-strong-01: #4D565E;\n --cds-text-primary: #EEF1F3;\n --cds-text-secondary: #A3ACB4;\n --cds-text-helper: #8A949C;\n --cds-icon-primary: #EEF1F3;\n --cds-icon-secondary: #A3ACB4;\n --cds-interactive: #08BDBA;\n --cds-focus: #3DDBD9;\n --cds-link-primary: #3DDBD9;\n --cds-support-success: #42BE65;\n --cds-support-warning: #F1C21B;\n --cds-support-error: #FF6B72;\n --cds-support-info: #08BDBA;\n --adl-support-unverifiable: #BE95FF;\n --adl-tint-run: rgba(8, 189, 186, .14);\n --adl-tint-ok: rgba(66, 190, 101, .11);\n --adl-tint-warn: rgba(241, 194, 27, .12);\n --adl-tint-err: rgba(250, 77, 86, .14);\n --adl-tint-unv: rgba(165, 110, 255, .14);\n --adl-console: #0B0E10;\n}\n\n:root[data-theme=\"light\"] {\n color-scheme: light;\n --cds-background: #F2F4F5;\n --cds-layer-01: #FFFFFF;\n --cds-layer-02: #F4F6F7;\n --cds-layer-03: #E6E9EB;\n --cds-layer-hover-01: #F4F6F7;\n --cds-border-subtle-01: #DDE1E4;\n --cds-border-strong-01: #8D969E;\n --cds-text-primary: #15191C;\n --cds-text-secondary: #4C555D;\n --cds-text-helper: #5F6870;\n --cds-icon-primary: #15191C;\n --cds-icon-secondary: #4C555D;\n --cds-interactive: #007D79;\n --cds-focus: #007D79;\n --cds-link-primary: #007D79;\n --cds-support-success: #198038;\n --cds-support-warning: #8E6A00;\n --cds-support-error: #DA1E28;\n --cds-support-info: #007D79;\n --adl-support-unverifiable: #8A3FFC;\n --adl-tint-run: rgba(0, 125, 121, .09);\n --adl-tint-ok: rgba(25, 128, 56, .08);\n --adl-tint-warn: rgba(241, 194, 27, .18);\n --adl-tint-err: rgba(218, 30, 40, .08);\n --adl-tint-unv: rgba(138, 63, 252, .08);\n --adl-console: #121619;\n}\n\n:root {\n --adl-console-text: #D6DCE0;\n --adl-console-muted: #8A949C;\n --adl-console-control: #20262B;\n --adl-console-control-hover: #2A3137;\n --adl-console-scroll-thumb: #4D565E;\n --adl-font-sans: \"IBM Plex Sans\", system-ui, sans-serif;\n --adl-font-mono: \"IBM Plex Mono\", ui-monospace, SFMono-Regular, Menlo, monospace;\n --adl-radius: 0;\n --adl-motion-productive: cubic-bezier(.2, 0, .38, .9);\n}\n\n* { box-sizing: border-box; }\nhtml, body { height: 100%; }\nbody {\n margin: 0; background: var(--cds-background); color: var(--cds-text-primary);\n font-family: var(--adl-font-sans); font-size: 13px;\n display: flex; flex-direction: column; overflow: hidden;\n}\nbutton { font-family: inherit; border-radius: var(--adl-radius); }\n:focus-visible { outline: 2px solid var(--cds-focus); outline-offset: -2px; }\n.mono, .big, .badge, .elabel, .stage-index, .stage-time, .stage-status,\n.stage-verdict, .nidx, .ntime, .nlabel, .stream, #run-log, .stage-console {\n font-family: var(--adl-font-mono); font-variant-numeric: tabular-nums;\n}\n.skip { position: absolute; left: -9999px; }\n.skip:focus { left: 8px; top: 8px; z-index: 100; background: var(--cds-layer-01);\n padding: 8px; }\n\n/* Header */\n.hdr {\n flex: 0 0 48px; display: flex; align-items: center; gap: 16px;\n padding: 0 16px; background: var(--cds-layer-01);\n border-bottom: 1px solid var(--cds-border-subtle-01);\n}\n.hdr-brand { display: flex; align-items: center; gap: 10px; font-weight: 600; }\n.mark { width: 10px; height: 10px; background: var(--cds-interactive); display: block; }\n.hdr-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }\n.stream { display: inline-flex; align-items: center; gap: 6px; font-size: 11px;\n letter-spacing: .08em; text-transform: uppercase; color: var(--cds-text-secondary); }\n.led { width: 8px; height: 8px; display: inline-block; background: currentColor; }\n.stream .led { background: var(--cds-interactive); }\n.stream.reconnecting { color: var(--cds-support-warning); }\n.stream.reconnecting .led { background: var(--cds-support-warning); }\n.hdr-action {\n min-width: 44px; min-height: 44px; display: inline-flex; align-items: center;\n justify-content: center; background: transparent; color: var(--cds-icon-primary);\n border: 1px solid transparent; cursor: pointer;\n}\n.hdr-action:hover { background: var(--cds-layer-hover-01); }\n.ic { width: 16px; height: 16px; display: block; position: relative; }\n.ic-sun, .ic-moon { position: absolute; top: 0; left: 0; display: block; }\n/* dark theme shows the sun (click to switch to light); light shows the moon */\n:root[data-theme=\"light\"] .ic-sun { display: none; }\n:root[data-theme=\"dark\"] .ic-moon { display: none; }\n\n/* Banners */\n#stream-banner {\n display: none; padding: 8px 16px; background: var(--adl-tint-warn);\n color: var(--cds-text-primary);\n border-bottom: 1px solid var(--cds-support-warning); font-size: 12px;\n}\n.outcome { display: none; padding: 10px 14px; border: 1px solid; font-size: 12px; }\n.outcome-success { background: var(--adl-tint-ok); border-color: var(--cds-support-success); }\n.outcome-failure { background: var(--adl-tint-err); border-color: var(--cds-support-error); }\n.outcome-title { font-weight: 700; letter-spacing: .08em; }\n.outcome-sub { color: var(--cds-text-secondary); margin-top: 2px; }\n\n/* Main column */\n.shell {\n flex: 1 1 auto; min-height: 0; width: 100%; max-width: 1600px; margin: 0 auto;\n padding: 12px 24px 16px; display: flex; flex-direction: column; gap: 12px;\n}\n.tiles { display: flex; gap: 1px; background: var(--cds-border-subtle-01);\n border: 1px solid var(--cds-border-subtle-01); }\n.tile {\n display: block; flex: 1 1 0; min-width: 0; padding: 8px 10px;\n background: var(--cds-layer-01);\n}\n.lbl { font-size: 11px; text-transform: uppercase; letter-spacing: .08em;\n color: var(--cds-text-helper); margin-bottom: 4px; }\n.big { font-size: 16px; color: var(--cds-text-primary); overflow-wrap: break-word; }\n#thread-id { cursor: pointer; user-select: all; background: var(--cds-layer-02);\n padding: 1px 4px; border: 1px solid var(--cds-border-subtle-01); font-size: 12px; }\n#thread-id.copied { border-color: var(--cds-support-success);\n color: var(--cds-support-success); }\n#total { display: none; }\n.total-note { font-size: 11px; color: var(--cds-text-helper); margin-left: 6px; }\n.meter { display: flex; gap: 2px; margin-top: 5px; }\n.meter span { width: 8px; height: 6px; background: var(--cds-border-strong-01); }\n.meter span.on { background: var(--cds-interactive); }\n.meter.exhausted span.on { background: var(--cds-support-error); }\n\n.verdicts { display: flex; flex-wrap: wrap; gap: 6px; }\n.badge { font-size: 11px; padding: 2px 6px; border: 1px solid var(--cds-border-strong-01);\n color: var(--cds-text-secondary); background: var(--cds-layer-01); }\n.badge-pass { border-color: var(--cds-support-success); color: var(--cds-support-success); }\n.badge-fail { border-color: var(--cds-support-warning); color: var(--cds-support-warning); }\n.badge-unverifiable { border-color: var(--adl-support-unverifiable);\n color: var(--adl-support-unverifiable); }\n.badge-unclear { border-color: var(--cds-border-strong-01); }\n\n/* Panels and pipeline */\n.panel { border: 1px solid var(--cds-border-subtle-01); background: var(--cds-layer-01);\n display: flex; flex-direction: column; min-height: 0; }\n.panel-h { display: flex; align-items: center; justify-content: space-between;\n padding: 6px 10px; border-bottom: 1px solid var(--cds-border-subtle-01);\n font-size: 11px; text-transform: uppercase; letter-spacing: .08em;\n color: var(--cds-text-helper); }\n#pipeline-panel { flex: 0 0 auto; min-height: 96px; max-height: 60vh; overflow: auto; }\n.graph-wrap { padding: 12px; display: flex; justify-content: center; }\n#graph { position: relative; margin: 0 auto; min-width: 112px; min-height: 80px; }\n.graph-msg { color: var(--cds-text-helper); font-size: 12px; margin: 8px; }\n\n.lower { flex: 1 1 auto; min-height: 0; display: flex; gap: 12px; }\n.panel-stages { flex: 999 1 640px; min-width: 0; }\n.panel-log { flex: 1 1 360px; min-width: 0; }\n#stages { overflow-y: auto; min-height: 0; }\n#run-log { list-style: none; margin: 0; padding: 6px 10px; overflow-y: auto;\n min-height: 0; font-size: 12px; }\n#run-log li { padding: 2px 0; border-bottom: 1px solid var(--cds-border-subtle-01);\n color: var(--cds-text-secondary); }\n#run-log li.started { color: var(--cds-text-primary); }\n#run-log li.interrupted { color: var(--cds-support-warning); }\n#run-log li.failed { color: var(--cds-support-error); }\n#run-log li.success { color: var(--cds-support-success); }\n\n/* Stations */\n.node {\n position: absolute; width: 112px; height: 76px; padding: 7px 9px;\n background: var(--cds-layer-01); border: 1px solid var(--cds-border-strong-01);\n display: flex; flex-direction: column; justify-content: space-between;\n overflow: hidden;\n}\n.nrow { display: flex; justify-content: space-between; font-size: 11px; color: var(--cds-text-helper); }\n.ntime { color: var(--cds-text-secondary); }\n.nname { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden;\n text-overflow: ellipsis; }\n.nstat { display: flex; align-items: center; gap: 5px; font-size: 11px;\n text-transform: uppercase; letter-spacing: .06em; }\n.nlabel { font-size: 11px; }\n.st-pass { background: var(--adl-tint-ok); border-color: var(--cds-support-success); }\n.st-pass .led, .st-pass .nlabel { background: var(--cds-support-success); }\n.st-pass .nlabel { background: transparent; color: var(--cds-support-success); }\n.st-running { background: var(--adl-tint-run); border: 2px solid var(--cds-interactive); padding: 6px 8px; }\n.st-running .led { background: var(--cds-interactive); animation: blink 1s steps(2) infinite; }\n.st-running .nlabel { color: var(--cds-interactive); }\n.st-running::after { content: \"\"; position: absolute; left: 0; bottom: 0; height: 3px;\n width: 100%; background: var(--cds-interactive); transform-origin: left;\n animation: sweep 1.4s var(--adl-motion-productive) infinite; }\n.st-fail { background: var(--adl-tint-warn); border-color: var(--cds-support-warning); }\n.st-fail .led { background: var(--cds-support-warning); }\n.st-fail .nlabel { color: var(--cds-support-warning); }\n.st-fatal { background: var(--adl-tint-err); border: 2px solid var(--cds-support-error); padding: 6px 8px; }\n.st-fatal .led { background: var(--cds-support-error); }\n.st-fatal .nlabel { color: var(--cds-support-error); }\n.st-unverifiable { background: var(--adl-tint-unv); border-color: var(--adl-support-unverifiable); }\n.st-unverifiable .led { background: var(--adl-support-unverifiable); }\n.st-unverifiable .nlabel { color: var(--adl-support-unverifiable); }\n.st-pending { background: var(--cds-layer-01); border: 1px dashed var(--cds-border-strong-01); }\n.st-pending .led { background: transparent; border: 1px solid var(--cds-border-strong-01); }\n.st-pending .nlabel { color: var(--cds-text-secondary); }\n\n.ln { position: absolute; }\n.ln-flow { background: var(--cds-border-strong-01); }\n.ln-flow.on { background: var(--cds-support-success); }\n.ln-back { color: var(--cds-border-strong-01); }\n.ln-back.on { color: var(--cds-support-warning); }\n.ln-back[data-dir=\"h\"] { border-top: 1px dashed currentColor; }\n.ln-back[data-dir=\"v\"] { border-left: 1px dashed currentColor; }\n.arr { position: absolute; width: 0; height: 0; }\n.arr-right { border-top: 5px solid transparent; border-bottom: 5px solid transparent;\n border-left: 7px solid var(--cds-border-strong-01); }\n.arr-flow.on.arr-right { border-left-color: var(--cds-support-success); }\n.arr-back.on.arr-up { border-bottom-color: var(--cds-support-warning); }\n.arr-up { border-left: 5px solid transparent; border-right: 5px solid transparent;\n border-bottom: 7px solid var(--cds-border-strong-01); }\n.elabel { position: absolute; transform: translateX(-100%); background: var(--cds-layer-02);\n border: 1px solid var(--cds-border-subtle-01); color: var(--cds-text-secondary);\n font-size: 11px; padding: 0 4px; white-space: nowrap; }\n\n/* Stages accordion */\n.stage { border-bottom: 1px solid var(--cds-border-subtle-01); }\n.stage-header { display: flex; align-items: center; gap: 6px; width: 100%;\n min-height: 44px; padding: 4px 10px; background: var(--cds-layer-01);\n color: var(--cds-text-primary); border: 0; text-align: left; cursor: pointer;\n border-left: 2px solid transparent; }\n.stage-header:hover { background: var(--cds-layer-hover-01); }\n.stage-header.status-running { border-left-color: var(--cds-interactive); }\n.stage-header.status-pass { border-left-color: var(--cds-support-success); }\n.stage-header.status-fail { border-left-color: var(--cds-support-warning); }\n.stage-header.status-fatal { border-left-color: var(--cds-support-error); }\n.stage-header.status-unverifiable { border-left-color: var(--adl-support-unverifiable); }\n.stage-glyph { width: 10px; color: var(--cds-text-helper); }\n.stage-index { font-size: 11px; color: var(--cds-text-helper); }\n.stage-header .led { color: var(--cds-border-strong-01); }\n.status-running .led { background: var(--cds-interactive); }\n.status-pass .led { background: var(--cds-support-success); }\n.status-fail .led { background: var(--cds-support-warning); }\n.status-fatal .led { background: var(--cds-support-error); }\n.status-unverifiable .led { background: var(--adl-support-unverifiable); }\n.stage-name { font-weight: 600; }\n.stage-status { font-size: 11px; text-transform: uppercase; color: var(--cds-text-secondary); }\n.stage-verdict { font-size: 11px; color: var(--cds-text-helper); }\n.stage-time { margin-left: auto; font-size: 11px; color: var(--cds-text-secondary); }\n.console-wrap { position: relative; max-height: 320px; overflow-y: auto;\n background: var(--adl-console); display: none; }\n.stage:not(.collapsed) .console-wrap { display: block; }\n.stage-console { margin: 0; padding: 8px 10px; color: var(--adl-console-text);\n font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; }\n.stage-console .cline { display: block; min-height: 1em; }\n.stage-console .omitted, .stage-console .stage-placeholder {\n color: var(--adl-console-muted); font-style: italic; }\n.copy-btn { position: sticky; top: 6px; float: right; margin: 6px 6px 0 0;\n width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;\n background: var(--adl-console-control); color: var(--adl-console-text);\n border: 0; cursor: pointer; }\n.copy-btn:hover { background: var(--adl-console-control-hover); }\n\n/* Scrollbars */\n* { scrollbar-width: thin; scrollbar-color: var(--cds-border-strong-01) transparent; }\n*::-webkit-scrollbar { width: 8px; height: 8px; }\n*::-webkit-scrollbar-thumb { background: var(--cds-border-strong-01); }\n*::-webkit-scrollbar-thumb:hover { background: var(--cds-text-helper); }\n.console-wrap { scrollbar-color: var(--adl-console-scroll-thumb) var(--adl-console); }\n\n@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }\n@keyframes sweep { 0% { transform: scaleX(0); } 50% { transform: scaleX(1); }\n 100% { transform: scaleX(0); } }\n\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after { animation: none !important; transition: none !important; }\n}\n\n@media (max-width: 760px) {\n body { overflow: auto; height: auto; }\n .shell { padding: 12px; }\n .tiles { flex-wrap: wrap; }\n .tiles .tile { flex: 1 1 45%; }\n .tiles .tile:last-child:nth-child(odd) { flex-basis: 100%; }\n .lower { flex-direction: column; }\n #pipeline-panel { overflow-x: auto; }\n}\n</style>\n<script>\n/*\n * pipeline-layout.js - reference implementation of the viz pipeline layout.\n *\n * Pure function, no DOM access, ES5 (matches the inline-script style of\n * src/agentic_dev_loop/viz/server.py PAGE). Usable as a <script> (exposes\n * window.layoutPipeline) or via require()/import in tests.\n *\n * Contract\n * layoutPipeline({\n * nodes: [\"brief\", \"deps\", \"dev\", ...], // display order (= /topology nodes)\n * flow: [{src, dst}], // on_pass chain (solid)\n * edges: [{src, dst, label|null}], // on_fail routes (dashed)\n * status: {nodeId: \"pass\"|\"running\"|\"fail\"|\"fatal\"|\"unverifiable\"|\"pending\"},\n * availWidth: 1200 // px available to the graph\n * }) -> {\n * width, height, rows, perRow,\n * nodes: [{id, index, x, y, w, h}],\n * lines: [{x, y, w, h, kind: \"flow\"|\"back\", on: bool, dir: \"h\"|\"v\"}],\n * arrows: [{x, y, dir: \"right\"|\"up\", kind, on}], // x,y = top-left of a CSS-border triangle box\n * labels: [{x, y, text}] // x = right edge (translateX(-100%)), y = top\n * }\n *\n * Algorithm\n * 1. Stations keep natural size (NW x NH). Choose the FEWEST rows R such that\n * the row (perRow = ceil(N/R)) plus routing margins fits availWidth.\n * perRow never drops below 2; if 2 still doesn't fit, the caller scrolls.\n * 2. Consecutive flow edges in a row = rail with arrow. Row change = wrap\n * connector: right stub -> down to the row's deepest gutter lane -> left\n * to the left channel -> down -> right into the next row's first station.\n * 3. Conditional edges route orthogonally:\n * U same row (back or forward): drop into gutter, run, rise into target.\n * C backward across rows: drop into source gutter, run left to a\n * left-hand channel, rise to the target row's gutter, run right,\n * rise into the target.\n * F forward across rows (e.g. -> failure terminal): mirrored through\n * a right-hand channel, descending.\n * 4. Crossing-free ordering (proved for U and C; F is best-effort):\n * gutter lanes, top -> bottom: U (shortest span first), C-into-row\n * (outer channel first), F-into-row, C-leaving-row (leftmost source\n * first), F-leaving-row, wrap connector last;\n * C channels: inner -> outer by (source row asc, source index asc);\n * target entry points: U rightmost, then C inner -> outer, then F,\n * stepping 14px leftwards across the target's bottom edge.\n */\n(function (root) {\n \"use strict\";\n\n var NW = 112, NH = 76, GAP = 24, STEP = NW + GAP;\n var LANE = 18, LANE_TOP = 14, LANE_PAD = 16, CH = 12;\n\n function layoutPipeline(opts) {\n var names = opts.nodes || [];\n var N = names.length;\n var status = opts.status || {};\n var avail = Math.max(0, opts.availWidth || 0);\n var flow = opts.flow || [];\n var condEdges = opts.edges || [];\n var idx = {};\n names.forEach(function (n, i) { idx[n] = i; });\n var reached = function (n) { return (status[n] || \"pending\") !== \"pending\"; };\n var failed = function (n) { var s = status[n] || \"pending\"; return s === \"fail\" || s === \"fatal\"; };\n\n // flow edges that are NOT to the next station are routed like conditional edges (solid)\n var rails = [], extraFlow = [];\n flow.forEach(function (f) {\n if (!(f.src in idx) || !(f.dst in idx)) { return; }\n if (idx[f.dst] === idx[f.src] + 1) { rails.push(f); }\n else { extraFlow.push({ src: f.src, dst: f.dst, label: null, kind: \"flow\" }); }\n });\n var routed = condEdges.filter(function (e) { return (e.src in idx) && (e.dst in idx); })\n .map(function (e) { return { src: e.src, dst: e.dst, label: e.label || null, kind: \"back\" }; })\n .concat(extraFlow);\n\n function plan(R) {\n var per = Math.max(1, Math.ceil(N / R));\n var rows = Math.max(1, Math.ceil(N / per));\n var pos = names.map(function (n, i) { return { r: Math.floor(i / per), c: i % per }; });\n var es = routed.map(function (e) {\n var si = idx[e.src], ti = idx[e.dst];\n return { label: e.label, kind: e.kind, dst: e.dst, si: si, ti: ti, rs: pos[si].r, rt: pos[ti].r };\n });\n var U = es.filter(function (e) { return e.rs === e.rt; })\n .sort(function (a, b) { return Math.abs(a.si - a.ti) - Math.abs(b.si - b.ti); });\n var C = es.filter(function (e) { return e.rs > e.rt; })\n .sort(function (a, b) { return a.rs - b.rs || a.si - b.si; });\n var F = es.filter(function (e) { return e.rs < e.rt; })\n .sort(function (a, b) { return a.rt - b.rt || b.si - a.si; });\n C.forEach(function (e, j) { e.j = j; });\n F.forEach(function (e, k) { e.k = k; });\n var multi = rows > 1;\n var O = multi ? CH + CH * C.length + 8 : 0;\n var core = per * STEP - GAP;\n var right = (multi ? 14 : 0) + (F.length ? 8 + CH * F.length : 0);\n return { per: per, rows: rows, pos: pos, U: U, C: C, F: F, O: O, core: core, width: O + core + right };\n }\n\n var L = plan(1);\n for (var R = 2; L.width > avail && L.per > 2; R++) { L = plan(R); }\n var O = L.O;\n\n // gutter lane assignment\n var gut = [];\n for (var g = 0; g < L.rows; g++) {\n var items = 0;\n var take = function () { return items++; };\n L.U.filter(function (e) { return e.rt === g; }).forEach(function (e) { e.laneS = take(); });\n L.C.filter(function (e) { return e.rt === g; }).sort(function (a, b) { return b.j - a.j; })\n .forEach(function (e) { e.laneT = take(); });\n L.F.filter(function (e) { return e.rt === g; }).forEach(function (e) { e.laneT = take(); });\n L.C.filter(function (e) { return e.rs === g; }).sort(function (a, b) { return a.si - b.si; })\n .forEach(function (e) { e.laneS = take(); });\n L.F.filter(function (e) { return e.rs === g; }).forEach(function (e) { e.laneS = take(); });\n var wrap = g < L.rows - 1 ? take() : -1;\n gut.push({ n: items, wrapLane: wrap });\n }\n var rowY = [0];\n gut.forEach(function (gt, g) {\n gt.h = gt.n ? LANE_TOP + (gt.n - 1) * LANE + LANE_PAD : 4;\n rowY.push(rowY[g] + NH + gt.h);\n });\n var laneY = function (g, l) { return rowY[g] + NH + LANE_TOP + l * LANE; };\n var nx = function (i) { return O + L.pos[i].c * STEP; };\n var ny = function (i) { return rowY[L.pos[i].r]; };\n\n var entryCount = {};\n L.U.concat(L.C, L.F).forEach(function (e) {\n var m = entryCount[e.ti] || 0;\n entryCount[e.ti] = m + 1;\n e.xe = Math.max(nx(e.ti) + 8, nx(e.ti) + NW - 14 - m * 14);\n });\n\n var lines = [], arrows = [], labels = [];\n function H(x1, x2, y, kind, on) { lines.push({ x: Math.min(x1, x2), y: y, w: Math.abs(x2 - x1), h: 0, dir: \"h\", kind: kind, on: !!on }); }\n function V(x, y1, y2, kind, on) { lines.push({ x: x, y: Math.min(y1, y2), w: 0, h: Math.abs(y2 - y1), dir: \"v\", kind: kind, on: !!on }); }\n\n rails.forEach(function (f) {\n var i = idx[f.src], on = reached(f.dst);\n if (L.pos[i + 1].r === L.pos[i].r) {\n H(nx(i) + NW, nx(i + 1), ny(i) + 37, \"flow\", on);\n arrows.push({ x: nx(i + 1) - 6, y: ny(i) + 33, dir: \"right\", kind: \"flow\", on: on });\n } else {\n var r = L.pos[i].r, xr = nx(i) + NW + 12, yw = laneY(r, gut[r].wrapLane), yn = ny(i + 1) + 37;\n H(nx(i) + NW, xr + 2, ny(i) + 37, \"flow\", on);\n V(xr, ny(i) + 37, yw, \"flow\", on);\n H(O - 12, xr + 2, yw - 1, \"flow\", on);\n V(O - 12, yw - 1, yn + 2, \"flow\", on);\n H(O - 12, O, yn, \"flow\", on);\n arrows.push({ x: O - 6, y: yn - 4, dir: \"right\", kind: \"flow\", on: on });\n }\n });\n\n L.U.concat(L.C, L.F).forEach(function (e) {\n var xs = nx(e.si) + NW / 2, top = ny(e.ti) + NH, y1 = laneY(e.rs, e.laneS);\n var on = e.kind === \"back\" && failed(names[e.si]);\n V(xs, ny(e.si) + NH, y1, e.kind, on);\n if (e.rs === e.rt) {\n H(e.xe, xs, y1, e.kind, on);\n V(e.xe, top + 6, y1, e.kind, on);\n } else {\n var xc = e.rs > e.rt\n ? O - CH - CH * (e.j + 1)\n : O + L.core + 14 + 8 + CH * e.k;\n var y2 = laneY(e.rt, e.laneT);\n H(xc, xs, y1, e.kind, on);\n V(xc, y2, y1, e.kind, on);\n H(xc, e.xe, y2, e.kind, on);\n V(e.xe, top + 6, y2, e.kind, on);\n }\n arrows.push({ x: e.xe - 5, y: top, dir: \"up\", kind: e.kind, on: on });\n if (e.label) { labels.push({ x: xs - 6, y: y1 - 9, text: e.label }); }\n });\n\n return {\n width: L.width,\n height: rowY[L.rows],\n rows: L.rows,\n perRow: L.per,\n nodes: names.map(function (n, i) { return { id: n, index: i, x: nx(i), y: ny(i), w: NW, h: NH }; }),\n lines: lines, arrows: arrows, labels: labels\n };\n }\n\n layoutPipeline.metrics = { NW: NW, NH: NH, GAP: GAP, STEP: STEP, LANE: LANE };\n root.layoutPipeline = layoutPipeline;\n if (typeof module !== \"undefined\" && module.exports) { module.exports = layoutPipeline; }\n})(typeof window !== \"undefined\" ? window : this);\n</script>\n</head>\n<body>\n<a class=\"skip\" href=\"#stages\">Skip to stages</a>\n<header class=\"hdr\">\n <div class=\"hdr-brand\"><span class=\"mark\" aria-hidden=\"true\"></span>\n <span class=\"hdr-name\">agentic-dev-loop viz</span></div>\n <div class=\"hdr-right\">\n <span id=\"stream\" class=\"stream\" role=\"status\"><span class=\"led\" aria-hidden=\"true\"></span>\n <span id=\"stream-label\">STREAM LIVE</span></span>\n <button id=\"theme-toggle\" class=\"hdr-action\" type=\"button\"\n data-testid=\"theme-toggle\" aria-label=\"Switch to light theme\">\n <span id=\"theme-icon\" class=\"ic\" aria-hidden=\"true\">\n <svg class=\"ic-sun\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"3.33\" fill=\"currentColor\"/>\n <path stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"\n d=\"M8 .67v2M8 13.33v2M.67 8h2M13.33 8h2M2.81 2.81l.95.95M12.24 12.24l.95.95M13.19 2.81l-.95.95M3.76 12.24l-.95.95\"/>\n </svg>\n <svg class=\"ic-moon\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\">\n <path d=\"M14 8.53A6 6 0 1 1 7.47 2 4.67 4.67 0 0 0 14 8.53Z\" fill=\"currentColor\"/>\n </svg>\n </span>\n </button>\n </div>\n</header>\n<div id=\"stream-banner\" role=\"status\" aria-live=\"polite\" data-testid=\"stream-banner\"></div>\n<main class=\"shell\">\n <section class=\"tiles\" aria-label=\"Run summary\">\n <div class=\"tile\"><div class=\"lbl\">Run</div>\n <div id=\"subject\" class=\"big\">agentic-dev-loop pipeline</div></div>\n <div class=\"tile\"><div class=\"lbl\">Attempt</div>\n <div id=\"attempt\" class=\"big\">?/?</div><div id=\"meter\" class=\"meter\"></div></div>\n <div class=\"tile\"><div class=\"lbl\">Thread ID</div>\n <div id=\"thread\" class=\"big\"><code id=\"thread-id\" tabindex=\"0\" role=\"button\" title=\"click to copy\">--</code></div></div>\n <div class=\"tile\"><div class=\"lbl\">Provider</div>\n <div id=\"provider\" class=\"big\">--</div></div>\n <div class=\"tile\"><div class=\"lbl\">Total</div>\n <div id=\"total\" class=\"big\"></div></div>\n </section>\n <div id=\"verdicts\" class=\"verdicts\" aria-label=\"Step verdicts\"></div>\n <div id=\"run-banner\" class=\"outcome\" role=\"status\" aria-live=\"polite\" data-testid=\"run-banner\"></div>\n <section id=\"pipeline-panel\" class=\"panel\" aria-label=\"Pipeline\">\n <div class=\"panel-h\"><span>Pipeline</span>\n <span class=\"legend mono\">solid = on_pass / dashed = on_fail</span></div>\n <div class=\"graph-wrap\"><div id=\"graph\" role=\"img\" aria-label=\"Pipeline graph\"></div></div>\n </section>\n <div class=\"lower\">\n <section class=\"panel panel-stages\" aria-label=\"Stages\">\n <div class=\"panel-h\"><span>Stages</span></div>\n <div id=\"stages\"></div>\n </section>\n <section class=\"panel panel-log\" aria-label=\"Event log\">\n <div class=\"panel-h\"><span>Event log</span></div>\n <ol id=\"run-log\" data-testid=\"run-log\"></ol>\n </section>\n </div>\n</main>\n<script>\n(function () {\n \"use strict\";\n\n var nodeStatus = {};\n var stageWord = {};\n var verdictSteps = new Set();\n var nodeIds = new Set();\n var currentVerdicts = {};\n var verdictFail = new Set();\n var verdictPass = new Set();\n var verdictUnverifiable = new Set();\n var verdictEnv = new Set();\n var fatalNodes = new Set();\n var maxAttempts = null;\n var attempt = null;\n var topology = null;\n var stations = {};\n var pendingCompletion = null;\n var stepTimes = {};\n var liveElapsed = {};\n var runStartWall = null;\n var totalTimer = null;\n var lastEventAt = 0;\n var reconnectAttempts = 0;\n var runActive = false;\n var runFailed = false;\n var topologyReady = false;\n var topologyFailed = false;\n var pendingEvents = [];\n var MAX_CONSOLE = 500;\n var PALETTE = [\"#0c0c0c\", \"#c50f1f\", \"#13a10e\", \"#c19c00\", \"#0037da\",\n \"#881798\", \"#3a96dd\", \"#cccccc\"];\n var BRIGHT = [\"#767676\", \"#e74856\", \"#16c60c\", \"#f9f1a5\", \"#3b78ff\",\n \"#b4009e\", \"#61d6d6\", \"#f2f2f2\"];\n var COPY_ICON = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\"'\n + ' height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"'\n + ' stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"'\n + ' aria-hidden=\"true\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\"'\n + ' rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1'\n + ' 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>';\n var CHECK_ICON = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\"'\n + ' height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"'\n + ' stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"'\n + ' aria-hidden=\"true\"><polyline points=\"20 6 9 17 4 12\"></polyline></svg>';\n\n function el(id) { return document.getElementById(id); }\n\n function layoutOf(opts) {\n if (typeof layoutPipeline === \"function\") { return layoutPipeline(opts); }\n if (typeof window !== \"undefined\" && typeof window.layoutPipeline === \"function\") {\n return window.layoutPipeline(opts);\n }\n if (typeof globalThis !== \"undefined\"\n && typeof globalThis.layoutPipeline === \"function\") {\n return globalThis.layoutPipeline(opts);\n }\n return null;\n }\n\n function zeroPad(n) { return n < 10 ? \"0\" + n : String(n); }\n function mmss(totalSeconds) {\n var s = totalSeconds % 60;\n var m = Math.floor(totalSeconds / 60);\n return m + \":\" + (s < 10 ? \"0\" : \"\") + s;\n }\n\n /* Theme ------------------------------------------------------------- */\n function applyTheme(theme) {\n var root = document.documentElement;\n if (root && root.setAttribute) { root.setAttribute(\"data-theme\", theme); }\n var btn = el(\"theme-toggle\");\n if (btn) {\n btn.setAttribute(\"aria-label\", theme === \"dark\"\n ? \"Switch to light theme\" : \"Switch to dark theme\");\n }\n }\n\n function initialTheme() {\n try {\n var stored = window.localStorage && window.localStorage.getItem(\"adl-theme\");\n if (stored === \"dark\" || stored === \"light\") { return stored; }\n } catch (err) { /* session-only */ }\n try {\n if (window.matchMedia\n && window.matchMedia(\"(prefers-color-scheme: light)\").matches) {\n return \"light\";\n }\n } catch (err) { /* default dark */ }\n return \"dark\";\n }\n\n var theme = initialTheme();\n applyTheme(theme);\n (function initThemeToggle() {\n var btn = el(\"theme-toggle\");\n if (!btn) { return; }\n btn.addEventListener(\"click\", function () {\n theme = theme === \"dark\" ? \"light\" : \"dark\";\n applyTheme(theme);\n try { window.localStorage.setItem(\"adl-theme\", theme); }\n catch (err) { /* session-only */ }\n });\n })();\n\n /* Run log ----------------------------------------------------------- */\n function logLine(text, cls) {\n var li = document.createElement(\"li\");\n if (cls) { li.className = cls; }\n li.textContent = text;\n var log = el(\"run-log\");\n if (!log) { return; }\n log.appendChild(li);\n log.scrollTop = log.scrollHeight;\n }\n\n function logConsole(html) {\n var li = document.createElement(\"li\");\n li.innerHTML = html;\n var log = el(\"run-log\");\n if (!log) { return; }\n log.appendChild(li);\n log.scrollTop = log.scrollHeight;\n }\n\n /* Summary tiles ----------------------------------------------------- */\n function renderMeter() {\n var box = el(\"meter\");\n if (!box) { return; }\n box.innerHTML = \"\";\n box.className = \"meter\";\n if (maxAttempts === null) { return; }\n var used = attempt || 0;\n for (var i = 0; i < maxAttempts; i++) {\n var seg = document.createElement(\"span\");\n seg.className = i < used ? \"on\" : \"\";\n box.appendChild(seg);\n }\n if (runFailed && used >= maxAttempts) { box.className = \"meter exhausted\"; }\n }\n\n function updateAttempt() {\n var line = el(\"attempt\");\n if (line) {\n line.textContent = attempt === null\n ? \"?/\" + (maxAttempts === null ? \"?\" : maxAttempts)\n : attempt + \"/\" + (maxAttempts === null ? \"?\" : maxAttempts);\n }\n renderMeter();\n }\n\n function renderTotal() {\n var line = el(\"total\");\n if (!line) { return; }\n line.innerHTML = \"\";\n if (runStartWall === null) {\n line.textContent = \"\";\n line.style.display = \"none\";\n } else {\n line.textContent = \"total \" + mmss(Math.max(0,\n Math.round(Date.now() / 1000 - runStartWall)));\n line.style.display = \"block\";\n var note = document.createElement(\"span\");\n note.className = \"total-note\";\n note.textContent = runActive ? \"LIVE\" : \"FINAL\";\n line.appendChild(note);\n }\n if (runActive && Date.now() - lastEventAt > 10000) { recover(); }\n }\n\n /* Status ------------------------------------------------------------ */\n function statusOf(n) {\n if (nodeStatus[n] === \"running\") { return \"running\"; }\n if (fatalNodes.has(n)) { return \"fatal\"; }\n if (nodeStatus[n] === \"fatal\") { return \"fatal\"; }\n if (verdictFail.has(n)) { return \"fail\"; }\n if (verdictPass.has(n)) { return \"pass\"; }\n if (verdictUnverifiable.has(n)) { return \"unverifiable\"; }\n if (verdictEnv.has(n)) { return \"env\"; }\n return nodeStatus[n] || \"pending\";\n }\n\n function renderStatusOf(n) {\n var s = statusOf(n);\n return s === \"env\" ? \"fatal\" : s;\n }\n\n function statusWord(n) {\n var s = renderStatusOf(n);\n if (s === \"pass\") { return currentVerdicts[n] ? \"PASS\" : \"DONE\"; }\n if (s === \"running\") { return \"RUNNING\"; }\n if (s === \"fail\") { return \"RETRY\"; }\n if (s === \"fatal\") { return currentVerdicts[n] === \"fail\" ? \"FAIL\" : \"FATAL\"; }\n if (s === \"unverifiable\") { return \"UNVERIF\"; }\n return \"PENDING\";\n }\n\n function updateBadges() {\n var box = el(\"verdicts\");\n if (!box) { return; }\n box.innerHTML = \"\";\n Array.from(verdictSteps).forEach(function (step) {\n var b = document.createElement(\"span\");\n var v = currentVerdicts[step];\n b.className = \"badge\";\n if (v === \"pass\") { b.className += \" badge-pass\"; }\n else if (v === \"fail\") { b.className += \" badge-fail\"; }\n else if (v === \"unverifiable\") { b.className += \" badge-unverifiable\"; }\n else if (v === \"env\") { b.className += \" badge-fail\"; }\n else { b.className += \" badge-unclear\"; }\n b.textContent = step + \": \"\n + (v === null || v === undefined ? \"no verdict\" : v);\n box.appendChild(b);\n });\n }\n\n function resetVerdicts() {\n verdictSteps.clear();\n verdictPass.clear();\n verdictFail.clear();\n verdictUnverifiable.clear();\n verdictEnv.clear();\n fatalNodes.clear();\n currentVerdicts = {};\n nodeStatus = {};\n stageWord = {};\n pendingCompletion = null;\n updateBadges();\n Array.from(nodeIds).forEach(function (n) { renderStageHeader(n); });\n renderGraph();\n }\n\n /* Pipeline graph ---------------------------------------------------- */\n function ensureStation(id) {\n if (stations[id]) { return stations[id]; }\n var s = document.createElement(\"div\");\n s.className = \"node st-pending\";\n s.id = \"station-\" + id;\n s.setAttribute(\"data-testid\", \"station-\" + id);\n var row = document.createElement(\"div\");\n row.className = \"nrow\";\n var idx = document.createElement(\"span\");\n idx.className = \"nidx\";\n var time = document.createElement(\"span\");\n time.className = \"ntime\";\n row.appendChild(idx);\n row.appendChild(time);\n var name = document.createElement(\"div\");\n name.className = \"nname\";\n var stat = document.createElement(\"div\");\n stat.className = \"nstat\";\n var led = document.createElement(\"span\");\n led.className = \"led\";\n led.setAttribute(\"aria-hidden\", \"true\");\n var label = document.createElement(\"span\");\n label.className = \"nlabel\";\n stat.appendChild(led);\n stat.appendChild(label);\n s.appendChild(row);\n s.appendChild(name);\n s.appendChild(stat);\n stations[id] = s;\n return s;\n }\n\n function nodeIndex(id) {\n if (!topology) { return 0; }\n var i = topology.nodes.indexOf(id);\n return i < 0 ? 0 : i;\n }\n\n function nodeTime(id) {\n if (nodeStatus[id] === \"running\" && liveElapsed[id] !== undefined) {\n return mmss(liveElapsed[id]);\n }\n if (stepTimes[id] !== undefined) { return mmss(stepTimes[id]); }\n return \"-\";\n }\n\n function updateStation(id) {\n var s = ensureStation(id);\n var status = renderStatusOf(id);\n s.className = \"node st-\" + status;\n var name = s.querySelector(\".nname\");\n var idx = s.querySelector(\".nidx\");\n var time = s.querySelector(\".ntime\");\n var label = s.querySelector(\".nlabel\");\n if (name) { name.textContent = id; }\n if (idx) { idx.textContent = zeroPad(nodeIndex(id) + 1); }\n if (time) { time.textContent = nodeTime(id); }\n if (label) { label.textContent = statusWord(id); }\n }\n\n function graphLabel() {\n if (!topology) { return \"Pipeline graph\"; }\n var parts = topology.nodes.map(function (n, i) {\n return zeroPad(i + 1) + \" \" + n + \" \" + statusWord(n);\n });\n var rows = stations.__rows || 1;\n return \"Pipeline graph: \" + parts.join(\", \") + \". \" + rows + \" row\"\n + (rows === 1 ? \"\" : \"s\") + \".\";\n }\n\n function makeLine(l) {\n var d = document.createElement(\"div\");\n d.className = \"ln ln-\" + l.kind + (l.on ? \" on\" : \"\");\n d.setAttribute(\"data-dir\", l.dir);\n d.style.left = l.x + \"px\";\n d.style.top = l.y + \"px\";\n d.style.width = (l.dir === \"h\" ? l.w : 2) + \"px\";\n d.style.height = (l.dir === \"v\" ? l.h : 2) + \"px\";\n return d;\n }\n\n function makeArrow(a) {\n var d = document.createElement(\"div\");\n d.className = \"arr arr-\" + a.dir + \" arr-\" + a.kind + (a.on ? \" on\" : \"\");\n d.style.left = a.x + \"px\";\n d.style.top = a.y + \"px\";\n return d;\n }\n\n function makeLabel(l) {\n var d = document.createElement(\"div\");\n d.className = \"elabel\";\n d.style.left = l.x + \"px\";\n d.style.top = l.y + \"px\";\n d.textContent = l.text;\n return d;\n }\n\n function renderGraph() {\n var box = el(\"graph\");\n if (!box) { return; }\n box.innerHTML = \"\";\n if (topologyFailed) {\n box.style.width = \"\";\n box.style.height = \"\";\n var msg = document.createElement(\"p\");\n msg.className = \"graph-msg\";\n msg.textContent = \"Graph unavailable - the event log below is still live\";\n box.appendChild(msg);\n box.setAttribute(\"aria-label\", \"Pipeline graph unavailable\");\n return;\n }\n if (!topology) {\n var loading = document.createElement(\"p\");\n loading.className = \"graph-msg\";\n loading.textContent = \"Loading graph...\";\n box.appendChild(loading);\n box.setAttribute(\"aria-label\", \"Loading pipeline graph\");\n return;\n }\n var status = {};\n topology.nodes.forEach(function (n) { status[n] = renderStatusOf(n); });\n var avail = (typeof window !== \"undefined\" && window.innerWidth)\n ? window.innerWidth : 1200;\n var res = layoutOf({\n nodes: topology.nodes,\n flow: topology.flow || [],\n edges: topology.edges || [],\n status: status,\n availWidth: avail\n });\n if (!res) { return; }\n box.style.width = res.width + \"px\";\n box.style.height = res.height + \"px\";\n res.lines.forEach(function (l) { box.appendChild(makeLine(l)); });\n res.arrows.forEach(function (a) { box.appendChild(makeArrow(a)); });\n res.labels.forEach(function (l) { box.appendChild(makeLabel(l)); });\n res.nodes.forEach(function (nd) {\n updateStation(nd.id);\n var s = stations[nd.id];\n s.style.left = nd.x + \"px\";\n s.style.top = nd.y + \"px\";\n box.appendChild(s);\n });\n stations.__rows = res.rows;\n box.setAttribute(\"aria-label\", graphLabel());\n }\n\n /* Stages ------------------------------------------------------------ */\n function addPlaceholder(pre) {\n var ph = document.createElement(\"div\");\n ph.className = \"stage-placeholder\";\n ph.textContent = \"No output\";\n pre.appendChild(ph);\n }\n\n function ensureSection(node) {\n var existing = document.getElementById(\"stage-\" + node);\n if (existing) { return existing; }\n var stages = el(\"stages\");\n var section = document.createElement(\"section\");\n section.className = \"stage collapsed\";\n section.id = \"stage-\" + node;\n section.setAttribute(\"data-testid\", \"stage-\" + node);\n\n var header = document.createElement(\"button\");\n header.className = \"stage-header\";\n header.type = \"button\";\n header.setAttribute(\"aria-expanded\", \"false\");\n header.setAttribute(\"aria-controls\", \"console-\" + node);\n header.addEventListener(\"click\", function () {\n var willCollapse = !section.classList.contains(\"collapsed\");\n section.classList.toggle(\"collapsed\");\n header.setAttribute(\"aria-expanded\", willCollapse ? \"false\" : \"true\");\n renderStageHeader(node);\n if (!willCollapse) {\n requestAnimationFrame(function () { tailSection(section); });\n }\n });\n\n var glyph = document.createElement(\"span\");\n glyph.className = \"stage-glyph\";\n glyph.setAttribute(\"aria-hidden\", \"true\");\n var index = document.createElement(\"span\");\n index.className = \"stage-index\";\n var led = document.createElement(\"span\");\n led.className = \"led\";\n led.setAttribute(\"aria-hidden\", \"true\");\n var name = document.createElement(\"span\");\n name.className = \"stage-name\";\n var status = document.createElement(\"span\");\n status.className = \"stage-status\";\n var verdict = document.createElement(\"span\");\n verdict.className = \"stage-verdict\";\n var time = document.createElement(\"span\");\n time.className = \"stage-time\";\n\n header.appendChild(glyph);\n header.appendChild(index);\n header.appendChild(led);\n header.appendChild(name);\n header.appendChild(status);\n header.appendChild(verdict);\n header.appendChild(time);\n\n var console = document.createElement(\"pre\");\n console.className = \"stage-console\";\n addPlaceholder(console);\n\n var wrap = document.createElement(\"div\");\n wrap.className = \"console-wrap\";\n wrap.id = \"console-\" + node;\n var copyBtn = document.createElement(\"button\");\n copyBtn.className = \"copy-btn\";\n copyBtn.type = \"button\";\n copyBtn.setAttribute(\"title\", \"Copy output\");\n copyBtn.setAttribute(\"aria-label\", \"Copy \" + node + \" console output\");\n copyBtn.setAttribute(\"data-testid\", \"copy-console-\" + node);\n copyBtn.innerHTML = COPY_ICON;\n copyBtn.addEventListener(\"click\", function () {\n copyConsole(console, copyBtn);\n });\n wrap.appendChild(copyBtn);\n wrap.appendChild(console);\n\n section.appendChild(header);\n section.appendChild(wrap);\n stages.appendChild(section);\n renderStageHeader(node);\n return section;\n }\n\n function renderStageHeader(node) {\n var section = ensureSection(node);\n var header = section.querySelector(\".stage-header\");\n if (!header) { return; }\n var glyph = header.querySelector(\".stage-glyph\");\n var index = header.querySelector(\".stage-index\");\n var name = header.querySelector(\".stage-name\");\n var status = header.querySelector(\".stage-status\");\n var verdict = header.querySelector(\".stage-verdict\");\n var time = header.querySelector(\".stage-time\");\n var collapsed = section.classList.contains(\"collapsed\");\n if (glyph) { glyph.textContent = collapsed ? \">\" : \"v\"; }\n if (index) { index.textContent = zeroPad(nodeIndex(node) + 1); }\n if (name) { name.textContent = node; }\n var word = stageWord[node] || \"\";\n if (status) { status.textContent = word; }\n var v = currentVerdicts[node];\n if (verdict) {\n verdict.textContent = (word === \"completed\" && v)\n ? \"verdict: \" + String(v).toUpperCase() : \"\";\n }\n if (time) {\n time.textContent = (word === \"completed\" && stepTimes[node])\n ? mmss(stepTimes[node])\n : (word === \"running\" && liveElapsed[node] !== undefined)\n ? mmss(liveElapsed[node]) : \"\";\n }\n header.className = \"stage-header status-\" + renderStatusOf(node);\n header.setAttribute(\"aria-expanded\", collapsed ? \"false\" : \"true\");\n }\n\n function setStageStatus(node, word) {\n stageWord[node] = word;\n renderStageHeader(node);\n }\n\n function flushPending() {\n if (pendingCompletion === null) { return; }\n var p = pendingCompletion;\n pendingCompletion = null;\n setStageStatus(p.node, p.word);\n }\n\n function clearConsoles() {\n stageWord = {};\n Array.from(nodeIds).forEach(function (n) {\n var section = ensureSection(n);\n var pre = consoleOf(section);\n if (pre) {\n pre.innerHTML = \"\";\n addPlaceholder(pre);\n }\n section.classList.add(\"collapsed\");\n renderStageHeader(n);\n });\n var log = el(\"run-log\");\n if (log) { log.innerHTML = \"\"; }\n }\n\n function consoleOf(section) {\n var wrap = section.querySelector(\".console-wrap\");\n return wrap ? wrap.querySelector(\".stage-console\") : null;\n }\n\n function scrollPortOf(section) { return section.querySelector(\".console-wrap\"); }\n\n function copyConsole(pre, btn) {\n var lines = Array.prototype.map.call(pre.querySelectorAll(\".cline\"),\n function (c) { return c.textContent; });\n if (lines.length === 0) {\n btn.setAttribute(\"title\", \"Nothing to copy\");\n return;\n }\n var text = lines.join(\"\\n\");\n function done(ok) {\n btn.innerHTML = ok ? CHECK_ICON : COPY_ICON;\n btn.setAttribute(\"title\", ok ? \"Copied!\" : \"Copy failed\");\n if (ok) {\n setTimeout(function () {\n btn.innerHTML = COPY_ICON;\n btn.setAttribute(\"title\", \"Copy output\");\n }, 1500);\n }\n }\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(function () { done(true); },\n function () { done(false); });\n } else {\n var ta = document.createElement(\"textarea\");\n ta.value = text;\n ta.style.position = \"fixed\";\n ta.style.top = \"-1000px\";\n document.body.appendChild(ta);\n ta.select();\n var ok = false;\n try { ok = document.execCommand(\"copy\"); } catch (err) { ok = false; }\n document.body.removeChild(ta);\n done(ok);\n }\n }\n\n function nearBottom(pre) {\n return pre.scrollHeight - pre.scrollTop - pre.clientHeight < 24;\n }\n\n function tailSection(section) {\n if (section.classList.contains(\"collapsed\")) { return; }\n var sp = scrollPortOf(section);\n if (sp) { sp.scrollTop = sp.scrollHeight; }\n }\n\n function appendConsole(section, html) {\n var pre = consoleOf(section);\n var sp = scrollPortOf(section);\n if (!pre) { return; }\n var stick = !section.classList.contains(\"collapsed\") && nearBottom(sp);\n var ph = pre.querySelector(\".stage-placeholder\");\n if (ph) { pre.removeChild(ph); }\n if (pre.querySelectorAll(\".cline\").length >= MAX_CONSOLE) {\n var first = pre.querySelector(\".cline\");\n if (first) { pre.removeChild(first); }\n var omitted = pre.querySelector(\".omitted\");\n if (!omitted) {\n omitted = document.createElement(\"div\");\n omitted.className = \"omitted\";\n pre.insertBefore(omitted, pre.firstChild);\n }\n var count = parseInt(omitted.getAttribute(\"data-n\") || \"0\", 10) + 1;\n omitted.setAttribute(\"data-n\", String(count));\n omitted.textContent = \"...\" + count + \" line\"\n + (count === 1 ? \"\" : \"s\") + \" omitted\";\n }\n var line = document.createElement(\"span\");\n line.className = \"cline\";\n line.innerHTML = html;\n pre.appendChild(line);\n if (stick && sp) { sp.scrollTop = sp.scrollHeight; }\n }\n\n function handleConsole(ev) {\n var html = ansiToHtml(ev.text);\n if (ev.node && nodeIds.has(ev.node)) {\n appendConsole(ensureSection(ev.node), html);\n } else {\n logConsole(html);\n }\n }\n\n function ansiToHtml(text) {\n var out = \"\";\n var i = 0;\n var ESC = \"\\x1b\";\n var cur = { fg: null, bg: null, bold: false, italic: false, underline: false };\n var open = false;\n\n function currentStyle() {\n var parts = [];\n if (cur.bold) { parts.push(\"font-weight:bold\"); }\n if (cur.italic) { parts.push(\"font-style:italic\"); }\n if (cur.underline) { parts.push(\"text-decoration:underline\"); }\n if (cur.fg !== null) { parts.push(\"color:\" + cur.fg); }\n if (cur.bg !== null) { parts.push(\"background-color:\" + cur.bg); }\n return parts.join(\";\");\n }\n function closeSpan() {\n if (open) { out += \"</span>\"; open = false; }\n }\n function pushSpan() {\n closeSpan();\n var s = currentStyle();\n if (s) { out += '<span style=\"' + s + '\">'; open = true; }\n }\n\n var html = text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n var n = html.length;\n while (i < n) {\n var idx = html.indexOf(ESC, i);\n if (idx === -1) { out += html.slice(i); break; }\n out += html.slice(i, idx);\n var j = idx + 1;\n if (j >= n) { i = idx + 1; continue; }\n var ch = html.charAt(j);\n if (ch === \"[\") {\n var k = j + 1;\n while (k < n) {\n var cc = html.charCodeAt(k);\n if (cc >= 0x40 && cc <= 0x7e) { break; }\n k++;\n }\n if (k >= n) { i = n; break; }\n var final = html.charAt(k);\n var params = html.slice(j + 1, k);\n if (final === \"m\") {\n var parts = params.split(\";\");\n var codes = [];\n for (var p = 0; p < parts.length; p++) {\n codes.push(parts[p] === \"\" ? 0 : parseInt(parts[p], 10));\n }\n var changed = false;\n for (var c = 0; c < codes.length; c++) {\n var code = codes[c];\n if (code === 0) {\n cur = { fg: null, bg: null, bold: false, italic: false, underline: false };\n changed = true;\n } else if (code === 1) { cur.bold = true; changed = true; }\n else if (code === 22) { cur.bold = false; changed = true; }\n else if (code === 3) { cur.italic = true; changed = true; }\n else if (code === 23) { cur.italic = false; changed = true; }\n else if (code === 4) { cur.underline = true; changed = true; }\n else if (code === 24) { cur.underline = false; changed = true; }\n else if (code >= 30 && code <= 37) { cur.fg = PALETTE[code - 30]; changed = true; }\n else if (code >= 90 && code <= 97) { cur.fg = BRIGHT[code - 90]; changed = true; }\n else if (code >= 40 && code <= 47) { cur.bg = PALETTE[code - 40]; changed = true; }\n else if (code >= 100 && code <= 107) { cur.bg = BRIGHT[code - 100]; changed = true; }\n else if (code === 39) { cur.fg = null; changed = true; }\n else if (code === 49) { cur.bg = null; changed = true; }\n else if (code === 38 || code === 48) {\n if (c + 2 < codes.length && codes[c + 1] === 5) {\n var val = codes[c + 2];\n var col;\n if (val < 8) { col = PALETTE[val]; }\n else if (val < 16) { col = BRIGHT[val - 8]; }\n else if (val < 232) {\n var cube = val - 16;\n var levels = [0, 95, 135, 175, 215, 255];\n col = \"rgb(\" + levels[Math.floor(cube / 36)] + \",\"\n + levels[Math.floor((cube % 36) / 6)] + \",\"\n + levels[cube % 6] + \")\";\n } else {\n var g = 8 + 10 * (val - 232);\n col = \"rgb(\" + g + \",\" + g + \",\" + g + \")\";\n }\n if (code === 38) { cur.fg = col; } else { cur.bg = col; }\n c += 2;\n changed = true;\n } else if (c + 4 < codes.length && codes[c + 1] === 2) {\n var rgb = \"rgb(\" + codes[c + 2] + \",\" + codes[c + 3] + \",\"\n + codes[c + 4] + \")\";\n if (code === 38) { cur.fg = rgb; } else { cur.bg = rgb; }\n c += 4;\n changed = true;\n }\n }\n }\n if (changed) { pushSpan(); }\n }\n i = k + 1;\n continue;\n }\n if (ch === \"]\") {\n var bel = html.indexOf(String.fromCharCode(7), j);\n var st = html.indexOf(ESC + String.fromCharCode(92), j);\n var end = -1;\n if (bel !== -1) { end = bel + 1; }\n if (st !== -1 && (end === -1 || st + 2 < end)) { end = st + 2; }\n if (end === -1) { i = n; break; }\n i = end;\n continue;\n }\n i = idx + 1;\n }\n closeSpan();\n return out;\n }\n\n /* Events ------------------------------------------------------------ */\n function handle(ev) {\n switch (ev.type) {\n case \"run_started\":\n flushPending();\n stepTimes = {};\n liveElapsed = {};\n runActive = true;\n runFailed = false;\n fatalNodes.clear();\n if (totalTimer !== null) { clearInterval(totalTimer); totalTimer = null; }\n runStartWall = (typeof ev.started_at === \"number\") ? ev.started_at : null;\n totalTimer = setInterval(renderTotal, 1000);\n renderTotal();\n clearConsoles();\n resetVerdicts();\n if (ev.attempt !== undefined) { attempt = ev.attempt; }\n maxAttempts = ev.max_attempts;\n var subject = el(\"subject\");\n if (subject) { subject.textContent = ev.subject; }\n if (ev.thread_id !== undefined) {\n var tid = el(\"thread-id\");\n if (tid) { tid.textContent = ev.thread_id; }\n }\n logLine(\"run started: \" + ev.subject + \" (thread \" + ev.thread_id + \")\",\n \"started\");\n updateAttempt();\n break;\n case \"node_started\":\n flushPending();\n if (!nodeIds.has(ev.node)) { break; }\n nodeStatus[ev.node] = \"running\";\n stageWord[ev.node] = \"running\";\n liveElapsed = {};\n liveElapsed[ev.node] = 0;\n renderGraph();\n break;\n case \"node_finished\":\n if (!nodeIds.has(ev.node)) { break; }\n if (ev.elapsed_seconds !== undefined) {\n stepTimes[ev.node] = (stepTimes[ev.node] || 0) + ev.elapsed_seconds;\n }\n if (!fatalNodes.has(ev.node)) {\n nodeStatus[ev.node] = ev.ok ? \"pass\" : \"fail\";\n }\n pendingCompletion = { node: ev.node, word: ev.ok ? \"completed\" : \"errored\" };\n setStageStatus(ev.node, ev.ok ? \"completed\" : \"errored\");\n renderGraph();\n break;\n case \"heartbeat\":\n if (nodeIds.has(ev.node) && nodeStatus[ev.node] === \"running\") {\n liveElapsed[ev.node] = ev.elapsed_seconds;\n renderStageHeader(ev.node);\n renderGraph();\n }\n break;\n case \"state\":\n if (ev.attempt !== undefined) { attempt = ev.attempt; }\n if (ev.step_verdicts !== undefined) {\n Object.keys(ev.step_verdicts).forEach(function (k) {\n verdictSteps.add(k);\n var v = ev.step_verdicts[k];\n if (v === \"pass\") { verdictPass.add(k); verdictFail.delete(k); verdictUnverifiable.delete(k); verdictEnv.delete(k); }\n else if (v === \"fail\") { verdictFail.add(k); verdictPass.delete(k); verdictUnverifiable.delete(k); verdictEnv.delete(k); }\n else if (v === \"unverifiable\") { verdictUnverifiable.add(k); verdictPass.delete(k); verdictFail.delete(k); verdictEnv.delete(k); }\n else if (v === \"env\") { verdictEnv.add(k); verdictPass.delete(k); verdictFail.delete(k); verdictUnverifiable.delete(k); }\n else { verdictPass.delete(k); verdictFail.delete(k); verdictUnverifiable.delete(k); verdictEnv.delete(k); }\n currentVerdicts[k] = v;\n });\n updateBadges();\n if (pendingCompletion !== null\n && (pendingCompletion.node in ev.step_verdicts)) {\n flushPending();\n }\n renderGraph();\n }\n if (ev.failed) {\n Object.keys(nodeStatus).forEach(function (n) {\n if (nodeStatus[n] === \"running\") { fatalNodes.add(n); }\n });\n logLine(\"state: failed\", \"failed\");\n renderGraph();\n }\n updateAttempt();\n break;\n case \"run_finished\":\n runActive = false;\n if (totalTimer !== null) { clearInterval(totalTimer); totalTimer = null; }\n var total = el(\"total\");\n if (ev.elapsed_seconds !== undefined) {\n runStartWall = null;\n if (total) {\n total.innerHTML = \"\";\n total.textContent = \"total \" + mmss(ev.elapsed_seconds);\n total.style.display = \"block\";\n var finalNote = document.createElement(\"span\");\n finalNote.className = \"total-note\";\n finalNote.textContent = \"FINAL\";\n total.appendChild(finalNote);\n }\n }\n flushPending();\n if (!ev.success) {\n runFailed = true;\n Object.keys(nodeStatus).forEach(function (n) {\n var s = statusOf(n);\n if (s === \"fail\" || s === \"running\") { nodeStatus[n] = \"fatal\"; }\n });\n renderGraph();\n }\n renderMeter();\n logLine(\"run finished: \" + (ev.success ? \"success\" : \"failure\"),\n ev.success ? \"success\" : \"failed\");\n var banner = el(\"run-banner\");\n if (banner) {\n banner.innerHTML = \"\";\n var title = document.createElement(\"div\");\n title.className = \"outcome-title\";\n title.textContent = ev.success ? \"SUCCESS\" : \"FAILURE\";\n var sub = document.createElement(\"div\");\n sub.className = \"outcome-sub\";\n sub.textContent = \"Run finished - \" + (el(\"subject\")\n ? el(\"subject\").textContent : \"\") + \" - attempt \"\n + (attempt === null ? \"?\" : attempt) + \"/\"\n + (maxAttempts === null ? \"?\" : maxAttempts)\n + \" - total \" + (ev.elapsed_seconds !== undefined\n ? mmss(ev.elapsed_seconds) : \"-\");\n banner.appendChild(title);\n banner.appendChild(sub);\n banner.className = \"outcome \"\n + (ev.success ? \"outcome-success\" : \"outcome-failure\");\n banner.style.display = \"block\";\n }\n break;\n case \"console\":\n handleConsole(ev);\n break;\n }\n }\n\n function loadTopology() {\n fetch(\"/topology\").then(function (r) { return r.json(); }).then(function (d) {\n var subject = el(\"subject\");\n if (subject) { subject.textContent = d.subject; }\n var provider = el(\"provider\");\n if (provider && d.provider) { provider.textContent = d.provider; }\n topology = {\n nodes: d.nodes || [],\n flow: d.flow || [],\n edges: d.edges || []\n };\n topology.nodes.forEach(function (n) {\n nodeIds.add(n);\n ensureSection(n);\n ensureStation(n);\n });\n topologyReady = true;\n renderGraph();\n pendingEvents.forEach(handle);\n pendingEvents = [];\n }).catch(function () {\n topologyFailed = true;\n topologyReady = true;\n logLine(\"failed to load topology\");\n renderGraph();\n pendingEvents.forEach(handle);\n pendingEvents = [];\n });\n }\n\n /* Stream ------------------------------------------------------------ */\n var es = null;\n\n function setStreamLive() {\n var stream = el(\"stream\");\n if (stream) { stream.className = \"stream\"; }\n var label = el(\"stream-label\");\n if (label) { label.textContent = \"STREAM LIVE\"; }\n }\n\n function setStreamReconnecting() {\n var stream = el(\"stream\");\n if (stream) { stream.className = \"stream reconnecting\"; }\n var label = el(\"stream-label\");\n if (label) { label.textContent = \"RECONNECTING\"; }\n }\n\n function connectStream() {\n var stream = new EventSource(\"/events\");\n stream.onmessage = function (e) {\n lastEventAt = Date.now();\n reconnectAttempts = 0;\n var banner = el(\"stream-banner\");\n if (banner) { banner.style.display = \"none\"; }\n setStreamLive();\n var ev;\n try { ev = JSON.parse(e.data); }\n catch (err) { logLine(\"bad event: \" + e.data); return; }\n if (!topologyReady) { pendingEvents.push(ev); return; }\n try { handle(ev); }\n catch (err) { logLine(\"bad event: \" + e.data); }\n };\n stream.onerror = function () { logLine(\"event stream interrupted\", \"interrupted\"); };\n return stream;\n }\n\n function recover() {\n var banner = el(\"stream-banner\");\n reconnectAttempts++;\n setStreamReconnecting();\n if (banner) {\n banner.textContent = \"Connection lost, reconnecting... attempt \"\n + reconnectAttempts + \"/3 (no event for 10 s)\";\n banner.style.display = \"block\";\n }\n if (reconnectAttempts > 3) {\n location.reload();\n return;\n }\n if (es) { es.close(); }\n es = connectStream();\n lastEventAt = Date.now();\n }\n\n loadTopology();\n\n setTimeout(function () {\n if (!topologyReady) {\n topologyReady = true;\n logLine(\"topology load timed out - event log only\");\n pendingEvents.forEach(handle);\n pendingEvents = [];\n }\n }, 5000);\n\n es = connectStream();\n\n document.addEventListener(\"visibilitychange\", function () {\n if (runActive && Date.now() - lastEventAt > 10000) { recover(); }\n });\n\n (function initThreadCopy() {\n var tid = el(\"thread-id\");\n if (!tid) { return; }\n\n function selectFallback() {\n // Explicit degradation: if the clipboard API is unavailable, select\n // the text so the user can copy it manually.\n try {\n var range = document.createRange();\n range.selectNodeContents(tid);\n var sel = window.getSelection();\n sel.removeAllRanges();\n sel.addRange(range);\n } catch (err) { /* selection is best-effort */ }\n }\n\n function showFeedback(value) {\n tid.textContent = \"Copied\";\n tid.classList.add(\"copied\");\n setTimeout(function () {\n tid.classList.remove(\"copied\");\n // Only restore when the feedback is still showing; a newer thread\n // id from run_started must not be clobbered.\n if (tid.textContent === \"Copied\") { tid.textContent = value; }\n }, 1200);\n }\n\n function copyThreadId() {\n var value = tid.textContent;\n if (!value || value === \"--\" || value === \"Copied\") { return; }\n if (!navigator.clipboard || !navigator.clipboard.writeText) {\n selectFallback();\n return;\n }\n navigator.clipboard.writeText(value).then(\n function () { showFeedback(value); }, selectFallback);\n }\n\n tid.addEventListener(\"click\", copyThreadId);\n tid.addEventListener(\"keydown\", function (ev) {\n if (ev.key === \"Enter\" || ev.key === \" \") {\n ev.preventDefault();\n copyThreadId();\n }\n });\n })();\n\n (function initRelayout() {\n if (typeof ResizeObserver !== \"undefined\") {\n var target = el(\"pipeline-panel\") || document.body;\n if (target) {\n new ResizeObserver(function () { renderGraph(); }).observe(target);\n }\n }\n window.addEventListener(\"resize\", function () { renderGraph(); });\n })();\n})();\n</script>\n</body>\n</html>\n"
@@ -367,12 +367,14 @@ class HeartbeatThread(threading.Thread):
367
367
  def _handler_class(bus: VizBus, topology_mermaid: str,
368
368
  subject: str, nodes: Iterable[str],
369
369
  flow: Iterable[dict] | None = None,
370
- edges: Iterable[dict] | None = None) -> type[BaseHTTPRequestHandler]:
370
+ edges: Iterable[dict] | None = None,
371
+ provider: str | None = None) -> type[BaseHTTPRequestHandler]:
371
372
  class Handler(BaseHTTPRequestHandler):
372
373
  protocol_version = "HTTP/1.1"
373
374
  viz_bus = bus
374
375
  viz_topology = topology_mermaid
375
376
  viz_subject = subject
377
+ viz_provider = provider
376
378
  viz_nodes = list(nodes)
377
379
  viz_flow = list(flow or ())
378
380
  viz_edges = list(edges or ())
@@ -392,6 +394,7 @@ def _handler_class(bus: VizBus, topology_mermaid: str,
392
394
  elif self.path == "/topology":
393
395
  body = json.dumps({"mermaid": self.viz_topology,
394
396
  "subject": self.viz_subject,
397
+ "provider": self.viz_provider,
395
398
  "nodes": self.viz_nodes,
396
399
  "flow": self.viz_flow,
397
400
  "edges": self.viz_edges})
@@ -445,7 +448,8 @@ class _ReuseSafeServer(ThreadingHTTPServer):
445
448
  def serve_viz(bus: VizBus, topology_mermaid: str, subject: str,
446
449
  port: int, nodes: Iterable[str] = (),
447
450
  flow: Iterable[dict] | None = None,
448
- edges: Iterable[dict] | None = None) -> tuple[_ReuseSafeServer, threading.Thread]:
451
+ edges: Iterable[dict] | None = None,
452
+ provider: str | None = None) -> tuple[_ReuseSafeServer, threading.Thread]:
449
453
  """Bind a loopback HTTP server on ``port`` and serve it on a daemon
450
454
  thread. Returns ``(httpd, thread)`` - the caller reads the real bound
451
455
  port from ``httpd.server_address[1]``.
@@ -455,7 +459,7 @@ def serve_viz(bus: VizBus, topology_mermaid: str, subject: str,
455
459
  free port. Any other bind error (e.g. ``EACCES`` on a privileged port)
456
460
  still fails loud, naming ``--viz-port``."""
457
461
  handler = _handler_class(bus, topology_mermaid, subject, nodes,
458
- flow, edges)
462
+ flow, edges, provider)
459
463
  try:
460
464
  httpd = _ReuseSafeServer(("127.0.0.1", port), handler)
461
465
  except OSError as exc: