app
Hero Studio
Public Made by Adomby adom
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. Every hero wears its P
← Commit history
Publish 0.23.2
4 files changed
+65−35
SKILL.md+14−7@@ -59,19 +59,26 @@ 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+60/min, page hero 20/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-for authed calls). The reference script defaults to 7s pacing (safe-margin); two accounts double the aggregate if you need speed. You do not-have to write the loop: a complete runnable+Generation is local (no wiki calls, no rate limits). Publishing is rate+limited PER signed-in account over a sliding one-minute window: pkg+publish 30/min, repo push 60/min, page hero (POST .../hero) 20/min, page+edit (PUT: title, brief, tags, source) 10/min, page create 10/min, history+reads (repo log) 600/min (60 anonymous). Existing pages: repo push + page+hero, so page hero binds at 20 parts/min (3 s apart). New pages: pkg+publish alone creates the page (the reference script never calls page+create), so 30/min (2 s apart). A part whose part.json has `source` also+gets `page edit --source`, which binds at 10/min (6 s apart). The reference+script picks that pace per part from the path it takes (`--pace <s>`+overrides). A 429 returns `{"error":"Rate limit exceeded","retry_after":N,+"code":"RATE_LIMITED"}` plus a Retry-After header; wait that long, then+back off. You do not have to write the loop: a complete runnable reference ships with this package at docs/bulk-publish-reference.mjs (installed under adom_modules/adom/hero-studio/docs/). It walks a parts directory (part.json + model.step + render.png per part), batches the heroes
docs/bulk-publish-reference.mjs+49−26@@ -15,7 +15,7 @@ Usage: node bulk-publish-reference.mjs --parts ./parts --org adom \ --style <saved-style> --human colby --quote "match the family style" \- [--pace 7] [--existing | --new] [--dry-run] [--studio-port 8846]+ [--pace <seconds>] [--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@@ -24,20 +24,30 @@ 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).+ repo (`repo push`) and applied with `page hero --image` (POST .../hero).+ 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- touches both, so the binding ceiling is 10 logical publishes/min per- account; repo push is 30/min; page edit 10/min. The default pace here is- 7s (~8.5/min) for margin; 6s is the exact line. 300 pages at 7s is about- 35 minutes. Faster option: split the run across TWO accounts (the cap is- per account) and run --pace 4 in each. On a 429 the server sends- Retry-After, which this script honors before its exponential backoff. */+ Rate limits (wiki, 2026-10-06): sliding 60-second window PER signed-in+ account.+ pkg publish 30/min+ repo push 60/min+ page hero (POST) 20/min+ page edit (PUT) 10/min title/brief/tags/source+ page create (POST) 10/min this script never calls it+ history reads (log) 600/min (60 anonymous)+ Existing page = repo push + page hero, so page hero binds: 20 parts/min,+ 3 s apart. New page = pkg publish only (it creates the page itself; no+ separate page create call): 30/min, 2 s apart. A part whose part.json+ has "source" also gets `page edit --source`, which binds at 10/min, so+ that part paces 6 s. The default pace is picked PER PART from the path it+ takes; --pace <s> overrides it for every part. 300 existing pages take+ about 15 minutes, 300 new pages about 10.+ A 429 carries {"error":"Rate limit exceeded","retry_after":N,+ "code":"RATE_LIMITED"} plus a Retry-After header; this script honors+ whichever the CLI printed (body, header, or its RATE_LIMITED hint's+ "wait Ns and retry"), then falls back to exponential backoff. */ import { execFileSync, execFile } from 'child_process'; import fs from 'fs'; import path from 'path';@@ -49,16 +59,29 @@ const ORG = arg('--org', 'adom'); const STYLE = arg('--style'); const HUMAN = arg('--human'); const QUOTE = arg('--quote');-const PACE = Number(arg('--pace', 7));+const PACE_ARG = arg('--pace');+const PACE = PACE_ARG === undefined ? null : Number(PACE_ARG); 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]');+ console.error('usage: bulk-publish-reference.mjs --parts <dir> --style <name> --human <who> --quote "<their words>" [--org adom] [--pace <seconds>] [--existing | --new] [--dry-run]'); process.exit(2); } const AI_THREAD = process.env.ADOM_AI_THREAD || 'hardware bulk publish'; const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));+// Seconds between parts, from the write that binds on the path taken (see+// the rate table above), unless --pace overrides it.+const paceFor = (existing, meta) => PACE !== null ? PACE : meta.source ? 6 : existing ? 3 : 2;+// A 429's wait, in whatever form the CLI printed it: the JSON body's+// retry_after, the Retry-After header, or the RATE_LIMITED hint's+// "wait Ns and retry".+const retryAfter = (out) => {+ const m = out.match(/"retry_after"\s*:\s*(\d+)/i) || out.match(/retry[- ]after[:\s]+(\d+)/i)+ || out.match(/wait (\d+)s and retry/i);+ return m ? Number(m[1]) : null;+};+const rateLimited = (out) => /\b429\b|RATE_LIMITED|rate limit/i.test(out); const STATE_FILE = path.join(PARTS, 'bulk-state.json'); const state = fs.existsSync(STATE_FILE) ? JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')) : { done: {}, failed: {} }; const save = () => fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 1));@@ -131,10 +154,10 @@ for (const p of partsManifest) { 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})`);+ } else if (rateLimited(push.out + hero.out)) {+ const ra = retryAfter(push.out + hero.out);+ const wait = ra !== null ? Math.max(ra, 1) : backoff;+ console.error(` ${p.ref}: rate-limited; waiting ${wait}s (${ra !== null ? 'server retry_after' : 'backoff'}, attempt ${attempt + 1})`); await sleep(wait); backoff *= 2; } else {@@ -144,7 +167,7 @@ for (const p of partsManifest) { } } save();- await sleep(PACE);+ await sleep(paceFor(true, meta)); continue; } // scaffold in place if package.json is absent@@ -174,10 +197,10 @@ for (const p of partsManifest) { state.done[p._dir] = { at: new Date().toISOString() }; delete state.failed[p.ref]; console.error(` ${p.ref}: published`);- } else if (/429|rate/i.test(pub.out)) {- const ra = pub.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})`);+ } else if (rateLimited(pub.out)) {+ const ra = retryAfter(pub.out);+ const wait = ra !== null ? Math.max(ra, 1) : backoff;+ console.error(` ${p.ref}: rate-limited; waiting ${wait}s (${ra !== null ? 'server retry_after' : 'backoff'}, attempt ${attempt + 1})`); await sleep(wait); backoff *= 2; } else {@@ -187,6 +210,6 @@ for (const p of partsManifest) { } } save();- await sleep(PACE);+ await sleep(paceFor(false, meta)); } console.error(`done: ${Object.keys(state.done).length} published, ${Object.keys(state.failed).length} failed (bulk-state.json has the ledger; re-running resumes)`);
package.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.23.1",+ "version": "0.23.2", "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. Every hero wears its PAGE OWNER's brand guide (<owner>/brand): colors, type, logo and background themes, Adom's included; an unresolved guide holds unbranded rather than borrowing Adom's look. Placement is fixed for all. The floating app window is the showcase: 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, showcase, type match) and a Generate that unlocks only fully confirmed. Includes showcase 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.23.1",+ "version": "0.23.2", "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. Every hero wears its PAGE OWNER's brand guide (<owner>/brand): colors, type, logo and background themes, Adom's included; an unresolved guide holds unbranded rather than borrowing Adom's look. Placement is fixed for all. The floating app window is the showcase: 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, showcase, type match) and a Generate that unlocks only fully confirmed. Includes showcase 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": [