From 6b7f62979ba70882943de297f8480d292fcba6b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 20:28:57 +0000 Subject: [PATCH] Sync generated provider output --- .../skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .../skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .../skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .../skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .../skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .grok/skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .../skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .kiro/skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .../skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .pi/skills/impeccable/reference/new-work.md | 2 +- .pi/skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .../skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .../skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .../skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .trae/skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .vibe/skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- .../skills/impeccable/reference/new-work.md | 2 +- .../skills/impeccable/reference/visualize.md | 4 +- .../impeccable/scripts/concept-seed.mjs | 52 +++++++++++++++---- .../impeccable/scripts/generate-image.mjs | 49 ++++++++++++++--- .../impeccable/scripts/serve-question.mjs | 38 ++++++++++++-- 80 files changed, 1952 insertions(+), 368 deletions(-) diff --git a/.agents/skills/impeccable/reference/new-work.md b/.agents/skills/impeccable/reference/new-work.md index b14e7f678..34d00647a 100644 --- a/.agents/skills/impeccable/reference/new-work.md +++ b/.agents/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .agents/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.agents/skills/impeccable/reference/visualize.md b/.agents/skills/impeccable/reference/visualize.md index fb0493a97..99858efba 100644 --- a/.agents/skills/impeccable/reference/visualize.md +++ b/.agents/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.agents/skills/impeccable/scripts/concept-seed.mjs b/.agents/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.agents/skills/impeccable/scripts/concept-seed.mjs +++ b/.agents/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.agents/skills/impeccable/scripts/generate-image.mjs b/.agents/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.agents/skills/impeccable/scripts/generate-image.mjs +++ b/.agents/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.agents/skills/impeccable/scripts/serve-question.mjs b/.agents/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.agents/skills/impeccable/scripts/serve-question.mjs +++ b/.agents/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.claude/skills/impeccable/reference/new-work.md b/.claude/skills/impeccable/reference/new-work.md index 2476008c5..3ac4c0d18 100644 --- a/.claude/skills/impeccable/reference/new-work.md +++ b/.claude/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.claude/skills/impeccable/reference/visualize.md b/.claude/skills/impeccable/reference/visualize.md index 2b1c7eaec..859d81986 100644 --- a/.claude/skills/impeccable/reference/visualize.md +++ b/.claude/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.claude/skills/impeccable/scripts/concept-seed.mjs b/.claude/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.claude/skills/impeccable/scripts/concept-seed.mjs +++ b/.claude/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.claude/skills/impeccable/scripts/generate-image.mjs b/.claude/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.claude/skills/impeccable/scripts/generate-image.mjs +++ b/.claude/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.claude/skills/impeccable/scripts/serve-question.mjs b/.claude/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.claude/skills/impeccable/scripts/serve-question.mjs +++ b/.claude/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.cursor/skills/impeccable/reference/new-work.md b/.cursor/skills/impeccable/reference/new-work.md index 79d4a06cc..7beda10b4 100644 --- a/.cursor/skills/impeccable/reference/new-work.md +++ b/.cursor/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .cursor/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.cursor/skills/impeccable/reference/visualize.md b/.cursor/skills/impeccable/reference/visualize.md index a8a1363bd..afea6311a 100644 --- a/.cursor/skills/impeccable/reference/visualize.md +++ b/.cursor/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.cursor/skills/impeccable/scripts/concept-seed.mjs b/.cursor/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.cursor/skills/impeccable/scripts/concept-seed.mjs +++ b/.cursor/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.cursor/skills/impeccable/scripts/generate-image.mjs b/.cursor/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.cursor/skills/impeccable/scripts/generate-image.mjs +++ b/.cursor/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.cursor/skills/impeccable/scripts/serve-question.mjs b/.cursor/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.cursor/skills/impeccable/scripts/serve-question.mjs +++ b/.cursor/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.gemini/skills/impeccable/reference/new-work.md b/.gemini/skills/impeccable/reference/new-work.md index 82bec7231..3f3203f30 100644 --- a/.gemini/skills/impeccable/reference/new-work.md +++ b/.gemini/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .gemini/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.gemini/skills/impeccable/reference/visualize.md b/.gemini/skills/impeccable/reference/visualize.md index 9863378f9..fc41f9169 100644 --- a/.gemini/skills/impeccable/reference/visualize.md +++ b/.gemini/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.gemini/skills/impeccable/scripts/concept-seed.mjs b/.gemini/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.gemini/skills/impeccable/scripts/concept-seed.mjs +++ b/.gemini/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.gemini/skills/impeccable/scripts/generate-image.mjs b/.gemini/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.gemini/skills/impeccable/scripts/generate-image.mjs +++ b/.gemini/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.gemini/skills/impeccable/scripts/serve-question.mjs b/.gemini/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.gemini/skills/impeccable/scripts/serve-question.mjs +++ b/.gemini/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.github/skills/impeccable/reference/new-work.md b/.github/skills/impeccable/reference/new-work.md index 505025232..1cd6cfd2c 100644 --- a/.github/skills/impeccable/reference/new-work.md +++ b/.github/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .github/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.github/skills/impeccable/reference/visualize.md b/.github/skills/impeccable/reference/visualize.md index 2d88e3cbe..1e8694ea3 100644 --- a/.github/skills/impeccable/reference/visualize.md +++ b/.github/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.github/skills/impeccable/scripts/concept-seed.mjs b/.github/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.github/skills/impeccable/scripts/concept-seed.mjs +++ b/.github/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.github/skills/impeccable/scripts/generate-image.mjs b/.github/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.github/skills/impeccable/scripts/generate-image.mjs +++ b/.github/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.github/skills/impeccable/scripts/serve-question.mjs b/.github/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.github/skills/impeccable/scripts/serve-question.mjs +++ b/.github/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.grok/skills/impeccable/reference/new-work.md b/.grok/skills/impeccable/reference/new-work.md index 77dc238aa..6cfd35162 100644 --- a/.grok/skills/impeccable/reference/new-work.md +++ b/.grok/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .grok/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.grok/skills/impeccable/reference/visualize.md b/.grok/skills/impeccable/reference/visualize.md index 978e4a9f5..a4f77c22f 100644 --- a/.grok/skills/impeccable/reference/visualize.md +++ b/.grok/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.grok/skills/impeccable/scripts/concept-seed.mjs b/.grok/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.grok/skills/impeccable/scripts/concept-seed.mjs +++ b/.grok/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.grok/skills/impeccable/scripts/generate-image.mjs b/.grok/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.grok/skills/impeccable/scripts/generate-image.mjs +++ b/.grok/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.grok/skills/impeccable/scripts/serve-question.mjs b/.grok/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.grok/skills/impeccable/scripts/serve-question.mjs +++ b/.grok/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.hermes/skills/impeccable/reference/new-work.md b/.hermes/skills/impeccable/reference/new-work.md index cf1a62fef..0f257d8ab 100644 --- a/.hermes/skills/impeccable/reference/new-work.md +++ b/.hermes/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .hermes/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.hermes/skills/impeccable/reference/visualize.md b/.hermes/skills/impeccable/reference/visualize.md index a9f045bd2..b0d9ad217 100644 --- a/.hermes/skills/impeccable/reference/visualize.md +++ b/.hermes/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.hermes/skills/impeccable/scripts/concept-seed.mjs b/.hermes/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.hermes/skills/impeccable/scripts/concept-seed.mjs +++ b/.hermes/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.hermes/skills/impeccable/scripts/generate-image.mjs b/.hermes/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.hermes/skills/impeccable/scripts/generate-image.mjs +++ b/.hermes/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.hermes/skills/impeccable/scripts/serve-question.mjs b/.hermes/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.hermes/skills/impeccable/scripts/serve-question.mjs +++ b/.hermes/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.kiro/skills/impeccable/reference/new-work.md b/.kiro/skills/impeccable/reference/new-work.md index b2e4c00af..0e4ca0dd2 100644 --- a/.kiro/skills/impeccable/reference/new-work.md +++ b/.kiro/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .kiro/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.kiro/skills/impeccable/reference/visualize.md b/.kiro/skills/impeccable/reference/visualize.md index 5f95f80c1..07f14bb53 100644 --- a/.kiro/skills/impeccable/reference/visualize.md +++ b/.kiro/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.kiro/skills/impeccable/scripts/concept-seed.mjs b/.kiro/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.kiro/skills/impeccable/scripts/concept-seed.mjs +++ b/.kiro/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.kiro/skills/impeccable/scripts/generate-image.mjs b/.kiro/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.kiro/skills/impeccable/scripts/generate-image.mjs +++ b/.kiro/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.kiro/skills/impeccable/scripts/serve-question.mjs b/.kiro/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.kiro/skills/impeccable/scripts/serve-question.mjs +++ b/.kiro/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.opencode/skills/impeccable/reference/new-work.md b/.opencode/skills/impeccable/reference/new-work.md index 067fc94a8..4bf00fd89 100644 --- a/.opencode/skills/impeccable/reference/new-work.md +++ b/.opencode/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .opencode/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.opencode/skills/impeccable/reference/visualize.md b/.opencode/skills/impeccable/reference/visualize.md index c49edb959..81136d309 100644 --- a/.opencode/skills/impeccable/reference/visualize.md +++ b/.opencode/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.opencode/skills/impeccable/scripts/concept-seed.mjs b/.opencode/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.opencode/skills/impeccable/scripts/concept-seed.mjs +++ b/.opencode/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.opencode/skills/impeccable/scripts/generate-image.mjs b/.opencode/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.opencode/skills/impeccable/scripts/generate-image.mjs +++ b/.opencode/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.opencode/skills/impeccable/scripts/serve-question.mjs b/.opencode/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.opencode/skills/impeccable/scripts/serve-question.mjs +++ b/.opencode/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.pi/skills/impeccable/reference/new-work.md b/.pi/skills/impeccable/reference/new-work.md index 12ba001e1..b932aa97f 100644 --- a/.pi/skills/impeccable/reference/new-work.md +++ b/.pi/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .pi/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.pi/skills/impeccable/reference/visualize.md b/.pi/skills/impeccable/reference/visualize.md index f761a5e15..bc5566a46 100644 --- a/.pi/skills/impeccable/reference/visualize.md +++ b/.pi/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.pi/skills/impeccable/scripts/concept-seed.mjs b/.pi/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.pi/skills/impeccable/scripts/concept-seed.mjs +++ b/.pi/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.pi/skills/impeccable/scripts/generate-image.mjs b/.pi/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.pi/skills/impeccable/scripts/generate-image.mjs +++ b/.pi/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.pi/skills/impeccable/scripts/serve-question.mjs b/.pi/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.pi/skills/impeccable/scripts/serve-question.mjs +++ b/.pi/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.qoder/skills/impeccable/reference/new-work.md b/.qoder/skills/impeccable/reference/new-work.md index 5ffc4b37c..72bedd404 100644 --- a/.qoder/skills/impeccable/reference/new-work.md +++ b/.qoder/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .qoder/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.qoder/skills/impeccable/reference/visualize.md b/.qoder/skills/impeccable/reference/visualize.md index 3c556f66f..3c13ad20f 100644 --- a/.qoder/skills/impeccable/reference/visualize.md +++ b/.qoder/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.qoder/skills/impeccable/scripts/concept-seed.mjs b/.qoder/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.qoder/skills/impeccable/scripts/concept-seed.mjs +++ b/.qoder/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.qoder/skills/impeccable/scripts/generate-image.mjs b/.qoder/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.qoder/skills/impeccable/scripts/generate-image.mjs +++ b/.qoder/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.qoder/skills/impeccable/scripts/serve-question.mjs b/.qoder/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.qoder/skills/impeccable/scripts/serve-question.mjs +++ b/.qoder/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.rovodev/skills/impeccable/reference/new-work.md b/.rovodev/skills/impeccable/reference/new-work.md index b0a4e9f82..bb4255e51 100644 --- a/.rovodev/skills/impeccable/reference/new-work.md +++ b/.rovodev/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .rovodev/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.rovodev/skills/impeccable/reference/visualize.md b/.rovodev/skills/impeccable/reference/visualize.md index 633b8d3af..b87a76581 100644 --- a/.rovodev/skills/impeccable/reference/visualize.md +++ b/.rovodev/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.rovodev/skills/impeccable/scripts/concept-seed.mjs b/.rovodev/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.rovodev/skills/impeccable/scripts/concept-seed.mjs +++ b/.rovodev/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.rovodev/skills/impeccable/scripts/generate-image.mjs b/.rovodev/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.rovodev/skills/impeccable/scripts/generate-image.mjs +++ b/.rovodev/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.rovodev/skills/impeccable/scripts/serve-question.mjs b/.rovodev/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.rovodev/skills/impeccable/scripts/serve-question.mjs +++ b/.rovodev/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.trae-cn/skills/impeccable/reference/new-work.md b/.trae-cn/skills/impeccable/reference/new-work.md index ba11263c7..d7d86c8bd 100644 --- a/.trae-cn/skills/impeccable/reference/new-work.md +++ b/.trae-cn/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .trae-cn/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.trae-cn/skills/impeccable/reference/visualize.md b/.trae-cn/skills/impeccable/reference/visualize.md index 38fe830e2..ffcfc3c5a 100644 --- a/.trae-cn/skills/impeccable/reference/visualize.md +++ b/.trae-cn/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.trae-cn/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae-cn/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.trae-cn/skills/impeccable/scripts/generate-image.mjs b/.trae-cn/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.trae-cn/skills/impeccable/scripts/generate-image.mjs +++ b/.trae-cn/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.trae-cn/skills/impeccable/scripts/serve-question.mjs b/.trae-cn/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.trae-cn/skills/impeccable/scripts/serve-question.mjs +++ b/.trae-cn/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.trae/skills/impeccable/reference/new-work.md b/.trae/skills/impeccable/reference/new-work.md index 201e56118..5ef0df348 100644 --- a/.trae/skills/impeccable/reference/new-work.md +++ b/.trae/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .trae/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.trae/skills/impeccable/reference/visualize.md b/.trae/skills/impeccable/reference/visualize.md index e935fda6a..0fb66affb 100644 --- a/.trae/skills/impeccable/reference/visualize.md +++ b/.trae/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.trae/skills/impeccable/scripts/concept-seed.mjs b/.trae/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.trae/skills/impeccable/scripts/concept-seed.mjs +++ b/.trae/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.trae/skills/impeccable/scripts/generate-image.mjs b/.trae/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.trae/skills/impeccable/scripts/generate-image.mjs +++ b/.trae/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.trae/skills/impeccable/scripts/serve-question.mjs b/.trae/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.trae/skills/impeccable/scripts/serve-question.mjs +++ b/.trae/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/.vibe/skills/impeccable/reference/new-work.md b/.vibe/skills/impeccable/reference/new-work.md index 15099cccd..dbb940efb 100644 --- a/.vibe/skills/impeccable/reference/new-work.md +++ b/.vibe/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .vibe/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/.vibe/skills/impeccable/reference/visualize.md b/.vibe/skills/impeccable/reference/visualize.md index 99fe64c17..32858988f 100644 --- a/.vibe/skills/impeccable/reference/visualize.md +++ b/.vibe/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/.vibe/skills/impeccable/scripts/concept-seed.mjs b/.vibe/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/.vibe/skills/impeccable/scripts/concept-seed.mjs +++ b/.vibe/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/.vibe/skills/impeccable/scripts/generate-image.mjs b/.vibe/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/.vibe/skills/impeccable/scripts/generate-image.mjs +++ b/.vibe/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/.vibe/skills/impeccable/scripts/serve-question.mjs b/.vibe/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/.vibe/skills/impeccable/scripts/serve-question.mjs +++ b/.vibe/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); } diff --git a/plugin/skills/impeccable/reference/new-work.md b/plugin/skills/impeccable/reference/new-work.md index 2476008c5..3ac4c0d18 100644 --- a/plugin/skills/impeccable/reference/new-work.md +++ b/plugin/skills/impeccable/reference/new-work.md @@ -36,7 +36,7 @@ Keep the visual system fixed. Derive five to seven materially different structur `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope surface --mode ` -The script assigns which structure gets built; your top-ranked structure is what every run would ship, so the dice come from outside. Never run the script for a local extension or a precisely specified narrow request; shape those directly. +The script deals three of your structures to the table; the dice decide which three reach the user, so the ranking rut stays broken while the user still holds a real choice. Present the three dealt structures on the decision page as full cards of equal salience, the dealt lead carrying kicker THE ROLL, with steer and re-roll; the user locks one in. No canon card and no pick card at surface scope: the world is settled, so every card visualizes composition, not identity. With image generation available and no standing code-led preference, each card declares a `comp` under `.impeccable/mocks/decision/`, generated after serving in reading order under the comp discipline in [visualize.md](visualize.md); anchor each of these comps on the established identity by passing a captured screenshot of a representative existing page as a reference image (the harness image tool's input image, or `generate-image.mjs --ref`) beside a prompt that leads with the new surface's structure and names DESIGN.md's palette, type, and component character, because a prose paraphrase of a design system drifts where a pixel reference does not. Without image generation, or under a standing code-led preference, each card instead carries a `wireframe` layout schematic (see `serve-question.mjs --schema`) that the page draws itself. Locking a card is the approval and sets the build path: a locked comp builds comp-led with that comp as the approved comp, discharging [visualize.md](visualize.md)'s three-option round with no second approval point; a locked wireframe builds code-led, its ambition carried by the direction contract. Never run the script for a local extension or a precisely specified narrow request; shape those directly. ### Create or replace the visual world diff --git a/plugin/skills/impeccable/reference/visualize.md b/plugin/skills/impeccable/reference/visualize.md index 2b1c7eaec..859d81986 100644 --- a/plugin/skills/impeccable/reference/visualize.md +++ b/plugin/skills/impeccable/reference/visualize.md @@ -1,12 +1,12 @@ # Visualize: Direction Comps & Asset Production -Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. +Load this from [new-work.md](new-work.md) on a comp-led build, when image generation is available (a harness-native tool or the API fallback context.mjs reports). A code-led execution contract skips this file by design, not by drift: its ambition lives in the written direction contract and is audited in behavior, so do not load it for a code-led round. PRODUCT.md and DESIGN.md are preconditions. New-work has already resolved the visual world; this file must not reopen it. A surface-scope structure round that already put three visualized cards before the user (new-work.md, established world) has discharged this round: the locked card’s comp is the approved comp, so record the approval and continue at After approval; generate nothing new. The purpose of a probe is to test composition, narrative, hierarchy, density, focal moment, signature use, and image requirements. It is not a second identity workshop. Keep DESIGN.md's palette, typography direction, material language, component character, imagery stance, and motion grammar fixed. ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session. Comp at the surface's own viewport: portrait at device size for a native app or mobile-first surface, desktop landscape otherwise; a phone screen comped landscape misstates the composition before anything gets built against it. Comps are the build thread's own work, never delegated: the thread that writes the comp prompts holds the direction's full context, and it has already seen every comp when the build starts. Open every image you produce or reference by its workspace-relative path, never an absolute one: sandboxed viewers reject absolute paths, and everything under the project root has a relative path. Base them on the real content and the surface concepts already developed with the user. On an established world, anchor every comp on the real identity: capture a screenshot of a representative existing page and pass it as a reference image (the harness image tool’s input image, or `generate-image.mjs --ref`); the prompt then leads with the new surface’s structure while the reference carries palette, type, and component character, because DESIGN.md words alone drift where a pixel reference does not. Name what the reference contributes and what it must not: chrome, palette, type, and component character carry over; the reference page’s own content does not, so a banner, hero, or card lifted verbatim from the reference is the reference leaking, not fidelity. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. The chosen card's decision comp is the first of the three: it already renders this direction at full fidelity under this file's discipline, so this round generates two more that vary what the first held fixed, and all three go to the approval point together. Only a round that arrives with no decision comp, a degraded roll, an identity-mode page, a direction pinned without the decision round, renders all three here. - A comp is a designed surface, not a picture of the subject. Lead the generation prompt with the surface's own structure, whatever regions this design actually has, named in order with their scale relationships; a page with no navigation states that instead of inventing one, and an unconventional surface states its unconventional skeleton. A prompt that leads with the world's atmosphere gets a vignette back: the model paints the fish market instead of the fish market's website. Self-check every render: if it could hang as a poster, or reads as a photograph or scene with some text on it, it is not a comp; regenerate with the layout scaffold stated more literally. - The inverse is also a failure: a surface with none of its subject in it. The subject appears as the content the regions exist to hold; the world dresses the frame and never displaces what the frame exists to show. The deletion usually rides in on the prompt's exclusion list, so exclusions bind invented claims, and a medium ban belongs to the committed imagery stance, never to caution. Before accepting a render, point at the subject: a render that depicts everything about the world and nothing of the subject fails however faithful its atmosphere, so regenerate with the subject's content named region by region. diff --git a/plugin/skills/impeccable/scripts/concept-seed.mjs b/plugin/skills/impeccable/scripts/concept-seed.mjs index 5b8e6a67a..1006e965b 100644 --- a/plugin/skills/impeccable/scripts/concept-seed.mjs +++ b/plugin/skills/impeccable/scripts/concept-seed.mjs @@ -338,6 +338,20 @@ export function renderConceptSeed({ }; const indexSalt = reroll === 0 ? 'index' : `index:reroll-${reroll}`; const buildIndex = 3 + Math.floor(unit(indexSalt) * (candidateCount - 2)); // 3..candidateCount + // Surface scope deals a hand of three grounded structures: one card is not + // a choice, and the full ranked list would hand selection back to the + // model's taste. The dice pick all three; the primary index leads. The + // no-lineup rule stays direction-only, where it was written for worlds. + const dealtIndices = [buildIndex]; + for (let draw = 0; scope === 'surface' && dealtIndices.length < Math.min(3, candidateCount); draw += 1) { + const idx = 1 + Math.floor(unit(`${indexSalt}:deal-${draw}`) * candidateCount); + if (!dealtIndices.includes(idx)) dealtIndices.push(idx); + if (draw > 64) { // hash repeats cannot stall the deal + for (let fill = 1; dealtIndices.length < Math.min(3, candidateCount); fill += 1) { + if (!dealtIndices.includes(fill)) dealtIndices.push(fill); + } + } + } // Local catalog first (private repo, evals, tests), then the roll API, // then a degraded assignment-only seed. The assigned index is pure local @@ -411,12 +425,22 @@ export function renderConceptSeed({ on named factual grounds, when the assignment cannot carry the product's truth or task; taste is never grounds.` : `After ordering the task's grounded structural candidates by resonance, - build candidate ${buildIndex} of your own grounded list; the assignment never - points at a challenger. The assignment is the roll, not a suggestion. - In an attended run, present the assigned structure and offer re-roll; never - present a ranked lineup to choose from. Re-roll yourself only when the - assignment fails audience identification or product clarity on named - factual grounds.`; + deal candidates ${dealtIndices.join(', ')} of your own grounded list to the + table; index ${buildIndex} leads, and the deal never points at a challenger. + The deal is the roll, not a suggestion: the dice decide which structures + reach the user, so the ranking rut stays broken while the user still gets a + real choice, and the full ranked list stays yours. In an attended run, + present the three dealt structures as full cards of equal salience, the + lead carrying kicker THE ROLL, with steer and re-roll, and let the user + lock one in; the world is already settled, so this choice is composition. + Visualize every dealt card: with image generation available and no + standing code-led preference, declare a comp per card and generate after + serving, lead first; otherwise author each card's wireframe field (see + serve-question --schema) and the page draws the schematic. Locking a card + approves its comp: a surface round that put three visualized structures on + the table replaces the three-option comp round in visualize.md. Re-roll + yourself only when every dealt structure fails audience identification or + product clarity on named factual grounds.`; const challengerInstruction = scope === 'direction' ? `Fuse each challenger before judging it: the challenger supplies the form @@ -490,7 +514,7 @@ assigned index is suspended this round and the user picks; seed key ${key}. ` : ''; return `${degradedHeader} -${degradedRegister}ASSIGNED INDEX: ${buildIndex} +${degradedRegister}${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels. @@ -514,8 +538,11 @@ channel: when a browser can open, present the direction on the decision page the no-browser fallback. ${authorityInstruction} A user- or brief-pinned decision beats the roll, always. -ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate -${buildIndex} of your own grounded list; seed key ${key}. +${scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate +${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`} `; } @@ -595,7 +622,7 @@ rivals to your habitual layout, and keep only what makes this product clearer.${ or IMPECCABLE_NO_TELEMETRY is set.\n` : ''; const assignedBlock = register === null - ? `ASSIGNED INDEX: ${buildIndex} + ? `${scope === 'direction' ? `ASSIGNED INDEX: ${buildIndex}` : `DEALT INDICES: ${dealtIndices.join(', ')} (index ${buildIndex} leads)`} ${promotedInstruction} The assignment exists to refuse the model's ranking rut, never to outrank the user or the brief. Never expose assignment metadata in user-facing labels.` @@ -621,8 +648,11 @@ craft bar, the finish level and commitment the build is expected to reach, never as a mockup to copy; your surface serves this product, not that render. `; const restated = register === null - ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate + ? (scope === 'direction' + ? `ASSIGNED INDEX (restated for truncated readers): ${buildIndex}. Build candidate ${buildIndex} of your own grounded list; seed key ${key}.` + : `DEALT INDICES (restated for truncated readers): ${dealtIndices.join(', ')}; index +${buildIndex} leads. Present all three dealt structures; seed key ${key}.`) : `REGISTER (restated for truncated readers): ${register}, user-requested; the assigned index is suspended this round; seed key ${key}.`; return `${scope.toUpperCase()} CONCEPT SEED (key: ${key}; mode: ${mode ?? 'unscoped'}; source: ${data.source}; approved pool: ${data.poolRevision}; ${data.approvedCount}/${data.catalogCount} human-approved; rerun with --scope ${scope}${mode ? ` --mode ${mode}` : ''} --from ${key}${reroll > 0 ? ` --reroll ${reroll}` : ''}${register ? ` --register ${register}` : ''} --candidate-count ${candidateCount} to reproduce this roll against this catalog revision) diff --git a/plugin/skills/impeccable/scripts/generate-image.mjs b/plugin/skills/impeccable/scripts/generate-image.mjs index ab03ef768..813b2e467 100644 --- a/plugin/skills/impeccable/scripts/generate-image.mjs +++ b/plugin/skills/impeccable/scripts/generate-image.mjs @@ -10,6 +10,11 @@ * * node generate-image.mjs --prompt "..." --out mock.png [--size 1536x1024] [--quality medium] * node generate-image.mjs --prompt-file prompt.txt --out mock.png + * node generate-image.mjs --prompt "..." --out mock.png --ref screenshot.png [--ref more.png] + * + * --ref anchors generation on input image(s) via the edits endpoint: pass a + * captured screenshot of a representative existing page when comping a new + * surface for an established world, so the identity comes from the real UI. */ import fs from 'node:fs'; import zlib from 'node:zlib'; @@ -212,12 +217,44 @@ if (!prompt || !out) { } const size = arg('size', '1536x1024'); const quality = arg('quality', 'medium'); +// Reference images (--ref, repeatable): route through the edits endpoint, +// which accepts input images. This is how a comp for an established world +// inherits the real UI's identity from a captured screenshot instead of a +// prose paraphrase of it; the prompt then describes the NEW surface and the +// reference carries palette, type, and component character. +const refs = (() => { + const found = []; + for (let i = 0; i < process.argv.length; i += 1) { + if (process.argv[i] === '--ref' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) found.push(process.argv[i + 1]); + } + return found; +})(); -const response = await fetch('https://api.openai.com/v1/images/generations', { - method: 'POST', - headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, - body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), -}); +let response; +if (refs.length) { + const form = new FormData(); + form.append('model', 'gpt-image-2'); + form.append('prompt', prompt); + form.append('size', size); + form.append('quality', quality); + form.append('n', '1'); + for (const ref of refs) { + const bytes = fs.readFileSync(ref); + const type = ref.endsWith('.png') ? 'image/png' : ref.endsWith('.webp') ? 'image/webp' : 'image/jpeg'; + form.append('image[]', new Blob([bytes], { type }), ref.split('/').pop()); + } + response = await fetch('https://api.openai.com/v1/images/edits', { + method: 'POST', + headers: { Authorization: `Bearer ${key}` }, + body: form, + }); +} else { + response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { Authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'gpt-image-2', prompt, size, quality, n: 1 }), + }); +} if (!response.ok) { console.error(`generate-image: API error ${response.status}: ${(await response.text()).slice(0, 300)}`); process.exit(1); @@ -235,6 +272,6 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); try { const { spawnSync } = await import('node:child_process'); spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); - fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2' }, null, 2)); + fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); diff --git a/plugin/skills/impeccable/scripts/serve-question.mjs b/plugin/skills/impeccable/scripts/serve-question.mjs index 3363e117f..2cc9fa3f1 100644 --- a/plugin/skills/impeccable/scripts/serve-question.mjs +++ b/plugin/skills/impeccable/scripts/serve-question.mjs @@ -185,7 +185,7 @@ if (hasFlag('schema')) { canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', comp: '.impeccable/mocks/decision/canon.webp' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); + console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/comp accept URLs or local paths; comp slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the comp, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no comp (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdict’s call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "IMPECCABLE’S PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challenger’s system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. A card may instead declare "wireframe" ({"cols":12,"rows":10,"regions":[{"label":"nav rail","x":0,"y":0,"w":3,"h":10,"accent":true}]}): the page draws it as a layout schematic in the media slot; surface-scope rounds use it on code-led builds, it never counts toward salience, and the card keeps its full read on the front. The comp slot carries the card’s full-fidelity direction comp (the legacy key "sketch" is accepted as an alias). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.'); process.exit(0); } @@ -472,13 +472,36 @@ function page() { } return ''; }; + // Wireframe media: a code-led card's layout schematic, authored as grid + // regions in the payload and drawn by the page; boxes and labels, no art. + // It fills the media slot only when the card has no imagery, and it never + // counts toward salience or earns a card back: the full read stays on the + // front, exactly like a text-only card. + const wire = (option) => { + const frame = option.wireframe; + if (!frame || !Array.isArray(frame.regions) || !frame.regions.length || media(option) || demoted(option)) return ''; + const cols = Number(frame.cols) > 0 ? Number(frame.cols) : 12; + const rows = Number(frame.rows) > 0 ? Number(frame.rows) : 10; + const pct = (n, total) => `${Math.max(0, Math.min(100, (n / total) * 100)).toFixed(2)}%`; + const cells = frame.regions.slice(0, 12).map((region) => { + const x = Number(region.x) || 0; + const y = Number(region.y) || 0; + const w = Math.max(Number(region.w) || 1, 0.5); + const h = Math.max(Number(region.h) || 1, 0.5); + return `
${esc(region.label || '')}
`; + }).join(''); + return ``; + }; const chooseLabel = (option) => option.isCanon ? 'Play it straight' : demoted(option) ? 'Adopt anyway' : 'Build this'; const cards = options.map((option, index) => `
-
+
${option.kicker ? `${esc(option.kicker)}` : demoted(option) ? 'Declined' : option.isCanon ? 'The standing door' : ''} - ${media(option)} + ${media(option) || wire(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

@@ -710,6 +733,15 @@ function page() { .kicker.declined-k { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } .card.declined button.choose { background: transparent; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); font-size: .85rem; padding: 8px 22px; } .card.declined button.choose:hover { background: var(--ks-graphite-2); border-color: var(--ks-text-muted); } + /* Wireframe media: the code-led schematic. Quiet boxes in the card's own + chrome; uniform salience across cards by construction, so it needs no + parity rules. */ + .media.wire { background: var(--ks-lacquer); border-bottom: 1px solid var(--ks-rule); } + .wire-field { position: absolute; inset: 12px 12px 26px; } + .wire-region { position: absolute; border: 1px solid oklch(78% 0 0 / 0.26); border-radius: 3px; background: oklch(78% 0 0 / 0.05); display: flex; align-items: center; justify-content: center; overflow: hidden; } + .wire-region span { font-family: var(--ks-mono); font-size: .55rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 2px 4px; } + .wire-region.accent { border-color: oklch(84% 0.19 80.46 / 0.5); background: oklch(84% 0.19 80.46 / 0.06); } + .wire-region.accent span { color: var(--ks-kinpaku-rich); } /* Thumb-scale inspiration: present, labeled, zoomable, and incapable of outshouting a text-only assigned card. */ .inspo { position: relative; flex: none; margin: 2px 0; width: 104px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; cursor: zoom-in; background: var(--ks-lacquer); }