← Commit history
SKILL.md+22−2
@@ -15,8 +15,17 @@ The studio opens as a bottom-dock WIZARD: seven steps (Package, Showcase image, Placement, Background, Headline and text, Finishing, Review and generate), one decision at a time, the hero always live above, a dashed ring marking what the current step edits. The old full control panel is one click-away (header "Classic") and keeps the component-mode extras. Component pages-ride the same wizard for now. The title ALWAYS bakes (it IS the message) and+away (header "Classic") and keeps the component-mode extras. Hardware+pages (--type hardware or its legacy spelling component) ride the same+wizard and the same composition: title, subtitle, and YOUR PART's render in+the showcase slot, floating bare by default (no window frame, bar, or+shadow; a fake app window around a 3D board render reads as a screenshot+of software). Load the render with `hero-studio vitrine --file+<render.png>` (a chip-thumbnailer `*-icon.png` is alpha-keyed and floats+cleanly); `--chrome none|fake` overrides the bare default. The studio's+"component" MODE in Classic is a sample-parts design lab (built-in photos+of a JST connector) that ignores the posed page and the vitrine; posing+never enters it. The title ALWAYS bakes (it IS the message) and its ink top-left is pixel-anchored at any font size (fixed leading + cap trim; the leading control is gone, the wire field remains inert). @@ -46,6 +55,17 @@ scales by gating the STYLE, not every part:    look. parts.json rows: {ref, title, kind?, vitrine?: <render path>,    headline?, subtitle?}. +EXISTING pages (a fleet that already lives on the wiki, e.g. 136 of the+137 basic parts) are not republished: push the PNG into the page repo and+point the page at it, `adom-wiki repo push <ref> --files docs/hero.png -m+"hero" && adom-wiki page hero <ref> --image docs/hero.png` (repo push+30/min, the hero call is a page edit at 10/min). The reference script does+this automatically when its probe finds the page (`--existing` / `--new`+force either path). Note that `page hero --image` accepts an unstamped+PNG today: the wiki's hero gate checks provenance at pkg publish, not on+this call, so the attestation a batch hero carries is your record, not an+enforced one; keep the human in the loop anyway.+ Generation is local (no wiki calls, no rate limits). Publishing is capped at 10 logical publishes/min PER ACCOUNT (sliding minute window; wiki#131 has the full table: repo push 30/min, page edit 10/min; IP adds nothing
docs/bulk-publish-reference.mjs+51−2
@@ -15,12 +15,20 @@    Usage:      node bulk-publish-reference.mjs --parts ./parts --org adom \        --style <saved-style> --human colby --quote "match the family style" \-       [--pace 4] [--dry-run] [--studio-port 8846]+       [--pace 7] [--existing | --new] [--dry-run] [--studio-port 8846]     Before running: pose ONE representative part, let a human Generate, then    `hero-studio style save <name>`. That approval gates every hero this    script produces (it rides each PNG's provenance). +   New vs existing pages: a part whose page does NOT exist yet is scaffolded+   and `pkg publish`ed. A part whose page ALREADY exists (Arav's basic-parts+   fleet: 136 of 137) is not republished; its hero is pushed into the page+   repo (repo push, 30/min) and applied with `page hero --image` (a page+   edit, 10/min). Both paths get the same gated, provenance-stamped PNG.+   Pass --existing to force the update path, --new to force publish; the+   default probes each page once (`adom-wiki page stats`, a read).+    Rate limits (exact, from the wiki team on adom/wiki#131): sliding    60-second window PER ACCOUNT (IP does not add a cap for authed calls).    pkg publish and page create are 10/min each, and a new-page publish@@ -44,6 +52,7 @@ const QUOTE = arg('--quote'); const PACE = Number(arg('--pace', 7)); const PORT = arg('--studio-port', process.env.HERO_STUDIO_PORT || '8846'); const DRY = has('--dry-run');+const FORCE_MODE = has('--existing') ? 'existing' : has('--new') ? 'new' : ''; if (!PARTS || !STYLE || !HUMAN || !QUOTE) {   console.error('usage: bulk-publish-reference.mjs --parts <dir> --style <name> --human <who> --quote "<their words>" [--org adom] [--pace 4] [--dry-run]');   process.exit(2);@@ -97,7 +106,47 @@ for (const p of partsManifest) {   if (state.done[p._dir] || state.failed[p.ref]) continue;   const dir = path.join(PARTS, p._dir);   const meta = JSON.parse(fs.readFileSync(path.join(dir, 'part.json'), 'utf8'));-  if (DRY) { console.error(`[dry] would publish ${p.ref}`); continue; }+  if (DRY) { console.error(`[dry] would ${FORCE_MODE || 'probe then publish/update'} ${p.ref}`); continue; }+  // existing page: push the hero into its repo and point the page at it.+  // No scaffold, no publish; the page keeps its README, files, and history.+  let exists = FORCE_MODE === 'existing';+  if (!FORCE_MODE) {+    const probe = await run('adom-wiki', ['page', 'stats', p.ref, '--json'], dir);+    exists = probe.ok && /"status":\s*"ok"/.test(probe.out);+  }+  if (exists) {+    if (!fs.existsSync(path.join(dir, 'docs', 'hero.png'))) {+      console.error(`  ${p.ref}: exists but no docs/hero.png was generated for it; skipping`);+      state.failed[p.ref] = 'no-hero';+      save();+      continue;+    }+    let applied = false;+    for (let attempt = 0, backoff = 30; attempt < 4 && !applied; attempt++) {+      const push = await run('adom-wiki', ['repo', 'push', p.ref, '--files', 'docs/hero.png', '-m', 'hero: gated style ' + STYLE], dir);+      const hero = push.ok ? await run('adom-wiki', ['page', 'hero', p.ref, '--image', 'docs/hero.png'], dir) : push;+      if (push.ok && hero.ok && !/"status":\s*"error"/.test(hero.out)) {+        applied = true;+        if (meta.source) await run('adom-wiki', ['page', 'edit', p.ref, '--source', meta.source], dir);+        state.done[p._dir] = { at: new Date().toISOString(), mode: 'existing' };+        delete state.failed[p.ref];+        console.error(`  ${p.ref}: hero updated (existing page)`);+      } else if (/429|rate/i.test(push.out + hero.out)) {+        const ra = (push.out + hero.out).match(/retry[- ]after[:\s]+(\d+)/i);+        const wait = ra ? Math.max(Number(ra[1]), 5) : backoff;+        console.error(`  ${p.ref}: rate-limited; waiting ${wait}s (${ra ? 'server Retry-After' : 'backoff'}, attempt ${attempt + 1})`);+        await sleep(wait);+        backoff *= 2;+      } else {+        console.error(`  ${p.ref}: FAILED: ${(push.out + hero.out).split('\n').find((l) => /error/i.test(l)) || 'unknown'}`);+        state.failed[p.ref] = 'update';+        break;+      }+    }+    save();+    await sleep(PACE);+    continue;+  }   // scaffold in place if package.json is absent   if (!fs.existsSync(path.join(dir, 'package.json'))) {     const pkg = {
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.19.8",+  "version": "0.19.9",   "description": "The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.",   "license": "MIT",   "tags": [
page.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.19.8",+  "version": "0.19.9",   "description": "The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.",   "license": "MIT",   "tags": [
pose.mjs+9−1
@@ -179,7 +179,15 @@ await jf('api/pages', {     { owner, slug, title, type: typeList[0], types: typeList.length > 1 ? typeList : null, brief: flags.brief },   ).filter(([, v]) => v))), });-const poseMode = (typeList[0] === 'component') ? 'component' : 'app';+// Every type poses into the app composition (title + subtitle + showcase+// window): that is the surface the wizard drives and the only one that honors+// an imported vitrine. The studio's legacy 'component' mode is a sample-parts+// design lab (built-in photos of a JST connector, its own text zone) that+// ignores the posed page AND the vitrine, so posing --type component into it+// produced heroes of a completely different part (Arav, basic-parts rollout).+// It stays reachable from Classic for what it is. Hardware pages get a bare,+// frameless showcase by default (app.js chrome auto).+const poseMode = 'app'; const samePageRepose = !!(state && state.page === ref && state.vitrine && state.vitrine.src); if (samePageRepose && !('fresh-vitrine' in flags)) {   console.error('note: re-posing ' + ref + ' with staged vitrine content preserved (' + Math.round(state.vitrine.src.length / 1024) + 'kB' + (state.vitrine.full ? ', full-hero import' : '') + '); pass --fresh-vitrine to start clean');
public/js/app.js+8−1
@@ -206,7 +206,11 @@ function render() {   // fake titlebar: real window screenshots carry their own chrome, so a   // second bar above it reads as an obvious fake (#11). Auto = off whenever   // an import is present; the CSS mock keeps it. Explicit choice wins.-  const chromeMode = vit.chrome || ((vit.src && !vit.full) ? 'none' : 'fake');+  // Hardware pages: an imported render is a physical part, not a program,+  // so auto = bare (no frame, bar, or shadow at all); a fake app window+  // around a 3D board render reads as a screenshot of software.+  const isHardware = canonType(d.type) === 'component';+  const chromeMode = vit.chrome || ((vit.src && !vit.full) ? (isHardware ? 'bare' : 'none') : 'fake');   hero.classList.toggle('no-chrome', chromeMode === 'none');   hero.classList.toggle('bare-chrome', chromeMode === 'bare');   $('vitrine-chrome').checked = chromeMode === 'fake';@@ -627,6 +631,9 @@ function clearTourGlow() { } function showTourStep() {   clearTourGlow();+  // the walkthrough is classic-only: in wizard mode the review step owns+  // spotlighting, and two rings on the stage read as one ring too many+  if (document.body.classList.contains('wizard')) { $('tourpop').style.display = 'none'; return; }   const confirm = state.confirm || {};   const flags = state.flags || {};   // a flagged step is reviewed (verdict: needs fixing), so the tour moves on;
public/js/wizard.js+5−2
@@ -126,7 +126,9 @@ const WIZ_STEPS = [   },   {     id: 'showcase', title: 'Showcase image',-    hint: 'The screenshot shown in your hero. Use your real app, not a mock.',+    hint: () => (canonType(wp().type) === 'component'+      ? 'The picture of your part. Use its real render or photo; it floats bare, no app window.'+      : 'The screenshot shown in your hero. Use your real app, not a mock.'),     summary: () => (state.vitrine && state.vitrine.src ? (state.vitrine.full ? 'Full-hero import' : 'Screenshot') : 'Drawn mock'),     options: () => [       { v: 'page',   label: 'From the wiki page', sub: 'The screenshot already on the page' },@@ -567,7 +569,7 @@ function wizardRender() {   wizRenderRail();   $('wintro-ref').textContent = state.page;   $('wd-title').textContent = (wizStepIndex(wizStep) + 1) + '. ' + s.title;-  $('wd-hint').textContent = s.hint;+  $('wd-hint').textContent = typeof s.hint === 'function' ? s.hint() : s.hint;   const next = WIZ_STEPS[wizStepIndex(wizStep) + 1];   $('wbtn-next').textContent = next ? 'Next: ' + next.title : 'Done';   $('wbtn-next').style.display = next && !s.review ? '' : 'none';@@ -649,6 +651,7 @@ $('wiz-classic').addEventListener('click', () => {   document.body.classList.toggle('classic', toClassic);   $('wiz-classic').textContent = toClassic ? 'Wizard' : 'Classic';   if (!toClassic) wizardRender();+  showTourStep(); // hand the spotlight to whichever mode we just entered });  /* boot: wizard is the default surface; a pose (verify) starts at Showcase