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.21.1
7 files changed
+41−4
data/pages.json+14@@ -1,4 +1,18 @@ [+ {+ "type": "skill",+ "title": "Adom brand",+ "author": "",+ "score": 0,+ "stars": 0,+ "installs30d": 0,+ "brief": "",+ "tags": [],+ "updated_at": "2026-09-18T21:08:09.341Z",+ "aiThread": "t",+ "owner": "adom",+ "slug": "brand"+ }, { "type": "skill", "title": "test5 brand",
package.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.21.0",+ "version": "0.21.1", "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.21.0",+ "version": "0.21.1", "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": [
public/js/app.js+3−1@@ -1119,7 +1119,9 @@ function computeLint() { items.push({ id: 'brand-unresolved', level: 'fail', el: null, msg: 'the brand guide for "' + owner + '" could not be resolved' + (g && g.error ? ' (' + g.error + ')' : '') + '. The wiki refuses a hero that does not wear the owner\'s confirmed guide, so Generate is locked: hero-studio doctor --owner ' + owner }); } else if (g.status === 'missing') { const st = g.starter || {};- items.push({ id: 'brand-missing', level: 'fail', el: null, msg: owner + ' has no brand guide' + (st.result === 'refused' ? ', and a starter could not be drafted for you: ' + (st.error || st.code) : st.result === 'drafting' || st.result === 'timeout' ? '; a starter is being drafted, re-pose in a moment' : '') + '. ' + who });+ items.push({ id: 'brand-missing', level: 'fail', el: null, msg: st.result === 'exists_unreadable'+ ? (st.error || owner + '/brand exists but this studio cannot read it') + '. ' + (st.hint || '')+ : owner + ' has no brand guide' + (st.result === 'refused' ? ', and a starter could not be drafted for you: ' + (st.error || st.code) : st.result === 'drafting' || st.result === 'timeout' ? '; a starter is being drafted, re-pose in a moment' : '') + '. ' + who }); } else if (g.status === 'invalid') { const fields = (g.errors || []).map((e) => (e && e.field) || '?').join(', '); items.push({ id: 'brand-invalid', level: 'fail', el: null, msg: owner + '/brand fails brand.json v1 validation' + (fields ? ' (' + fields + ')' : '') + '. The wiki refuses a hero on an unconfirmed guide, so Generate is locked until those fields are fixed at the Brand step. ' + who });
public/js/wizard.js+2@@ -747,6 +747,8 @@ function brandStepBody(host) { d.className = 'hint wiz-brand-call'; d.textContent = st.result === 'refused' ? 'A starter guide could not be drafted for ' + (g.owner || 'this owner') + ': ' + (st.error || st.code) + (st.hint ? ' ' + st.hint : '')+ : st.result === 'exists_unreadable'+ ? (st.error || (g.owner + '/brand exists but this studio cannot read it')) + '. ' + (st.hint || '') : st.result === 'unreachable' ? 'The registry could not be reached to draft a starter guide: ' + (st.error || '') + '. Pose again when it answers.' : 'A starter guide is being drafted for ' + (g.owner || 'this owner') + '; pose again in a moment and it opens populated.';
server.js+12−1@@ -551,7 +551,18 @@ async function draftStarter(owner) { if (r.status === 200 || brandOwnerFor(state.page) !== owner) break; await new Promise((res) => setTimeout(res, 1000)); }- outcome.result = 'timeout';+ if (r.status === 200) {+ // The registry says the guide exists, the resolver says there is+ // none: the two answers came from different identities, or this+ // studio's token is not what it thinks. Say that, rather than leave+ // the step waiting for a draft that will never come.+ outcome.result = 'exists_unreadable';+ outcome.error = owner + '/brand exists on the registry but this studio cannot read it';+ outcome.hint = 'the studio reads the guide as the human at the gate (the container key at /var/run/adom/api-key); check that key is present and that this identity can read ' + owner + '/brand';+ logLine('warn', 'brand starter for ' + owner + ': exists, but the resolver answers missing (identity mismatch?)');+ } else {+ outcome.result = 'timeout';+ } } else { outcome = { attempted: true, result: 'refused', code: body.code || ('HTTP_' + r.status), error: body.error || ('HTTP ' + r.status), hint: body.hint || null }; logLine('warn', 'brand starter for ' + owner + ' refused: ' + outcome.error);
test/brand.mjs+8@@ -96,6 +96,8 @@ let brandRouteBroken = false; // starter route (0.21.0): who gets a draft, who is refused const DRAFTED = new Set(); const STARTER_REFUSED = new Set(['nobodyelse']);+// the registry says exists, the resolver keeps saying missing: two identities+const STARTER_EXISTS_UNREADABLE = new Set(['ghostco']); const starterCalls = []; const stub = http.createServer((req, res) => {@@ -122,6 +124,7 @@ const stub = http.createServer((req, res) => { const o = decodeURIComponent(m[1]); starterCalls.push(o); if (STARTER_REFUSED.has(o)) return send(403, { error: 'Only ' + o + ' can edit ' + o + '/brand.', code: 'BRAND_EDITORS', hint: 'ask ' + o });+ if (STARTER_EXISTS_UNREADABLE.has(o)) return send(200, { ok: true, owner: o, status: 'exists', drafting: false }); if (GUIDES[o] || DRAFTED.has(o)) return send(200, { ok: true, owner: o, status: 'exists', drafting: false }); // 202 now; the draft "lands" on the next resolver read setTimeout(() => DRAFTED.add(o), 300);@@ -275,6 +278,11 @@ async function main() { ok(' and names the refusal', g.starter && g.starter.result === 'refused' && g.starter.code === 'BRAND_EDITORS', JSON.stringify(g.starter)); r = await post('generate', { fromGate: true }); ok(' Generate stays locked on it', r.ok === false && /no brand guide/.test(r.error || ''), JSON.stringify(r).slice(0, 200));+ g = await pose('ghostco/brand');+ eq('exists on the registry but unreadable here stays missing', g.status, 'missing');+ ok(' and is named as an identity problem, not a draft in progress', g.starter && g.starter.result === 'exists_unreadable' && /cannot read/.test(g.starter.error || ''), JSON.stringify(g.starter));+ r = await post('generate', { fromGate: true });+ ok(' Generate stays locked and says so', r.ok === false && /cannot read it/.test(JSON.stringify(r)), JSON.stringify(r).slice(0, 220)); // 4c. 0.21.0: Generate on a starter the human may edit confirms it in the // same click; the stamp is what the registry holds AFTER the push.