← Commit history
install.sh+11−3
@@ -1,6 +1,14 @@ #!/usr/bin/env bash set -euo pipefail H="$(cd "$(dirname "$0")"&&pwd)"-mkdir -p "$HOME/.claude/skills/adom-desktop-kicad-bridge"-cp -f "$H/SKILL.md" "$HOME/.claude/skills/adom-desktop-kicad-bridge/SKILL.md"-echo "[adom-desktop-kicad-bridge] skill installed."+D="$HOME/.claude/skills"+mkdir -p "$D/kicad-bridge"+cp -f "$H/SKILL.md" "$D/kicad-bridge/SKILL.md"+for sk in kicad-interaction kicad-3d-models kicad-tour kicad-bridge-dev kicad-bridge-publish kicad-bridge-hero; do+  mkdir -p "$D/$sk"+  cp -f "$H/skills/$sk/SKILL.md" "$D/$sk/SKILL.md"+done+cp -f "$H/skills/kicad-tour/tour_runner.py" "$D/kicad-tour/tour_runner.py"+cp -f "$H/skills/kicad-bridge-hero/compose_hero.py" "$D/kicad-bridge-hero/compose_hero.py"+rm -rf "$D/adom-desktop-kicad-bridge"   # legacy install dir from pre-rename versions+echo "[kicad-bridge] skills installed."
package.json+6−3
@@ -1,6 +1,6 @@ {   "slug": "kicad-bridge",-  "version": "1.0.19",+  "version": "1.0.20",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the KiCad bridge. The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [@@ -24,10 +24,13 @@     "package.json",     "install.sh",     "uninstall.sh",-    "page.json",     "skills/kicad-interaction/SKILL.md",     "skills/kicad-3d-models/SKILL.md",     "skills/kicad-tour/SKILL.md",-    "skills/kicad-tour/tour_runner.py"+    "skills/kicad-tour/tour_runner.py",+    "skills/kicad-bridge-dev/SKILL.md",+    "skills/kicad-bridge-publish/SKILL.md",+    "skills/kicad-bridge-hero/SKILL.md",+    "skills/kicad-bridge-hero/compose_hero.py"   ] }\ No newline at end of file
page.json+6−3
@@ -4,7 +4,7 @@   "slug": "kicad-bridge",   "title": "KiCad - the KiCad Bridge",   "brief": "Reference implementation of the KiCad bridge — multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.",-  "version": "1.0.19",+  "version": "1.0.20",   "tags": [     "kicad",     "pcb",@@ -116,11 +116,14 @@     "package.json",     "install.sh",     "uninstall.sh",-    "page.json",     "skills/kicad-interaction/SKILL.md",     "skills/kicad-3d-models/SKILL.md",     "skills/kicad-tour/SKILL.md",-    "skills/kicad-tour/tour_runner.py"+    "skills/kicad-tour/tour_runner.py",+    "skills/kicad-bridge-dev/SKILL.md",+    "skills/kicad-bridge-publish/SKILL.md",+    "skills/kicad-bridge-hero/SKILL.md",+    "skills/kicad-bridge-hero/compose_hero.py"   ],   "org": "adom",   "name": "kicad-bridge"
skills/kicad-bridge-hero/SKILL.mdadded+111
@@ -0,0 +1,111 @@+---+name: kicad-bridge-hero+user-invocable: false+description: DEVELOPER-only skill (user-invocable:false — maintainers, never everyday users). The complete recipe for making/refreshing the kicad-bridge page hero in the Adom bridge-family style (matching Pup and Fusion), getting it through the Hero Studio human gate, and applying it to the page + pkg. Encodes every measured layout number, the brand font doctrine, the real-window vitrine capture workflow, and the gate/publish traps we hit shipping the first one. Trigger words — kicad hero, hero image, new hero, refresh hero, hero studio, HERO_STUDIO_REQUIRED, HERO_REQUIRED, bridge family hero, page billboard.+---++# kicad-bridge-hero++How the kicad-bridge hero gets made. First shipped 2026-08-09 after ~14 iterations;+this skill exists so the next one takes two. Full papercut list we filed upstream:+**adom/hero-studio issue #566** — read it before assuming the studio guides you.++## The one-command core++```bash+python3 skills/kicad-bridge-hero/compose_hero.py <vitrine-capture.png> /tmp/hero-vNN.png+```++`compose_hero.py` (in this directory) clones Pup's measured geometry, renders the+title/subtitle, drops in your capture, and **asserts the ink alignment** before saving.+It needs `reference-pup-hero.png` — Pup's true published asset, the source of the logo+crop, pill crop, background gradient corners, and every layout number. It lives in the+kicad-bridge **page repo** next to this skill (not in the pkg tarball — no-images rule);+if missing locally: `adom-wiki repo download pup-bridge --extract /tmp/pup-repo` and+copy `hero-v9.png` in (NOT `repo show` — it mangles binaries to UTF-8).++## Brand doctrine (things John corrected us on — do not rediscover)++- **Title font is Familjen Grotesk BOLD (700)**, not Satoshi. The rule lives in+  hero-studio's own billboard CSS (`.h-title`), nowhere else. Satoshi is for the+  subtitle (inherited from body) and UI chrome. Fonts live in+  `skills/kicad-tour/hero/fonts/`.+- **Title is just "KiCad"** — the one word users type to their AI. The wiki page+  title ("KiCad - the KiCad Bridge") carries the rest dynamically.+- **One pill, baked, at Pup's exact spot** (crop `(1796,36,1964,108)` from the+  reference, pasted verbatim). No second pill, no baked wiki URL: the homepage+  overlay adds its own dynamic pill/slug at render time.+- **Left ink edges must be logo 79 / title 97 / subtitle 83** (Pup's exact stagger).+  Fonts lie about bearings (Familjen reported 0 for "K", drew 16px) — always+  measure rendered pixels and correct; the script does this automatically.+- **Vitrine = Pup's floating-window rect**: (920,150), 1030x1025, radius 24, soft+  shadow. It does NOT bleed any edge (we assumed bleed for days; measurement says no).+- NO em-dashes in any hero/subtitle copy.++## Vitrine capture (real window, never a mockup)++1. Board: `tour-pack-rp2040/template/rp2040-breakout.kicad_pcb` (the annotated+   J1 USB-C / U1 QFN / Y1 crystal board). Ship it over if needed:+   `send_files {"filePaths":[...], "dest":"C:/Users/<u>/Documents/adom-kicad-rp2040"}`+   (the arg is `dest`, not destDir — wrong arg silently lands in Downloads).+2. `kicad_open_board {"filePath":...}` (absolute Windows path; no %VAR% expansion),+   then `kicad_open_3d_viewer`.+3. **Resize the 3D viewer to the slot before capturing** so the capture drops in+   1:1 with zero scaling: `desktop_set_window_bounds {"hwnd":N,"x":200,"y":20,"w":1030,"h":1025}`+   (args are w/h, not width/height).+4. Dark mode: Windows OS dark mode gives KiCad 10 dark titlebar/menus/panels. The+   3D viewport background is theme-bound (`_builtin_default` isn't editable), so it+   stays lavender — that's fine; Pup's vitrine also pairs a dark card with a light+   window interior.+5. A human-posed camera angle beats anything scripted. Ask John to frame it, then+   `desktop_screenshot_window {"hwnd":N}` — the PNG lands in the container at+   `screenshots/adom-bridge/window/`. Zoom-to-fit is a toolbar click+   (`kicad_click {"hwnd":N,"x":375,"y":100,"relative":false}`); there is no Home-key fit.++## Hero Studio gate (mandatory — registry enforces provenance)++Only the studio's export passes `HERO_STUDIO_REQUIRED`; a human must click Generate.++```bash+node <hero-studio>/server.js --port 88XX &     # OWN port; check `ps` first — port+                                               # collisions once stomped pup's gate+curl -X POST :88XX/state -d '{"mode":"app","page":"adom/kicad-bridge", ...,+  "overlay":{"pill":false,"scores":false,"author":false,"slug":false}}'+hero-studio vitrine --file /tmp/hero-vNN.png --full --port 88XX+```++Traps (all in issue #566):+- **Turn the overlay sim OFF via /state** (above). Its flags default ON, persist+  server-side across reloads, and draw a fake pill + slug URL over your hero —+  three review rounds were lost to "why 2 pills / why a wiki url".+- The reviewer views the gate through **pup**: open the container-proxy URL in a+  session YOU own, `foreground:true` needs a `foregroundReason` quoting the user.+  Signal updates with `pup_alert_window` (taskbar flash), never re-foreground.+- "broken_images_6" on import = empty placeholder slots, not your hero. Verify with+  one screenshot, don't chase it.+- Page renamed recently? The studio caches page data at boot: run its+  `refresh-data.sh`, restart your instance, re-POST state + re-import the vitrine+  (restart wipes in-memory state AND the reviewer's ticked checkboxes — warn them).++## Apply after Generate++```bash+cp <hero-studio>/generated/hero-kicad-bridge-<ts>.png kicad-bridge-hero-vN.png+adom-wiki repo push adom/kicad-bridge --files kicad-bridge-hero-vN.png -m "hero vN: ..."+adom-wiki page hero adom/kicad-bridge --image kicad-bridge-hero-vN.png+```++The generated PNG carries a `tEXt` provenance comment (`generator: hero-studio`) —+that's what the registry checks. Keep the exact file; recompressing strips it.++Then refresh the live page in the reviewer's pup window (cache-bust query param)+and verify the render with a screenshot — never just say "applied".++## Pkg gotcha that looked like a hero problem++`pkg publish` failing `HERO_STUDIO_REQUIRED` with `"reason":"no_hero"` ++`"new_page":true` means your staging `package.json` still has a **stale slug**+(ours said `adom-desktop-kicad-bridge` post-rename, so the registry saw a hero-less+new page). Fix the slug; the pkg needs no `hero` field at all — the check reads the+page. Also: stage from a clean dir; a stale `page.json` overlaid from an old tarball+draws a `PAGE_JSON_IGNORED` warning and can resurrect deleted files.
skills/kicad-bridge-hero/compose_hero.pyadded+120
@@ -0,0 +1,120 @@+#!/usr/bin/env python3+"""Compose the kicad-bridge hero in the Adom bridge-family style (Pup/Fusion/KiCad).++Clones the Pup hero's measured geometry exactly, drops in a fresh vitrine capture,+and ASSERTS the ink alignment before saving. See SKILL.md in this directory for the+full workflow (capture -> compose -> Hero Studio gate -> apply).++Usage:+    python3 compose_hero.py <vitrine-capture.png> <out.png>++The vitrine capture should be a real KiCad window screenshotted at exactly+1030x1025 (resize it first with desktop_set_window_bounds {"w":1030,"h":1025}).+Anything else gets LANCZOS-scaled, which softens UI text.+"""+import sys+from pathlib import Path++import numpy as np+from PIL import Image, ImageDraw, ImageFilter, ImageFont++HERE = Path(__file__).parent+PUP = HERE / "reference-pup-hero.png"          # Pup's true published asset (hero-v9)+FONTS = HERE.parent / "kicad-tour" / "hero" / "fonts"++W, H = 2000, 1250+TITLE = "KiCad"+SUB_1A, SUB_1B = "Your real KiCad, ", "driven by AI"+SUB_2A, SUB_2B = "entirely in the ", "background."   # accent word(s) in teal+TEAL = (0, 230, 220)++# Measured off Pup's asset — every bridge hero shares these (see SKILL.md "Layout numbers"):+LOGO_CROP, LOGO_AT = (60, 48, 335, 130), (60, 48)     # ADOM wordmark, verbatim from Pup+PILL_CROP, PILL_AT = (1796, 36, 1964, 108), (1796, 36)  # BRIDGE pill, verbatim from Pup+INK_EDGES = (79, 97, 83)        # logo / title / subtitle left ink edges+CAP_TOP = 263                    # title cap line+SUB_Y1, SUB_Y2 = 501, 565        # subtitle line tops+VITRINE_AT, VITRINE_W, VITRINE_H = (920, 150), 1030, 1025+RADIUS, SHADOW_BLUR, SHADOW_DY = 24, 28, 12+++def ink_edges(im):+    a = np.array(im).astype(int)+    diff = np.abs(a - a[700, 400]).sum(axis=2) > 45+    def left(y0, y1):+        for x in range(40, 900):+            if diff[y0:y1, x].sum() > 2:+                return x+    return left(48, 132), left(263, 460), left(495, 640)+++def compose(title_dx, sub_dx, pup, base, tf, sf):+    hero = base.copy()+    hero.paste(pup.crop(LOGO_CROP), LOGO_AT)+    hero.paste(pup.crop(PILL_CROP), PILL_AT)+    d = ImageDraw.Draw(hero)+    x, y = title_dx, CAP_TOP - tf.getbbox(TITLE[0])[1]+    for ch in TITLE:                      # -5px tracking, per the studio's .h-title letter-spacing+        d.text((x, y), ch, font=tf, fill=(230, 237, 243))+        x += d.textlength(ch, font=tf) - 5+    scol = (232, 238, 244)+    w = d.textlength(SUB_1A, font=sf)+    d.text((sub_dx, SUB_Y1), SUB_1A, font=sf, fill=scol)+    d.text((sub_dx + w, SUB_Y1), SUB_1B, font=sf, fill=scol)+    w = d.textlength(SUB_2A, font=sf)+    d.text((sub_dx, SUB_Y2), SUB_2A, font=sf, fill=scol)+    d.text((sub_dx + w, SUB_Y2), SUB_2B, font=sf, fill=TEAL)+    return hero+++def main(capture_path, out_path):+    if not PUP.exists():+        sys.exit(f"missing {PUP}\nFetch it: adom-wiki repo download pup-bridge --extract /tmp/pup-repo\n"+                 f"then copy hero-v9.png here. (Do NOT use `repo show` — it mangles binaries.)")+    pup = Image.open(PUP).convert("RGB")+    pa = np.array(pup)+    # Background: bilinear gradient through Pup's four corner colors.+    tl, tr = pa[6, 6].astype(float), pa[6, W - 7].astype(float)+    bl, br = pa[H - 7, 6].astype(float), pa[H - 7, W - 7].astype(float)+    xs = np.linspace(0, 1, W)[None, :, None]+    ys = np.linspace(0, 1, H)[:, None, None]+    base = Image.fromarray((tl * (1 - xs) * (1 - ys) + tr * xs * (1 - ys)+                            + bl * (1 - xs) * ys + br * xs * ys).astype(np.uint8))++    # Title: Familjen Grotesk BOLD (brand doctrine, hero-studio .h-title = 700).+    # 242px makes cap height match Pup's measured 155px.+    tf = ImageFont.truetype(str(FONTS / "FamiljenGrotesk-Bold.ttf"), 242)+    sf = ImageFont.truetype(str(FONTS / "Satoshi-Regular.ttf"), 52)++    # Calibrate: fonts lie about left bearings (Familjen reported 0 for K, drew 16).+    # Render, measure actual ink, correct, re-render, assert.+    t_dx, s_dx = 98, 84+    lo, ti, su = ink_edges(compose(t_dx, s_dx, pup, base, tf, sf))+    t_dx += INK_EDGES[1] - ti+    s_dx += INK_EDGES[2] - su+    hero = compose(t_dx, s_dx, pup, base, tf, sf)+    assert ink_edges(hero) == INK_EDGES, f"alignment drifted: {ink_edges(hero)} != {INK_EDGES}"++    # Vitrine: real window capture in Pup's exact floating-window rect.+    win = Image.open(capture_path).convert("RGB")+    if win.size != (VITRINE_W, VITRINE_H):+        print(f"WARNING: capture is {win.size}, scaling to {(VITRINE_W, VITRINE_H)} (softens text; "+              f"prefer resizing the real window before capture)")+        win = win.resize((VITRINE_W, VITRINE_H), Image.LANCZOS)+    mask = Image.new("L", (VITRINE_W, VITRINE_H), 0)+    ImageDraw.Draw(mask).rounded_rectangle((0, 0, VITRINE_W - 1, VITRINE_H - 1), radius=RADIUS, fill=255)+    sh = Image.new("L", (W, H), 0)+    vx, vy = VITRINE_AT+    ImageDraw.Draw(sh).rounded_rectangle((vx, vy + SHADOW_DY, vx + VITRINE_W, vy + SHADOW_DY + VITRINE_H),+                                         radius=RADIUS, fill=110)+    sh = sh.filter(ImageFilter.GaussianBlur(SHADOW_BLUR))+    hero = Image.composite(Image.new("RGB", (W, H), (0, 0, 0)), hero, sh)+    hero.paste(win, VITRINE_AT, mask)+    hero.save(out_path)+    print(f"saved {out_path}  (alignment {INK_EDGES} verified)")+++if __name__ == "__main__":+    if len(sys.argv) != 3:+        sys.exit(__doc__)+    main(sys.argv[1], sys.argv[2])
uninstall.sh+6−1
@@ -1,2 +1,7 @@ #!/usr/bin/env bash-rm -rf "$HOME/.claude/skills/adom-desktop-kicad-bridge"+set -euo pipefail+D="$HOME/.claude/skills"+rm -rf "$D/kicad-bridge" "$D/kicad-interaction" "$D/kicad-3d-models" "$D/kicad-tour" \+       "$D/kicad-bridge-dev" "$D/kicad-bridge-publish" "$D/kicad-bridge-hero" \+       "$D/adom-desktop-kicad-bridge"+echo "[kicad-bridge] skills removed."