app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
Publish 1.0.76
3 files changed
+162−27
dashboard/kicad_dashboard.py+160−25@@ -35,7 +35,6 @@ PROXY NOTE (paid for live): adom.cloud STRIPS query strings on sub-paths, so the target rides the PATH (/api/status/<target>/force), never ?target=. The SSE route also needs the padding + no-transform headers or the proxy buffers every byte. """-import collections import json import os import queue@@ -136,7 +135,7 @@ def say_send(phrase): button opened a tab and delivered nothing. Still NEW threads only: resume and typing-into-existing stay parked.- Every failure path returns ok:False with a readable `why`; the page then+ Every failure returns ok:False with a readable `why`; the page then error-toasts AND copies the phrase, so a user is never stranded.""" import urllib.request @@ -157,8 +156,8 @@ def say_send(phrase): cur = (d.get("updater") or d).get("current_version") or d.get("current_version") if cur and _ver_tuple(cur) < MIN_SHELL_VERSION: return {"ok": False, "shellTooOld": True, "currentVersion": str(cur),- "why": f"your Hydrogen ({cur}) predates the Send to AI fix; "- f"update to 1.0.142 or newer to enable it. The phrase was copied instead."}+ "why": f"your Hydrogen ({cur}) predates the Send to AI fix; update to "+ f"1.0.142 or newer to enable it. The phrase was copied instead."} except Exception: pass # cannot read the updater: fall through rather than block a healthy shell @@ -184,30 +183,169 @@ def say_send(phrase): hint = d.get("_hints") or d.get("_hint") or res.get("_hints") or "" if isinstance(hint, dict): hint = " ".join(str(v) for v in hint.values())- return {"ok": False, "why": ("the AI panel opened a thread but did not confirm delivery. "- + (str(hint)[:200] if hint else "")).strip()}+ return {"ok": False, "why": ("the AI panel opened a thread but did not confirm "+ "delivery. " + (str(hint)[:200] if hint else "")).strip()} return {"ok": True, "deliveredTo": str(delivered), "sid": d.get("sid")} -# Console ring + logger. RESTORED 2026-08-24: deleting the dormant-# stage_phrase() body took these with it, because they sat between it and-# build_status(). The grep-the-installed-artifact checks all passed while the-# app could not even boot - a static check proves a string is present, only-# RUNNING it proves the program works.-CONSOLE = collections.deque(maxlen=400)+def ab(target, verb, args=None, timeout=140):+ """One bridge verb. Returns the parsed payload dict (never raises)."""+ cmd = ["adom-bridge", "--ai-thread", AI_THREAD]+ if target and target != "-":+ cmd += ["--target", target]+ cmd += [verb, json.dumps(args or {})]+ try:+ p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)+ d = json.loads(p.stdout or "{}")+ # ab wraps some payloads in output-as-JSON-string; unwrap when it is one+ out = d.get("output")+ if isinstance(out, str) and out.strip().startswith("{"):+ try:+ inner = json.loads(out)+ merged = dict(d)+ merged.update(inner.get("data") if isinstance(inner.get("data"), dict) else inner)+ return merged+ except Exception:+ pass+ return d+ except subprocess.TimeoutExpired:+ return {"error": f"no answer within {timeout}s", "timedOut": True}+ except Exception as e:+ # never leak a raw Python exception (a TimeoutExpired repr used to land+ # verbatim in the Bridge LED as "Command '['adom-bridge', ...]'")+ return {"error": str(e).split("\n")[0][:120]}+++def get_targets():+ try:+ p = subprocess.run(["adom-bridge", "targets"], capture_output=True, text=True, timeout=30)+ d = json.loads(p.stdout or "{}")+ return [t.get("name") for t in (d.get("targets") or []) if t.get("name")]+ except Exception:+ return []+++# ------------------------------------------------------------------- SSE hub+class Hub:+ def __init__(self):+ self._clients = []+ self._lock = threading.Lock()++ def add(self):+ q = queue.Queue(maxsize=64)+ with self._lock:+ self._clients.append(q)+ return q++ def drop(self, q):+ with self._lock:+ if q in self._clients:+ self._clients.remove(q)++ def count(self):+ with self._lock:+ return len(self._clients)++ def broadcast(self, event, data):+ msg = f"event: {event}\ndata: {json.dumps(data)}\n\n"+ with self._lock:+ for q in list(self._clients):+ try:+ q.put_nowait(msg)+ except queue.Full:+ pass+++HUB = Hub()+CONSOLE = [] _console_lock = threading.Lock() def log(line):- """One line to stdout and to the /console ring the dashboard exposes."""- line = f"[{time.strftime('%H:%M:%S')}] {line}"+ with _console_lock:+ CONSOLE.append(f"{time.strftime('%H:%M:%S')} {line}")+ del CONSOLE[:-300]+ print(line, file=sys.stderr, flush=True)+++# ------------------------------------------------------------------ LED model+DEV_MODE = bool(os.environ.get("KICAD_DASHBOARD_DEV"))+++def _suite_led():+ """DEV-ONLY (John, 2026-08-18: "the daily suite should not be in here. no+ normal adom user runs that. that's just for me as the main dev"). Shown only+ with --dev / KICAD_DASHBOARD_DEV=1: the nightly verb-suite verdict plus+ container-cron liveness (the cron died unnoticed for 4 days once).""" try:- with _console_lock:- CONSOLE.append(line)+ try:+ hb = time.time() - os.path.getmtime("/tmp/cron-heartbeat")+ if hb > 600:+ return {"label": "Daily suite", "state": "err",+ "detail": f"container cron DEAD ({int(hb // 60)} min since heartbeat)",+ "_hint": "The container's cron stopped, so the 6:30am verb suite will not run. "+ "Restart cron in the container and re-check."}+ except OSError:+ pass+ rp = "/home/adom/.kicad-verbtest-last-report.json"+ if not os.path.exists(rp):+ rp = "/tmp/kicad-verbtest/report.json"+ age_h = (time.time() - os.path.getmtime(rp)) / 3600+ t = (json.load(open(rp)).get("totals")) or {}+ det = f"{t.get('pass', '?')} pass / {t.get('fail', '?')} fail ({int(age_h)}h ago)"+ if age_h > 48:+ return {"label": "Daily suite", "state": "warn", "detail": f"stale: {det}",+ "_hint": "No suite run in over 48h. Check the cron entry and /tmp/kicad-verbtest/cron.log."}+ if t.get("fail"):+ return {"label": "Daily suite", "state": "warn", "detail": det,+ "_hint": "Failures are triaged on the wiki issues; the suite files a ticket only for NEW ones."}+ return {"label": "Daily suite", "state": "ok", "detail": det,+ "_hint": "Every verb passed on the last nightly run."} except Exception:- pass- print(line, flush=True)-+ return {"label": "Daily suite", "state": "off", "detail": "no suite report yet",+ "_hint": "The nightly suite writes ~/.kicad-verbtest-last-report.json; "+ "it runs at 6:30am when the box is idle."}++++# ── The dock card's ROLLUP (wiki #43, John) ────────────────────────────────+# Without a top-level `state` the dock takes our WORST led, so a yellow+# "update available" turns the whole card yellow while the bridge answers+# perfectly. That is not what a user should read off a card. We own the+# rollup, so we own the judgement about which facets are load-bearing.+#+# I initially declined this, reasoning that a disagreement between our rollup+# and the dock's derivation would be a signal worth seeing. John's argument is+# better: the card is a USER-facing health light, not a debugging surface, and+# an advisory LED must not be able to raise an alarm.+_CRITICAL_LEDS = ("relay", "box", "bridge", "kicad") # can this thing work at all+_ADVISORY_LEDS = ("update", "focus", "presence", "plugin") # informational, never alarming+++def rollup_state(leds: dict) -> dict:+ """Green when the bridge can actually do its job; yellow only when a+ CRITICAL facet is degraded; red when one is broken."""+ def st(k):+ return (leds.get(k) or {}).get("state")+ crit = [st(k) for k in _CRITICAL_LEDS if st(k)]+ if "err" in crit:+ worst, why = "err", [k for k in _CRITICAL_LEDS if st(k) == "err"]+ elif "warn" in crit:+ worst, why = "warn", [k for k in _CRITICAL_LEDS if st(k) == "warn"]+ else:+ worst, why = "ok", []+ degraded = [k for k in _ADVISORY_LEDS if st(k) in ("warn", "err")]+ return {+ "state": worst,+ "reason": (", ".join(why) if why else+ ("healthy" + (f" (advisory: {', '.join(degraded)})" if degraded else ""))),+ "criticalLeds": list(_CRITICAL_LEDS),+ "advisoryLeds": list(_ADVISORY_LEDS),+ "_hint": ("Top-level rollup for the dock card LED. Advisory facets (an available "+ "update, focus bounces, user presence, a plugin that only degrades "+ "background navigation) never turn the card yellow, because the bridge "+ "still works. Only relay/box/bridge/kicad can."),+ } def build_status(target): """The LED payload. Each facet: {label, state: ok|warn|err|off, detail, _hint}."""@@ -884,12 +1022,9 @@ def do_action(target, name, args): if name == "say": return {"success": True, "phrases": say_phrases()} if name == "try_phrase":- # RETIRED 2026-08-24. This was the staged-prompt path, parked on ship- # night and then superseded by Send to AI (say_send, one call, new- # threads only). Its implementation is DELETED rather than dormant:- # it contained an /ai-threads/resume call, and resume stays parked by- # ruling, so leaving it alive-but-flagged put a forbidden call one- # flag flip from running. Nothing calls this; answer honestly.+ # RETIRED 2026-08-24: superseded by Send to AI (say_send: one call, new+ # threads only). Body DELETED rather than dormant because it contained+ # an /ai-threads/resume call, and resume stays parked by ruling. return {"success": False, "retired": True, "error": "try_phrase is retired. Use the dashboard's Send to AI " "(POST /api/say-send), which opens a NEW thread and submits.",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "kicad-bridge",- "version": "1.0.75",+ "version": "1.0.76", "type": "app", "description": "Skills for your container so your AI knows how to drive the KiCad bridge. The bridge runtime itself is the release zip; Adom Bridge loads that.", "tags": [
page.json+1−1@@ -4,7 +4,7 @@ "slug": "kicad-bridge", "title": "KiCad - the KiCad Bridge", "brief": "Reference implementation of the KiCad bridge — multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.",- "version": "1.0.75",+ "version": "1.0.76", "tags": [ "kicad", "pcb",