master
Download ZIP Propose a change
Name Last commit message Last updated
docs Publish 1.1.4 · Kyle Bergstedt 3mo ago
skills Publish 1.1.4 · Kyle Bergstedt 3mo ago
._arch.png Publish 1.1.4 · Kyle Bergstedt 3mo ago
._bridge-dev-template.md Publish 1.1.4 · Kyle Bergstedt 3mo ago
._bridge-publish-template.md Publish 1.1.4 · Kyle Bergstedt 3mo ago
._bridge-templates-README.md Publish 1.1.4 · Kyle Bergstedt 3mo ago
._bridge-user-skill-template.md Publish 1.1.4 · Kyle Bergstedt 3mo ago
._bridges-hero.png Publish 1.1.4 · Kyle Bergstedt 3mo ago
._bridges-SDK.md Publish 1.1.4 · Kyle Bergstedt 3mo ago
._hero-billboard-v2.png Publish 1.1.4 · Kyle Bergstedt 3mo ago
._hero-billboard.png Publish 1.1.4 · Kyle Bergstedt 3mo ago
._hero-v2.png Publish 1.1.4 · Kyle Bergstedt 3mo ago
._hero.png Publish 1.1.4 · Kyle Bergstedt 3mo ago
._install.sh Publish 1.1.4 · Kyle Bergstedt 3mo ago
._outcomes.png Publish 1.1.4 · Kyle Bergstedt 3mo ago
._package.json Publish 1.1.4 · Kyle Bergstedt 3mo ago
._page.json Publish 1.1.4 · Kyle Bergstedt 3mo ago
._README.md Publish 1.1.4 · Kyle Bergstedt 3mo ago
._SKILL.md Publish 1.1.4 · Kyle Bergstedt 3mo ago
._uninstall.sh Publish 1.1.4 · Kyle Bergstedt 3mo ago
._verbs.png Publish 1.1.4 · Kyle Bergstedt 3mo ago
arch.png Upload 1 file(s) · John Lauer 5mo ago
bridge-dev-template.md rename: adom hydrogen + hydrogen-* naming · Kyle Bergstedt 3mo ago
bridge-publish-template.md Templates are now fetchable from this page (the old pointer went to AD's private repo); new human sub-readme on skill p… · John Lauer 3mo ago
bridge-templates-README.md Templates are now fetchable from this page (the old pointer went to AD's private repo); new human sub-readme on skill p… · John Lauer 3mo ago
bridge-user-skill-template.md Templates are now fetchable from this page (the old pointer went to AD's private repo); new human sub-readme on skill p… · John Lauer 3mo ago
bridges-hero.png Migrate asset from Helium wiki · Kyle Bergstedt 5mo ago
bridges-SDK.md SDK: line of demarcation (ab is an OS, your bridge is an app), the bridge keyword, and a PITFALLS section of eight real… · John Lauer 2mo ago
hero-billboard-v2.png new billboard hero (single full-bleed app screenshot) · John Lauer 4mo ago
hero-billboard.png new billboard hero (single full-bleed app screenshot) · John Lauer 4mo ago
hero-v2.png Upload 1 file(s) · John Lauer 4mo ago
hero.png Upload 1 file(s) · John Lauer 5mo ago
install.sh Add adompkg installer files · Kyle Bergstedt 5mo ago
outcomes.png Upload 1 file(s) · John Lauer 5mo ago
package.json Publish 1.1.4 · Kyle Bergstedt 3mo ago
page.json Update page: DEPRECATED: Bridge SDK (moved into Adom Bridge) · John Lauer 3mo ago
README.md SDK: line of demarcation (ab is an OS, your bridge is an app), the bridge keyword, and a PITFALLS section of eight real… · John Lauer 2mo ago
SKILL.md SDK: line of demarcation (ab is an OS, your bridge is an app), the bridge keyword, and a PITFALLS section of eight real… · John Lauer 2mo ago
uninstall.sh Add adompkg installer files · Kyle Bergstedt 5mo ago
verbs.png Upload 1 file(s) · John Lauer 5mo ago

name: adom-bridge-sdk description: >- START HERE to build a bridge for Adom Bridge - a small local server that adds a namespace of verbs (kicad_, fusion_, pup_, your_prefix_) to Adom Bridge. The orientation skill: the ONE principle (rich _hint/_next in every verb's OUTPUT, because the AI reads CLI output and almost never your on-disk SKILL.md), the three artifacts you publish, where every file lives, the ab-core-vs-author boundary, and a map to the rest of the SDK skill library. Read this first, then load the specific skill for the phase you are in.

Bridge SDK: build, publish, and surface a bridge for Adom Bridge

A bridge is a small local program (Python, Node, Rust, anything) that ab spawns and routes a namespace of verbs to over HTTP. KiCad, Fusion 360, Puppeteer (pup), native-browser, Blender are bridges. The modern flow has THREE moving parts you own. Get all three right and a cloud/WSL2 container can discover your bridge from a plain-English request, confirm it's installed, and drive it with zero tribal knowledge:

  1. Runtime: a versioned Release .zip + a manifest on your wiki page. ab's bridge_install streams it onto the user's machine and spawns it.
  2. Skills: an adom-wiki pkg tarball carrying your consumer SKILL.md(s). A container installs it into ~/.claude/skills/ (or ~/.codex/skills/) so the AI there knows your verbs.
  3. Discoverability: discovery_triggers on your wiki page so a user saying "open my app in pup" surfaces YOU via adom-wiki discover find.

THE ONE PRINCIPLE THAT MATTERS MOST: read this before anything else. The AI almost never reads your on-disk SKILL.md. It ALWAYS reads the OUTPUT of a CLI call. So your leverage is in the _hint / _next / related / pitfalls you return on EVERY verb response. A verb that returns {success:true} and nothing else leaves the AI guessing and retry-looping. A verb that returns what just happened, the obvious next verb, the related verbs, and the pitfall to avoid gets driven perfectly on the first try. ab relays every field of your response verbatim to the calling AI. Treat each verb's output as the documentation the AI will actually read. (Your SKILL.md is for the human author + the rare AI that does read it; your verb OUTPUT is for the AI in the loop.)


⛔ STOP — the LINE OF DEMARCATION: ab is an operating system, your bridge is an app

If you remember one thing from this SDK: a bridge NEVER modifies ab. Every line of logic specific to your app lives in YOUR bridge. The correct amount of ab-core change for a new bridge or a bridge feature is ZERO. This is not style — it is the architecture, and violating it breaks the thing that makes bridges work.

ab is an operating system for the desktop. Like a kernel it does two jobs: it provides generic, app-agnostic primitives (windows, screenshots, input, files, shell, WSL, taskbar/window identity, jump lists, notifications, tunnels) that know nothing about any specific app — desktop_set_window_identity does not know what a browser is, it applies whatever icon/name/AUMID you hand it — and it loads, unloads, reloads, and updates bridges on the fly (bridge_install, the wiki auto-sync, hot respawn). ab stays deliberately stable and generic so it almost never has to change for a new bridge.

Your bridge is an app running on that OS — a fluid, hot-loadable asset with its own process, its own skills, its own wiki page, its own release cadence entirely independent of ab. It owns 100% of the logic specific to its host app or tool, and when it needs the desktop to do something it calls one of ab's existing generic verbs over the loopback direct API (see adom-bridge-sdk-callback). The payoff: your bridge ships and updates from your own page with no ab release. The moment your code lives in ab you have chained your feature to ab's release train and made the kernel carry your app's weight — the opposite of what a dynamic, loadable bridge is for.

The three AI failure modes (all seen live, 2026-08-28)

A capable coding AI slides into all three. Guard against them explicitly:

  1. Reproducing capability ab already ships. Before writing ANY desktop capability — and absolutely before proposing an ab change — search ab's existing verbs (/verbs, the adom-bridge-verbs skill, or just ask the ab thread). Real case: a bridge shipped a 2,668-line ab-core module re-implementing window identity, taskbar overlay, and jump lists that ab already provided generically and another bridge was already using. The check is one grep. Do it.
  2. Baking your app's name into ab. The same case hardcoded a forced Adom.<App>. AUMID namespace, app-named jump-list labels, and an app-named router verb into ab-core. If ab would end up knowing your app's name, the line has been crossed. Your app-specifics stay in your bridge, which passes its own appId / icon / labels into ab's generic verbs. If you catch your AI editing src-tauri/, stop.
  3. Pulling app-AWARE correctness logic UP into the host. The subtlest one. Strict window binding — "bind this operation to an exact HWND + owner PID + process-start so I never act on the wrong window" — feels like it belongs in the kernel because it is about windows. It does not: doing it requires knowing how your app creates its windows (process/window/creation-time relationships, correlating a spawned browser to a session). That is narrow app knowledge and ab must never have it. Do the identify-and-verify work in YOUR bridge, then hand the resolved HWND + your identity to ab's dumb, generic paint verbs. The bridge decides which window and proves it; ab never learns why.

The rare, legitimate exception — and the process

Sometimes a bridge genuinely needs a generic ab primitive that does not exist yet. That is real and welcome. What is gated is the CONTENT, not the channel:

  1. Confirm it doesn't already exist. Search the verb surface first. Most "I need a new verb" is "there's already a generic one."
  2. Apply the two-part test: could a completely unrelated app use this verb unchanged? AND can ab implement it knowing nothing app-specific? If either answer is no, it belongs in your bridge — no channel makes it acceptable. When an existing generic verb is close but not strict enough for you, ask for a generic hardening of that verb; never fork an app-specific copy of it, and never ask ab to absorb app-aware logic (failure mode 3).
  3. Then pick the channel by who you are:
    • Adom team — just send a PR. It is the efficient path and it is welcome. The PR must still pass the two-part test and touch only generic surface: a reviewer's first question is "does ab now know anything about your app?"
    • Third-party author — file a discussion on the adom-bridge page proposing the verb as a generic contract, and the ab thread adds it (or invites a PR once the contract is agreed).

Being on the team does not widen the line — it only shortens the path to proposing something that was already generic. A team PR that adds an app-named namespace, app-named labels, or app-aware binding logic to ab-core gets the same answer as anyone else's: build it in the bridge.

Your bridge's KEYWORD — the short word a user says

Declare ONE short, unambiguous keyword and use it everywhere. A user says "open my app in pup" and the AI must resolve that single word, through ab's layers, to your skills and verbs. That only works if the word is declared and consistent. Requirements:

  • Short and speakable (pup, kicad, pw) — this is what a human types in chat, not your product name.
  • Unambiguous — avoid a word that collides with a common term or another bridge's niche.
  • Consistent everywhere: your bridge.json name, your verb prefix (<keyword>_…), your discovery_triggers phrasings, your README's opening line, and your card. If the keyword is pw, the verbs are pw_* and the README says so in the first paragraph.
  • State it explicitly in your README's first section: "Say pw to the AI to reach this bridge." A keyword agreed in conversation but written nowhere does not exist.

⚠ PITFALLS — real failures from real bridges, and how to avoid each

Every entry here happened. They are listed in the order they tend to bite, and each is written as symptom → why it happens → fix, because the ones that hurt are the ones that look fine while you are making them.

1. Building your feature into ab instead of into your bridge

Symptom: you have a PR against adom-bridge for something your bridge needs. Why: the capability sounds like a desktop primitive ("I need the taskbar to show my app"), so extending the host feels natural — and if you never enumerated ab's existing verbs, it also feels necessary. Fix: the capability is almost certainly already generic in ab (window identity, taskbar overlay, jump lists, screenshots, input, files, notifications). Call it. See the LINE OF DEMARCATION above. Measured cost of getting this wrong: a 2,668-line ab-core module duplicating verbs another bridge was already using.

2. Your app's name ending up in ab

Symptom: ab-core contains your app's name — a forced Adom.<YourApp>. namespace, app-named labels, an app-named router verb. Why: you needed some identity string, and hardcoding it in the host was the shortest path. Fix: every app-specific value is an ARGUMENT your bridge passes into a generic verb. grep -ri "<yourapp>" <ab>/src-tauri/src/ must return nothing.

3. Pulling app-AWARE correctness logic up into the host

Symptom: you want ab to do strict binding — "act only on the exact HWND + owner PID + process-start, never the wrong window." Why: it is about windows, so it feels like kernel work. Fix: it is not — doing it well requires knowing how your app creates its windows (process/window timing, correlating a spawned process to a session), which is precisely the knowledge ab must not carry. Resolve and verify in YOUR bridge; hand ab a resolved HWND for its dumb generic paint verbs. The bridge decides which window and proves it; ab never learns why. This is the subtlest one on the list — it passes the "is it generic?" sniff test and still fails the two-part test.

4. Shipping something nobody can RUN

Symptom: your work sits unreviewed and untried. Why: it is only runnable if someone builds and runs a modified core ab — nobody will do that to try out a feature — or your releases are all dev/updater-ineligible candidates, so bridge_install has nothing to resolve. Fix: a bridge's whole value proposition is that anyone can install it in one command and use it in a minute. Ship at least one qualified, updater-eligible release with its runtime .zip, and keep 100% of the feature inside the bridge so trying it never requires touching ab. A change people must review is worth far less than a thing people can just use.

5. Shipping something nobody can SEE

Symptom: your README is all prose; a reader cannot tell what your bridge does or whether it works. Why: you can see it working on your own machine, so the gap is invisible to you. Fix: screenshots of the real outcome, and for anything VISUAL (taskbar tiles, jump lists, overlays, rendered output) before/after shots — prose cannot convey it and nobody can verify it otherwise. pup's README carries 13 images, kicad's 8, plus demo videos; a new bridge shipped 0 for an entirely visual feature and its reviewer had no way to evaluate it at all. desktop_screenshot_window makes capturing this scriptable. This matters most for a bridge aimed at outside users: an internal bridge can lean on the author being in the room, but for everyone else the page IS the product.

6. A keyword that exists only in conversation

Symptom: you and a colleague agreed the bridge is "pw", but an AI never resolves it. Why: the word was never written into bridge.json, the verb prefix, the discovery triggers, or the README. Fix: declare it once and use it in all four places — see "Your bridge's KEYWORD" above.

7. Stale naming and a missing page kit

Symptom: your page reads as abandoned next to the other bridges. Why: slugs and copy written before the rename (Adom Bridge, not "Adom Desktop"), no root SKILL.md (only a skill-package/ directory), no 1.6-aspect hero (so ab cover-crops the OG-card fallback and clips it), and none of the standard kit (install.sh/uninstall.sh, CHANGELOG.md, PUBLISHING.md, billboard art). Fix: the page-identity block of the self-audit checklist (adom-bridge-sdk-audit) is a literal punch list for this.

8. Assuming ab will forward what it never received

Symptom: you add an arg to a verb and it silently does nothing. Why: something in the chain rebuilt the payload from a fixed allowlist instead of forwarding verbatim. Fix: round-trip test it — send an unknown arg and have your bridge echo its received keys. Every arg you send must arrive.


Where every file lives: the two artifacts, their deps, and which skills go where

Two DISTINCT artifacts, two DIFFERENT homes, two DIFFERENT consumers. Crossing them is the #1 inconsistency across bridge repos, so pin it here before anything else. (Live audit 2026-07-06: fusion + pup do this right; kicad had 61 loose adom-bridge-kicad-v*.zip on /files + a mesa-llvmpipe-x64.zip; blender dumped 2 loose zips too.)

RUNTIME artifact (Artifact 1) SKILLS pkg (Artifact 2)
What it is your bridge's actual CODE your consumer SKILL.md docs
Runs on the user's DESKTOP (ab spawns it) nothing runs it (docs the CONTAINER reads)
Published as a versioned Release (release create + release upload --platform any) an adom-wiki pkg (pkg publish --org adom)
Installed by bridge_install {manifestUrl} → ab streams, unzips, spawns pkg install adom/<slug> / sync_skills → ~/.claude/skills + ~/.codex/skills
Contains server code + bridge.json at the zip ROOT + assets. Node: SOURCE ONLY, no node_modules. ONLY: ROOT SKILL.md, skills/<name>/SKILL.md (user skills), package.json, install.sh, uninstall.sh. Text only: the whole tarball should be a few KB.
NEVER contains the skills pkg ANY image (hero / screenshots: those are WIKI PAGE assets, see below), any binary, the Release zip, src/, node_modules, dev/publish skills, or the server's npm deps. If tar tzf shows a .png/.jpg/.zip/.exe, you bundled bloat. Remove it.
its package.json (node) the bridge SERVER's runtime deps ab installs at spawn: dependencies:{ puppeteer, … } the pkg manifest: files[] (user skills), discovery_triggers, scripts, AND dependencies:{"adom/adom-bridge":"^1.9.x"} (the container needs the ab CLI + core skills to drive you), but NOT the server's npm deps

Yes, a node bridge has TWO package.json files and they are NOT interchangeable. One rides inside the Release zip and lists the bridge server's runtime deps (puppeteer, sharp, ...) that ab installs on the desktop at spawn. The other is the pkg manifest in the skills tarball: it lists your user-skill files[] + discovery_triggers AND declares dependencies:{"adom/adom-bridge":"^1.9.x"}. pkg install resolves that npm-style, so installing your bridge's skill pkg also pulls the adom-bridge CLI + core skills the container needs to drive you. What the pkg manifest must NOT list is the server's npm deps (those are desktop-side, in the zip). Put them there and you bloat every container install, since the container never runs your server.

Where each file physically lives (this is the map that ends the inconsistency):

  • bridge.json → the root of the Release zip (ab reads it post-unzip). Carries version + updateManifestUrl.
  • The Release .zip → upload it as a Release asset (release upload … --platform any) and point the manifest url at its release-download path: https://wiki.adom.inc/download/adom/<slug>/<ver>/<file>.zip (verified to serve anon 206). fusion + pup do exactly this, so copy them. A loose /files blob technically serves too (206), so this is a HYGIENE + right-home rule, not "it won't work": the /files store is your git page repo (for the manifest, text, images), so a binary there accumulates forever (kicad has 61 stale -v*.zip), isn't pinned to a Release, and the /blob/... URL is just an alias of the same blob (same sha), so you can't tidy it. ⚠ Migrating off /files is a SEQUENCE, not a delete: (1) release upload the zip, (2) repoint the manifest url at the /download/ path, (3) verify it serves anon, (4) only then repo rm the old /files copies. NEVER repo rm a zip while your manifest still points at it (or at its /blob alias). That breaks bridge_install + auto-update instantly.
  • The manifest (adom-bridge-<name>-manifest.json, this naming is the consistent one, keep it) → POSTed to your page /files. Its url points at the Release download (https://wiki.adom.inc/download/adom/<slug>/<ver>/<file>.zip), base-relative + extension-agnostic. updateManifestUrl (in the manifest AND in bridge.json) points back at this manifest.
  • A big RUNTIME ASSET (a headless-GL pack like Mesa, a browser download) → declare it in prewarm.assets so ab downloads + manages it, or bundle it in your seed. NOT a loose /files blob: kicad's mesa-llvmpipe-x64.zip on /files is the anti-pattern.
  • Your hero image + screenshots → WIKI PAGE assets: set them on the page (the page's hero / repo push the image to the page), NEVER in the skills tarball. A container that pkg installs your skills never renders a hero, so a bundled hero (a real one was 1 MB) is pure download bloat on every install. Do NOT put hero: docs/hero.png + docs/** in the pkg package.json, because that is exactly what stuffs the image into the tarball.

The three kinds of skill, and where each goes (this IS the demarcation, one table):

Skill kind Who reads it Lives in In the pkg tarball? Public?
USER (the public / consumer skill) a consumer AI driving your verbs your pkg YES, that is the pkg's whole job yes (the pkg is public)
DEV a maintainer editing the bridge your source repo NO, never only if your repo is public
PUBLISH a maintainer releasing the bridge your source repo NO, never only if your repo is public

The one rule: the pkg tarball carries USER skills only; DEV + PUBLISH skills live in the source repo. Do NOT "scope" dev/publish into the pkg with user-invocable:false, since that still ships them (pure bloat). Open-vs-closed source only sets your repo's visibility; it never changes what's in the pkg. adom-bridge core does exactly this: user/integration skills in the pkg; RELEASE / WIKI_PUBLISH / SIGNING / AD_RELAUNCH live only in the source repo.


The boundary: what ab owns vs what YOU own

The #1 mistake is filing ab bugs for bridge issues. The split:

ab core owns (don't reimplement): the CLI/relay//command passthrough (ab relays your JSON verbatim, every field); spawning/reaping your process, the stable port, single-instance, the persistent-respawn supervisor, cache install + auto-update; host/process facts (running? pid? port?), the status-chip rendering, the GUI card, the activity log; status/_hint classification for timeouts/disconnects (ab authors those, you never stamp them).

You own: your verbs + their behavior + their rich hints; your /health (+ optional chip fields); your <prefix>describe catalog; your wiki page (hero, brief, discovery_triggers, docs); your published manifest + Release zip; and your skills pkg.

If a bridge verb misbehaves → fix the bridge. If a generic ab capability is missing (a new lifecycle verb, a relay field) → file it against ab, but don't re-absorb the bridge into ab.


⭐ REQUIRED: every bridge MUST expose a <prefix>uninstall verb (John, 2026-08-12)

An Adom employee reported that uninstalling left cruft behind, because ab's uninstaller could remove ab's own files but had no way to clean up what each BRIDGE created. Bridges install their own state OUTSIDE ab's directory: a host-app add-in (the Fusion add-in lives in %APPDATA%, not ab's dir), per-bridge caches, downloaded runtimes/models, registry keys, temp dirs. Only the bridge knows where all of that is.

So the contract now requires: every bridge exposes a <prefix>uninstall verb (e.g. kicad_uninstall, fusion_uninstall, pup_uninstall) that removes EVERYTHING that bridge created on the machine: its add-in/plugin files in host-app dirs, its caches, its registry entries, its temp files, anything outside ab's own bridges-cache (which ab removes). It must be:

  • Idempotent and non-interactive (safe to call when already clean; never prompt/block, since it runs during a silent /S uninstall cascade).
  • Best-effort per-item (one un-removable file must not abort the rest; report what was and wasn't removed).
  • Scoped to your own artifacts ONLY: never touch the host app itself (don't uninstall KiCad/Fusion), other bridges, or user data.

ab's uninstaller enumerates the installed bridges and calls each bridge's <prefix>uninstall before removing ab, so an uninstall cleans up the whole ecosystem. Declare the verb in your bridge.json (an uninstall field naming the verb) so ab knows to call it; if absent, ab logs that the bridge left cleanup to the user. Add it to your describe catalog and skill like any other verb.


⭐ REQUIRED: every slow verb reports MEASURED progress - percent, ETA, and cold-vs-warm (John, 2026-08-18)

This was SUGGESTED on 2026-08-14 and that was not enough: a live Fusion surface button still sat on "starting the app ... cold start can take a couple of minutes", which is a shrug, not data. A UI cannot draw a progress bar from a sentence, the user cannot tell slow from wedged, and the number never narrows as the work proceeds. So it is now a RULE.

The shape. Any verb whose p50 exceeds ~3s returns a progress block on the in-progress path AND on the final frame:

"progress": {
  "phase": "cold_start",
  "stepLabel": "Loading the electronics workspace",
  "percent": 38,
  "elapsedSec": 27,
  "estimatedSec": 71,
  "etaSec": 44,
  "confidence": "measured",
  "startKind": "cold"
}
  1. percent is MONOTONIC - coarse (step N of M) is fine, backwards is not. null only when genuinely unknowable, and then confidence:"unknown" plus a moving stepLabel so the user sees life.
  2. estimatedSec is REVISED as you learn. A static constant is not an estimate; hit a checkpoint early and the estimate shrinks.
  3. COLD vs WARM is measured and reported (startKind, and say it in stepLabel too: "Fusion is already running - reusing it"). They differ by 10-50x, so one pessimistic sentence covering both is the actual bug. Keep a ROLLING MEASURED HISTORY per phase in your own state dir (last ~5 cold + last ~5 warm, p50/p90), seed estimates from THAT, and expose it in describe so a UI can say "typically 45s cold, 3s warm on THIS machine" before the user clicks. That is what makes confidence:"measured" true rather than decorative.
  4. A long verb is POLLABLE and says so: return {started:true, jobId, statusVerb:"<prefix>_progress", progress:{...}} and keep the same block flowing from the poll verb until done/failed. Never block silently past your own declared timeout.
  5. Terminal frames carry ACTUALS: percent:100 + real elapsedSec + measured:{phase, seconds, startKind} (feeds your history and ab's logs). On failure, name the phase it died in + failedSteps so the bar turns red in the right place instead of hanging at 38%.

ab's side of the deal - what is TRUE TODAY, and WHERE your block gets drawn. ab forwards progress VERBATIM: no parsing, no per-app logic (same generic-renderer rule as the status chip and stepShot), so whatever you emit reaches the caller unaltered. Shipped and verified: the block survives the round trip through adom-bridge to a cloud caller, and the web-control demo page renders it - bar, step label, percent/ETA, cold-vs-warm, a dimmed fill when your confidence is not measured, an amber stalled bar on blockedBy, and your stepShot frame verbatim as the step's evidence thumbnail.

The renderer is the WEB, and that is the point. Your progress is drawn where a person is actually looking: the web-control demo page (http://127.0.0.1:24681) is the REFERENCE IMPLEMENTATION, and it exists so wiki.adom.inc can render your bar the same way for real users. ab's own window deliberately draws NO progress bar - it is a background agent's console the user barely opens, so a bar there would be effort spent where nobody looks. (ab 2.0.66/67 shipped one by mistake; 2.0.68 removed it. ab still emits the block internally, so a future surface can pick it up with no ab release.) Design for the web surface, not for ab's window.

Still owed: ab does not yet report its OWN slow operations in this shape. The SSE /command/stream progress you may have seen is ab's RECIPE-STEP runner (step N of M, usuallySec), not your block being streamed.

What that means for you: emit BOTH shapes, because they drive different halves of the bar. ab reads two, matched on shape alone (never on a bridge name):

  1. The terminal frame - a top-level progress on the slow verb's own response. This is the one progressContract already promises, and it always arrives at percent:100: by the time the verb answers, the work is done. It is a completion receipt, so on its own it paints a bar that flashes "done" and never moves.
  2. The live poll - an active[] array of those same blocks, one per in-flight phase, returned by your fast progress verb (<prefix>_progress) while the slow work is still running. This is the one that makes the bar MOVE, because a caller polling once a second produces a stream of rising frames. An empty active[] means idle and draws nothing.

If you only ship #1, the user watching your 40-second verb sees nothing until it finishes. When several phases are in flight ab draws the least complete one, since the honest answer to "how far along is this?" is bounded by the slowest phase.

blockedBy is the one that earns the most trust. When the work is parked on a HUMAN - a modal dialog, a login, a save prompt - set it, and ab's status line says "waiting on you: <what>", turns the fill amber, stops advancing it, and keeps the bar up instead of timing it out. A bar that keeps sliding while nothing can happen without the user is worse than no bar at all.

Why this matters more than a demo page: ab is the only process that sees every bridge response, so a cloud AI polling from a container now drives a bar on the USER'S screen. The person at the keyboard finally sees what a remote agent is doing to their machine - something no container-side UI can provide.

The demo verbs already prove the shape works (<prefix>_demo_progress streams percent/stepLabel/elapsedSec/estimatedSec and the web-control page draws a live bar from it). The gap was that ONLY the demo verb did it. Instrument the rest: *_start (the cold-start case that triggered this), *_show_*, install/export/build.

Tracking: kicad-bridge#32, fusion-bridge#32.


Background: the original duration-hint suggestion (John, 2026-08-14) - now folded into the rule above

Launching KiCad, waiting for a Symbol Editor or a 3D board to load, cold-starting Fusion: these take tens of seconds, and a caller that shows "usually about 35s" beats a spinner with no end in sight. ab already streams live progress for long recipes (the web-control demo's POST /command/stream heart-beats every step and reports the ACTUAL tookMs), but the honest EXPECTATION ("usually ~35s") should come from you, the bridge, because only you know your app.

So, when you can: declare a rough expected duration for each slow verb, and ideally track a rolling actual average and return it. Concretely:

  • In bridge.json, alongside your timeouts block, add an optional verbDurations map ({ "kicad_open_symbol_editor": 30, "kicad_open_board": 35, ... }, seconds) that ab and callers can read to show "usually ~Ns" and to size a timeout honestly.
  • Optionally, keep a rolling average of the last N real runs per verb (a tiny on-disk counter) and surface it in that verb's response (e.g. timing:{ usuallySec, lastSec, samples }). A measured number beats a guess, and it self-tunes to the user's machine (a fast SSD vs a slow VM).
  • These are HINTS for UX and timeout-sizing, never a gate: a verb that runs long is not a failure, and ab's per-verb timeouts remain the hard budget.

This keeps the "ab renders, the bridge knows its app" boundary: ab does not hardcode "KiCad takes 35s" (that would rot the moment KiCad changes), it just forwards what your bridge reports.


⭐ SUGGESTED: expose a <prefix>_state verb that returns your app's FULL current state (John, 2026-08-14)

A user or AI should be able to ask a bridge, at any moment, "what does the app you control look like RIGHT NOW?" and get back the complete picture, not one guessed window. Today a caller that wants to see KiCad screenshots the first window whose title contains "KiCad" and gets ONE of its two windows; Fusion has a main window plus palettes, so a single title-match grabs a skinny panel. That is wrong, and it is wrong because only the BRIDGE knows the real window set (it tracks its app's parent/child hwnds). A generic title match cannot.

So, when your app has a meaningful visual/logical state: expose a <prefix>_state verb (e.g. kicad_state, fusion_state, blender_state) that returns the app's current STATE OF THE UNION:

  • A screenshot ARRAY of EVERY window the app owns: the parent + all child/related windows (project manager + symbol editor + PCB editor + any open dialog), each captured by its real hwnd. Return them as base64 ({ shots: [{ title, hwnd, image, width, height }] }) so a WEB page or AI can render them inline without a file pull. (File paths are fine for the CLI too, but base64 is what makes this usable from the web-control demo and from an AI that cannot reach the desktop's filesystem.)
  • Plus the logical state you already know: what document/board/design is open, which editor is focused, is a modal dialog blocking, unsaved-changes flag, current tool, plus whatever else a caller would need to decide the next move.

Why this matters: it turns "drive blind and hope" into "look, then act." An AI can call <prefix>_state before and after an action to confirm it landed; a human demo (like the web-control page) can show the app's true state instead of a cropped sub-window. ab already has the generic pieces (desktop_list_windows + desktop_screenshot_base64 {hwnd}), but the bridge produces the CORRECT set because it owns the window identities, and that is the whole point of the ab-renders / bridge-knows-its-app boundary. Keep it read-only and non-gated (it only observes), and make it cheap enough to poll.


⭐ EDA bridges: the EIGHT SURFACES contract, <prefix>_show_* (John, 2026-08-15)

If your bridge drives an electronics design tool (KiCad, Fusion 360, Altium, and whatever comes next), implement these eight verbs. They are not a demo checklist; they are the complete set of things a user has to be able to DO from the Adom Wiki to close the circle from a wiki component / molecule / board all the way to the end app the EE actually designs in. A bridge that implements all eight plus <prefix>_state gets the web-control demo, the wiki round-trip, and the cross-app parity test for free, with zero ab-side work, which is the whole point of keeping ab generic.

# Surface Verb What it opens
1 Symbol <prefix>_show_symbol one schematic symbol in the symbol editor, full pinout
2 Footprint <prefix>_show_footprint one footprint / land pattern, pads + layers
3 3D chip <prefix>_show_3d_chip the single component's 3D model
4 Library <prefix>_show_library the library browser scoped to one library
5 Schematic <prefix>_show_schematic the design's schematic sheet
6 2D board <prefix>_show_2d_board the PCB layout editor
7 3D board <prefix>_show_3d_board the populated board in 3D
8 Project <prefix>_show_project the project/design at its top level

Rules that make the set usable rather than merely present:

  • show is not open. An open verb takes a file path or a cloud URN and has app-specific arg shapes; a show verb takes the smallest identifier that names the surface (a symbol name plus its library, a file path, nothing at all when the app already has the design loaded) and is responsible for getting the app into that view whatever state it starts from. Having fusion_open_lbr does NOT satisfy #4, and having kicad_open_board does NOT satisfy #6. Ship the symmetric show pair.
  • They open in the BACKGROUND and are therefore useless without <prefix>_state. A show verb that returns {success:true, activeWorkspace:"..."} and no image leaves the caller unable to prove anything happened. The state verb above is what renders the result. Treat the eight verbs and the state verb as ONE deliverable: six show verbs with no state verb rendered zero surfaces in the demo for a full day, which read as "the feature does not work."
  • Say when a surface is COLD-capable and when it is WARM-ONLY. KiCad's 3D chip and 3D board are reliable only once KiCad is already up (kicad-bridge #26). That is a legitimate answer, but it must come back in the verb's own _hint so the caller can sequence rather than guess.
  • A prerequisite miss returns a _hint naming the exact next call, never a bare failure: "needs a library open, call <prefix>_open_lbr first" or "run the demo first to generate the sample project."
  • Zero-setup where you can. Prefer pointing the demo-facing default at the app's STOCK libraries (KiCad uses Device / Resistor_SMD) so a fresh box shows something real with nothing staged. Where stock content does not exist, bundle a sample in your zip like Fusion's sample .lbr.

Status as of 2026-08-15: KiCad 0.9.105 implements all eight plus kicad_state (3D chip and 3D board warm-only). Fusion 1.9.16 implements six and is missing fusion_show_library, fusion_show_project, and fusion_state (#28). Altium has no bridge yet; when one is written, this table is its spec.


⭐ REQUIRED: the background/foreground RESPONSE contract - one shape, canonicalized (wiki #82, 2026-08-19)

John's ruling on foreground etiquette (a window opens in the BACKGROUND by default; a foreground move needs a stated human reason and a 3s ab caption first) shipped in kicad 0.9.131 and fusion 1.9.51-52 within a day of each other. The ARGS agreed - foreground: true + foregroundReason + the caption - and that part is settled. The RESPONSE markers diverged, so a runner had to special-case each bridge. Canonicalizing now, before Altium and the next bridge copy whichever one they happened to read first.

The rule: what a caller has to branch on is a CONTRACT and gets a stable, non-underscore key. What a caller merely reads is a HINT and keeps the _ prefix. That is the whole basis of the choices below, and it is the thing to apply when a future field is added.

Semantics CANONICAL Was
where the window went windowEtiquette: "background" | "foreground" kicad openedInBackground: true
a bare foreground:true was refused errorCode: "foreground_reason_required" kicad _foregroundRefused
the quietness audit log <prefix>_state.focusEvents[] kicad kicad_state.focusEvents (already right)
guidance for the AI _hint (unchanged) both

windowEtiquette over openedInBackground, because a boolean cannot grow. The states are already three, not two (background, reason-gated foreground, refused), and a bool forces every later state into a second flag that can contradict the first. An enum also reads correctly on verbs that are not "open" - a raise, a close, a dialog dismissal all have etiquette, and none of them "opened in background". fusion's shape was the more durable one.

errorCode over _foregroundRefused, because a runner must be able to BRANCH. fusion asked for this explicitly and is right: a _-prefixed marker is by convention advisory text for the AI, so keying control flow off it makes a doc string load-bearing. foreground_reason_required is the stable code; keep the _hint alongside it explaining what to pass.

focusEvents is blessed as the canonical name (kicad's, unchanged) on the <prefix>_state verb: an append-only log of every focus change your bridge caused or observed, with a timestamp and a cause. An auditable quietness log is a real assert; "nobody complained" is not. fusion is adding it.

⛔ REQUIRED: an envelope is scoped to a STEP, never to an orchestration

fusion's lesson, and it is the one most likely to bite the next bridge, so it is a rule and not a footnote. fusion's tour armed ONE background-suppression envelope sized from an estimate of how long the run would take. Real runs blew through it - cold boots, dialog stalls - the guard expired mid-run, and a window escaped to the foreground. That was the exact foreground leak John caught live.

An estimate cannot bound a step whose duration you do not control. Re-arm the envelope per step or per operation, so the guard's lifetime is always shorter than the thing it is guarding, and an overrun costs you a re-arm rather than a leak. This is the same reasoning as the measured-progress rule above: do not let a number you guessed become a thing the system depends on.

Measured, not theoretical (fusion, 2026-08-19): a tour armed for ~376s actually ran 485-890s, and every step after the envelope expired silently reverted to foreground-stealing - it took John's focus while he was working in other threads. The fix is one line per step. Nesting is refcounted, so re-arming costs nothing - there is no efficiency argument for the single big envelope, only the illusion of tidiness.

This is REQUIRED rather than recommended for one reason: the failure is invisible from inside your bridge. Nothing errors, no log line fires, and you only learn about it when a human notices their window jumped. A rule whose violation is undetectable by its author has to be stated as a rule.

Both existing shapes may be emitted together during a migration window (windowEtiquette plus the old key) so a pinned runner does not break, but the OLD keys are deprecated on sight and should be dropped once your consumers are on the new one.

⭐ REQUIRED: PRUNE WHAT YOU WRITE - a bridge owns its own scratch directory (kicad-bridge#40, 2026-08-20)

If your bridge writes a file per operation - screenshots, exports, logs, temp renders - you must prune it. ab will not, and should not: ab's janitor covers ab's OWN directories, and a bridge's scratch space is the bridge's. ab deleting files out of it would be ab reaching into storage it does not own.

This is a REQUIRED rule rather than advice because the failure is invisible until it is large. Measured on one real box: a bridge writing captures per call had accumulated 226 files / 37.5 MB, growing, with nothing pruning it - and it went unnoticed for months because nothing errors. Nobody sees a directory that merely gets bigger.

The retention shape that works (from kicad-bridge 0.9.203, which is a good default to copy rather than reinvent):

  • an age cap (48h is a sane default for evidence captures),
  • a hard file ceiling (300) as a backstop for a burst,
  • oldest-first eviction over the ceiling - this one is load-bearing, because it guarantees an in-flight capture is never the file you delete,
  • rate-limited (once per 10 minutes) so a burst of captures never pays for the sweep,
  • and when you RENAME a directory, drain the old one and remove it, or every existing install keeps its orphans forever.

Write under your own bridge cache, next to the other bridges, not a hand-rolled path:

%LOCALAPPDATA%\Adom Bridge\bridges-cache\<your-bridge>\screenshots\

Two traps worth naming, both hit in the wild:

  • Do not invent a top-level directory like C:\tmp\<something>. It is outside every janitor, invisible to the user's mental model of where apps keep things, and it survives your uninstall.
  • Check the NAME is current. A directory created under a retired product name keeps that name propagating long after the rename - and a user auditing their disk finds a folder for an app that no longer exists.

Unit-test the retention rule against a synthetic tree (aged-out removed, fresh kept, legacy drained) rather than asserting it - the whole point is that nobody will notice when it silently stops working.

The rest of the SDK library

The SDK is a skill library, not one file, so you load only the phase you are in:

Skill Load it when
adom-bridge-sdk orientation (this skill): the boundary, the artifacts, where files live
adom-bridge-sdk-manifest writing or fixing bridge.json
adom-bridge-sdk-runtime spawn, runtimes, loopback bind, readiness, LEDs, logging
adom-bridge-sdk-callback calling ab verbs back, caller provenance, window branding
adom-bridge-sdk-publish shipping: the zip + manifest, the skills pkg, discovery, the checklist
adom-bridge-sdk-audit auditing a shipped bridge, staying current, filing issues

Humans wanting the narrative tour (with screenshots) should read the sub-readme on the Adom Bridge page instead: https://wiki.adom.inc/adom/adom-bridge - "Build your own bridge".