← 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.

John Lauer ·c5ef5fc142 ·1mo ago ·parent f010fe8
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 `&mdash;`, 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.