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