DEPRECATED: Bridge SDK (moved into Adom Bridge)
Public Made by Adomby adom
Bridge SDK guide: bridge.json schema, kind:python/node/exe, hello-python + hello-rust reference templates, packaging + lifecycle commands.
| Name | Last updated |
|---|---|
| docs | 3d ago |
| skills | 3d ago |
| ._arch.png | 3d ago |
| ._bridge-dev-template.md | 3d ago |
| ._bridge-publish-template.md | 3d ago |
| ._bridge-templates-README.md | 3d ago |
| ._bridge-user-skill-template.md | 3d ago |
| ._bridges-hero.png | 3d ago |
| ._bridges-SDK.md | 3d ago |
| ._hero-billboard-v2.png | 3d ago |
| ._hero-billboard.png | 3d ago |
| ._hero-v2.png | 3d ago |
| ._hero.png | 3d ago |
| ._install.sh | 3d ago |
| ._outcomes.png | 3d ago |
| ._package.json | 3d ago |
| ._page.json | 3d ago |
| ._README.md | 3d ago |
| ._SKILL.md | 3d ago |
| ._uninstall.sh | 3d ago |
| ._verbs.png | 3d ago |
| arch.png | 1mo ago |
| bridge-dev-template.md | 3d ago |
| bridge-publish-template.md | 16d ago |
| bridge-templates-README.md | 16d ago |
| bridge-user-skill-template.md | 16d ago |
| bridges-hero.png | 2mo ago |
| bridges-SDK.md | 2d ago |
| hero-billboard-v2.png | 1mo ago |
| hero-billboard.png | 1mo ago |
| hero-v2.png | 1mo ago |
| hero.png | 1mo ago |
| install.sh | 2mo ago |
| outcomes.png | 1mo ago |
| package.json | 3d ago |
| page.json | 2d ago |
| README.md | 2d ago |
| SKILL.md | 2d ago |
| uninstall.sh | 2mo ago |
| verbs.png | 1mo ago |
DEPRECATED: The Bridge SDK now lives on the Adom Bridge page.
The SDK is documentation of Adom Bridge, not a separate product, so it has moved there:
- Humans: the illustrated tour is Build your own bridge on the Adom Bridge page.
- AIs building a bridge: the reference is now a skill LIBRARY, split by task phase, shipping in the Adom Bridge package. Install with
adom-wiki pkg install adom/adom-bridge, then loadadom-bridge-sdk(orientation) plus-manifest,-runtime,-callback,-publishor-auditfor the phase you are in. A single 91KB file made you load everything to answer one question.This page is kept for reference while the move settles. Audit against the skills, not this page.
name: adom-bridge-bridge-sdk description: Author, PUBLISH, and make-discoverable an adom-bridge bridge — a small local server that adds a namespace of verbs (kicad_, fusion_, browser_, your_prefix_) to adom-bridge. Use when building, publishing, updating, prewarming, or debugging a bridge, or deciding what skills to ship. Covers the AD-core-vs-author boundary; the #1 rule (rich hints in EVERY verb's OUTPUT, because the AI reads CLI output, never your on-disk skill); bridge.json; where every file lives (the Release-zip-vs-skills-pkg split, the TWO package.json files [server deps in the zip; the pkg declares dependencies:{adom/adom-bridge}], manifest + runtime-asset placement); the pkg carries ONLY user skills while dev + publish skills live in your source repo (not the pkg); the THREE things you publish (runtime Release zip + manifest, container skills pkg, wiki discovery_triggers); prewarm; and the auto-discover → install-check → use flow.
Bridge SDK — build, publish, and surface a bridge for Adom Bridge
A bridge is a small local program (Python, Node, Rust, anything) that AD 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:
- Runtime — a versioned Release
.zip+ a manifest on your wiki page. AD'sbridge_installstreams it onto the user's machine and spawns it. - Skills — an
adom-wiki pkgtarball carrying your consumer SKILL.md(s). A container installs it into~/.claude/skills/(or~/.codex/skills/) so the AI there knows your verbs. - Discoverability —
discovery_triggerson your wiki page so a user saying "open my app in pup" surfaces YOU viaadom-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/pitfallsyou 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. AD 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.)
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 — 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 — AD 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} → AD 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 AD 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 AD 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 AD 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 (AD reads it post-unzip). Carriesversion+updateManifestUrl.- The Release
.zip→ upload it as a Release asset (release upload … --platform any) and point the manifesturlat its release-download path:https://wiki.adom.inc/download/adom/<slug>/<ver>/<file>.zip(verified to serve anon 206). fusion + pup do exactly this — copy them. A loose/filesblob technically serves too (206), so this is a HYGIENE + right-home rule, not "it won't work": the/filesstore 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) — you can't tidy it. ⚠ Migrating off/filesis a SEQUENCE, not a delete: (1)release uploadthe zip, (2) repoint the manifesturlat the/download/path, (3) verify it serves anon, (4) only thenrepo rmthe old/filescopies. NEVERrepo rma zip while your manifest still points at it (or at its/blobalias) — that breaksbridge_install+ auto-update instantly. - The manifest (
adom-bridge-<name>-manifest.json, this naming is the consistent one — keep it) → POSTed to your page/files. Itsurlpoints 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.assetsso AD downloads + manages it, or bundle it in your seed. NOT a loose/filesblob — kicad'smesa-llvmpipe-x64.zipon/filesis the anti-pattern. - Your hero image + screenshots → WIKI PAGE assets — set them on the page (the page's hero /
repo pushthe image to the page), NEVER in the skills tarball. A container thatpkg 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 puthero: docs/hero.png+docs/**in the pkgpackage.json— that is exactly what stuffs the image into the tarball.
The three kinds of skill — 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 — 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 AD owns vs what YOU own
The #1 mistake is filing AD bugs for bridge issues. The split:
AD core owns (don't reimplement): the CLI/relay//command passthrough (AD 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 (AD 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 AD capability is missing (a new lifecycle verb, a relay field) → file it against AD, but don't re-absorb the bridge into AD.
Calling AD back (outbound) — you reach the FULL AD verb set (v1.9.84)
Your callback channel is AD's loopback direct API: POST <ADOM_DIRECT_API_URL>/command (env var; fallback ~/.adom/direct-api-port = host:port). As of AD v1.9.84 this reaches every dispatchable AD verb, not just desktop_* — same dispatcher the CLI uses, identical JSON back. GET <ADOM_DIRECT_API_URL>/commands lists the full set so you can capability-probe (don't hardcode).
appis OPTIONAL — inferred from the command. JustPOST {"command":"<verb>","args":{…}}. (Passappto override the inference.)- Top-level verbs you can now call:
notify_user(walk the user through something — returns{action:"displayed"}),notify_response(poll a toast button/input),targets(list the OTHER ADs on the relay),ping,bridge_list,runtimes,status, … — plus everydesktop_*and every other bridge's verbs. - Cross-AD (
target): add"target":"<clientName>"(fromtargets) or"all"to route the call to a PEER AD via the relay — e.g. you run on a VM but the user is on their laptop:notify_userwithtarget:"<laptop clientName>"reaches them where they are."attended"isn't resolvable yet (use a concrete clientName). X-Adom-Bridge-Token(optional, attribution). AD injectsADOM_BRIDGE_TOKENinto your spawn env. Send it as theX-Adom-Bridge-Tokenheader and AD badges your calls "bridge" in its Activity Log — nice for the user's transparency. It is NOT an approval gate: you're trusted by transport (AD binds 127.0.0.1 only + the user consented by installing you), so your calls — includingwrite_file/run_script— run ungated. A stale token → 403 (re-read it from your env after an AD restart); omitting it is fine.cliRequiredverbs (pull_file/send_files/shell_execute) return412 cli_required— they need the streaming/approval CLI path, not this HTTP channel.
# Discover, then call — no hardcoding.
curl -s "$ADOM_DIRECT_API_URL/commands"
curl -s -X POST "$ADOM_DIRECT_API_URL/command" -H 'Content-Type: application/json' \
-H "X-Adom-Bridge-Token: $ADOM_BRIDGE_TOKEN" \
-d '{"command":"notify_user","args":{"title":"Heads up","body":"…"}}'
Notify the user AND get their answer back (notify_user — two separate axes)
Your bridge can prompt the user with a native toast and read their reply — the same notify_user verb the AI uses. It has two independent axes; don't conflate them (full ref: the adom-bridge-notify skill / skills/NOTIFY.md):
- Axis 1 — get the answer back. There is no server-push, so either
wait:true(thenotify_usercall blocks and returns the user's choice inline — best for a one-shot prompt) or fire-and-continue then pollnotify_events {sinceSeq}(drains every click) /notify_response {id}. The click is recorded durably, so it survives even on a headless box. - Axis 2 — reveal a window (
focus). A LOCAL desktop action, not a reply.focus:{hwnd|cacheKey|titleContains|app|host}raises a window on the user's screen when they click.focus:{app:"<your-exe>"}points the click at a window YOUR bridge drives;focus:{host:true}points it at the app AD is embedded under (Hydrogen Desktop). Passfocuswhenever the toast is about a window, so the click lands there instead of opening AD.
# Ask, and get the answer back inline:
curl -s -X POST "$ADOM_DIRECT_API_URL/command" -H "X-Adom-Bridge-Token: $ADOM_BRIDGE_TOKEN" \
-d '{"command":"notify_user","args":{"title":"Overwrite board?","scenario":"reminder",
"buttons":[{"label":"Overwrite"},{"label":"Cancel"}],"wait":true,"timeoutSec":90}}'
# -> { "pending": false, "action": "Overwrite" }
The response always carries _capabilities / _couldHaveDoneBetter when a call left something on the table — read them; they teach the verb from use.
Caller provenance: WHO asked (v1.9.180)
A user runs roughly 20 AI threads against one desktop. Until v1.9.180 AD relayed every command indistinguishably, so a bridge received no caller identity at all. Three consequences, all real:
- You could not arbitrate concurrent agents — two threads grabbing the same session looked identical to you.
- No bridge log could attribute a verb to the thread that issued it, so interference was impossible to reconstruct afterwards.
- Any agent's
bridge_killdestroyed every other agent's work, silently, with no way for anyone to see who did it.
Filed as wiki issue adom/adom-bridge#34 by AdityaAngajala, with the pup half at adom/adom-desktop-puppeteer-bridge#22.
What you now receive, for free
AD stamps three headers on every relayed bridge request — all four request paths (browser, kicad, fusion, and the dynamic third-party dispatch):
| Header | What it carries |
|---|---|
X-Adom-Caller-Thread |
The AI thread / conversation tab name, self-reported by the caller |
X-Adom-Caller-Container |
The container it runs in |
X-Adom-Caller-Reason |
The per-call justification, when the caller supplied one (control chars stripped, truncated to 300 chars) |
You do nothing to receive these. No manifest field, no signature change, no opt-in. Reading them is the opt-in part.
Trust rule — read this before you use them for anything
These values are SELF-ASSERTED, exactly like reason. Use them for attribution, logging, UX and arbitration hints. NEVER for authorization, and never as a security boundary.
AD's own security model is transport-based (the adom-bridge-permissions skill has the full model): a caller is trusted because of how it reached AD, not because of what it says about itself. A header a caller supplies about itself cannot gate anything — if you write if (thread === "admin-tab") allowDangerousThing(), you have written a hole, not a check.
One asymmetry worth knowing: the container name in X-Adom-Caller-Container is the one the caller claimed. AD separately knows a verified container (the authenticated relay connection name) and shows the user both when they disagree. You only get the claimed one, so treat it as a label, not an identity.
What to do with it
1. Log it. Put the thread in your per-verb log line. This is the single highest-value use, and it is three lines of code:
// node — read once, at the top of your request handler
const caller = {
thread: req.headers['x-adom-caller-thread'] || 'unidentified',
container: req.headers['x-adom-caller-container'] || 'unknown',
reason: req.headers['x-adom-caller-reason'] || null,
};
console.log(`[verb] ${verb} session=${sessionId} caller="${caller.thread}"`);
# python — same idea; header lookup is case-insensitive on BaseHTTPRequestHandler
caller_thread = self.headers.get('X-Adom-Caller-Thread', 'unidentified')
caller_container = self.headers.get('X-Adom-Caller-Container', 'unknown')
caller_reason = self.headers.get('X-Adom-Caller-Reason')
log.info('[verb] %s session=%s caller="%s"', verb, session_id, caller_thread)
That one field is what makes an interference report reconstructable: [verb] browser_eval session=rev caller="chip-fetcher tab 3" answers "who moved my page?" instantly, where a bare [verb] browser_eval session=rev never can.
2. Arbitrate. If your bridge has sessions or resources an agent can own — pup sessions are the motivating case — record the owning thread at creation time and check it on every later use:
// on create
sessions.set(id, { ...state, ownerThread: caller.thread });
// on use by a DIFFERENT thread
const s = sessions.get(id);
if (s.ownerThread && s.ownerThread !== caller.thread) {
// warn loudly by default; refuse only if this is destructive
result._warning =
`session ${id} is owned by "${s.ownerThread}" — you are "${caller.thread}". ` +
`Two agents driving one session will interfere. Coordinate, or open your own session.`;
}
The recommended posture:
- Warn loudly by default. A second agent reading or screenshotting a session is usually fine, and hard-blocking it turns a shared machine into a locked one.
- Refuse only for destructive operations — closing, resetting, navigating away, deleting.
- Always name the current owner in the message. The whole point is that the second agent can now go coordinate with a thread it can identify. An error that says "session busy" without a name is barely better than no header at all.
3. Label the UI. If your bridge drives a visible window, put the owning thread where the user can see it — window title, taskbar tooltip, or jump-list name — via desktop_set_window_identity (and desktop_taskbar's thumbnailTooltip for uncapped hover text). The user is the one with 20 tabs open; showing which tab owns which window is the fix for their side of the problem. Full guide: the adom-bridge-window-identity skill, and the "Branding + driving a window you borrowed" section immediately below.
4. Report it back. Include the owning thread in your status / list verbs, so an agent can discover it is not alone before it stomps on something:
// <prefix>_list_sessions
{ "sessions": [ { "id": "rev", "ownerThread": "chip-fetcher tab 3", "url": "…" } ],
"_hint": "A session with an ownerThread other than yours is being driven by another AI thread. Open your own session rather than sharing one." }
Notifications already carry it
notify_user derives its toast attribution line from the caller automatically, so a toast your bridge sends on an agent's behalf already names the thread — you do not have to build that string. Pass an explicit attribution string only when you want to override it.
⚠ REQUIRED — when YOU call an AD verb, forward the identity
This is the half most bridges will miss, and it is the half that makes the whole feature work or fail.
Your bridge calls AD verbs to do its job: desktop_screenshot_window, desktop_set_window_identity, notify_user, desktop_taskbar. When you make that call while carrying out a verb an AI thread asked you for, you are acting on that thread's behalf — so echo the identity you were handed straight back onto your own request. Otherwise the chain dies at your doorstep: the user's Activity Log says "pup did this", the originating thread vanishes, and the approval toast asks them to authorize a nameless bridge.
Echo all four headers, verbatim:
// node — inside the handler for a verb an AI asked you for
const fwd = {
'X-Adom-Caller-Thread': req.headers['x-adom-caller-thread'] || '',
'X-Adom-Caller-Container': req.headers['x-adom-caller-container'] || '',
'X-Adom-Caller-Reason': req.headers['x-adom-caller-reason'] || '',
'X-Adom-Caller-Delegate': 'pup', // ← YOUR bridge name
};
await fetch(`http://127.0.0.1:${adPort}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...fwd },
body: JSON.stringify({ command: 'desktop_screenshot_window', args: { hwnd } }),
});
fwd = {
'X-Adom-Caller-Thread': self.headers.get('X-Adom-Caller-Thread', ''),
'X-Adom-Caller-Container': self.headers.get('X-Adom-Caller-Container', ''),
'X-Adom-Caller-Reason': self.headers.get('X-Adom-Caller-Reason', ''),
'X-Adom-Caller-Delegate': 'kicad', # YOUR bridge name
}
X-Adom-Caller-Delegate is the one you add rather than forward. It names you, and it is what lets AD show the user both facts at once: chip-fetcher tab 3 (via pup). Attributing the call to the thread alone would hide who ran it; attributing it to the bridge alone would hide who asked. Omit the header and AD falls back to the generic via bridge, which is honest but much less useful.
When you are acting on your OWN behalf — a health poll, a timer, a crash cleanup — do NOT forward a stale identity. Send your own instead:
{ "command": "notify_user",
"args": { "caller": { "aiThread": "pup bridge (self)", "containerName": "local" }, … } }
An explicit caller block in the args always beats the headers, so this is unambiguous.
Two things AD does for you here: explicit args win over forwarded headers (so the override above just works), and AD carries the delegate forward onto any onward hop, so a chain two bridges deep still shows the original delegate.
Where it comes from (the caller side)
Useful to understand, though none of it is your responsibility:
# Per call — the CORRECT mechanism:
adom-bridge-cli --ai-thread "chip-fetcher tab 3" <verb> '<json>'
The adom-bridge CLI attaches a caller block to every verb automatically. A relay call with no aiThread is REFUSED by AD with errorCode: caller_identity_required before it ever reaches a bridge.
Correction, 2026-07-25 (AD 1.9.182): an earlier version of this page told callers to
export ADOM_AI_THREADonce per session. That advice was wrong and has been withdrawn. Roughly 20 AI threads share one container and one$HOME, so the env var scopes to the shell environment, not to the thread — every thread then reports the same name, which is worse than no name at all because it is confident and plausible. It also failed silently: tool shells are non-interactive, so an export appended below~/.bashrc'scase $- in *i*early-return never ran. The env var is still honored, but only fits a container running exactly one agent (CI, a single-purpose job). If your bridge's own docs repeat the export advice, please correct them.
So by the time a request reaches you, identity is normally present. The exception is loopback callers — HD, another bridge, or the local CLI — which are trusted by transport and not yet required to identify (that gate tightens once bridges adopt the forwarding contract above). Code for the absent case (|| 'unidentified'), never assume the header exists.
If your bridge wants to REQUIRE identity — use the shared contract
Some verbs are worth refusing anonymously. If you do, use AD's exact refusal shape so the AI sees one consistent remedy across the whole ecosystem instead of a different fix per bridge:
{ "success": false, "errorCode": "caller_identity_required",
"error": "'<verb>' needs to know WHO is asking — the user runs many AI threads and needs to see which one drove this.",
"supplyIdentity": {
"bestWay": "adom-bridge-cli --ai-thread \"<your conversation name>\" <verb> '<json>'",
"orInArgs": { "caller": { "aiThread": "<name>", "containerName": "<container>" } }
},
"_hint": "This is attribution, not a permission check — the name is self-reported and nothing is gated on it." }
Reusing caller_identity_required verbatim is the point: an AI that learned the remedy once applies it everywhere.
Self-audit
- Does every verb log line include the caller thread?
- Do your session-creating verbs record the owning thread on the session?
- Do your session-using verbs warn (or, if destructive, refuse) when a different thread arrives — naming the current owner?
- Does any window you drive show the owning thread to the user (title / tooltip / jump-list)?
- Do your status / list verbs report
ownerThreadso an agent can discover it is not alone? - When you call an AD verb on a thread's behalf, do you FORWARD all three caller headers plus your own
X-Adom-Caller-Delegate? - When you call an AD verb on your own behalf (poll, timer, cleanup), do you send your own
callerblock rather than a stale forwarded one? - If your docs told callers to
export ADOM_AI_THREAD, have you corrected them to the per-call--ai-threadflag? - Have you confirmed you use these headers nowhere as an authorization check?
Branding + driving a window you borrowed (window identity + taskbar, v1.9.106–1.9.153)
If your bridge drives a window another process owns — the classic case is a Chrome for Testing window that otherwise reads "Test" with Chrome's icon — AD gives you a generic verb family to make it present as YOUR app across every Windows shell surface, and to show live status on its taskbar button. AD stays generic: it applies whatever name/icon/command you pass; it knows nothing about your app. Full guide: the adom-bridge-window-identity skill (https://wiki.adom.inc/api/v1/pages/adom-bridge/files/docs/WINDOW_IDENTITY.md or the repo skills/WINDOW_IDENTITY.md). The essentials:
- Two halves, you need both:
desktop_register_app_identity {appId, displayName, iconPath, shortcut}teaches Windows what an AUMID means (HKCU only, no UAC;shortcut:falsefor a per-session id with no Start Menu spam), anddesktop_set_window_identity {hwnd, appId, iconPath, displayName, relaunchCommand}stamps it on the live window. Stamp without register → Alt-Tab / taskbar fall back to the owning exe's icon. - Per-session identity: register
shortcut:falseper window (e.g.Adom.Pup.<sessionId>) so each keeps its own taskbar button;desktop_unregister_app_identity {appId}on close. - Jump list + header:
desktop_set_window_jumplist {appId, tasks, hwnd?, headerIcon?, headerCommand?}adds the right-click menu AND (withhwnd+headerIcon) brands the header row (the app tile above Pin/Close) with no Start Menu shortcut. - Taskbar status:
desktop_taskbar {hwnd, progress?, overlay?, flash?, thumbnailTooltip?, thumbnailClip?}paints glanceable state on any window's button. New in v1.9.153:thumbnailTooltipis the ONLY uncapped hover-text surface (the window title truncates ~25-30 chars; this does not, and takes newlines;""clears it), andthumbnailClip:{x,y,w,h}crops the live hover preview to a region (nullresets). Overlay compositing (overlay.avatar/profileDir) preserves a browser profile face while adding your mark. - Icon format — read the
iconCheckin every response. The identity verbs inspect any icon you pass and returniconCheck {kind,sizes,issues,ok,hint}. The traps it catches: a 256px.icosize that is not PNG-compressed renders as the generic white-document icon (Vista+ rule); SVG can't be loaded by the Win32 shell; the AUMID registry icon wants a.ico/.png, not an.exe. IficonCheck.okis false, fix the file and re-stamp — don't ship the broken icon. Ship a multi-size.ico(16/24/32/48/256, the 256 PNG-compressed). - The live-tile trap: re-stamping the SAME appId with a new icon updates the title bar + Alt-Tab but NOT the taskbar tile (Win11 bakes it at button creation). To change a live tile, register + stamp a DIFFERENT appId; the button is recreated in ~1s.
Runtime contract — AD provisions Node/Python; you bind loopback (v1.9.63 / v1.9.69)
The single most important change for a Node or Python bridge. As of AD v1.9.63 you no longer bootstrap your own interpreter, and you MUST bind loopback. Four rules:
Don't bootstrap your own Node/Python. AD owns a global, portable, no-UAC runtime under
~/.adom/adom-runtimes/: a system install if one's on PATH, else a portable copy it downloads + prewarms in the background on first launch. AD spawns yourentrypointwith that interpreter by absolute path. So delete everywinget install node/desktop_install_node/ "please install Python" / self-download step from your bridge and its skills.spawn.kind: "node"|"python"IS your runtime declaration — AD reads it and provisions the runtime. Anyone can check status with theruntimesverb (state ∈ absent|installing|ready|failed,source ∈ system|cache, version). AD pins the version (Node 22.11.0 / Python 3.12.13 today) — need a different major? ask the AD-core thread; don't ship your own.Bind
ADOM_BIND_HOST, never0.0.0.0. AD passes anADOM_BIND_HOSTenv var (always127.0.0.1) to every bridge it spawns. A bare listen that defaults to0.0.0.0pops a Windows Firewall "allow access?" dialog on first run — and AD's hard guarantee to users is no firewall prompt by default.- Node:
server.listen(PORT, process.env.ADOM_BIND_HOST || '127.0.0.1', cb) - Python:
HTTPServer((os.environ.get('ADOM_BIND_HOST', '127.0.0.1'), PORT), Handler)
- Node:
Ship the Release zip source-only — AD installs your deps, and v1.9.69 makes it bulletproof. A Node bridge's zip carries SOURCE only (no
node_modules). AD reuses the bundled seed'snode_modulesviaNODE_PATHwhen versions match, else runsnpm install --include=optionalfor you with the managed runtime's own npm. As of v1.9.69 AD puts the managed runtime's dir on the child PATH for that install AND for your spawned process — so a dep's postinstall /node-gyp/prebuild-installthat shells out to a barenode/npm/npx(puppeteer's ownnode install.mjs,sharp,keytar,better-sqlite3,bcrypt) resolves even on a machine with no system Node. You do NOT add the runtime to PATH yourself — AD guarantees it. (Python: the interpreter dir +Scripts/are on PATH too, so pip can build wheels.)Bundle deps in your SEED for an instant first spawn. Source-only zip + a bundled-seed
node_modulesat the same version = AD reuses the seed viaNODE_PATH(instant); a wiki version newer than the seed forces one coldnpm install(now PATH-correct, but slower). Bump the seed in lockstep with each Release.
Net: declare spawn.kind, bind ADOM_BIND_HOST, ship source-only. AD does the rest — no UAC, no firewall prompt, no 'node' is not recognized.
Staying current — catch up on SDK changes yourself (no prompt needed)
This SDK evolves. When it does, AD posts a re-audit notice to YOUR bridge's wiki discussion and re-publishes this billboard. You do NOT need a human to hand you a prompt — self-serve at the start of a work session:
- Check your discussion for a notice.
adom-wiki discussion list <your-slug>(or open your page's Discussions tab) and read any "SDK update" / "re-audit" thread. A notice can be EDITED after posting (a correction), so always read the current body, not a cached earlier version. - Re-read this billboard, especially "Where every file lives" (above) and the "Self-audit checklist" (below):
https://wiki.adom.inc/adom/adom-bridge-sdk(orcurl -s https://wiki.adom.inc/api/v1/pages/adom-bridge-sdk/files/README.md). - Run the Self-audit checklist (next section) against your code,
bridge.json, skills, README, and wiki page. Fix every miss. - Reply on the discussion with what you changed or any blockers. That thread is the ONLY channel AD uses to reach you, so closing the loop there is how AD knows you are current.
Ask the AI to report bugs, in the OUTPUT, not the skill file
You cannot reproduce every environment. Your bridge runs on machines with browser installs, OS versions, security policies, hardware and concurrency patterns you will never have locally. The AI driving your bridge is standing on exactly that machine. It is the only party holding the failing response, and the moment it holds it is the only moment it will file anything.
So make the invitation part of your verb output, the same way _hint is. This is THE ONE PRINCIPLE at the top of this page applied to feedback: the AI almost never reads your on-disk SKILL.md, and it ALWAYS reads the output of a CLI call.
The evidence that this channel works. In roughly two days, wiki issues filed against the puppeteer bridge produced: a CPU-pegging orphaned-renderer bug that froze a laptop and forced a reboot (#14, #21), a respawn failure that broke the bridge on every idle cycle (#15), a session-tracking bug where windows were open and drivable but reported as zero (#16), a repo-versus-release divergence that would have silently reverted five releases (#19), and a concurrency and readiness report that caught a live blocking regression shipped hours earlier (#22). Not one of those would have been found on a maintainer's machine. They came from real installs: different Chrome and Edge combinations, different AD versions, concurrent agents, a macOS box, a stalled download.
Return a _reportIssues field naming YOUR page's issue list, with the exact commands:
{
"success": false,
"errorCode": "…",
"_hint": "…",
"_reportIssues": "THIS MAY BE A <BRIDGE> BUG. PLEASE REPORT IT. Check for a duplicate with `adom-wiki issue list adom/<your-slug>`, then file: `adom-wiki issue create adom/<your-slug> --title \"<one-line symptom>\" --category bug-report --body \"<what you ran, what you expected, what happened, the VERBATIM response JSON, the bridge version from bridge_list, and the OS>\"`. Reports from real machines are the only way this bridge gets correct. (An AD-core problem, such as the relay, bridge lifecycle or caller identity, belongs on adom/adom-bridge instead.)"
}
When to attach it. Throttle it, or the AI learns to skip it:
| Moment | Why |
|---|---|
| Always on a failure | The highest-value moment: a bug just happened and the evidence is still in the response. |
| Once per bridge process, on the first verb | Orientation, so the AI knows where to report before anything breaks. |
| Occasionally after that (e.g. every ~40th call) | A quiet reminder on a long-running session without taxing every response. |
A line on every response is noise that trains the caller to ignore the field. A line on every failure is a bug report waiting to be filed.
Make it actionable, not a slogan. "Report bugs" produces nothing. Give the AI the exact adom-wiki issue create command, your slug, a --category, and the list of evidence to include (the verbatim response JSON and your bridge version are the two that make a report reproducible). Tell it to check for a duplicate first: an issue list call costs nothing and keeps your page clean.
Route the report to the repo that owns the code. Say explicitly, in the same string, that AD-core problems (the relay and /command passthrough, spawn and provisioning, bridge lifecycle, caller identity) go to adom/adom-bridge, not to your page. Authors get this wrong in the other direction too, which is the next section: a misfiled issue is a note to yourself that the people who can fix it never read.
Let the user turn it off. Ship a settings toggle (pup uses browser_configure {reportIssues:"off"}) so a user who finds it noisy can silence it without losing functionality. Default it on: the cost is a few tokens on failures, and the benefit is the bug class you would otherwise ship to every user.
Reference implementation: the puppeteer bridge does this at its single sendJSON chokepoint (v1.9.95), so all ~155 response sites are covered from one place, and it never overwrites a _hint a handler already set.
Filing issues against AD (and the other repos you depend on)
You WILL find AD-core bugs and gaps while building your bridge. That is expected and useful. But file them in the RIGHT repo's issue list, or the people who can fix them never see it.
The rule: an issue lives on the issue list of the repo that OWNS the code that must change.
- A bug in YOUR bridge's verb / hint / self-heal / status chip goes to YOUR page's issue list (or you just fix it, since you own it).
- A missing or broken AD-CORE capability (a lifecycle verb, a spawn / provision / bind behavior, the relay passthrough dropping a field, a manifest field AD must honor, the status-chip contract) goes to
adom/adom-bridge, NOT your own page. AD-core's maintainers and AI watchadom/adom-bridge; they do NOT read your bridge page's issues. An AD ask filed on your own page is a note to yourself. - A wiki or CLI bug goes to
adom/wiki. A sibling bridge's bug goes to that bridge's page.
How (the CLI):
adom-wiki issue list adom/adom-bridge # FIRST: skim for a dup, comment if one exists
adom-wiki issue create adom/adom-bridge \
--title "<verb or subsystem>: <the gap in one line>" \
--category bug-report \
--body "$(cat your-writeup.md)"
Write the body like a good bug report: what you hit live (with the date and the AD version), the measured behavior, the AD-owned root cause if you found one, and a concrete ask. Separate clearly what is AD-core's to fix from what is yours, and say so explicitly, so the two are never conflated.
After you file: the AD-core thread replies on that issue. Read the reply, it may change your plan (for example "fixed in AD X.Y, bump your pin"). That issue thread is the cross-repo channel; keep the loop closed there.
Why this matters most for AD: AD-core spawns you, provisions your runtime, relays your verbs, and installs you, so AD is the repo you will file against most often. Get the destination right the first time. The established precedent all sits on adom/adom-bridge: #26 and #27 (pup to AD, Chrome for Testing), #16 (pup to AD, window-verb interception), #3 / #20 / #21 (Bridge SDK asks).
Self-audit checklist — run this when asked to "audit against the SDK"
When a maintainer says "audit your code, skills, README, and wiki page against the Bridge SDK," this is the checklist. Each item names its section below; fix every miss, then re-check.
bridge.json (the manifest in your zip):
-
spawn.kind∈python|node|exe|external-http, withentrypoint(NOTprocess/cmd/args);healthEndpointis insidespawn;port:0lets AD pick. (→ bridge.json) -
updateManifestUrl→ YOUR page's manifest (durable auto-update);docs→ your own page;homepageis awiki.adom.incURL, NEVERgithub.com. -
detectdeclared iff you drive a HOST APP (KiCad/Fusion/Altium…); omitted if you bring your own runtime (pup).prewarm.assetsdeclared for heavy downloads (e.g.chrome-for-testing). (→ Cold-start tiers) -
timeoutsdeclared if ANY verb can run longer than 60s (a big export/render/cloud-open) — with a matchingstatusVerbto poll. Third-party bridges were 60s-capped before v1.9.79; this is how you raise it. (→ bridge.json)
Artifacts & deps (the two-artifact split — the #1 inconsistency) (→ Where every file lives):
- Your Release
.zipis a Release asset (release upload), NOT a loose/filesblob — and old version zips are NOT piling up on/files(audit for stray*-v*.zip). - The server's runtime deps live in the ZIP's
package.json(node) /requirements.txt(python). The pkg'spackage.jsondeclaresdependencies:{"adom/adom-bridge":"^1.9.x"}(sopkg installpulls the AD CLI + core skills) and does NOT list the server's npm deps. - A big runtime asset (Mesa, a browser) is a
prewarm.assetor seed-bundled — NOT a loose/filesblob. - Your manifest (
adom-bridge-<name>-manifest.json) is on/files; itsurlpoints at the Release download, not at a/filesblob. - PAYLOAD:
unzip -l <your zip> | sort -k1 -n -r | headshows runtime only — no.mp4/.png/.jpg/screenshots/CHANGELOG/.github. A runtime zip is normally well under 1 MB; if yours is bigger, justify every large entry.tar tzf <your tgz>shows skills only — zero media, zero binaries. Media belongs on the PAGE. (→ Payload rules) - PAGE COPY: your package
descriptionAND README each carry the one-sentence statement thatpkg installinstalls the bridge's SKILLS for your container, not the bridge runtime. One sentence, not an essay. (→ Artifact 2)
Runtime (Node/Python bridges) (→ Runtime contract):
- You bind
ADOM_BIND_HOST(loopback default), never0.0.0.0. - You do NOT bootstrap your own Node/Python — no
winget/desktop_install_node/self-download in code or skills.spawn.kindis the declaration; AD provisions it. - Release zip is source-only (no
node_modules); you rely on AD'snpm install(PATH-correct as of v1.9.69) + a lockstep bundled seed for instant spawn. You do NOT put the runtime on PATH yourself.
Verbs & output (what the AI actually reads):
- EVERY verb returns a rich
_hint(+_next/related/pitfalls). (→ THE ONE PRINCIPLE) - A
<prefix>_describeverb lists your whole catalog. (→ Verb contract) - No verb blocks on a heavy install/download — background it, report
installing/warming;<prefix>_readinessis read-only. (→ Cold-start tiers) -
/healthreturnsled/summary/tooltiptruthfully. (→ Health + status chip)
Discovery (can a plain request find you?):
- 10+ user-task
discovery_triggers(everyday phrasings, NOT dev jargon). No triggers = invisible. (→ Artifact 3) - You ran
discover preview --jsonon 5-7 real user queries and land top-3 for each. - Triggers stay in YOUR niche — you don't poach a sibling's (login/forms → native-browser, not pup).
discovery_pitchis an INSTRUCTION.
Skills & docs (what the container + humans read):
- Your pkg ships ONLY your USER skill(s) — a real
skills/<name>/SKILL.md(core user skill + user sub-skills) with aParent skill:first body line; every path listed EXPLICITLY (no globs) infiles[]+ install.sh/uninstall.sh. (→ Artifact 2 + The SKILL SET) - DEV + PUBLISH skills are in your source REPO, NOT the pkg — not in
files[], not scoped in viauser-invocable:false. A developer gets them by cloning the source; the normal user never needs them. Open-vs-closed source is irrelevant (it only sets repo visibility). (→ Where every file lives) - Your pkg's
package.jsondeclaresdependencies:{"adom/adom-bridge":"^1.9.x"}sopkg installpulls the AD CLI + core skills the container needs. (→ Where every file lives) - Your USER skill leads with the cold-start "read before you panic" section + the 8 conventions, and points the AI at
bridge_readiness. (→ USER-skill conventions) - README + wiki page + skills all AGREE with the above (no stale github links, no wrong schema, triggers vetted).
bridge.json — the manifest that ships INSIDE your zip
{
"manifest_version": 1,
"name": "blender", // registry name → verb prefix "blender_"
"displayName": "Blender", // in-app chip label (the tool's real name)
"version": "1.2.0", // numeric; a newer CACHE copy supersedes the bundled seed
"description": "Drive Blender headless + GUI",
"author": "you",
"license": "MIT",
"docs": "https://wiki.adom.inc/adom/adom-bridge-blender-bridge", // your OWN page (drives skillPkg + hero)
"hero": "https://.../hero.png", // optional; else AD uses your wiki page hero, else a monogram. Ship it at 1.6 aspect (≈2000×1250) — see "Card hero + update cost"
"spawn": {
"kind": "python", // python | node | exe | external-http (NOT "process")
"entrypoint": "server.py", // the file/exe AD launches; AD AUTO-APPENDS `--port <port>` to argv
"port": 0, // 0 = AD picks a STABLE port (recommended). Bind 127.0.0.1:<that port>.
"persistent": true, // AD's supervisor respawns it if it dies
"healthEndpoint": "/health", // INSIDE spawn. AD probes it for the chip (default /health)
"stopMethod": "kill", // kill | sigterm | graceful_endpoint
"killImageName": "python.exe" // image name for taskkill on stop (kind:python/node)
},
"verbPrefixes": ["blender_"], // any verb with these prefixes routes to you
"verbs": ["blender_render", "blender_status", "blender_describe"], // every verb (routing + Verbs tab)
"statusVerb": "blender_status", // verb a caller polls after a long op times out
"updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/adom-bridge-blender-bridge/files/blender-manifest.json",
"timeouts": { // v1.9.79 — per-verb HTTP budgets (seconds); see note below
"default": 60, // fallback for any verb not matched (absent → AD's global 60s)
"verbs": { "render": 600, "export_step": 360 }, // exact BARE verb names (no prefix)
"prefixes": [ { "prefix": "export_", "seconds": 120 } ] // fallback rules, first match wins
},
// v1.9.47 cold-start (see "Cold-start tiers" below):
"detect": { // Tier 2 — how AD DETECTS your HOST APP. AD NEVER installs it.
"hostApp": "Blender", // omit the whole `detect` block if your bridge brings its own runtime
"appPathsExe": "blender.exe", // (e.g. pup ships its own browser → no host app)
"paths": { "windows": ["%ProgramFiles%\\Blender Foundation\\*\\blender.exe"] }
},
"prewarm": { "assets": [] } // Tier 1 — heavy runtime assets AD prewarms (e.g. ["chrome-for-testing"])
}
spawn.kindispython|node|exe|external-http— there is no"process"kind, and nocmd/args/cwd. AD launchesspawn.entrypointand automatically appends--port <port>to its argv; your server parses--port. (external-http= AD doesn't spawn, only probes a port you declare via adiscoveryblock.)spawn.port: 0→ AD allocates a STABLE port (persisted across respawns + AD restarts) and passes it. Bind127.0.0.1:<port>. NEVER hardcode a port (Hyper-V/Docker silently reserve ranges — see PORTS.md).spawn.healthEndpointlives insidespawn(not at the top level), default/health.updateManifestUrl— the full URL of YOUR manifest on YOUR page. Declare it inbridge.jsonso it survives a republish; AD polls it (on launch + every 4h) and auto-updates users. (AD also remembers the install URL in~/.adom/bridge-install-urls.jsonas a fallback, but the declared field is the durable, author-intended path.)docs— point at YOUR dedicated wiki page. AD derives your skillPkg (adom/<slug>) and your card hero from this. Don't point it at a shared/structural page.homepage— awiki.adom.incURL (your own page, orhttps://wiki.adom.inc/adom/adom-bridgefor the platform). NEVERgithub.com/...— GitHub is source-backup only; the wiki is the canonical home. (Ahomepage: github.com/adom-inc/adom-bridgeis a stale default to fix.)detect/prewarm— the v1.9.47 cold-start declarations (full rules in "Cold-start tiers" below).detect= your HOST APP (AD detects, never installs);prewarm= your heavy RUNTIME assets (AD prewarms/auto-installs). A node bridge that ships its deps still works withoutprewarm; declareprewarm.assetsonly for big downloaded assets like a browser.timeouts(v1.9.79) — per-verb HTTP budgets (seconds) AD honors so a SLOW verb (a big export, a cloud open, a long render) isn't cut off before it finishes. Resolution: exactverbs(BARE names, no prefix) → first matchingprefixes→default→ AD's global 60s. A timeout is NON-terminal: AD returns{stillRunning:true, statusVerb, timeoutSeconds}and the caller polls yourstatusVerb(so ALSO declarestatusVerb). ⚠ Before v1.9.79 a third-party bridge was hard-capped at 60s with no way to raise it — if any of your verbs can run longer, declaretimeouts(this is the fix). AD reads it with ZERO AD change; the two bundled EDA bridges keep a labeled built-in table only until they declare their own.
Artifact 1 — the RUNTIME: publish your .zip as a RELEASE + a manifest
This is the code AD streams onto the user's machine. Two files on your page:
- A versioned Release
.zipof your bridge dir (server +bridge.jsonat the zip ROOT + any assets). Ship it as a Release (the downloadable, version-pinned artifact), not a loose/filesblob:
(Node bridges: the zip carries SOURCE only — keepadom-wiki release create adom/adom-bridge-<bridge>-bridge <version> --title "..." --changelog "..." adom-wiki release upload adom/adom-bridge-<bridge>-bridge <version> your-bridge-v<version>.zip --platform anynode_modulesout; AD/the seed handle deps. See Prewarm.) - The manifest JSON (e.g.
blender-manifest.json) — identity + where the zip is — POSTed to your page/files:{ "name":"blender", "version":"1.2.0", "url":"https://wiki.adom.inc/download/adom/adom-bridge-blender-bridge/1.2.0/blender-v1.2.0.zip", // base-relative, extension-agnostic (.bin works where wiki blocks .zip) "sha256":"…", "size":123456, "verbPrefixes":["blender_"], "healthEndpoint":"/health", "updateManifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-bridge-blender-bridge/files/blender-manifest.json" }
Users install/update with AD's bridge_install {manifestUrl} → AD downloads the zip, sha256-verifies, unzips into %LOCALAPPDATA%\Adom Bridge\bridges-cache\<name>\, rescans, and remembers the URL for auto-update.
To ship a new version: bump version in BOTH bridge.json (in the zip) and the manifest, re-release upload the zip, re-POST the manifest. Users auto-get it on the next 4h poll, or immediately via refresh_bridges {name}. A bundled SEED (a plugins/<name>/ folder shipped in AD's NSIS) is the offline/first-run fallback; your published cache copy with a higher version supersedes it (cache-over-bundled, version-aware). ⚠ A release upload REPLACES a same-platform asset ONLY when the FILENAME is IDENTICAL — a different name (a version bump -v1.2.0 → -v1.3.0) ADDS a second asset, leaving a stale zip on the release. Keep ONE asset per release: overwrite the same filename, or adom-wiki release delete-asset <owner/slug> <ver> "<old-filename>" after a rename. (A stale extra won't break the manifest-URL install, but it clutters the release.)
Artifact 2 — the SKILLS pkg: container-side docs (.claude/skills / .codex/skills)
Your runtime runs on the user's DESKTOP; the AI driving it runs in a cloud/WSL2 CONTAINER. That container needs your consumer SKILL.md locally to know your verbs. Ship it as its own adom-wiki pkg (skills only, NO binaries):
Layout (reference: adom/adom-wiki-skillpack):
- The pkg carries ONLY your USER skill(s). Each is a real
skills/<name>/SKILL.md. A ROOTSKILL.md(required by the registry) is the pack entry point:user-invocable: true, adescriptionfull of trigger words. Large surface → a core user skill + user sub-skills, but every skill in the pkg is a user skill. - DEV + PUBLISH skills do NOT go in the pkg — they live in your source repo (a developer gets them by cloning to edit the bridge). Do NOT list them in
files[], and do NOT "scope" them in withuser-invocable:false(that still ships them and bloats every container install). - Declare
dependencies:{"adom/adom-bridge":"^1.9.x"}inpackage.json—pkg installresolves it npm-style, so installing your bridge's skill pkg also pulls the AD CLI + core skills the container needs to drive you. - Each sub-skill's body opens with a
Parent skill:line (first line after the frontmatter):Parent skill: **adom-bridge-<bridge>-bridge**. - List every user-skill path EXPLICITLY — no globs — in
package.jsonfiles[]ANDinstall.sh/uninstall.sh(oneinstall_skill <dest> <src>line each, kept in sync). The publish linter REQUIRES explicit listing. (The old "useskills/**" advice is dead.)
# Stage dir: ROOT SKILL.md + skills/<name>/SKILL.md per USER skill (dev/publish stay in the repo),
# package.json {name:"adom-bridge-<bridge>-bridge", version, discovery_triggers,
# dependencies:{"adom/adom-bridge":"^1.9.x"},
# files:[ every USER-skill path, explicitly ], scripts:{install,uninstall}}.
# TWO traps that still bite — verify each with `tar tzf` before publishing:
# 1. ⚠ pkg pack/publish HONORS the stage dir's .gitignore — a gitignored skills/ ships
# a 3 MB tarball with ZERO skills. Set .gitignore aside across pack+publish, restore after.
# 2. ⚠ Keep the tarball LEAN — NO release zip, NO src/ tree, NO hero PNGs (it was 16 MB
# before guards). Skills + docs only.
adom-wiki pkg pack # then: tar tzf <tgz> → every skills/<name>/SKILL.md
# listed in files[] present, ZERO binaries
adom-wiki pkg publish --org adom # publishes adom/adom-bridge-<bridge>-bridge
skill_count on your page counts the USER skills your published PKG carries — so it should be SMALL (1 for a single-skill bridge, a handful for a core + sub-skills). If it looks inflated, you probably shipped dev/publish skills that belong in the repo instead. The metric is NOT "SKILL.md files in the repo," so a type:skill page can read skill_count: 0 while its repo holds many — don't chase parity across page types (wiki-side quirk).
A container gets it without anyone asking:
bridge_installreturnsskillPkg+installSkill(AD derivesadom/<slug>from yourdocs) and the_hinttells the AI to run it.adom-bridge-cli sync_skillsinstalls every connected desktop's bridge skillPkgs (the CLI also runs it once/day in the background), so a container that connects a week later self-heals your skill.- The wiki page's Skills tab lists the skills your pkg ships, so the page itself advertises them.
Install to BOTH agent homes. A container may run Claude OR Codex — your pkg's install.sh must deploy the user skill to ~/.claude/skills/ AND ~/.codex/skills/ (Codex is a real second skills home now). adom-wiki pkg update keeps both fresh.
⭐ REQUIRED: say on your PAGE that pkg install gives SKILLS, not the bridge
Your page carries two artifacts that look interchangeable and are not, and users (and AIs) reasonably assume adom-wiki pkg install adom/<your-bridge> installs the bridge. It does not. Installing the package alone gives skills with no bridge; bridge_install alone gives a bridge whose calling AI has no skills. Most setups want both, and nothing on a default bridge page says so.
So state it, in ONE plain sentence, in both your package description and your README. Keep this line, or something equivalent:
This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.
Keep it to one sentence. The package description renders as your page subtitle AND the text under the Install card, so a multi-paragraph architecture essay there actively hurts: it buries the one fact a visitor needs. (AD-core overshot exactly this and had to trim it back.) The long version belongs in your README body, not the card.
⚠ Empirically, neither card renders a per-item caption today (verified in-page 2026-07-07): the release title/changelog does not surface on the Download card, and the package description surfaces only as the page subtitle, never at the Install card itself. Setting both fields is still correct hygiene, and they do show on the Releases tab, but do not assume the cards self-explain until the wiki exposes a caption slot (filed as adom/wiki #93).
⛔ Payload rules — both artifacts are RUNTIME-ONLY / SKILLS-ONLY. Media lives on the PAGE.
This is the single easiest way to hurt every one of your users, and it is invisible from your machine.
The real case (fusion, caught 2026-07-19): adom/fusion-bridge's release zip was 16.7 MB, of which ~15.3 MB was demo MP4s, screenshots and repo docs. Every user's machine streamed that on bridge_install, and again on every auto-update. Stripped to runtime-only it is 456 KB — 97% smaller — with zero functional change.
Nobody was careless. The zip was built from git ls-files (or by zipping the repo), which sweeps in exactly the things that legitimately belong on your WIKI PAGE: heroes, screenshots, demo videos, architecture diagrams, long-form docs.
Three destinations, three payloads:
| destination | carries | never carries |
|---|---|---|
Release .zip (streamed to every user's machine) |
server code, handlers, add-in/plugin code, bridge.json, resources the runtime READS AT EXECUTION TIME |
media of any kind, screenshots, demo videos, README/CHANGELOG, .github/, tests, dev docs, node_modules |
pkg tarball (extracted into every container's $HOME) |
SKILL.md + your USER skills/**, install.sh/uninstall.sh, package.json, page.json |
media, the release zip, src/, dev/publish skills, binaries |
the PAGE (/files, served to browsers) |
hero, screenshots, demo videos, diagrams, long-form docs | binaries (see the release/pkg split) |
Media on the page is served to a browser when someone looks at it. Media in an artifact is pushed to every machine that installs you, forever, on every update. That is the whole distinction.
How to not do this:
- Build the zip from an explicit whitelist, or from
git ls-filesminus media and repo cruft:git ls-files | grep -viE '\.(mp4|webm|png|jpg|jpeg|gif|svg|zip)$' \ | grep -viE '^(screenshots|docs|demo|\.github)/' \ | grep -viE '(CHANGELOG|PUBLISHING)\.md$' | zip -@ my-bridge-v1.2.3.zip - Know your number. A bridge runtime zip is normally well under 1 MB. If yours is bigger, that is not automatically wrong (a bridge with real runtime assets can be), but it IS your cue to list the contents and justify every entry before uploading.
- Inspect before you publish, every time — this is the step that actually catches it:
Sorting by size is the trick: the offender is always at the top, and a glance tells you whether you are shipping code or a video.unzip -l my-bridge-v1.2.3.zip | sort -k1 -n -r | head -20 # biggest files first tar tzf my-pkg.tgz # zero media, zero binaries - Keep
files[]an explicit text-only list (every USER-skill path, no broad globs). A glob is how images get in.
A publish-time lint would catch this automatically and has been asked for: a warning when a release asset or pkg tarball contains video/image files, with the size breakdown. That lives in the wiki CLI, not in your bridge, so until it ships the inspect step above is the only thing standing between you and a 15 MB download on every user's machine.
Reference: the AD-core pkg itself follows this — 18 files, zero .png/.mp4/.exe, and its docs/** images were removed from files[] in 2026-07-06 for exactly this reason (4.56 MB of PNGs, including a 1.38 MB hero, were shipping inside every container install).
Artifact 3 — DISCOVERABILITY: discovery_triggers so a plain request finds you
The wiki has an auto-discover index, and it is the difference between a usable bridge and an invisible one. Declare discovery_triggers (the phrases a user actually says) on your page, and a plain request matches them via adom-wiki discover find / the in-app discovery an AI runs.
adom-wiki discover triggers adom/adom-bridge-<bridge>-bridge --add "open my pcb" "export gerbers" # add triggers
adom-wiki discover preview --json "open my pcb design" # what would surface + the SCORE + WHY
adom-wiki discover find "screenshot my website" # the live find an AI runs
How scoring actually works (measured 2026-06-30 — know this or you'll guess wrong):
score = 3×(distinct triggers matched) + 1×(tags) + 1×(brief/text). Trigger matches dominate (3× each); tags/title/brief barely move it.- NO TRIGGERS = INVISIBLE. A bridge with a perfect title ("Adom Bridge - Blender Bridge") + a matching tag scored ZERO for the literal query "blender" — it never appeared in
discover preview. The scorer ignores title/tags when there are no triggers. Every shipped bridge MUST carrydiscovery_triggersor it cannot be found by the discovery an AI uses. (discover searchfull-texts the readme, but that is NOT the path discovery takes.) - More good triggers = higher rank. Each match is +3, so 10 user-task phrasings beat 2. The healthy bundled bridges carry 30-40 triggers; a struggling one had 14 — all dev jargon — and lost EVERY user query.
The cardinal rule: triggers are USER-TASK phrasings, NOT dev jargon. A real user types "open my pcb design" / "export gerber files" / "design a circuit board" — NOT "reverse bridge" / "kicad_bridge_call" / "fork the kicad bridge". A trigger list full of architecture terms scores 0 on every real query (kicad's actual failure: 7/7 user queries missed, and the Fusion bridge out-ranked KiCad on PCB queries because Fusion happened to carry a couple of fuzzy "pcb"/"export" triggers). Put 10+ everyday task phrasings first; technical aliases (verb names) are a small minority at the end.
VET it — never ship triggers you didn't test:
- Write 5-7 queries a typical user would type to get your tool (these double as your page
sample_prompts). adom-wiki discover preview --json "<each query>"→ readdata.scoring[](array order = rank). You must land in the top 3. (Rate-limited: space calls ~2 s; expect a 429 after ~25.)- For every query where you're NOT top-3, add that exact phrasing as a trigger and re-test. If a sibling bridge out-ranks you on a shared word (Fusion vs KiCad on "pcb"), add MORE exact phrasings so your 3×-per-trigger total wins.
Claim YOUR niche — don't poach a sibling's territory. Discovery routes by whose triggers match; if two bridges match a query the user lands on whichever has more triggers, NOT whichever is actually right. So your triggers must describe what YOU are genuinely best at — and you must LEAVE OUT cases a sibling owns. Canonical example: logging into a site / filling a form on a signed-in page is the native-browser extension's job — it drives the user's REAL browser with their REAL logins — NOT puppeteer's. Pup is a fresh Chrome-for-Testing profile with no saved logins; it's for non-login automation (scraping, screenshots, testing, public pages). So pup must NOT carry log in / fill a form on a site I'm signed into triggers, and native-browser MUST. When a task needs a capability you DON'T have, make your discovery_pitch point at the bridge that does — and tell the user to install it (e.g. "for logged-in form-filling, install the native-browser extension") — rather than claim it and do it badly.
discovery_pitchis the one-liner the snippet quotes back to the AI — auto-generated fromdiscovery_triggers+discovery_pitch, no separate upload step. Make it an INSTRUCTION, not a tagline.- Make your
brief/discovery_pitchan instruction — it's what the AI reads from the find result. Tell it the next moves: "…checkadom-bridge-cli bridge_listto confirm the bridge is installed; if it is, drive its<prefix>_*verbs. If the container lacks the skill,adom-wiki pkg install adom/adom-bridge-<bridge>-bridge(oradom-bridge-cli sync_skills)."
The end-to-end flow your three artifacts enable
User says "show me my app in pup" → the AI:
adom-wiki discover find "show me my app in pup"→ your bridge surfaces (Artifact 3).adom-bridge-cli bridge_list→ is your bridge installed in AD? If not,bridge_install {your manifestUrl}(Artifact 1).- Is your skill pkg installed in THIS container (
~/.claude/skills/or~/.codex/skills/)? If not,adom-wiki pkg install adom/<slug>/sync_skills(Artifact 2). - Drive your verbs — guided by the rich hints you return (the principle at the top).
Prewarm — make the first call instant
A cold first call that pays multi-minute setup (npm install, downloading a runtime, a heavy import) blows the caller's timeout. Pay that cost in the background BEFORE the user asks:
- Bundle deps in your SEED. Pup ships a bundled
node_modules/in its NSIS seed so a fresh PC can spawn with nonpm install. When you re-seed from a Release zip, keep the bundled deps. - Idempotent warm-up. Make spawning + a no-op health/setup pass idempotent so a background prewarm is a no-op once warm.
- AD prefetches for you on first launch — standalone AND embedded (HD-bundled) — gated on availability. (Pup's Node + Chrome-for-Testing prefetch runs once per install in both modes as of AD v1.9.45.) If your bridge needs a heavy runtime, expose a cheap idempotent "ensure ready" path AD/your seed can trigger, and surface a clean
errorCode(e.g.node_not_found) when a prereq is missing so the AI can install it (desktop_install_node) instead of hanging. browser_readiness-style verb. Offer a fast verb that reports{ready:true|false, …}so a caller (or a fresh-PC test) can confirm warmth without triggering work.
Card hero + update cost — two gotchas that bite
Hero: ship a 1.6-aspect image, or AD crops the OG-card fallback. AD's bridge card renders the hero in a banner that is aspect-ratio: 1.6 with object-fit: cover. So:
- Ship a 1.6-aspect hero (≈2000×1250), full-bleed — like pup & kicad. It fills the banner cleanly.
- If you ship no hero (
bridge.jsonhas noheroAND your wiki page has nohero_path), AD falls back to your page's auto-generated OG card (1200×630 = 1.90 aspect, the social-card size).coverscales that wider image to the banner's height and clips the sides — the left text panel gets cut off. (That cropped, off-center look is the tell of a hero-less bridge — it's not an AD bug.) - Fix: publish a real hero at 1.6 (the
adom-wikipage hero, or aheroURL inbridge.json). Verify by opening your card in AD — the title/text must not be clipped.
Update cost: keep the Release zip source-only; re-seed the NSIS bundle in lockstep. When a user clicks the card's "↑ v…" update badge, AD downloads your Release zip into the cache and (for a node bridge) provisions deps:
- The Release zip MUST be source-only — no
node_modules. AD provisions node deps at spawn: it reuses the bundled seed'snode_modulesviaNODE_PATHwhen the cache version ≤ the seed version (instant), and runs a realnpm installonly when the wiki version is newer than the bundled seed. - So a wiki version much newer than your bundled seed forces a cold
npm installon the next spawn — slow for a heavy tree. When you publish a new Release, bump the bundled NSIS seed in lockstep (same version) so most users get the instant NODE_PATH reuse, not a cold install. - AD (v1.9.52+) never blocks the UI on this: the update downloads straight to cache (no wasteful seed re-copy — the old code deep-copied pup's 73 MB / 4698-file seed and then discarded it, the "updating… forever" hang), the badge shows live progress (
downloading…→ done) with a 30s "installing…" fallback, and your bridge keeps serving the old version until the new one is ready. A lean zip + a lockstep seed is what keeps it fast.
Cold-start tiers, host-app detection & the bridge_readiness probe (v1.9.47)
A bridge has TWO kinds of dependency, and AD treats them OPPOSITELY. Get this right or a first-install probe (e.g. HD's "what tools do I have") triggers a slow install at the worst moment.
Tier 1 — your RUNTIME (Node, Python, your own browser download): AD prewarms + auto-installs it. This is your plumbing. On first boot AD provisions Node/Python in the background — a portable, no-UAC copy under ~/.adom/adom-runtimes/ (or a system install if one's on PATH), NOT winget/MSI (see Runtime contract above) — and prewarms heavy assets you declare. You may also download your own runtime asset (pup downloads Chrome-for-Testing) — fine, it's yours. But do it in the BACKGROUND: a *_status / *_readiness / ANY probe verb must NEVER block on a heavy download. Spawn your server fast (it usually doesn't need the asset to start), kick the download off detached, and report state:"warming" until ready. A synchronous-download-on-first-verb is the exact footgun this release fixed (pup's 150 MB Chrome fetch used to block the probe).
Tier 2 — the HOST APP (KiCad, Fusion, Altium, …): AD DETECTS it by default and installs it ON REQUEST — never pre-emptively, and NEVER by telling the user to do it themselves. Two rules that sound opposed but aren't:
- Don't install it unasked. A multi-GB app nobody asked for is rude, so by default AD only DETECTS presence (a machine with KiCad belongs to a KiCad user; EDA users are mono-tool — a Fusion user isn't a KiCad user). AD won't prewarm/spawn your bridge while the app is absent — a resource decision, not a "go away."
- But the cardinal Adom rule overrides everything: the AI does EVERYTHING for the user — it must NEVER tell the user to do something the AI can do itself. So your bridge MUST know how to install the host app (
desktop_install_kicad/ winget / your own install verb) and just DO it the instant the user wants it. Your "not installed" hint must OFFER to install it ("KiCad isn't installed — want me to install it for you?"), then do it on a yes — it must NEVER say "download it from kicad.org." Pointing the user at a manual download is the breach.
Bridge install LEVELS — how eagerly each bridge gets onto the machine. The AI ALWAYS does the install; the level only sets the TIMING/CONSENT:
| Level | Bridge(s) | Behavior |
|---|---|---|
| 0 · Built-in / MUST | Hydrogen Desktop (hd) |
The platform itself — the editor AD lives in. Always present; not "installed" by AD. |
| 1 · Fundamental — pre-emptive | Puppeteer (pup) |
Browser automation is foundational to Adom. AD pre-installs its RUNTIME (Node + Chrome-for-Testing) ASAP on first boot, no asking. |
| 2 · Recommended — proactive offer | native-browser (the browser EXTENSION half) | The user's REAL logged-in browser (login/forms). Since v1.9.142 the DESKTOP BRIDGE half ships bundled in the AD installer (0.2 MB, zero deps, persistent:true — its extension-free verbs like nbrowser_open_os_window/nbrowser_profiles/nbrowser_os_windows work day one). What remains level-2 is the in-browser EXTENSION: AD/the bridge proactively SUGGESTS it and it installs on a yes (a browser-extension step the user confirms), unlocking full in-page drive. |
| 3 · On-request host app | KiCad, Fusion, Altium, … | Detect by default; NEVER pre-install (huge app, rude unasked). The moment the user wants it, the AI installs it AUTOMATICALLY (winget / desktop_install_*) — NEVER tells the user to do it manually. |
| 4 · Explicit third-party | any bridge_install'd bridge |
The user opts in by installing it. |
The through-line: the AI never makes the user do something it can do itself. The only knob is when — pre-emptive (pup), proactively-offered (native-browser), or on-request (host apps).
Declare both in bridge.json:
"detect": { // Tier 2 — how AD finds your HOST APP (detection only)
"hostApp": "KiCad",
"appPathsExe": "kicad.exe", // optional: Windows App Paths registry lookup (most reliable)
"paths": { // optional: candidate paths, %VAR% expanded, one * = a version dir
"windows": ["%ProgramFiles%\\KiCad\\*\\bin\\kicad.exe"],
"macos": ["/Applications/KiCad/KiCad.app"],
"linux": ["/usr/bin/kicad"]
}
},
"prewarm": { "assets": ["chrome-for-testing"] } // Tier 1 — heavy runtime assets AD prewarms + reports
A bridge that brings its OWN runtime (pup ships its own browser) omits detect — it has no external host app. AD infers the runtime from spawn.kind (node→Node, python→Python).
The read-only probe: bridge_readiness. AD aggregates all of this into ONE side-effect-free verb the setup AI calls — it never spawns a bridge, never installs/downloads. Per bridge: {hostApp, hostAppInstalled, runtime, runtimeReady, assets, state} where state ∈ ready (use it) | warming (runtime ok, a heavy asset still downloading in the background — re-poll, don't trigger it) | needs-runtime (node/python installing) | no-app (host app not installed → not relevant here). Plus a top-level edaToolsInstalled list — the one-line answer to "what EDA tools do I have." Tell your users (in your skill): for a "what do I have / is it ready" question call bridge_readiness, NOT your *_status verb — a *_status may spawn your bridge; readiness never does.
Your two own cold-start verbs (above) still apply: <prefix>_readiness (read-only {ready, installing, installProgressPct, lastError}) + <prefix>_prewarm ({wait}-able). Those drive YOUR bridge specifically; bridge_readiness is the AD-level aggregate that detects + summarizes across all bridges without spawning any.
The SKILL SET you should ship (not one SKILL.md — a small library)
Don't cram everything into one file. Ship three kinds of skill, each with a different reader and a different home — the USER skill goes in the pkg; the DEV and PUBLISH skills stay in your source repo:
- A PUBLISH skill — lives in your source REPO (for the bridge's OWN cloud/dev thread): how to cut the Release zip, POST the manifest, bump versions, publish the skills pkg (with the
.gitignoretrap), setdiscovery_triggers, and re-seed AD's bundled copy. This keeps your future self from getting the publish dance wrong. - A DEV skill — lives in your source REPO (for whoever edits the bridge): architecture, the verb contract, how to test locally, the boundary with AD — and, front and center, the rich-hints-in-output rule: the AI never reads this file; it reads your verb OUTPUT, so every verb must return
_hint/_next/related/pitfalls. Make that the loudest thing in the dev skill. - USER skill(s) — shipped in your pkg → the container: how a cloud AI drives your verbs to get real work done. If the verb surface is large, split it: a small core USER skill (the 80% workflow + a pointer to the sub-skills) plus sub-skills by area (e.g.
…-recording,…-electronics,…-aps-search). A 2,000-line monolith won't get read; a tight core skill that names the sub-skills will. (Pup ships exactly this shape: a core bridge skill + user sub-skills.)
Only the USER skill(s) go in your pkg (Artifact 2), each a real skills/<name>/SKILL.md. The DEV + PUBLISH skills stay in your source repo — a developer gets them by cloning to edit the bridge; they are NOT in the pkg's files[], and you do NOT "scope" them into the pkg with user-invocable:false (that still ships them and bloats every container install). Open-vs-closed source doesn't change this — it only sets whether your repo's dev/publish files are publicly readable.
Start from the templates — don't write these from scratch. Fill-in-the-blanks files for each of the three skill kinds, generalized from pup's battle-tested set. Fetch them from THIS page (they used to be cited as a path inside AD's own repo, which is private, so the pointer was unfollowable for exactly the cloud authors it addressed — fixed 2026-07-25):
BASE=https://wiki.adom.inc/api/v1/pages/adom-bridge-sdk/files
curl -fsSL $BASE/bridge-dev-template.md -o dev-skills/<bridge>-dev.md
curl -fsSL $BASE/bridge-publish-template.md -o dev-skills/<bridge>-publish.md
curl -fsSL $BASE/bridge-user-skill-template.md -o skills/<bridge>/SKILL.md
bridge-dev-template.md— architecture, the rich-hints rule, caller-provenance handling (logging the thread, arbitrating sessions, forwarding on your own AD callbacks).bridge-publish-template.md— the whole release/pkg/discovery dance and every trap (the.gitignoreset-aside, the lean-tarball rule, release-and-pkg-ship-together).bridge-user-skill-template.md— the consumer skill, already encoding all eight conventions below plus the--ai-threadidentity line.
Copy the one you need, swap <bridge>/<prefix> for your names, delete what doesn't apply. How your skills reach a container in the first place (the runtime installs on the desktop, the skills install in the container, and the two travel by different routes): docs/bridge-skill-propagation.md on this page.
USER-skill conventions — what every consumer skill must teach (from pup's shape)
These eight patterns are what make a cloud AI actually succeed with your bridge on the first try. The user-skill template encodes them; bake them into whatever you ship:
- Open with a "FIRST-TIME / COLD-START — read before you panic" section. A fresh PC may lack your runtime or owe a one-time download, and the AI's default failure mode is to declare defeat. Lead with a response → meaning → what-you-do table that frames
*_installing/not_readyas EXPECTED, keyed offerrorCode(not prose). - Standardize two cold-start verbs:
<prefix>_readinessand<prefix>_prewarm.readiness→{ready, installing, installProgressPct, lastError};prewarmkicks off setup without real work and supports{"wait":true}to block. Gives every bridge a uniform "poll until ready, then proceed" loop. (Complements the runtime-side Prewarm section above.) - End the
description:frontmatter with aTrigger words:line mixing verb names AND plain-English phrases a user would actually type ("take a screenshot of", "log into"). This is what makes a user-invocable skill activate — and it feeds yourdiscovery_triggers(Artifact 3). - Include a short "Core verbs" table (Command | Description | Key args) for the 80% path, and point at
<prefix>_describe '{}'for the full machine-readable catalog. The AI reads the table to act fast;describeis the completeness escape hatch — don't bloat the skill with every verb. - "Background by default — don't disrupt the user." Your bridge drives the AI's workspace, not the user's screen: lower/un-focus after automation, never force-foreground except to SHOW the user something, nudge via a taskbar flash (
*_alert_window) instead of stealing focus. Standardize*_lower_os_window/*_raise_os_window/*_alert_window. - Teach "
ok:trueis not enough" — verify what you did. Verbs that return on operation start (not completion) need a cheap post-check (an eval/read verb, else a screenshot) before reporting success — 404s, login walls, and empty states land silently. - Sessions: reuse-don't-reopen + stable identifiers. Tell the AI to
*_statusbefore opening a new session (especially after compaction), reuse a match, and pick a stableprofileso logins/state persist across runs. Prevents duplicate windows and the lost-login footgun. - "Surface
_hintverbatim." Every verb returns an actionable_hinton not-ready/failure; instruct the AI to relay it to the user unchanged, and branch on the stableerrorCode(not the prose). This closes the loop with THE ONE PRINCIPLE at the top — the hint IS your UI.
Verb contract (quick reference)
- Naming: every verb is
<name>_<verb>(blender_render,blender_status). AD routes any verb with your prefix to you. - Request: AD POSTs
{ "command":"<verb>", "args":{…} }to your port. Response: JSON with at leastsuccess(bool); human/AI output inoutput, errors inerror(+ anerrorCodefor machine-actionable failures). AD relays everything else verbatim — so ALWAYS add_hint/_next/related/pitfalls. - Long-running verbs: return promptly with a job id + set
statusVerb; the caller polls instead of blocking. On a timeout AD auto-returns{stillRunning:true, statusVerb, timeoutSeconds}. If a verb genuinely blocks past 60s (a synchronous export/render), also declare atimeoutsblock (v1.9.79) so AD's HTTP budget matches — otherwise AD cuts the connection at 60s (third-party bridges were hard-capped there before v1.9.79). <prefix>describe: return your full verb catalog ({verbs:[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example,hint,related,pitfalls}]}). AD caches it (~/.adom/bridge-verbs/<name>.json) and shows it in the GUI Verbs tab — but remember,describeis read rarely; the per-call hints are what carry the AI.- Keyboard input: route through AD's
desktop_press_key— do NOT reimplement chords. AD's key verb already does full modifier chords ("shift+s","ctrl+shift+p","alt+f4"— modifiers held via SendInput, key tapped, modifiers released in reverse), foregrounds the target window first ({window|titleContains|hwnd}) so CEF/Qt apps like Fusion receive it, and every bridge inherits it via the/commanddesktop passthrough ({"command":"press_key", ...}on the desktop namespace). A bridge-local*_send_keythat parses only single keys (the Fusion bridge'sfusion_send_keycirca v1.6.x —"shift+s"→ "Unknown key") duplicates AD's work and drifts; keep such a verb only for SendMessage-to-exact-hwnd cases AD's focus-first model can't cover, and say so in itsdescribe. - Host-app-optional verbs (v1.9.76): declare
detect.hostAppOptionalVerbs(bare names, e.g.["readiness","describe"]) inbridge.jsonso AD forwards those verbs to your running bridge even when your host app is absent — AD's built-in read-only set {readiness, describe, get_app_state, status} is always forwarded. And declaredetect.installVerb(e.g."kicad_upgrade") sobridge_readinessrecommends YOUR installer over the generic winget fallback.
Health + status chip
Your /health (200) makes the chip green. Optionally return led (green/yellow/red), summary (short label), tooltip (full hover). AD owns the offline (gray) state when your endpoint is unreachable — you can never assert offline.
Lifecycle verbs (AD gives these to USERS — know them to guide users)
bridge_install {manifestUrl} · bridge_list · bridge_info/bridge_detail {name} · bridge_log_read {name, sinceOffset?} · bridge_kill {name} · bridge_pause {name} · refresh_bridges {name?} · bridge_check_updates. A bridge installed via bridge_install auto-updates with no further opt-in; updateManifestUrl in bridge.json makes it durable across republish.
"My bridge keeps exiting and I can't see why" (read this). Your own stdout log truncates on every respawn, so a crash-then-restart erases its own cause. bridge_log_read therefore also tails AD's lifecycle audit log for your bridge, a separate append-only file (~/.adom/bridge-logs/<name>.ad.log) that survives respawns, shown under an ===== AD bridge lifecycle ===== header. It records every spawn, reap, duplicate-collapse, and bridge_kill with the reason, so you can tell "AD reaped it as a duplicate" or "the bridge_kill verb stopped it" or "it was reaped before a fresh spawn" apart from "it crashed on its own." If the audit log shows AD never stopped it, the exit is inside your process.
Logging: AD owns the FILE, you own the VOLUME
Every bridge writes to stdout, AD captures it, and nobody asks the volume question until someone's log is a firehose. The split:
- AD owns the FILE. AD spawns you and captures stdout/stderr, so the file and its rotation are AD's. A bridge cannot rotate a file another process holds open, and bounded logs have to be a platform guarantee rather than per-author boilerplate.
- You own the VOLUME. How much you emit is the half a tidy platform cannot fix for you.
(READING the log is the section immediately above: bridge_log_read plus AD's separate lifecycle audit. This section is about what you WRITE.)
Do NOT write your own parallel log file. Two log files means two sources of truth and double the disk for the same bytes. pup built one (a rotated pup.log) and deliberately reverted it. If you think you need your own log, what you probably want is for AD to scope bridge_log_read to your bridge: file that against adom/adom-bridge (see "Filing issues against AD" above) instead of forking the log.
The five patterns that keep a bridge out of firehose territory:
Never log per-item inside a loop over live state. This is the single biggest source of volume. A drag-detection check logged one line PER TAB per check, all of them saying "(same)", so a 12-tab window emitted 12 useless lines every time the check fired. Log the exception (the tab that moved), never the roll call.
Collapse consecutive identical lines. Wrap
console.logonce, hold the last message, and emit^ last line repeated Nxwhen it changes. A retry loop then costs 2 lines instead of 200. Collapse only EXACT consecutive repeats, so no distinct event is lost. About 15 lines of code.Log static facts once per session, not per call. "no favicon on this page" was emitted on every taskbar-badge update. It is a property of the page, not an event, and because the repeats were not consecutive the collapser could not catch them. Gate it behind a per-session flag.
DO log every incoming verb, one compact line. This is the one place to ADD volume. A log that records what the bridge DID (park, identity, overlay) but never WHAT WAS ASKED of it cannot answer "what foregrounded this window?" after the fact. Include only the args that change behavior, and skip your own log-read verb so reading the log does not pollute it. Put the caller thread on this line too (the shape is in "Caller provenance" above):
[verb] browser_open_window session=parkedge url=https://example.com owner=parktestNever let logging throw. Wrap the whole path in try/catch. A logging failure must not take the bridge down.
Publish checklist (the whole modern flow)
- Page —
adom-wiki page create adom/adom-bridge-<bridge>-bridgewith a hero + an instructionalbrief+discovery_triggers. - Runtime —
release create+release uploadthe versioned.zip; POST the manifest to/files; setupdateManifestUrlin BOTH the zip'sbridge.jsonand the manifest. Before uploading:unzip -l <zip> | sort -k1 -n -r | head— runtime only, no media. (→ Payload rules) - Skills pkg — stage ROOT SKILL.md +
skills/<slug>/SKILL.md(core USER skill + sub-skills) +discovery_triggers;pkg pack(tar tzfit — skills only, zero media/binaries) →pkg publish --org adom(mind the.gitignoretrap). Yourdescriptioncarries the one-sentence "this installs SKILLS, not the runtime" line. (→ Artifact 2) - Discovery —
discover triggersto confirm;discover preview "<a real user phrase>"to confirm you surface. - Re-seed AD's bundled copy (AD-core thread, not you) — extract your Release zip over
plugins/<name>/keeping bundled deps; ships in the next NSIS as the offline fallback. - Verify —
bridge_install {manifestUrl}on a desktop →bridge_listshows yourversion+skillPkg;bridge_check_updatesreaches your page; a containersync_skillspulls your USER skill;discover find "<phrase>"surfaces you with an instructional brief.
Reference bridge = pup (adom/adom-desktop-puppeteer-bridge): live discovery_triggers, a skills pkg with a core + sub-skills, a Release zip + manifest with updateManifestUrl, a bundled seed with prewarmed node_modules, and rich per-verb hints. Copy its shape. (scripts/sample-bridges/hello-python + hello-rust are the minimal skeletons.) KiCad, Fusion, Blender, Puppeteer, and the native-browser bridge (in the adom-browser-extension repo) are cloud-owned — don't edit them from the AD repo. When an AD release changes a bridge-facing contract — or when THIS SDK doc itself changes — AD posts a migration notice to each of their wiki discussions (adom-bridge-{kicad,fusion,blender,puppeteer}-bridge + adom-browser-extension). Author-facing takeaway: watch your bridge's own wiki DISCUSSION/issues list — that's where an "SDK changed, please re-audit against the billboard" notice lands, and where you reply with your plan or blockers. It's the only channel AD uses to reach you; AD never edits your repo.