skill
Adom UI Design System
Public Made by Adomby adom
How the AI builds and tests interfaces people love to use in Adom. Ships as one installable skill that bundles focused skills (tooltips, floating-windows, toasts, responsive-ui, app-shell, ai-driven-apps, clickable-paths).
← Commit history
3.0.0: split four skills out of newbie-panel - control-api-proxy, dash-lifecycle, verify-your-work, shared-machine. A new app builder would never have found them inside a skill about a panel.
6 files changed
+630−425
package.json+5−1@@ -1,6 +1,6 @@ { "slug": "adom-ui-design",- "version": "2.23.0",+ "version": "3.0.0", "type": "skill", "description": "The Adom UI Design System plus a bundle of focused UI skills (tooltips, floating-windows, toasts, responsive-ui, app-shell, ai-driven-apps, clickable-paths, dark-default, input-feedback, settings-layout). One install drops the whole set of skill files onto your container, plus shared icon assets (the settings cog) so every Adom app draws the same glyph.", "dependencies": {},@@ -21,9 +21,11 @@ "skills/ai-driven-apps/SKILL.md", "skills/app-shell/SKILL.md", "skills/clickable-paths/SKILL.md",+ "skills/control-api-proxy/SKILL.md", "skills/dark-default/SKILL.md", "skills/dash-demo/SKILL.md", "skills/dash-header/SKILL.md",+ "skills/dash-lifecycle/SKILL.md", "skills/dash-screenshot/SKILL.md", "skills/dock-dashboard/SKILL.md", "skills/floating-windows/SKILL.md",@@ -35,9 +37,11 @@ "skills/say-strip/SKILL.md", "skills/scrollbars/SKILL.md", "skills/settings-layout/SKILL.md",+ "skills/shared-machine/SKILL.md", "skills/status-leds/SKILL.md", "skills/toasts/SKILL.md", "skills/tooltips/SKILL.md",+ "skills/verify-your-work/SKILL.md", "uninstall.sh" ], "scripts": {
skills/control-api-proxy/SKILL.mdadded+239@@ -0,0 +1,239 @@+---+name: control-api-proxy+user-invocable: true+description: >-+ How an Adom app talks to the Hydrogen control API without CORS, without a hardcoded port, and+ without silently corrupting what it forwards. Proxy the two routes (/updater/status and+ /ai-threads/run-prompt) through your OWN server so the page stays same-origin, read the+ control port from ~/.adom/hydrogen-control-url at REQUEST time because it is dynamic per ah+ launch, answer 503-with-a-hint rather than 404 when ah is absent, and PASS THE RESPONSE+ THROUGH UNCHANGED - parse for what YOU decide, never for what you HAND BACK. A JSON round+ trip reorders keys, loses integer precision past 2^53, turns 1.0 into 1, and turns a non-JSON+ body into a fabricated error object, all while every status probe stays green. Ships+ proxy-fidelity.py to prove it byte-for-byte against a stub with no user machine involved.+ Also: a catch-all route and a blanket OPTIONS handler make three separate checks pass for the+ wrong reason. Read BEFORE wiring any app to the control API or the AI threads API. Trigger+ words: send to ai, ai-threads, run-prompt, updater status, control api, proxy route, CORS,+ same-origin, version gate, delivered_to, hydrogen-control-url, preflight, catch-all, 404.+---++Parent skill: **adom-ui-design** (works with **newbie-panel**, **verify-your-work**)++# Talking to the control API from your own server++Every rule below was found by an app that had already shipped the route and believed it worked.+One had never wired it at all; two were corrupting the body; one was answering its own probe+from a catch-all. `assets/tools/proxy-fidelity.py` in this pack settles all four in one command+and needs no access to anyone's machine.++## PROXY THE TWO API CALLS THROUGH YOUR OWN SERVER++`apiBase` defaults to `''`, same origin, and it should stay that way. Add two routes to your+own server and forward them to the Hydrogen control API using whatever discovery you already+have (`~/.adom/hydrogen-control-url`, 47084 as the fallback):++```+GET /updater/status -> control API /updater/status+POST /ai-threads/run-prompt -> control API /ai-threads/run-prompt (120s, matching the module)+```++Three reasons, and parts-search, nb and pup each found them separately: it removes any CORS+dependency on ah; it keeps exactly ONE copy of the send logic in the fleet rather than one+per app; and it keeps working in both deployment shapes, the container-served dashboard and+the desktop-bridge-served one, where a direct fetch to the control port would be cross-origin+or unreachable. The dynamic control port also stays server-side rather than being baked into+a page.++**⛔ DO NOT PARSE AND RE-EMIT. BYTE-COMPARE YOUR PROXY AGAINST A STUB.**++"Pass through unchanged" is easy to agree with and easy to violate, because **the natural way+to write this route in most languages is parse-then-re-emit**, which silently reorders keys,+drops whitespace, and can turn a numeric version into a string. The module compares+`updater.current_version` NUMERICALLY after splitting on dots, and reads `delivered_to` as an+ARRAY, so a re-serialisation that changes either type changes behaviour while every "does it+return 200" probe still passes.++**The harness ships in this pack: `assets/tools/proxy-fidelity.py`.** python3 stdlib only, no+dependencies.++```sh+python3 proxy-fidelity.py --app-cmd "your-server --port $PORT" # the mode to use+python3 proxy-fidelity.py --self-test # watch it go red first+```++`--app-cmd` creates a throwaway `HOME`, publishes a stub control URL into it, starts your app+with `$PORT` and `$HOME`, runs the assertions and tears it all down. **It never touches a real+workspace**, so it is safe in a build gate. There is a `--base --home` mode for an+already-running app, but prefer `--app-cmd`: the `--base` mode works only because the control+URL is read at REQUEST time, so **an app that caches the URL at startup silently tests+nothing.** Exits non-zero on any RED.++**TO PROVE THE PER-REQUEST READ RATHER THAN ASSERT IT** (parts-search's method, and stronger+than either mode alone): start your app with `~/.adom/hydrogen-control-url` pointing at a port+with **nothing listening on it**, then let the harness repoint the file at its stub while the+process keeps running.++```+app up against the dead URL -> 200+harness repoints the file -> PASS / PASS / PASS+file restored afterwards+```++**A startup-pinned URL cannot pass that**: it would still be talking to a closed port.+`--app-cmd` alone cannot distinguish per-request from pinned-at-startup, because the file is+already correct when the app boots. This is the difference between "I read the code and it+looks per-request" and a falsifiable result.++**It cannot create a thread in John's list.** The POST assertion sends `submit:false` and only+ever to its own stub, never to a real control API.++It checks three things, and all three are failures a 200-probe sails past:++| assertion | the bug it catches |+|---|---|+| response sha256 identical, including awkward whitespace and a nested `delivered_to` | parse-then-re-emit |+| a non-200 status survives (upstream 207 returned as 207) | a hand-built `200 {ok:true}` wrapper |+| an unproxied path under the same prefix returns YOUR 404 | blanket-proxying the control API |++**A JSON round trip is lossy in three ways that all look harmless.** pup's relay used+`JSON.parse` then `JSON.stringify`, not serde, so it did not alphabetise and therefore looked+fine. It still: **loses precision on an integer past 2^53**, turns **`1.0` into `1`**, and+worst, turns a **NON-JSON upstream body into an invented `{"error":...}` object** — converting+a valid response into a fabricated failure that no status code contradicts. That third one is+the reason "it returns 200" proves nothing about a proxy.++**PARSE FOR WHAT YOU DECIDE. NEVER PARSE FOR WHAT YOU HAND BACK.** parts-search's rule, from+fixing its own route: it still parses the upstream JSON for its own decisions, and forwards+the response bytes untouched with the `Content-Type` carried through. Those are two different+jobs and only one of them is allowed to re-serialise.++**The harness caught the app that INVENTED this pattern**, which is the strongest thing that+can be said for it. parts-search wrote the first same-origin proxy in the fleet and was RED on+assertion 1, with damage worse than the negative control's: its route parsed into a serde+`Value` and re-emitted, and serde's map is ordered, so the round trip **alphabetised the+keys** at both levels. `updater` moved to the end and `build` moved ahead of `current_version`+inside it. Same JSON, different bytes, different reading order for anything that streams or+diffs it.++**And the asymmetry in which route failed is the usual distribution:** its POST already+returned the upstream status, because it had written that one deliberately while hunting+phantom send failures. The GET was the one with a hand-written `200` beside it. **The route+someone thought about carefully was fine; the one-liner next to it was not.**++Run this before spending a slot. A round trip through his box tells you less and costs more.++**Run `--self-test` first, every time.** Its negative control is a proxy whose only sin is one+line of parse-then-re-emit, and it goes RED on two of the three while the third stays GREEN.+That third staying green is the point: **the assertions are independent, so a run tells you+WHICH bug you have** rather than only that something is wrong. The diff it prints on the+byte-for-byte failure shows the damage is purely `{"updater":{` becoming `{"updater": {` —+JSON that is equal, bytes that are not, and behaviour that can differ. The self-test exits 2+if the negative control somehow passes, i.e. if the harness itself is broken.++**READ THE CONTROL PORT AT REQUEST TIME. IT IS DYNAMIC PER ah LAUNCH.** Read+`~/.adom/hydrogen-control-url`; do not hardcode 47084 or any port you once observed. A pinned+port works until his next ah restart and then fails silently, which is the worst failure shape+available: it was working, nothing you changed broke it, and nothing says so.++**WHEN THE CONTROL API IS UNREACHABLE, ANSWER 503 WITH A HINT, NEVER 404.** project-manager's+routes return 503 naming the missing file when there is no ah on the host (its container, or+any non-ah machine). The reason is diagnostic, and it comes from a real ambiguity my own probe+hit: **a 404 is indistinguishable from "the route was never wired".** I read a 404 from its+dashboard and told it the route was registered on a wrong path; it was in fact never wired at+all. A 503 with a hint separates "wired but the backend is absent here" from "not wired",+which are different bugs with different owners.++**PASS THE RESPONSE THROUGH UNCHANGED.** Do not reshape the JSON on the way back. The module+reads `delivered_to` itself (it is an ARRAY) and separates *delivered* from *opened but not+confirmed*; an app that "helpfully" normalises the payload collapses three states into two,+and the retry that follows double-sends a prompt that already landed. That has happened to+two apps already. Keep the 120s timeout on `run-prompt` too, so a queued call is not turned+into a phantom failure.++## ⛔ A MISSING GATE READS AS AN ALLOWED GATE. PROBE IT WITH `checkSend()`.++`gate()` fails OPEN on purpose, so a broken probe cannot stop a user sending. The consequence+is invisible and kicad found it in itself: its `/updater/status` route **had been lost in a+rebuild**, so the gate received HTML, caught, and returned open. Its version gate had been+silently no-opping, and nothing anywhere said so. It could not have found this by reading its+own code, **because the code it was reading no longer existed.**++```js+await AdomNewbiePanel.checkSend() // -> {ok, rows:[gate wired, version, minimum, send wired]}+```++**It never posts a prompt.** The run-prompt route is probed with `OPTIONS`, so it reports+"wired" without creating a thread in anyone's list. Negative-controlled before shipping: with+no routes it goes red on all three; with a stub gate and a wired POST it goes green.++Run it beside `selfCheck`. `selfCheck` proves the panel is right; `checkSend` proves the button+on it can do anything.++## ⛔ A CATCH-ALL ROUTE MAKES THREE DIFFERENT CHECKS PASS FOR THE WRONG REASON++**Test it in one line:**++```sh+curl -o /dev/null -w '%{http_code}' <your base>/nonsense # 404 expected. 200 means read on.+```++nb served `page()` for every unmatched path. Three consequences, and it had a clean pass on all+three before it looked:++1. **The 404 watch on a virgin install could never fire.** A missing settings PNG, demo mp3 or+ favicon came back as HTML 200 instead of 404 — and the launch-console 404 watch is the ONE+ thing watched on a fresh install.+2. **`proxy-fidelity` row 3 passed wrongly.** It asserts "not upstream's 418"; a catch-all+ returns 200, which is not 418, so a blanket-proxy check passed on an app that was blanket+ -serving.+3. **`checkSend`'s "send route wired" row passed wrongly**, and this only surfaced once #1 was+ fixed. The module probes with **OPTIONS**; nb's route matched **POST only**, so the+ catch-all was answering the probe with HTML 200. With unmatched paths 404ing, the row went+ red and honest. **If your dashboard proxies those two routes and matches POST only, your+ green on that row is your catch-all, not your route.** Answer a real CORS preflight (204).++This is kicad's "passes for the wrong reason" arriving three times in one app, hours after it+was named. **A permissive default does not fail your checks; it answers them.**++**THERE IS A SECOND CATCH-ALL, AND IT IS THE ONE THAT MATTERS.** kicad fixed its GET catch-all+and its send row stayed green, because its `do_OPTIONS` returned **204 for every path+unconditionally**:++```+OPTIONS /nonsense -> 204+OPTIONS /ai-threads/run-prompt -> 204 indistinguishable, which is the point+```++So an app can fix the obvious catch-all and still have a row that tests nothing. **Extend the+one-line test to the preflight:**++```sh+curl -o /dev/null -w '%{http_code}' -X OPTIONS <base>/nonsense # must also 404+```++**`checkSend` now defends itself against this** (module 2.21.0): it probes the real route AND a+path that cannot exist, and requires the two to ANSWER DIFFERENTLY. "Not 404" was too weak an+assertion. Controlled both ways: a blanket preflight reads `route 204, nonsense 204` and goes+red; honest routing reads `route 204, nonsense 404` and passes. **If a server answers a path+that cannot exist, it is answering the shape of the question rather than the question.**++The structural fix, and the one to copy: **both handlers read ONE route table**, so a route can+never be live for OPTIONS and dead for GET. Two independent notions of "does this route exist"+was the actual defect.++**⛔ DO NOT FIX THIS WITH A BLANKET 404, AND HERE IS THE REAL CONSTRAINT.** adom.cloud mounts a+dashboard under a sub-path AND FORWARDS THE PREFIX, so the app's own root arrives as+`/apps/kicad`, which is shape-identical to a typo. **A blanket 404-unless-slash passes the+one-line test and breaks the app in production.** kicad's rule instead:++- 404 if the path is **asset-shaped** (a known file extension), whoever asks+- 404 if it is inside a **namespace you own** (`/api/`, `/ui/`, `/updater/`, `/assets/`,+ `/ai-threads/`), whoever asks+- always serve `/`+- otherwise serve the shell only for a **real browser navigation** (`Accept: text/html`)++**Stated with its caveat rather than as a clean claim, which is how kicad reported it:** a+browser typing a wrong extensionless URL still gets the shell. That is accepted, because what+the 404 watch actually watches is missing pngs, mp3s, favicons and js — all of which carry an+extension or an owned prefix and 404 regardless of client. Verified against an image `Accept`+header. **Do not fix a passes-for-the-wrong-reason bug by shipping another one.**
skills/dash-lifecycle/SKILL.mdadded+135@@ -0,0 +1,135 @@+---+name: dash-lifecycle+user-invocable: true+description: >-+ Getting a NEW build of your app in front of the user, and proving it is the one they are+ looking at. Publishing is not installing, installing is not running, and running is not what+ the open page has loaded: "current" is THREE separate claims and any one of them can lie+ alone. Covers the dock installing a package without restarting the process, the "(deleted)"+ Binary LED that every dashboard already emits when it is stale, why a container-served app+ and a bridge-served app have completely different retirement paths, how to rehearse an+ upgrade against your REAL predecessor from the registry (the retirement path's input is the+ OLD build's output, so it cannot be tested by reading your own source), and why you must+ retire by PORT OWNERSHIP rather than argv - one app killed an unrelated dashboard because+ every instance of a tool has identical argv. Read BEFORE shipping a new version of any Adom+ app or dashboard. Trigger words: deploy my app, ship a new version, my changes are not+ showing, stale dashboard, restart the server, retire the old process, serve verb, upgrade+ path, which version is running, dock installed but nothing changed, port already in use.+---++Parent skill: **adom-ui-design** (works with **dock-dashboard**, **verify-your-work**)++# Shipping a build, and proving it is the one on screen+Every rule here was paid for on 2026-08-25, when seven dashboards were "published" and five of+them were serving builds hours old while their authors reported conformance from their own+containers.++## ⛔ "CURRENT" IS THREE SEPARATE CLAIMS, AND ANY ONE OF THEM CAN LIE ALONE++parts-search hit all three in a single slot:++| claim | how it lies |+|---|---|+| **the INSTALLED version** | `pkg install` succeeded, so the binary on disk is new |+| **the RUNNING process version** | the old process is still serving: 2.0.7 installed, **2.0.3 running** |+| **the version the DOCUMENT has** | `open-or-refresh` reported success while the page **stayed on module 2.1.1** |++Only a reload from inside the page moved the third one. So verifying an install proves the+first, `/api/status` proves the second, and **only `AdomNewbiePanel.version` read out of the+live document proves the third.** Report all three or you are reporting one and implying the+others.+++## FIRST: WHICH LIFECYCLE IS YOUR DASHBOARD ON? THEY ARE NOT THE SAME PROBLEM.++**A container-served dashboard** runs as its own process. `pkg install` replaces the binary and+leaves the old process running, so it needs the retirement path below, and that path is+usually untested.++**A bridge-served dashboard** (pup) is served by the DESKTOP bridge, not by a container+process. The container helper only prints the bridge's URL, so **installing the package+updates a helper that serves nothing.** What actually retires it is `bridge_install`, which ab+kills and respawns. That path is the opposite of untested: pup has driven it eight times in a+day and watched the version move each time.++**Establish which one you are before rehearsing anything**, or you will practise a path you do+not have while skipping the check you do. pup's `/api/status` showed 2.0.345 against a+container package of 2.0.348, which looks exactly like the stale-process symptom and is a+different mechanism with a different fix.++**What a bridge restart does to open windows, measured:** normal windows SURVIVE, tab and URL+intact, because the browser is detached and the new bridge re-adopts it over the debugging+port. pup has a 94-minute-old window that lived through at least two `bridge_install`s.+**The exception is a dialog-blocked tab, which does NOT survive** — the modal holds the target+in a state the reconnect cannot drive. So restarting the bridge is a usable last-resort unwedge+for a stuck native dialog, and it costs you exactly the window that was stuck.+++## ⛔ REHEARSE YOUR UPGRADE AGAINST YOUR REAL PREDECESSOR. THE INPUT IS THE OLD BUILD'S OUTPUT.++**This section is for container-served dashboards.**++Since the dock installs without restarting (below), every app needs a `serve` that RETIRES the+running instance. Almost every app will have tested that against itself and therefore tested+only the idempotent branch.++**The retirement path's input is the OLD build's output**, so it cannot be tested by reading+the new build's source. It reads a pid out of a state file **the old binary wrote**, then+refuses to kill unless `/proc/<pid>/cmdline` matches the argv it expects. Two silent failures+are reachable only against a real predecessor: the old build might not publish a pid at all,+or might publish a different argv shape. Either way there is no kill, the port stays held, and+`ensure` returns "port is held by a server I could not stop" — **on John's machine, in a slot,+after the queue has waited for you.**++**Fetch your own predecessor from the registry and rehearse the real upgrade**, in a throwaway+`HOME` on a spare port. No shell, no quota, about two minutes:++```+<old> serve --port 89xx -> OK: started (vOLD), pid N+<new> serve --port 89xx -> WARN: running instance is OLD, this is NEW; restarting+/api/status -> NEW+pid N -> gone+<new> serve again -> OK: already running (vNEW), same pid, NO restart+```++**Both halves matter.** The mismatch must restart AND the match must not: an upgrade step that+restarts every time is a different bug wearing the same green tick.++**⛔ A PREDECESSOR THAT PREDATES THE FEATURE CANNOT TEST THE FEATURE.** fusion retired two real+predecessors in one pass and then declined to call its retirement proven, correctly: the older+one predates the supersession-retirement code, so it could never have stood down on its own+and had to be retired by hand. **The path that matters — a current build retiring ITSELF+unattended — is still untested until a build that HAS the code is the one being replaced.**+Killing an old process proves your killer works; it does not prove the thing you shipped+works. Record which half you actually exercised.++parts-search rehearsed exactly this before its slot and found its predecessor does publish the+pid in the expected shape. An accident made its test harder than the real case, which is worth+copying: its old and new binaries sat at DIFFERENT paths while on John's box both are+`~/.local/bin/<tool>`, and it still matched, because the comparison is on the argv[0] TAIL+with the rest exact. **So the tolerant part and the strict part were both exercised.**+++## ⛔ THE DOCK INSTALLS A PACKAGE. IT DOES NOT RESTART THE RUNNING PROCESS.++**Publishing is not deploying, and installing is not running.** lbr found John's box serving+adom-lbr **2.14.1 and 2.13.0** while five versions had shipped past him. He had been looking+at a dashboard from hours earlier the entire time.++**Every app already emits the signal and nobody reads it:** a stale server reports its own+binary as **`(deleted)`** in the Binary LED, because the file it was launched from has been+replaced underneath it. That is a precise, free stale-process detector sitting in the status+row of every dashboard in the fleet.++So, before you conclude anything about a dashboard on his machine:++```+running dashboard version == the version you published?+Binary LED == "(deleted)"? -> you are looking at a stale process+```++This explains most of a day's confusion: apps reporting conformance from their own container+while his pane rendered an older build, `AdomNewbiePanel is not defined` read as "the module+is broken" when it meant "this build was never installed here", and audit rows describing+markup that had already been replaced in the registry. **Check the running version first, and+restart the process, not just the install.**
skills/newbie-panel/SKILL.md+12−424@@ -21,6 +21,18 @@ Parent skill: **adom-ui-design** (works with **dash-header**, **dash-demo**, # The newbie panel +## Where the rest of this moved++This skill is the PANEL. Four things that grew up inside it are now their own skills, because+somebody building a new Adom app needs them and would never look for them here:++| you want | read |+|---|---|+| wiring Send to AI, the version gate, proxying the control API | **control-api-proxy** |+| shipping a build and proving it is the one on screen | **dash-lifecycle** |+| knowing whether your check checked anything | **verify-your-work** |+| driving a machine somebody is sitting at | **shared-machine** |+ ## ⛔ STOP. DO NOT BUILD THIS PANEL. MOUNT THE MODULE. **`assets/module/newbie-panel.js` in this pack IS the panel.** One file, no dependencies, no@@ -89,130 +101,6 @@ Verify the bytes you serve match the pack: `md5sum` or `sha256sum` the served fi did this and both reported a byte-for-byte match, which is the check that makes "I serve the module" mean something. -### PROXY THE TWO API CALLS THROUGH YOUR OWN SERVER--`apiBase` defaults to `''`, same origin, and it should stay that way. Add two routes to your-own server and forward them to the Hydrogen control API using whatever discovery you already-have (`~/.adom/hydrogen-control-url`, 47084 as the fallback):--```-GET /updater/status -> control API /updater/status-POST /ai-threads/run-prompt -> control API /ai-threads/run-prompt (120s, matching the module)-```--Three reasons, and parts-search, nb and pup each found them separately: it removes any CORS-dependency on ah; it keeps exactly ONE copy of the send logic in the fleet rather than one-per app; and it keeps working in both deployment shapes, the container-served dashboard and-the desktop-bridge-served one, where a direct fetch to the control port would be cross-origin-or unreachable. The dynamic control port also stays server-side rather than being baked into-a page.--**⛔ DO NOT PARSE AND RE-EMIT. BYTE-COMPARE YOUR PROXY AGAINST A STUB.**--"Pass through unchanged" is easy to agree with and easy to violate, because **the natural way-to write this route in most languages is parse-then-re-emit**, which silently reorders keys,-drops whitespace, and can turn a numeric version into a string. The module compares-`updater.current_version` NUMERICALLY after splitting on dots, and reads `delivered_to` as an-ARRAY, so a re-serialisation that changes either type changes behaviour while every "does it-return 200" probe still passes.--**The harness ships in this pack: `assets/tools/proxy-fidelity.py`.** python3 stdlib only, no-dependencies.--```sh-python3 proxy-fidelity.py --app-cmd "your-server --port $PORT" # the mode to use-python3 proxy-fidelity.py --self-test # watch it go red first-```--`--app-cmd` creates a throwaway `HOME`, publishes a stub control URL into it, starts your app-with `$PORT` and `$HOME`, runs the assertions and tears it all down. **It never touches a real-workspace**, so it is safe in a build gate. There is a `--base --home` mode for an-already-running app, but prefer `--app-cmd`: the `--base` mode works only because the control-URL is read at REQUEST time, so **an app that caches the URL at startup silently tests-nothing.** Exits non-zero on any RED.--**TO PROVE THE PER-REQUEST READ RATHER THAN ASSERT IT** (parts-search's method, and stronger-than either mode alone): start your app with `~/.adom/hydrogen-control-url` pointing at a port-with **nothing listening on it**, then let the harness repoint the file at its stub while the-process keeps running.--```-app up against the dead URL -> 200-harness repoints the file -> PASS / PASS / PASS-file restored afterwards-```--**A startup-pinned URL cannot pass that**: it would still be talking to a closed port.-`--app-cmd` alone cannot distinguish per-request from pinned-at-startup, because the file is-already correct when the app boots. This is the difference between "I read the code and it-looks per-request" and a falsifiable result.--**It cannot create a thread in John's list.** The POST assertion sends `submit:false` and only-ever to its own stub, never to a real control API.--It checks three things, and all three are failures a 200-probe sails past:--| assertion | the bug it catches |-|---|---|-| response sha256 identical, including awkward whitespace and a nested `delivered_to` | parse-then-re-emit |-| a non-200 status survives (upstream 207 returned as 207) | a hand-built `200 {ok:true}` wrapper |-| an unproxied path under the same prefix returns YOUR 404 | blanket-proxying the control API |--**A JSON round trip is lossy in three ways that all look harmless.** pup's relay used-`JSON.parse` then `JSON.stringify`, not serde, so it did not alphabetise and therefore looked-fine. It still: **loses precision on an integer past 2^53**, turns **`1.0` into `1`**, and-worst, turns a **NON-JSON upstream body into an invented `{"error":...}` object** — converting-a valid response into a fabricated failure that no status code contradicts. That third one is-the reason "it returns 200" proves nothing about a proxy.--**PARSE FOR WHAT YOU DECIDE. NEVER PARSE FOR WHAT YOU HAND BACK.** parts-search's rule, from-fixing its own route: it still parses the upstream JSON for its own decisions, and forwards-the response bytes untouched with the `Content-Type` carried through. Those are two different-jobs and only one of them is allowed to re-serialise.--**The harness caught the app that INVENTED this pattern**, which is the strongest thing that-can be said for it. parts-search wrote the first same-origin proxy in the fleet and was RED on-assertion 1, with damage worse than the negative control's: its route parsed into a serde-`Value` and re-emitted, and serde's map is ordered, so the round trip **alphabetised the-keys** at both levels. `updater` moved to the end and `build` moved ahead of `current_version`-inside it. Same JSON, different bytes, different reading order for anything that streams or-diffs it.--**And the asymmetry in which route failed is the usual distribution:** its POST already-returned the upstream status, because it had written that one deliberately while hunting-phantom send failures. The GET was the one with a hand-written `200` beside it. **The route-someone thought about carefully was fine; the one-liner next to it was not.**--Run this before spending a slot. A round trip through his box tells you less and costs more.--**Run `--self-test` first, every time.** Its negative control is a proxy whose only sin is one-line of parse-then-re-emit, and it goes RED on two of the three while the third stays GREEN.-That third staying green is the point: **the assertions are independent, so a run tells you-WHICH bug you have** rather than only that something is wrong. The diff it prints on the-byte-for-byte failure shows the damage is purely `{"updater":{` becoming `{"updater": {` —-JSON that is equal, bytes that are not, and behaviour that can differ. The self-test exits 2-if the negative control somehow passes, i.e. if the harness itself is broken.--**READ THE CONTROL PORT AT REQUEST TIME. IT IS DYNAMIC PER ah LAUNCH.** Read-`~/.adom/hydrogen-control-url`; do not hardcode 47084 or any port you once observed. A pinned-port works until his next ah restart and then fails silently, which is the worst failure shape-available: it was working, nothing you changed broke it, and nothing says so.--**WHEN THE CONTROL API IS UNREACHABLE, ANSWER 503 WITH A HINT, NEVER 404.** project-manager's-routes return 503 naming the missing file when there is no ah on the host (its container, or-any non-ah machine). The reason is diagnostic, and it comes from a real ambiguity my own probe-hit: **a 404 is indistinguishable from "the route was never wired".** I read a 404 from its-dashboard and told it the route was registered on a wrong path; it was in fact never wired at-all. A 503 with a hint separates "wired but the backend is absent here" from "not wired",-which are different bugs with different owners.--**PASS THE RESPONSE THROUGH UNCHANGED.** Do not reshape the JSON on the way back. The module-reads `delivered_to` itself (it is an ARRAY) and separates *delivered* from *opened but not-confirmed*; an app that "helpfully" normalises the payload collapses three states into two,-and the retry that follows double-sends a prompt that already landed. That has happened to-two apps already. Keep the 120s timeout on `run-prompt` too, so a queued call is not turned-into a phantom failure.- ### onToggle tells you WHO toggled `onToggle(open, source)` where source is `'user'` (the X or the header button) or `'api'`@@ -606,25 +494,6 @@ If your page needs a wider margin for its own content, put it on the app body BE not on the container the panel mounts in. The panel is the full-width band at the top; the app's own gutter is the app's business. -### ⛔ A MISSING GATE READS AS AN ALLOWED GATE. PROBE IT WITH `checkSend()`.--`gate()` fails OPEN on purpose, so a broken probe cannot stop a user sending. The consequence-is invisible and kicad found it in itself: its `/updater/status` route **had been lost in a-rebuild**, so the gate received HTML, caught, and returned open. Its version gate had been-silently no-opping, and nothing anywhere said so. It could not have found this by reading its-own code, **because the code it was reading no longer existed.**--```js-await AdomNewbiePanel.checkSend() // -> {ok, rows:[gate wired, version, minimum, send wired]}-```--**It never posts a prompt.** The run-prompt route is probed with `OPTIONS`, so it reports-"wired" without creating a thread in anyone's list. Negative-controlled before shipping: with-no routes it goes red on all three; with a stub gate and a wired POST it goes green.--Run it beside `selfCheck`. `selfCheck` proves the panel is right; `checkSend` proves the button-on it can do anything.- ### ⛔ RETIRE BY PORT OWNERSHIP, NEVER BY argv. kicad KILLED A BYSTANDER. Rehearsing its upgrade (above) against a real predecessor, kicad's retirement retired the old@@ -708,75 +577,6 @@ the asset stood still. **When both move together, a version match is luck rather correctness, and the sweep passes for the wrong reason.** A check that can pass for the wrong reason has not been shown to work; it has been shown not to have complained yet. -### ⛔ A CATCH-ALL ROUTE MAKES THREE DIFFERENT CHECKS PASS FOR THE WRONG REASON--**Test it in one line:**--```sh-curl -o /dev/null -w '%{http_code}' <your base>/nonsense # 404 expected. 200 means read on.-```--nb served `page()` for every unmatched path. Three consequences, and it had a clean pass on all-three before it looked:--1. **The 404 watch on a virgin install could never fire.** A missing settings PNG, demo mp3 or- favicon came back as HTML 200 instead of 404 — and the launch-console 404 watch is the ONE- thing watched on a fresh install.-2. **`proxy-fidelity` row 3 passed wrongly.** It asserts "not upstream's 418"; a catch-all- returns 200, which is not 418, so a blanket-proxy check passed on an app that was blanket- -serving.-3. **`checkSend`'s "send route wired" row passed wrongly**, and this only surfaced once #1 was- fixed. The module probes with **OPTIONS**; nb's route matched **POST only**, so the- catch-all was answering the probe with HTML 200. With unmatched paths 404ing, the row went- red and honest. **If your dashboard proxies those two routes and matches POST only, your- green on that row is your catch-all, not your route.** Answer a real CORS preflight (204).--This is kicad's "passes for the wrong reason" arriving three times in one app, hours after it-was named. **A permissive default does not fail your checks; it answers them.**--**THERE IS A SECOND CATCH-ALL, AND IT IS THE ONE THAT MATTERS.** kicad fixed its GET catch-all-and its send row stayed green, because its `do_OPTIONS` returned **204 for every path-unconditionally**:--```-OPTIONS /nonsense -> 204-OPTIONS /ai-threads/run-prompt -> 204 indistinguishable, which is the point-```--So an app can fix the obvious catch-all and still have a row that tests nothing. **Extend the-one-line test to the preflight:**--```sh-curl -o /dev/null -w '%{http_code}' -X OPTIONS <base>/nonsense # must also 404-```--**`checkSend` now defends itself against this** (module 2.21.0): it probes the real route AND a-path that cannot exist, and requires the two to ANSWER DIFFERENTLY. "Not 404" was too weak an-assertion. Controlled both ways: a blanket preflight reads `route 204, nonsense 204` and goes-red; honest routing reads `route 204, nonsense 404` and passes. **If a server answers a path-that cannot exist, it is answering the shape of the question rather than the question.**--The structural fix, and the one to copy: **both handlers read ONE route table**, so a route can-never be live for OPTIONS and dead for GET. Two independent notions of "does this route exist"-was the actual defect.--**⛔ DO NOT FIX THIS WITH A BLANKET 404, AND HERE IS THE REAL CONSTRAINT.** adom.cloud mounts a-dashboard under a sub-path AND FORWARDS THE PREFIX, so the app's own root arrives as-`/apps/kicad`, which is shape-identical to a typo. **A blanket 404-unless-slash passes the-one-line test and breaks the app in production.** kicad's rule instead:--- 404 if the path is **asset-shaped** (a known file extension), whoever asks-- 404 if it is inside a **namespace you own** (`/api/`, `/ui/`, `/updater/`, `/assets/`,- `/ai-threads/`), whoever asks-- always serve `/`-- otherwise serve the shell only for a **real browser navigation** (`Accept: text/html`)--**Stated with its caveat rather than as a clean claim, which is how kicad reported it:** a-browser typing a wrong extensionless URL still gets the shell. That is accepted, because what-the 404 watch actually watches is missing pngs, mp3s, favicons and js — all of which carry an-extension or an owned prefix and 404 regardless of client. Verified against an image `Accept`-header. **Do not fix a passes-for-the-wrong-reason bug by shipping another one.**- ### ⛔ GREP THE ENTITIES TOO nb's Layer 2 heading carried an em-dash written as `—`, so it **survived every plain-text@@ -1295,58 +1095,6 @@ regardless: a copy button whose only path requires a permission the host may not button that fails silently for some users, and there is no downside to the second path. **Verify with a real click before claiming the user-facing bug is fixed.** -## Handing over a shared machine: the release must be witnessed by BOTH parties--Verifying inside ah means driving John's laptop, and two threads driving it at once is what-crashed it. So the shell is handed out one thread at a time. The handoff protocol that-actually holds:--- **THE COORDINATOR IS THE REGISTRY, AND A RELEASE IS NOT COMPLETE UNTIL IT SAYS SO.** Telling- the next holder fixed the telling, not the TRACKING: two threads later, both had released- and each believed the other held it, and the coordinator was reconstructing the state from- two messages that disagreed. **Reconstructing who holds a shared machine is the same class- of error as inferring it.** A thread that has released should say so in the same words every- time; the coordinator confirms the machine is genuinely idle before handing it on, and the- next holder starts only on an explicit handover naming it. nb's position is the correct one- to copy: *"hand it to me explicitly and I will treat that message as the handover; I will not- start on an inference that it is free."*-- **The coordinator must publish the next holder's ADDRESS, not just its position.** The rule- below assumes the holder can address its successor, and in this fleet it could not: peers- appear as opaque handles with nothing mapping a handle to an app. fusion released correctly- and then could not complete the direct half, and it refused to guess, because messaging the- wrong thread "the shell is yours" is worse than messaging none: it hands a shared machine to- someone who was not waiting for it. **A protocol step nobody can execute degrades silently- to the thing it replaced.**-- **The releasing thread messages the coordinator AND the next thread directly.** fusion- started a pass on the coordinator's word alone, because project-manager released to the- coordinator and never messaged fusion. That works until one message is delayed, and then two- threads are driving the same machine, each correctly believing it has the slot.-- **The taking thread says what it is assuming**, so a wrong assumption is visible before it- does damage rather than after. fusion's "your message is my only evidence the shell is free,- kill my slot and I will back out" is the right shape.-- **A thread that sees zero-height rects on a tab it expects to be active should assume it has- LOST the shell, not that the page is broken.** project-manager inferred exactly that,- stopped, and reported the attempt rather than driving a click into a window another thread- might have been mid-pass in.--The general form, worth more than the panel: **on a shared resource, a handoff is not complete-when the holder lets go. It is complete when the next holder has been told, by the holder.**--## ⛔ "CURRENT" IS THREE SEPARATE CLAIMS, AND ANY ONE OF THEM CAN LIE ALONE--parts-search hit all three in a single slot:--| claim | how it lies |-|---|---|-| **the INSTALLED version** | `pkg install` succeeded, so the binary on disk is new |-| **the RUNNING process version** | the old process is still serving: 2.0.7 installed, **2.0.3 running** |-| **the version the DOCUMENT has** | `open-or-refresh` reported success while the page **stayed on module 2.1.1** |--Only a reload from inside the page moved the third one. So verifying an install proves the-first, `/api/status` proves the second, and **only `AdomNewbiePanel.version` read out of the-live document proves the third.** Report all three or you are reporting one and implying the-others.- ## ⛔ "DOES NOT TRUNCATE TODAY" AND "HAS ROOM" ARE DIFFERENT CLAIMS `selfCheck` now carries a **phrase headroom** row: every phrase must have **at least 8px of@@ -1432,166 +1180,6 @@ refusing sends — correctly, but worth knowing it is a boundary rather than a c At his width the two-column layout is close to its limit on BOTH axes. Nothing to change today; something to check first if a dashboard ever renders one column unexpectedly. -## ⛔ PUT A POSITIVE CONTROL ON A CLEAN RESULT, NOT ONLY A NEGATIVE ONE ON A CHECK--parts-search has reported "0 of 6 truncating" on this exact question before, when the truth was-2 of 6, by measuring the cell instead of the clipping span. **All six spans returning an-identical 343 is what a dead instrument looks like.** So before believing the clean result it-lengthened one span in place and watched `scrollWidth` go 343 -> 823 -> 343. **It responds to-text, therefore the reading is real.**--The distinction is worth holding: a NEGATIVE control proves a check can fail. A POSITIVE-control proves a measurement is still connected to the thing it measures. **Its wrong version-of that number looked exactly like its right one**, and only the poke told them apart.--## FIRST: WHICH LIFECYCLE IS YOUR DASHBOARD ON? THEY ARE NOT THE SAME PROBLEM.--**A container-served dashboard** runs as its own process. `pkg install` replaces the binary and-leaves the old process running, so it needs the retirement path below, and that path is-usually untested.--**A bridge-served dashboard** (pup) is served by the DESKTOP bridge, not by a container-process. The container helper only prints the bridge's URL, so **installing the package-updates a helper that serves nothing.** What actually retires it is `bridge_install`, which ab-kills and respawns. That path is the opposite of untested: pup has driven it eight times in a-day and watched the version move each time.--**Establish which one you are before rehearsing anything**, or you will practise a path you do-not have while skipping the check you do. pup's `/api/status` showed 2.0.345 against a-container package of 2.0.348, which looks exactly like the stale-process symptom and is a-different mechanism with a different fix.--**What a bridge restart does to open windows, measured:** normal windows SURVIVE, tab and URL-intact, because the browser is detached and the new bridge re-adopts it over the debugging-port. pup has a 94-minute-old window that lived through at least two `bridge_install`s.-**The exception is a dialog-blocked tab, which does NOT survive** — the modal holds the target-in a state the reconnect cannot drive. So restarting the bridge is a usable last-resort unwedge-for a stuck native dialog, and it costs you exactly the window that was stuck.--## ⛔ REHEARSE YOUR UPGRADE AGAINST YOUR REAL PREDECESSOR. THE INPUT IS THE OLD BUILD'S OUTPUT.--**This section is for container-served dashboards.**--Since the dock installs without restarting (below), every app needs a `serve` that RETIRES the-running instance. Almost every app will have tested that against itself and therefore tested-only the idempotent branch.--**The retirement path's input is the OLD build's output**, so it cannot be tested by reading-the new build's source. It reads a pid out of a state file **the old binary wrote**, then-refuses to kill unless `/proc/<pid>/cmdline` matches the argv it expects. Two silent failures-are reachable only against a real predecessor: the old build might not publish a pid at all,-or might publish a different argv shape. Either way there is no kill, the port stays held, and-`ensure` returns "port is held by a server I could not stop" — **on John's machine, in a slot,-after the queue has waited for you.**--**Fetch your own predecessor from the registry and rehearse the real upgrade**, in a throwaway-`HOME` on a spare port. No shell, no quota, about two minutes:--```-<old> serve --port 89xx -> OK: started (vOLD), pid N-<new> serve --port 89xx -> WARN: running instance is OLD, this is NEW; restarting-/api/status -> NEW-pid N -> gone-<new> serve again -> OK: already running (vNEW), same pid, NO restart-```--**Both halves matter.** The mismatch must restart AND the match must not: an upgrade step that-restarts every time is a different bug wearing the same green tick.--**⛔ A PREDECESSOR THAT PREDATES THE FEATURE CANNOT TEST THE FEATURE.** fusion retired two real-predecessors in one pass and then declined to call its retirement proven, correctly: the older-one predates the supersession-retirement code, so it could never have stood down on its own-and had to be retired by hand. **The path that matters — a current build retiring ITSELF-unattended — is still untested until a build that HAS the code is the one being replaced.**-Killing an old process proves your killer works; it does not prove the thing you shipped-works. Record which half you actually exercised.--parts-search rehearsed exactly this before its slot and found its predecessor does publish the-pid in the expected shape. An accident made its test harder than the real case, which is worth-copying: its old and new binaries sat at DIFFERENT paths while on John's box both are-`~/.local/bin/<tool>`, and it still matched, because the comparison is on the argv[0] TAIL-with the rest exact. **So the tolerant part and the strict part were both exercised.**--## ⛔ THE DOCK INSTALLS A PACKAGE. IT DOES NOT RESTART THE RUNNING PROCESS.--**Publishing is not deploying, and installing is not running.** lbr found John's box serving-adom-lbr **2.14.1 and 2.13.0** while five versions had shipped past him. He had been looking-at a dashboard from hours earlier the entire time.--**Every app already emits the signal and nobody reads it:** a stale server reports its own-binary as **`(deleted)`** in the Binary LED, because the file it was launched from has been-replaced underneath it. That is a precise, free stale-process detector sitting in the status-row of every dashboard in the fleet.--So, before you conclude anything about a dashboard on his machine:--```-running dashboard version == the version you published?-Binary LED == "(deleted)"? -> you are looking at a stale process-```--This explains most of a day's confusion: apps reporting conformance from their own container-while his pane rendered an older build, `AdomNewbiePanel is not defined` read as "the module-is broken" when it meant "this build was never installed here", and audit rows describing-markup that had already been replaced in the registry. **Check the running version first, and-restart the process, not just the install.**--## ⛔ A BEHAVIOURAL GATE CANNOT SEE A DELETED STYLESHEET--The mirror of the missing-function rule below, and the more expensive half.--lbr's over-greedy regex in 2.14.0 destroyed two things. The missing `api()` broke BEHAVIOUR,-so its gates caught it. It also **deleted three stylesheets**, and those broke only-APPEARANCE, so the page passed every structural, behavioural and conformance check **for five-releases** while, in John's dock: the Send to KiCad / Send to Fusion bar rendered as bare-labels and unstyled white buttons, the send-evidence panel had no styling, and neither did the-demo's transport controls. All three are features he asked for by name. Markup fine,-JavaScript fine, handlers running, `selfCheck` green.--**It was found by looking at the screenshot. Nothing else could have found it.**--The fix that generalises: assert a **signature computed property per component**. jsdom has no-layout engine but it DOES resolve the cascade, so a deleted stylesheet can fail the build.-Negative-control it.--**And every app that took a regex-based edit in that era should LOOK at its own pane once,-not merely check it.**--(lbr also wrote an id-orphan scanner and deleted it: 70 false positives, because most ids are-styled through class or descendant rules. **A check nobody can keep is worse than none.**)--## ⛔ A STRUCTURAL AUDIT CANNOT SEE A MISSING FUNCTION--lbr shipped a panel whose markup and CSS were provably correct and which rendered **zero-prompts** in the user's ah. Its 22-check headless byte-audit passed. One root cause produced-three separate symptoms:--> A non-greedy regex in a toast rewrite swallowed the adjacent `api()` helper — the single-> fetch wrapper every call site used. The page threw `ReferenceError: api is not defined` on-> its first call, and because each loader swallowed its own errors, the page went SILENT: no-> prompt rows, no copy buttons to measure, and a red "not answering" pill that was its own-> catch-path fallback.--Two rules fall out, and they are the ones a byte-audit will never give you:--- **Assert BEHAVIOUR, not only markup and CSS.** "The markup is right" is not "the page- works". A structural check cannot see a missing function any more than it can see a- breakpoint. Load the page and assert that rows actually EXIST at runtime.-- **Do not let every call site swallow its own errors.** Silence turned one deleted function- into an empty panel that looked deliberate. A page that fails should say so.--**Ship the self-check that makes this class impossible to hide** (lbr's, and worth copying):--```js-// 1.5s after load: an OPEN panel with zero rows is a bug, not a state.-setTimeout(() => {- if (panelIsOpen() && rowCount() === 0) toast('error', 'Prompts failed to load — reload');-}, 1500);-```--An empty panel is never a legitimate resting state, so a page that finds itself in one should-say so rather than present it as design.- ## Three traps that make a CORRECT-LOOKING panel render wrong All found by parts-search on 2026-08-25, and all passed every computed-style check it ran.
skills/shared-machine/SKILL.mdadded+93@@ -0,0 +1,93 @@+---+name: shared-machine+user-invocable: true+description: >-+ Etiquette for driving a machine somebody else is using, and for handing it between agents.+ Two agents driving one desktop is how it gets crashed, so the shell is handed out one holder+ at a time - and a handoff is not complete when the holder lets go, it is complete when the+ next holder has been told BY THE HOLDER, addressed by a handle the coordinator published.+ Also covers the trap that put three clicks into a user's foreground apps: COORDINATES ARE NOT+ THE INPUT, FOCUS IS. desktop_click dispatches to whatever owns the foreground at that+ instant, while desktop_screenshot_window reads through occlusion by design, so a correct+ screenshot is not evidence a click will land. Use pup_assisted_click, verify focus+ IMMEDIATELY BEFORE EACH native event, and never steal focus from live work without asking.+ Read BEFORE clicking, typing or restarting anything on a machine a person is using. Trigger+ words: drive the desktop, click on his machine, foreground the window, steal focus, take the+ shell, hand off the slot, who has the machine, desktop_click, assisted click, real click.+---++Parent skill: **adom-ui-design** (works with **dash-screenshot**, **verify-your-work**)++# Driving a machine somebody is sitting at+**The person is using this computer right now.** Everything below exists because something+here went into their live work: three clicks into a foreground chat window, an agent typing a+username into an open document, and a desktop crashed by two agents driving it at once.++## ⛔ COORDINATES ARE NOT THE INPUT. FOCUS IS.++`desktop_click` dispatches to **whatever owns the foreground at that instant**, so a correct+rect proves nothing. One agent's coordinates were right, computed two independent ways that+agreed to the pixel, its screenshot looked perfect, and six windows sat in front of the target:+three clicks went into the user's Edge and their chat.++**`desktop_screenshot_window` reads through occlusion BY DESIGN** — that is what makes it safe+for capturing background windows without stealing focus. **The same property makes a screenshot+worthless as evidence that a click will land.** Two capabilities that look like a matched pair+and are not.++**Use `pup_assisted_click`** rather than hand-rolling raise-then-click. It polls+`osGetForegroundWindow()` until it EQUALS the target hwnd **and has been stable for 1200ms**,+caps at 120s, returns false rather than dispatching anyway, and captions "click back into THIS+window" at most three times before going silent, because an unattended wait can run for hours+and a permanent nag is its own disturbance.++**Verify IMMEDIATELY BEFORE EACH native event, not once per sequence.** Three clicks means+three verifications: focus can move between event one and event two, and that is how half a+password lands in the wrong window. One up-front check followed by N dispatches is the shape+that causes this.++**The failure is silent on the side that matters.** A stray click produces no error, no+rejection and no log. The only signal is an absence — a toast that never appears — and absences+are exactly what a busy operator does not notice. So the check must be STRUCTURAL, not+something you remember to do.++**Foregrounding steals focus from live work. That is a disruption to a person, so it gets+asked, not assumed.**+++## Handing over a shared machine: the release must be witnessed by BOTH parties++Verifying inside ah means driving John's laptop, and two threads driving it at once is what+crashed it. So the shell is handed out one thread at a time. The handoff protocol that+actually holds:++- **THE COORDINATOR IS THE REGISTRY, AND A RELEASE IS NOT COMPLETE UNTIL IT SAYS SO.** Telling+ the next holder fixed the telling, not the TRACKING: two threads later, both had released+ and each believed the other held it, and the coordinator was reconstructing the state from+ two messages that disagreed. **Reconstructing who holds a shared machine is the same class+ of error as inferring it.** A thread that has released should say so in the same words every+ time; the coordinator confirms the machine is genuinely idle before handing it on, and the+ next holder starts only on an explicit handover naming it. nb's position is the correct one+ to copy: *"hand it to me explicitly and I will treat that message as the handover; I will not+ start on an inference that it is free."*+- **The coordinator must publish the next holder's ADDRESS, not just its position.** The rule+ below assumes the holder can address its successor, and in this fleet it could not: peers+ appear as opaque handles with nothing mapping a handle to an app. fusion released correctly+ and then could not complete the direct half, and it refused to guess, because messaging the+ wrong thread "the shell is yours" is worse than messaging none: it hands a shared machine to+ someone who was not waiting for it. **A protocol step nobody can execute degrades silently+ to the thing it replaced.**+- **The releasing thread messages the coordinator AND the next thread directly.** fusion+ started a pass on the coordinator's word alone, because project-manager released to the+ coordinator and never messaged fusion. That works until one message is delayed, and then two+ threads are driving the same machine, each correctly believing it has the slot.+- **The taking thread says what it is assuming**, so a wrong assumption is visible before it+ does damage rather than after. fusion's "your message is my only evidence the shell is free,+ kill my slot and I will back out" is the right shape.+- **A thread that sees zero-height rects on a tab it expects to be active should assume it has+ LOST the shell, not that the page is broken.** project-manager inferred exactly that,+ stopped, and reported the attempt rather than driving a click into a window another thread+ might have been mid-pass in.++The general form, worth more than the panel: **on a shared resource, a handoff is not complete+when the holder lets go. It is complete when the next holder has been told, by the holder.**
skills/verify-your-work/SKILL.mdadded+146@@ -0,0 +1,146 @@+---+name: verify-your-work+user-invocable: true+description: >-+ How to know your check actually checked something. A green result carries "in the environment+ I happened to measure" and we keep reading it as "everywhere". Covers the discipline that+ caught eleven wrong conclusions in one day: NEGATIVE-control every check (a green you have+ never seen go red is a screenshot of a function that might always return true), break the+ property each row CLAIMS to watch rather than any property, POSITIVE-control a clean result+ (a metric that can only return one value on success is not evidence of success), refuse+ rather than degrade when an instrument's preconditions are unmet, test a fix by reproducing+ the failure instead of reading the diff, and ask whether a failing check is failing the+ CORRECT implementations. Also: a structural audit cannot see a missing function, a+ behavioural gate cannot see a deleted stylesheet, and zero is almost never a measurement.+ Read BEFORE trusting any check, audit, conformance run or clean pass. Trigger words: verify,+ negative control, self-test, is my check working, prove it, conformance, audit, ralph test,+ it passed but, green check, false positive, measurement artifact.+---++Parent skill: **adom-ui-design** (works with **dash-lifecycle**, **dock-dashboard**)++# Knowing whether your check checked anything+**A green check you have never watched go red is a claim, not evidence.** Everything below+came out of a single day in which eleven confident, cleanly-running checks returned wrong+answers, and every one was caught by someone running the implementation against a real page+rather than trusting the prose.++## The core disciplines++**NEGATIVE-CONTROL EVERY CHECK.** Break the thing it checks and watch it fail. If it still+passes, it was never testing what you thought. And **break the property each row CLAIMS to+watch, not just any property**: breaking the background proves the harness runs; breaking the+typeface proves the typeface row is wired to the typeface.++**POSITIVE-CONTROL A CLEAN RESULT.** A negative control proves a check CAN fail; a positive+control proves a measurement is still CONNECTED to what it measures. Three apps wrote three+different phrase-width expressions and all three returned exactly 0 on a healthy panel. **A+metric that can only return one value on success is not evidence of success.** Ask what a+HEALTHY reading looks like before you report an unhealthy one.++**REFUSE, DO NOT DEGRADE.** When an instrument can detect that its own preconditions are+unmet, it must refuse rather than report. A partial result from an unrendered pane produced+eight green rows beside two red ones, which reads as a specific bug; an all-zero result reads+as an invalid run. **The mixed one is the trap.** And make the refusal safe to consume: a guard+whose failure mode is a TypeError in someone else's stack is a guard that will be misread.++**TEST A FIX BY REPRODUCING THE FAILURE, NOT BY READING THE DIFF.** Confirming the line is+present proves the edit landed and nothing about the failure mode.++**ASK WHETHER A FAILING CHECK IS FAILING THE CORRECT IMPLEMENTATIONS.** A check that is wrong+at random is noise. A check systematically wrong about the WELL-BUILT cases actively teaches+the fleet to break itself, because the fastest way to go green is to adopt the looser+structure. And the cost of a wrong check is not only a false verdict: it is the real damage+people do to themselves complying with it.++**A CHECK THAT CAN PASS FOR THE WRONG REASON HAS NOT BEEN SHOWN TO WORK.** It has been shown+not to have complained yet. A permissive default does not fail your checks; it answers them.++**A CHECKER TESTED AGAINST FIXTURES ITS AUTHOR WROTE INHERITS ITS AUTHOR'S ASSUMPTIONS.** The+rows that catch the next bug are the ones nobody thought to write. The ask that surfaces them+is not "tell me if you disagree", because prose reads fine; it is **"here is the code, run it+against your page and report what it does"**.++**PUBLISH YOUR ARITHMETIC, NOT JUST YOUR VERDICT.** A wrong conclusion that ships with the+number that produced it can be checked; a verdict alone cannot.++**EVIDENCE EXPIRES.** Re-check that a rule's justification still EXISTS before citing it. A+correct measurement of a case that no longer ships is not wrong, it is gone.++**STATE THE ENVIRONMENT ALONGSIDE THE RESULT.** A machine that differs from the target in any+way a bug can depend on will confidently pass code that fails there. Device pixel ratio, pane+width, installed fonts and browser engine are the four that bite. A pre-check on your own box+is a DIAGNOSIS; only the target machine is proof.++**ZERO IS ALMOST NEVER A MEASUREMENT. It is the shape of having measured nothing.**+++## ⛔ A BEHAVIOURAL GATE CANNOT SEE A DELETED STYLESHEET++The mirror of the missing-function rule below, and the more expensive half.++lbr's over-greedy regex in 2.14.0 destroyed two things. The missing `api()` broke BEHAVIOUR,+so its gates caught it. It also **deleted three stylesheets**, and those broke only+APPEARANCE, so the page passed every structural, behavioural and conformance check **for five+releases** while, in John's dock: the Send to KiCad / Send to Fusion bar rendered as bare+labels and unstyled white buttons, the send-evidence panel had no styling, and neither did the+demo's transport controls. All three are features he asked for by name. Markup fine,+JavaScript fine, handlers running, `selfCheck` green.++**It was found by looking at the screenshot. Nothing else could have found it.**++The fix that generalises: assert a **signature computed property per component**. jsdom has no+layout engine but it DOES resolve the cascade, so a deleted stylesheet can fail the build.+Negative-control it.++**And every app that took a regex-based edit in that era should LOOK at its own pane once,+not merely check it.**++(lbr also wrote an id-orphan scanner and deleted it: 70 false positives, because most ids are+styled through class or descendant rules. **A check nobody can keep is worse than none.**)+++## ⛔ A STRUCTURAL AUDIT CANNOT SEE A MISSING FUNCTION++lbr shipped a panel whose markup and CSS were provably correct and which rendered **zero+prompts** in the user's ah. Its 22-check headless byte-audit passed. One root cause produced+three separate symptoms:++> A non-greedy regex in a toast rewrite swallowed the adjacent `api()` helper — the single+> fetch wrapper every call site used. The page threw `ReferenceError: api is not defined` on+> its first call, and because each loader swallowed its own errors, the page went SILENT: no+> prompt rows, no copy buttons to measure, and a red "not answering" pill that was its own+> catch-path fallback.++Two rules fall out, and they are the ones a byte-audit will never give you:++- **Assert BEHAVIOUR, not only markup and CSS.** "The markup is right" is not "the page+ works". A structural check cannot see a missing function any more than it can see a+ breakpoint. Load the page and assert that rows actually EXIST at runtime.+- **Do not let every call site swallow its own errors.** Silence turned one deleted function+ into an empty panel that looked deliberate. A page that fails should say so.++**Ship the self-check that makes this class impossible to hide** (lbr's, and worth copying):++```js+// 1.5s after load: an OPEN panel with zero rows is a bug, not a state.+setTimeout(() => {+ if (panelIsOpen() && rowCount() === 0) toast('error', 'Prompts failed to load — reload');+}, 1500);+```++An empty panel is never a legitimate resting state, so a page that finds itself in one should+say so rather than present it as design.+++## ⛔ PUT A POSITIVE CONTROL ON A CLEAN RESULT, NOT ONLY A NEGATIVE ONE ON A CHECK++parts-search has reported "0 of 6 truncating" on this exact question before, when the truth was+2 of 6, by measuring the cell instead of the clipping span. **All six spans returning an+identical 343 is what a dead instrument looks like.** So before believing the clean result it+lengthened one span in place and watched `scrollWidth` go 343 -> 823 -> 343. **It responds to+text, therefore the reading is real.**++The distinction is worth holding: a NEGATIVE control proves a check can fail. A POSITIVE+control proves a measurement is still connected to the thing it measures. **Its wrong version+of that number looked exactly like its right one**, and only the poke told them apart.