← Commit history
SKILL.md+29
@@ -493,6 +493,35 @@ cannot always reach them itself (a private guide needs the token, and headless Chrome refuses a self-signed registry outright). Only roles the resolved guide names are served, so it is not an open proxy. +### Generate confirms the guide (0.21.0)++From 0.21.0 the wiki REFUSES a studio hero (`400 BRANDING_POLICY`, on the+hero route and on publish) unless its stamp says `branding: <page owner>` and+`brand_status: confirmed`. Nobody should ever see that refusal, so the studio+makes it impossible to produce:++- **Missing guide:** on pose the studio asks the registry to draft a starter+  (`POST /api/owners/<owner>/brand/starter`) and waits for it, so the Brand+  step opens populated. Once per owner per session; the outcome rides on the+  guide as `starter` (`drafting`, `exists`, `refused`, `unreachable`).+- **Starter guide the human may edit:** Generate confirms it in the same+  click: brand.json goes out with `status: confirmed`, `confirmed_by` (the+  human's handle, from the registry) and `confirmed_at`, and the stamp is read+  from the registry AFTER that push, never from the pose. The Brand step says+  so, so the click is informed; the Confirm card stays for approving without+  generating.+- **Anything that cannot end confirmed** keeps Generate locked with the+  resolver's own words (`editable_by.text`): a guide this identity cannot+  edit, an invalid one, a refused draft, an unreachable registry. The linter+  shows these as hard failures. Never a fall back to Adom's or a neutral look:+  the wiki would refuse the result.+- Whose guide: the page's org tag (`org_name`), else the URL owner. An+  adom-org page under a user namespace wears `adom`, which is what the wiki+  checks against.++Stamps below 0.21.0 get a warning from the wiki, not a refusal, so an older+studio keeps working until it updates.+ ### The Brand step, and who may confirm  When the guide is anything but `confirmed`, the wizard inserts a **Brand**
data/pages.json+14
@@ -1,4 +1,18 @@ [+ {+  "type": "skill",+  "title": "test5 brand",+  "author": "",+  "score": 0,+  "stars": 0,+  "installs30d": 0,+  "brief": "",+  "tags": [],+  "updated_at": "2026-09-18T21:06:07.423Z",+  "aiThread": "t",+  "owner": "test5",+  "slug": "brand"+ },  {   "type": "component",   "title": "Nametag Blank Large LED",
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.20.11",+  "version": "0.21.0",   "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.20.11",+  "version": "0.21.0",   "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+26−21
@@ -1102,32 +1102,37 @@ function computeLint() {     items.push({ id: 'import-multi-message', level: 'warn', el: 'fullimport-img', msg: 'the imported image reads as ' + bandScan.busyCells + ' of 12 zones text/graphic-dense: that is several competing messages, and a hero sends ONE. The title should be the message; white space is what makes it land' });   } -  // 6e. brand provenance: a hero wears the PAGE OWNER's brand, so the colors-  // and fonts on screen must trace to that owner's resolved guide. This reads-  // the COMPUTED style off the live title, not the state we intended to set,-  // so a var that failed to apply (missing font file, unparsed color, a theme-  // layer painting over the brand) is caught here rather than after publish.+  // 6e. brand provenance: from 0.21.0 the wiki REFUSES a studio hero that is+  // not stamped with the page owner's confirmed guide, so Generate is locked+  // (level fail) whenever this click could not end that way. A starter guide+  // the human may edit is fine: Generate confirms it in the same click. The+  // drawn-pixel checks below read COMPUTED style, so a font that fell back or+  // a theme painting over the brand is caught here, not after publish.   {     const g = state.guide || null;-    const owner = String(state.page || '').split('/')[0] || '';-    const foreign = !!owner && owner !== 'adom';+    const owner = (g && g.owner) || String(state.page || '').split('/')[0] || '';     const brand = (g && g.brand) || null;     const applied = hero.classList.contains('branded');-    if (!g || g.status === 'unavailable') {-      if (foreign) items.push({ id: 'brand-unresolved', level: 'warn', el: null, msg: 'the brand guide for "' + owner + '" could not be resolved' + (g && g.error ? ' (' + g.error + ')' : '') + ', so this hero is wearing Adom\'s colors and fonts on a page that is not Adom\'s. Resolve ' + owner + '/brand before publishing: hero-studio doctor --owner ' + owner });-    } else if (g.status === 'missing' && foreign) {-      items.push({ id: 'brand-missing', level: 'warn', el: null, msg: owner + ' has no brand guide, so this hero wears Adom\'s look on ' + owner + '\'s page. The wizard\'s Brand step writes a starter guide for ' + owner + ' to confirm' });-    } else if (g.status === 'private' && foreign) {-      items.push({ id: 'brand-private', level: 'warn', el: null, msg: owner + '/brand is not readable with this identity, so its colors and fonts cannot be applied and this hero wears Adom\'s look' });-    } else if (g.status === 'invalid') {-      const fields = (g.errors || []).map((e) => (e && e.field) || '?').join(', ');-      items.push({ id: 'brand-invalid', level: 'warn', el: null, msg: owner + '/brand fails brand.json v1 validation' + (fields ? ' (' + fields + ')' : '') + ', so none of it is applied and this hero wears Adom\'s look. Fix those fields at the Brand step' });-    } else if (brand && !applied) {-      items.push({ id: 'brand-not-applied', level: 'warn', el: null, msg: owner + '/brand resolved (' + g.status + ') but the hero is not rendering branded, so these colors and fonts are not the ones this page will publish with' });-    } else if (brand && applied) {-      if (g.status === 'starter') {-        items.push({ id: 'brand-starter', level: 'warn', el: null, msg: owner + '/brand is a STARTER guide nobody has confirmed: this hero is being drawn from colors and fonts ' + owner + ' has not approved. Confirm it at the Brand step (a human confirms, never a thread) before this hero is published' });+    const who = (g && g.editable_by && g.editable_by.text) || ('only ' + owner + ' can edit ' + owner + '/brand');+    if (state.mode === 'app') {+      if (!g || g.status === 'unavailable') {+        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 });+      } 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 });+      } else if (g.status !== 'confirmed' && !g.can_edit) {+        items.push({ id: 'brand-locked', level: 'fail', el: null, msg: owner + '/brand is ' + g.status + ' and this identity cannot confirm it, so Generate is locked. ' + who });+      } else if (g.status === 'starter' && g.can_edit) {+        items.push({ id: 'brand-starter', level: 'info', el: null, msg: 'Generate also confirms ' + owner + '/brand in this click, recording you as the human who approved these colors and fonts. Check them on the hero first' });       }+    }+    if (brand && !applied && g && g.status !== 'invalid') {+      items.push({ id: 'brand-not-applied', level: 'warn', el: null, msg: owner + '/brand resolved (' + g.status + ') but the hero is not rendering branded, so these colors and fonts are not the ones this page will publish with' });+    }+    if (brand && applied) {       // the drawn pixels vs the guide: computed font family and title ink       const titleEl = $('el-title');       if (titleEl && vis(titleEl)) {
public/js/wizard.js+29−5
@@ -720,21 +720,45 @@ function brandStepBody(host) {   // Confirm: the human action, and the ONLY way this step completes. It was a   // muted card at the end of a row and read as decoration, so it says plainly   // what it does and what happens if nobody clicks it.+  // 0.21.0: the wiki refuses a hero that does not wear the owner's CONFIRMED+  // guide, so the click that makes the hero is the click that confirms it.+  // Say so here, so the click is informed; the Confirm card stays as the+  // explicit path for someone who wants to approve without generating.   if (g.status !== 'confirmed' && g.can_edit) {     const call = document.createElement('div');     call.className = 'hint wiz-brand-call';-    call.textContent = 'Nobody has approved this look yet. Check the hero above, then click Confirm: '-      + 'that records you as the human who approved it and stamps it into every hero made from it. '-      + 'Until then each hero carries a warning that these colors were never approved.';+    call.textContent = g.status === 'starter'+      ? 'Nobody has approved this look yet. Check it on the hero above and adjust it here. '+        + 'Generate confirms this guide in the same click, recording you as the human who approved it, '+        + 'and every hero made from it after that carries that approval. Confirm now if you would rather approve without generating.'+      : 'Fix the fields named above, then Generate: it confirms the guide in the same click.';     notes.appendChild(call);   }+  if (g.status !== 'confirmed' && !g.can_edit) {+    const lock = document.createElement('div');+    lock.className = 'hint wiz-brand-call';+    lock.textContent = 'Generate is locked: the wiki refuses a hero that does not wear ' + (g.owner || 'this owner') + '\u2019s confirmed guide, and only someone who can edit it may confirm it. '+      + ((g.editable_by && g.editable_by.text) || '');+    notes.appendChild(lock);+  }+  if (g.status === 'missing' && g.starter) {+    const st = g.starter;+    const d = document.createElement('div');+    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 === '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.';+    notes.appendChild(d);+  }   const confirmCard = wizCard({     v: 'confirm',-    label: g.status === 'confirmed' ? 'Confirmed' : (g.can_edit ? 'Confirm this brand' : 'Cannot confirm'),+    label: g.status === 'confirmed' ? 'Confirmed' : (g.can_edit ? 'Confirm now' : 'Cannot confirm'),     sub: g.status === 'confirmed'       ? 'approved by ' + ((g.brand && g.brand.confirmed_by) || 'a human')       : (g.can_edit-        ? 'you are ' + (g.owner || 'this owner') + '; click to approve'+        ? 'approve without generating; Generate does this too'         : ((g.editable_by && g.editable_by.who) ? 'only ' + g.editable_by.who + ' can' : 'you do not have edit rights')),   }, false, () => {     if (!g.can_edit) return;
server.js+125−5
@@ -395,7 +395,7 @@ async function refreshPageLive(ref) {   const pg = pj.page || pj;   const [owner, slug] = ref.split('/');   const i = PAGES.findIndex((x) => x.owner === owner && x.slug === slug);-  const live = { owner, slug, title: pg.title, brief: pg.brief, type: pg.type === 'hardware' ? 'component' : pg.type, kind: pg.kind, live: true };+  const live = { owner, slug, title: pg.title, brief: pg.brief, type: pg.type === 'hardware' ? 'component' : pg.type, kind: pg.kind, org: pg.org_name || null, live: true };   const entry = Object.assign({ author: '', score: 0, stars: 0, installs30d: 0, tags: [] }, i >= 0 ? PAGES[i] : {}, Object.fromEntries(Object.entries(live).filter(([, v]) => v != null)));   if (i >= 0) PAGES[i] = entry; else PAGES.unshift(entry);   bumpPages();@@ -512,9 +512,90 @@ async function resolveBrandGuide(owner, { fresh = false } = {}) { }  /// Pull the guide for the posed page into state and tell every open surface.+/// Whose guide a page wears. The wiki judges a hero against the page's ORG+/// tag, not its URL: an adom-org page under a user namespace must wear adom.+/// The live page row carries that tag as org_name; without one, the URL owner.+function brandOwnerFor(ref) {+  const [owner, slug] = String(ref || '').split('/');+  const row = PAGES.find((x) => x.owner === owner && x.slug === slug);+  return (row && row.org) || owner || '';+}++// Owners this session already asked the registry to draft a starter for, so+// a missing guide is drafted once, not on every re-render; and what came of+// it, so a refusal or a slow draft is explained on every render, including a+// ?fresh=1 read that bypasses the guide cache.+const starterTried = new Set();+const starterOutcome = new Map();++/// Draft the owner's starter guide on demand (0.21.0, brand-policy handoff):+/// POST .../brand/starter answers 202 drafting (poll the resolver until it is+/// there, a few seconds), 200 exists (idempotent), or 403 BRAND_EDITORS when+/// this identity may not edit that owner's guide. Never throws: the outcome+/// rides on the guide as `starter` so the Brand step can say what happened.+async function draftStarter(owner) {+  const REG = process.env.ADOM_WIKI_REGISTRY || 'https://wiki.adom.inc';+  const headers = { 'Content-Type': 'application/json' };+  try { headers.Authorization = 'Bearer ' + fs.readFileSync('/var/run/adom/api-key', 'utf8').trim(); } catch {}+  let outcome;+  try {+    const r = await fetch(REG + '/api/owners/' + encodeURIComponent(owner) + '/brand/starter', { method: 'POST', headers, body: '{}' });+    const body = await r.json().catch(() => ({}));+    if (r.status === 202 || r.status === 200) {+      outcome = { attempted: true, result: r.status === 202 ? 'drafting' : 'exists' };+      logLine('info', 'brand starter for ' + owner + ': ' + outcome.result);+      // the draft takes a few seconds; the Brand step must open populated+      for (let i = 0; i < 15; i++) {+        const g = await resolveBrandGuide(owner, { fresh: true });+        if (g.status !== 'missing') return { guide: g, starter: outcome };+        if (r.status === 200 || brandOwnerFor(state.page) !== owner) break;+        await new Promise((res) => setTimeout(res, 1000));+      }+      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);+    }+  } catch (e) {+    outcome = { attempted: true, result: 'unreachable', error: String(e && e.message || e).slice(0, 200) };+  }+  const g = await resolveBrandGuide(owner, { fresh: true });+  return { guide: g, starter: outcome };+}++// One in-flight refresh per owner: a pose fires refreshGuide from pageChanged+// AND from the wizard's GET /brand?fresh=1, and the second used to skip the+// starter draft (owner already tried) and answer `missing` while the first+// was still polling. Both callers now await the same work.+const guideInflight = new Map();+ async function refreshGuide({ fresh = false } = {}) {-  const owner = String(state.page || '').split('/')[0];-  const guide = await resolveBrandGuide(owner, { fresh });+  const owner = brandOwnerFor(state.page);+  let p = guideInflight.get(owner);+  if (!p) {+    p = (async () => {+      let guide = await resolveBrandGuide(owner, { fresh });+      // A missing guide is drafted, once, so the Brand step is never empty+      // and a 0.21.0 hero can always be made to wear a confirmed guide.+      if (guide && guide.status === 'missing' && state.mode === 'app' && owner && !starterTried.has(owner)) {+        starterTried.add(owner);+        const drafted = await draftStarter(owner);+        starterOutcome.set(owner, drafted.starter);+        guide = drafted.guide;+      }+      if (guide && starterOutcome.has(owner)) {+        guide = Object.assign({}, guide, { starter: starterOutcome.get(owner) });+        brandCache.set(owner, { at: Date.now(), guide });+      }+      return guide;+    })().finally(() => guideInflight.delete(owner));+    guideInflight.set(owner, p);+  }+  const guide = await p;+  // A draft can take seconds; if the human posed another page meanwhile,+  // this result is about the OLD owner and must not become the new page's+  // guide (it stamped one page's hero with another owner's branding).+  if (brandOwnerFor(state.page) !== owner) return guide;   state.guide = guide;   logLine('info', 'brand guide: ' + owner + ' -> ' + (guide ? guide.status : 'none')     + (guide && guide.sha ? ' (' + String(guide.sha).slice(0, 8) + ')' : ''));@@ -1328,15 +1409,54 @@ function route(req, res, url, p) {               attestation: prior.attestation || prior.approval || undefined,             };             logLine('info', 'attested re-generate for ' + state.page + ' anchored to the published hero\'s provenance (' + prior.generator + ' ' + (prior.version || '?') + ')');-            proceed();+            confirmThenProceed();           })           .catch((e) => refuse('could not read the published hero\'s provenance: ' + e.message,             'the anchor for a cross-session attest is the page\'s current hero; check the page ref and that its hero is reachable, or fall back to an in-session human Generate'));         return;       }     }-    proceed();+    confirmThenProceed();     return;+    // 0.21.0 (brand-policy handoff): the wiki refuses a hero from this version+    // on that is not stamped with the page owner's CONFIRMED guide. So a+    // refusal must be impossible to produce here: a starter guide the human+    // may edit is confirmed by this same click (what `adom-wiki brand confirm`+    // does), and anything else that cannot end confirmed keeps Generate+    // locked with the resolver's own words. The stamp is read AFTER the push.+    function confirmThenProceed() {+      if (state.mode !== 'app') return proceed();+      const g = state.guide;+      const owner = g && g.owner;+      if (!g || g.status === 'unavailable') {+        return refuse('the brand guide for ' + (owner || 'this page') + ' could not be resolved, and the wiki refuses a hero that does not wear the owner\'s confirmed guide',+          'check the registry (hero-studio doctor --owner ' + (owner || '<owner>') + ') and pose again');+      }+      if (g.status === 'confirmed') return proceed();+      if (g.status === 'starter' && g.can_edit) {+        const human = fromGate ? '' : ((attest && attest.human) || '');+        return confirmBrandGuide(human, fromGate)+          .then((out) => {+            if (!out.ok) return refuse('confirming ' + owner + '/brand failed: ' + (out.error || out.errorCode), out.hint || 'fix the guide at the Brand step and try again');+            if (!state.guide || state.guide.status !== 'confirmed') {+              return refuse('the registry still reports ' + owner + '/brand as ' + ((state.guide && state.guide.status) || 'unknown') + ' after the confirm push', 'the hero would be refused by the wiki; re-read the guide and try again');+            }+            proceed();+          })+          .catch((e) => refuse('confirming ' + owner + '/brand failed: ' + String(e && e.message || e).slice(0, 200), 'try again, or confirm it with adom-wiki brand confirm ' + owner));+      }+      const who = (g.editable_by && g.editable_by.text) || ('only ' + owner + ' can edit ' + owner + '/brand');+      if (g.status === 'missing') {+        const st = g.starter || {};+        return refuse(owner + ' has no brand guide and one could not be drafted' + (st.error ? ' (' + st.error + ')' : ''),+          st.hint || (who + '; a hero cannot be made until the guide exists'));+      }+      if (g.status === 'invalid') {+        return refuse(owner + '/brand fails validation, and the wiki refuses a hero on an unconfirmed guide',+          'fix the named fields (' + (g.errors || []).map((e) => e.field).join(', ') + ') at the Brand step; ' + who);+      }+      return refuse('Generate is locked: ' + owner + '/brand is ' + g.status + ' and this identity cannot confirm it', who);+    }     function proceed() {     generating = true;     const slug = state.mode === 'component'
test/brand.mjs+76−3
@@ -93,6 +93,10 @@ const GUIDES = {  const pushes = []; 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']);+const starterCalls = [];  const stub = http.createServer((req, res) => {   const u = new URL(req.url, 'http://stub');@@ -100,10 +104,29 @@ const stub = http.createServer((req, res) => {   let m = u.pathname.match(/^\/api\/owners\/([^/]+)\/brand$/);   if (m && req.method === 'GET') {     if (brandRouteBroken) return send(500, { error: 'stub is down' });-    const g = GUIDES[decodeURIComponent(m[1])];+    const o = decodeURIComponent(m[1]);+    const g = GUIDES[o];+    if (!g && DRAFTED.has(o)) {+      // what the registry drafts: a starter the owner can edit+      return send(200, { status: 'starter', version: 1, sha: 'f'.repeat(40), can_edit: true, owner_kind: 'user', visibility: 'public',+        page: o + '/brand', url: 'http://stub/' + o + '/brand', editable_by: { rule: 'owner-only', owner_kind: 'user', who: [o], text: 'Only ' + o + ' can edit ' + o + '/brand.' },+        brand: { brand_manifest: '1.0', owner: o, name: o, status: 'starter', colors: { primary: '#394dd0', background: '#0c0d18', text: '#ffffff' },+          typography: { display: { family: 'Inter' }, body: { family: 'Inter' } }, logos: { mark: 'assets/mark.svg' } },+        assets: { mark: 'http://stub/' + o + '/brand/files/assets/mark.svg', fonts: [] }, hints: [], warnings: [], errors: [] });+    }     if (!g) return send(404, { error: 'no brand guide', hints: [{ level: 'warning', code: 'BRAND_GUIDE_STARTER', message: 'no guide yet' }] });     return send(200, g);   }+  m = u.pathname.match(/^\/api\/owners\/([^/]+)\/brand\/starter$/);+  if (m && req.method === 'POST') {+    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 (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);+    return send(202, { ok: true, owner: o, status: 'drafting', drafting: true });+  }   m = u.pathname.match(/^\/api\/pages\/([^/]+)\/brand\/files$/);   if (m && req.method === 'POST') {     let body = '';@@ -111,11 +134,19 @@ const stub = http.createServer((req, res) => {     req.on('end', () => {       let parsed = null;       try { parsed = JSON.parse(body); } catch {}-      pushes.push({ owner: decodeURIComponent(m[1]), body: parsed });+      const o = decodeURIComponent(m[1]);+      pushes.push({ owner: o, body: parsed });+      try {+        const bj = (parsed.files || []).find((f) => f.path === 'brand.json');+        const b = JSON.parse(bj.encoding === 'base64' ? Buffer.from(bj.content, 'base64').toString('utf8') : bj.content);+        if (b.status === 'confirmed' && GUIDES[o]) { GUIDES[o].status = 'confirmed'; GUIDES[o].brand = b; GUIDES[o].sha = 'e'.repeat(40); }+      } catch {}       send(200, { ok: true, sha: 'e'.repeat(40) });     });     return;   }+  // the human at the gate, as the registry names them+  if (u.pathname === '/auth/me') return send(200, { user: { username: 'starterco' } });   // the page rows the studio pulls for titles: answer empty so nothing hangs   if (u.pathname.startsWith('/api/pages')) return send(404, { error: 'not found' });   send(404, { error: 'not found' });@@ -168,7 +199,9 @@ async function main() {     ok('  relays the server hints', (g.hints || []).some((h) => h.code === 'BRAND_GUIDE_STARTER'));     ok('  relays editable_by.text verbatim', (g.editable_by || {}).text === 'Only starterco can edit starterco/brand.'); -    g = await pose('nobodyco/brand');+    // (nobodyelse: the registry refuses to draft for this identity, so the+    // 404 stays missing; nobodyco, drafted on pose, is covered in 4b)+    g = await pose('nobodyelse/brand');     eq('a 404 is missing, not an error', g.status, 'missing');     ok('  and never borrows Adom\'s brand body', !g.brand); @@ -229,6 +262,46 @@ async function main() {       ok('  logos survive the confirm', !!(written.logos && written.logos.mark));     } +    // 4b. 0.21.0: a missing guide is drafted on pose, once, so the Brand step+    // is never empty; a refused draft is carried on the guide with the reason.+    console.log('starter auto-draft:');+    g = await pose('nobodyco/brand');+    eq('a missing guide is drafted and resolves as starter', g.status, 'starter');+    eq('  the starter route was called once for that owner', starterCalls.filter((o) => o === 'nobodyco').length, 1);+    ok('  the outcome rides on the guide', g.starter && g.starter.result === 'drafting', JSON.stringify(g.starter));+    g = await pose('nobodyelse/brand');+    eq('a refused draft leaves the guide missing', g.status, 'missing');+    eq('  and was tried exactly once for that owner across both poses', starterCalls.filter((o) => o === 'nobodyelse').length, 1);+    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));++    // 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.+    console.log('confirm inside Generate:');+    GUIDES.starterco.status = 'starter'; GUIDES.starterco.brand.status = 'starter';+    const before = pushes.length;+    await pose('starterco/brand');+    let gen21 = await post('generate', { fromGate: true, renderer: 'local' });+    ok('Generate on an editable starter renders', gen21.ok === true, JSON.stringify(gen21).slice(0, 300));+    eq('  and pushed the confirm', pushes.length, before + 1);+    let lastPush = pushes[pushes.length - 1];+    let wb = null; try { const bj = lastPush.body.files.find((f) => f.path === 'brand.json'); wb = JSON.parse(bj.content); } catch {}+    ok('  brand.json went out confirmed, by the human', wb && wb.status === 'confirmed' && wb.confirmed_by === 'starterco' && !!wb.confirmed_at, JSON.stringify(wb).slice(0, 200));+    if (gen21.ok && gen21.file) {+      const meta = await readPngComment(path.join(ROOT, 'generated', gen21.file));+      eq('  the stamp says confirmed (post-push), not starter', meta && meta.brand_status, 'confirmed');+      eq('  branding is the owner', meta && meta.branding, 'starterco');+      eq('  and the stamp is versioned 0.21.0 so the wiki judges it', meta && meta.version, '0.21.0');+      try { fs.unlinkSync(path.join(ROOT, 'generated', gen21.file)); } catch {}+    }+    await pose('lockedco/brand');+    const lockedBefore = pushes.length;+    gen21 = await post('generate', { fromGate: true });+    ok('Generate on a starter this identity cannot edit is refused', gen21.ok === false && /locked/.test(gen21.error || ''), JSON.stringify(gen21).slice(0, 200));+    ok('  with the resolver\'s own editor rule', /Only lockedco can edit/.test(JSON.stringify(gen21)));+    eq('  and nothing was pushed', pushes.length, lockedBefore);+     // 5. provenance names the brand the hero was drawn with, under exactly     // the four keys the handoff fixes: branding, brand_sha, brand_version,     // brand_status. A consumer reading a published hero can tell which guide