From a0fd0475f5746575555571ef4fbfcab045ed22e1 Mon Sep 17 00:00:00 2001 From: Abdul Wahab Date: Wed, 15 Jul 2026 17:52:32 +0500 Subject: [PATCH] Compose cue palettes with six competing persona subagents Replace the single-mind palette composition in the visual-cues pipeline with a three-wave studio: six persona specialists compose in parallel, a new OKLab similarity command plus peer critiques build each persona's scorecard, then each revises and generates its hero and artifact sheet. AI-assisted (Cursor agent), directed by abdulwahabone. Co-authored-by: Cursor --- skill/reference/document.md | 2 +- skill/reference/visual-cues.md | 273 +++++++++++++++++++++++++-------- skill/scripts/visual-cues.mjs | 120 ++++++++++++++- 3 files changed, 330 insertions(+), 65 deletions(-) diff --git a/skill/reference/document.md b/skill/reference/document.md index 825abd3cc..330ad7a6a 100644 --- a/skill/reference/document.md +++ b/skill/reference/document.md @@ -407,7 +407,7 @@ Interview answers are words; a palette is easier picked by eye. Before writing t - **No native path**: pause and {{ask_instruction}} whether the user wants generated visual cues to pick a palette by eye. *"I can generate a few small palette-and-mood images so you choose a direction visually instead of from descriptions. That needs an image-generation API key, stored as `IMAGE_GEN_API_KEY` in `.impeccable/.env`. Add one, or skip straight to the seed?"* If a key arrives, write it to `.impeccable/.env`, confirm that file is listed in the project's `.gitignore` (add it if missing; a committed key is a leak), and ask which provider it belongs to so you call the right API. - **The user opts out, or no key arrives**: go to Step 5 and seed from the answers alone. -When generation is available, **stop and load [visual-cues.md](visual-cues.md)** and follow its pipeline; it owns everything from the one-line user announcement and concept drafting through parallel or serial generation, cropping, and `cues.json`. Do not restate its mechanics here or in chat. When its `cues.json` is written, tell the user the cues are ready and **end your turn**. The pick round is a separate later step; Steps 5-6 run after that pick, or immediately when the user opted out of generation. +When generation is available, **stop and load [visual-cues.md](visual-cues.md)** and follow its pipeline; it owns everything from the one-line user announcement and the persona palette studio through parallel or serial generation, cropping, and `cues.json`. Do not restate its mechanics here or in chat. When its `cues.json` is written, tell the user the cues are ready and **end your turn**. The pick round is a separate later step; Steps 5-6 run after that pick, or immediately when the user opted out of generation. ### Step 5: Write seed DESIGN.md diff --git a/skill/reference/visual-cues.md b/skill/reference/visual-cues.md index b2cb1ad5a..a0e4b8cd4 100644 --- a/skill/reference/visual-cues.md +++ b/skill/reference/visual-cues.md @@ -2,7 +2,7 @@ Loaded by `{{command_prefix}}impeccable document` seed mode (Step 4) when image generation is available. Input: the five seed interview answers, the asset observations from seed Step 2, and PRODUCT.md. Output: cue images plus `cues.json` under `.impeccable/visual-cues/`, ready for the user to pick from by eye in a later round. -Tell the user once, before generating: *"Generating visual cues; this can take a minute or two."* Then work without narration. Chat carries no per-image commentary, no prompt dumps, no palette tables; the folder is the deliverable. +Tell the user once, before starting: *"Generating visual cues; this can take a few minutes."* Then work without narration. Chat carries no per-image commentary, no palette tables, no scorecards, no prompt dumps; the folder is the deliverable. ## The two images @@ -33,46 +33,99 @@ HERO [slug].png (1500x1500) ARTIFACT SHEET masters/[slug]-artifacts Example, hero = a flower atelier's worktable: sheet quadrants carry the wrapping ribbon, a single stem, a row of loose petals, and the ceramic vase. All four are visible in the hero scene. -## Step 1: Compose the palettes from the brief +## The studio -The palette is designed before any image exists; the image stages it. Do **not** generate first and read colors off the result: that yields moody near-monochromes, not a usable system. And do not pull colors from a generator or seed bank: everything a designer would research is already in hand. Work the way designers work, brief first: +Palettes come from **six competing specialists**, not from you. One mind composing six palettes converges on one taste, and six versions of one mood defeat the pick round. Each specialist is a subagent locked to a **persona**: a different method of searching color space (object association, cultural reframing, remote analogy, self-imposed constraint, audience perspective-taking, emotional sequencing). Same brief, same output format, different search method; the separation is what makes the six palettes genuinely different. -1. **Reread the brief.** PRODUCT.md (personality, audience, positioning, what the product sells or shows), the interview answers (Q1 color strategy and hue anchor, Q4 the three named references, Q5 the anti-reference), and the seed Step 2 asset observations. This is the moodboard material. -2. **Write one mood phrase per palette**, specific enough to compose from. Good: "dawn delivery run, cut stems in cold water, the city still gray". Bad: "modern and clean", "warm and inviting"; a phrase that fits any brand composes nothing. -3. **Map the mood to color.** Pick each palette's hue territory from what the mood should make this audience feel and what the named references actually look like: color psychology plus reference study, not a random draw. The Q1 hue anchor leads one or two palettes; the others take territories the brief also supports (an adjacent hue, a complement, a dark register, a warm register). Six palettes on one hue are six versions of one mood; the pick round exists so the user can choose between genuinely different color stories. The anti-reference (Q5) is a hard constraint on all six. +The studio runs three waves: -Then compose each palette as four exact hex values with a **60-30-10 balance**. These are website/app colors, headed for tokens, not scene colors: +1. **Compose** (Step 2): each persona designs one palette from the brief and defends every color. +2. **Score** (Step 3): a similarity script flags overlapping palettes, and each persona critiques the other five. Each persona gets a scorecard. +3. **Revise and generate** (Step 4): each persona improves its palette against its scorecard, then builds its prompts and generates its two images. -- **neutral** (~60%, the dominant): the surface, what most of a screen will be. An off-white or near-white with a temperature tint, or a near-black when the mood calls for dark. Never pure `#FFFFFF` or `#000000`. -- **primary** (~30%): the brand color, the mood's main carrier. Must read clearly against the neutral. -- **secondary**: structure and support: an adjacent hue, or the primary shifted in lightness and chroma. Visibly a different swatch, not a darker copy of primary. -- **tertiary** (~10%, the accent): the most saturated of the four and used smallest; distinct in hue from primary so it keeps signal value. +Subagents start without your context, so every spawn task is self-contained: it carries the brief packet, the persona, and every rule the subagent needs. Paste the shared blocks below into tasks **verbatim**; a summarized rule is a dropped rule. + +## Step 1: Assemble the brief packet + +Write one self-contained text block that a specialist with zero context can design from. Include, in full: + +- **The product**: from PRODUCT.md, what it is, sells, or shows; the audience; the positioning; the personality words. +- **The interview**: Q1 color strategy and hue anchor, Q2 type direction, Q4 the three named references, Q5 the anti-reference. State that the anti-reference is a hard constraint on every palette. +- **The assets**: the seed Step 2 observations (logo colors, recurring materials, photo moods). + +Label it `BRIEF PACKET` and reuse the same block verbatim in every spawn of every wave; a packet that drifts between waves invalidates the comparison. Do **not** add your own palette leanings to it: the personas do the leaning. + +## The six personas + +The number is the persona's **priority** in the similarity check: on a collision, the lower number keeps its territory and the higher number moves. + +1. **The Ecological Naturalist**: derive every color from real materials, organisms, weather, or landscapes in the product's world. Name the physical source of each hex. No abstract "brand blue" thinking; the palette must feel materially plausible, textural, grounded. +2. **The Cross-Cultural Anthropologist**: treat color as cultural meaning. Compare at least two cultural lenses relevant to this audience, find where the meanings align and where they diverge, and turn that tension into the palette. Do not stereotype or flatten into cliché. +3. **The Analogy Hacker**: never start from the product category. Choose one distant domain (a jazz progression, a thermal camera, a medieval manuscript, a subway map, a laboratory stain chart) and translate its structure into color logic. The palette should never emerge from category convention, yet feel coherent once explained. +4. **The Constraint Poet**: before composing, invent three to five severe but fruitful constraints ("one accent only", "every color must survive dusk", "mineral tones plus one synthetic intruder"), then compose the strongest palette inside them. Do not relax the rules; tension is the point. +5. **The Audience Empath**: design from the audience's exact emotional and cognitive state at their first critical encounter with the product: what they need to feel, notice, and trust in that moment. The brand's ego does not vote. +6. **The Emotion Dramaturge**: build the palette as an emotional arc, not a static board. Define the felt sequence of using this product (invitation, curiosity, tension, confidence, release) and assign hue, lightness, and saturation to its beats. + +Accessibility and implementation stay **out** of the personas: the PALETTE RULES block carries the contrast requirements for everyone. A persona whose identity is "the contrast checker" composes cautious mud. + +## Shared blocks + +Paste these into spawn tasks where the templates call for them. They are the single source of the craft rules; never restate them loosely. + +### PALETTE RULES + +```text +Compose exactly four hex values with a 60-30-10 balance. These are +website/app colors, headed for design tokens, not scene colors: + +- neutral (~60%, the dominant): the surface, what most of a screen will + be. An off-white or near-white with a temperature tint, or a near-black + when the mood calls for dark. Never pure #FFFFFF or #000000. +- primary (~30%): the brand color, the mood's main carrier. Must read + clearly against the neutral. +- secondary: structure and support: an adjacent hue, or the primary + shifted in lightness and chroma. Visibly a different swatch, not a + darker copy of primary. +- tertiary (~10%, the accent): the most saturated of the four and used + smallest; distinct in hue from primary so it keeps signal value. Hard rules: +- Write one mood phrase specific enough to compose from. Good: "dawn + delivery run, cut stems in cold water, the city still gray". Bad: + "modern and clean"; a phrase that fits any brand composes nothing. +- Every color earns its place: for each role, one line on what it does + and why it fits this product. A color you cannot justify in one line + gets replaced, not kept because it looks nice. +- Contrast is non-negotiable: primary must read clearly on the neutral; + tertiary must pop against both. A palette that fails either is not done. +- Any two roles must be nameable apart at a glance. A dark green primary + next to a dark green neutral is one color, not two. +- The brief's anti-reference is a hard constraint. +``` -- **Every color earns its place.** For each role, state in one line what it does and why it fits this product. A color you can't justify in one line gets replaced, not kept because it looks nice. The lines guide your composition; they don't go in chat or `cues.json`. -- **Contrast is non-negotiable.** Primary must read clearly on the neutral; tertiary must pop against both. A palette that fails either is not done. -- Within one palette, any two roles must be nameable apart at a glance. A dark green primary next to a dark green neutral is one color, not two. -- Across the set, every palette takes a **different direction**: a different mood phrase and a different harmony scheme (analogous deepened, complementary accent, dark-dominant, warm neutral with the anchor demoted to accent, near-monochrome with one vivid accent). At most two palettes may share a hue family; if two would look alike as four swatches side by side, replace one. +### CONCEPT RULES -Done when: 6 (or 4 when serial, Step 5b) palettes exist, each with a mood phrase from the brief, four hexes with roles and reasons, and no two palettes interchangeable. +```text +Attach one one-line cue concept to your palette; the hero image stages it. -## Step 2: Draft the concepts +- The concept lives in the product's own world, named with the brief's + own nouns. A concept that could belong to any other product is not + done; sharpen it until it could only be this brand. +- Give it a material world (botanical, ceramic, paper, textile, metal, + glass, stone, food) as the supporting cast around the product's + subject, never a replacement for it. +- Name four artifact objects, each passing three tests: chosen from + inside the scene, so it plausibly sits in the hero composition; + compact, so it sits centered in a square-ish quadrant (no edge-to-edge + ladles or full-width garlands); carrying no writing (no tags, labels, + packaging, printed cards, or stationery), because text on an artifact + ruins it. +- Name the concept with a two-word slug (amber-dusk, coastal-glass). +``` -Attach one one-line cue concept to each palette. **Every concept lives in the product's own world.** Reread PRODUCT.md first: what the product is, who it serves, what it sells or shows. The subject of every hero scene comes from that world, named with the product's own nouns. A concept that could belong to any other product is not done; sharpen it until it could only be this brand. Diversity comes from the rest of the matrix: +### HERO PROMPT skeleton -- Each concept leads with a different anchor from what you already hold: its palette's mood phrase, the color strategy (interview Q1), the type direction (Q2), the three named references (Q4), PRODUCT.md's positioning and personality, the seed Step 2 asset observations. -- Give each concept a distinct **material world**: botanical, ceramic, paper/print, textile, metal, glass, stone, food. The material world is the supporting cast around the product's subject, never a replacement for it. For a florist, ceramic means the vases and the kiln-room shelf behind the arrangement; paper means the wrapping bench; the flowers stay in frame. -- Vary the scene's register too: one concept at work (hands mid-task), one at rest (the finished thing displayed), one in detail (macro), one in place (the room). Six takes on one world beats six unrelated worlds. -- Name the four artifact objects now. Three tests, each against a known failure: chosen **from inside the scene**, so each plausibly sits in the hero composition; **compact**, so it sits centered in a square-ish quadrant (no edge-to-edge ladles or full-width garlands); carrying **no writing** (no tags, labels, packaging, printed cards, or stationery), because a branded tag invites the model to render typography, and text on an artifact ruins it. -- The anti-reference (Q5) is a shared negative constraint on every concept. -- Name each concept with a two-word slug (`amber-dusk`, `coastal-glass`). The slug is the cue id in filenames and `cues.json`. - -Done when: every palette has a concept with a slug, a subject from the product's world, a material world no other concept uses, and four named artifacts. - -## Step 3: Build the hero prompt - -Expand each concept into a self-contained hero prompt using this template. Write it like screenplay direction, not a keyword list: subject doing something, in a place, in a light. Name every palette color twice, as a plain-language color and as its hex, and tie each to a physical carrier in the scene; a hex with no carrier gets ignored. Keep prohibitions to the final guardrail line. +Written like screenplay direction, not a keyword list: subject doing something, in a place, in a light. Every palette color is named twice, as a plain-language color and as its hex, and tied to a physical carrier in the scene; a hex with no carrier gets ignored. Fill every `[bracketed]` slot; never leave template language in the prompt. ```text One full-bleed photograph, 1500x1500 pixels: [one atmospheric scene from @@ -81,7 +134,7 @@ The scene contains [artifact A], [artifact B], [artifact C], and [artifact D], all plainly visible. Lighting: [direction and quality, e.g. "low afternoon window light raking in from the left"]. Camera: [framing and lens, e.g. "85mm still life at waist level, shallow depth of field"]. -Mood: [two or three adjectives from PRODUCT.md's personality]. The scene +Mood: [two or three adjectives from the brief's personality]. The scene is art-directed to a strict four-color story, every color plainly visible: [color name] ([neutral hex]) as the dominant ground and backdrop, about 60% of the frame; [color name] ([primary hex]) carried by [the main @@ -91,11 +144,9 @@ object]. Rich, saturated, editorial color. Photorealistic, real texture. No text, no labels, no numbers, no borders, no watermark. ``` -Fill every `[bracketed]` slot from the concept; never leave template language in the prompt. Done when every concept has a complete hero prompt whose four colors each have a named carrier. +### SHEET PROMPT skeleton -## Step 4: Build the artifact-sheet prompt - -The sheet prompt is the same for every concept except the object list. It runs as the **second** generation, with the concept's hero attached as the reference/input image, so the objects match the scene instead of being reinvented. Describe it as plain product photography; any word that implies an editorial layout ("catalog", "sheet", "spread", "grid") invites the model to design a page with titles and captions instead of photographing objects: +Runs as the **second** generation with the hero attached as the reference/input image, so the objects match the scene instead of being reinvented. It describes plain product photography; any word that implies an editorial layout ("catalog", "sheet", "spread", "grid") invites the model to design a page with titles and captions. The cream is constant across all cues (`#FDFCF6`; the artifacts land on this exact surface downstream); never restyle it per concept. ```text Using the attached photograph as the exact reference for objects, materials, @@ -120,58 +171,154 @@ No frames, no cell borders, no dividing lines, no watermark, no typography of any kind; the background stays one uninterrupted #FDFCF6 everywhere. ``` -The cream is constant across all cues (`#FDFCF6`; the artifacts land on this exact surface downstream), so never restyle it per concept. Done when every concept has its sheet prompt with the four artifacts assigned to quadrants. +## Step 2: Round 1, compose (wave 1) -## Step 5a: Generate in parallel (harness has subagents) - -If the harness has any subagent/spawn tool, parallel is **required**: run all 6 concepts as one **wave**, every spawn call emitted in the same tool-call round, one concept per subagent, each with a self-contained task (subagents start without your context). **Never generate the images yourself one at a time when a subagent tool exists**; a serial loop in a subagent-capable harness is a failure, not a fallback. Attach the harness's image-generation skill to each spawn when the harness expects that (Codex: the `imagegen` skill). +If the harness has any subagent/spawn tool, parallel is **required**: emit all six spawns in the same tool-call round, one persona per subagent. **Never run the studio's rounds yourself one persona at a time when a subagent tool exists**; a serial loop in a subagent-capable harness is a failure, not a fallback. (No subagent tool at all: Step 5.) Spawn each subagent with this task, filling the slots: ```text -You generate exactly two images in sequence and report. Use the harness's -native image generation tool. Do not fall back to CLIs or APIs; do not edit -repo files. +You are a color specialist composing one brand palette. Reply in chat +only; do not edit repo files. -1. Generate the HERO image, 1500x1500 (or the nearest supported square), - with this prompt: +PERSONA: [the persona's full numbered entry from The six personas] -[the full Step 3 hero prompt for this concept] +[the BRIEF PACKET, verbatim] -2. Generate the ARTIFACT SHEET, same size, passing the hero image you just - generated as the reference/input image (the tool's image-edit or - reference-image mode), with this prompt: +[the PALETTE RULES block, verbatim] -[the full Step 4 sheet prompt for this concept] +Design ONE palette in your persona's method. Reply with exactly this +format and nothing else: -3. Look at the sheet you generated. If any object crosses the canvas edge or - the horizontal or vertical centerline, or any text appears anywhere, +MOOD [your mood phrase] +primary=#RRGGBB [one-line reason] +secondary=#RRGGBB [one-line reason] +tertiary=#RRGGBB [one-line reason] +neutral=#RRGGBB [one-line reason] +``` + +Collect all six replies and keep each persona's palette, mood, and reasons; every later wave needs them. Close each agent after collecting. + +Done when: six palettes exist, one per persona, each in the reply format. + +## Step 3: Score the round + +Each persona gets a **scorecard** with two parts: the script's similarity verdict and the peers' critiques. Scorecards live in your context, not in chat. + +**Similarity (scripted).** Run once with all six palettes, numbered by persona: + +```text +node {{scripts_path}}/visual-cues.mjs similarity \ + "1:primary=#RRGGBB;secondary=#RRGGBB;tertiary=#RRGGBB;neutral=#RRGGBB" \ + "2:..." "3:..." "4:..." "5:..." "6:..." +``` + +The script compares every pair in OKLab and prints `verdicts`: per persona, either empty (clear) or the conflicts it must move away from (a primary sharing a hue family with a lower number's, or most roles near-duplicating a lower number's). Priority is the persona number: the lower number keeps its territory, the higher number revises. + +**Peer critique (wave 2).** Spawn six subagents in one tool-call round; each persona reviews the other five. Task template: + +```text +You are [persona name], one of six color specialists who each designed a +palette for the same brief. Every specialist is fighting for the palette +that serves this brand best, and so are you. Critique your five rivals: +not taste notes, but brand arguments grounded in the brief (wrong for the +audience, ignores the anti-reference, accent too weak against the neutral, +mood any brand could claim). Reply in chat only; do not edit repo files. + +[the BRIEF PACKET, verbatim] + +Your own palette (#[N]): [mood + palette + reasons] + +The rivals: +[every other persona's number, mood, palette, and reasons] + +Reply with exactly five lines, one per rival, and nothing else: +[rival number]: [your sharpest brand-grounded criticism, one sentence] +``` + +Assemble each persona's scorecard: its similarity verdict lines plus the five critiques it received. Close the critique agents after collecting. + +Done when: six scorecards exist, each holding the script verdict and five peer critiques. + +## Step 4: Round 2, revise and generate (wave 3) + +Spawn six subagents in one tool-call round, one per persona. Attach the harness's image-generation skill to each spawn when the harness expects that (Codex: the `imagegen` skill). Task template: + +```text +You are [persona name], a color specialist. You designed a palette; the +studio reviewed it. Improve it, then stage it in two images. Use the +harness's native image generation tool; do not fall back to CLIs or APIs; +do not edit repo files. + +PERSONA: [the persona's full numbered entry] + +[the BRIEF PACKET, verbatim] + +[the PALETTE RULES block, verbatim] + +Your round-1 palette: [mood + palette + reasons] + +Your scorecard: +[the similarity verdict lines, or "similarity: clear"] +[the five peer critiques] + +1. Revise your palette against the scorecard. The similarity verdict is + binding: if it names a conflict, move to a different hue territory; + that ground belongs to the lower number. Weigh each peer critique for + the brand, not for politeness: fix what it genuinely breaks, keep what + it does not. Stay in your persona's method throughout. + +2. Draft your concept for the revised palette: + +[the CONCEPT RULES block, verbatim] + +3. Build the hero prompt from this skeleton and generate the HERO image, + 1500x1500 (or the nearest supported square): + +[the HERO PROMPT skeleton, with its fill rules] + +4. Build the sheet prompt from this skeleton and generate the ARTIFACT + SHEET, same size, passing the hero you just generated as the + reference/input image (the tool's image-edit or reference-image mode): + +[the SHEET PROMPT skeleton, with its notes] + +5. Look at the sheet you generated. If any object crosses the canvas edge + or the horizontal or vertical centerline, or any text appears anywhere, regenerate the ARTIFACT SHEET once: same reference image, same prompt, - plus this line appended: "Make every object smaller, at most half of its - quadrant, pulled in tight to its quadrant's center, with even more empty - cream between the objects and around the edges." Never retry more than - once; keep the second sheet regardless. -4. Reply with exactly these three lines and nothing else: + plus this line appended: "Make every object smaller, at most half of + its quadrant, pulled in tight to its quadrant's center, with even more + empty cream between the objects and around the edges." Never retry more + than once; keep the second sheet regardless. + +6. Reply with exactly these four lines and nothing else: COMPLETED [slug] HERO [absolute path to the hero PNG] ARTIFACTS [absolute path to the final sheet PNG] +PALETTE primary=#RRGGBB;secondary=#RRGGBB;tertiary=#RRGGBB;neutral=#RRGGBB If either generation fails, reply instead with one line: -ERROR [slug] [short reason] +ERROR [persona number] [short reason] ``` -Six concepts fit the observed Codex ceiling of 6 concurrent subagents, so one wave normally covers everything. If a spawn is rejected with a thread-limit error, collect the accepted wave, close those agents to release their slots, then run a second wave for the rejects. If every spawn in the first wave ERRORs because subagents lack the image tool, fall back to Step 5b. Close every agent after collecting its report. +Six spawns fit the observed Codex ceiling of 6 concurrent subagents, so each wave normally runs whole. If a spawn is rejected with a thread-limit error, collect the accepted spawns, close those agents to release their slots, then run a second pass for the rejects. If every spawn in this wave ERRORs because subagents lack the image tool, fall back to Step 5's generation loop using the round-1 palettes and scorecards you already hold. Close every agent after collecting its report. If two reports share a slug, rename one before Step 6 (the crop `--slug` flag controls the filenames). -Done when: every concept has either a three-line COMPLETED report or an ERROR line. An ERROR concept is dropped, not retried more than once; five good cues beat a stalled pipeline. +Done when: every persona has either a four-line COMPLETED report or an ERROR line. An ERROR persona is dropped, not retried more than once; five good cues beat a stalled pipeline. -## Step 5b: Generate in series (no subagents) +## Step 5: Serial path (no subagents) -Only when the harness has no subagent tool at all: generate **4** concepts yourself, one pair at a time (hero, then sheet with the hero as reference), with the same Step 3 and Step 4 prompts. After each pair: apply the same look-and-retry rule as the subagent task, and record the same facts a subagent would report (slug, both paths). Same done-condition as 5a, over 4 concepts. +Only when the harness has no subagent tool at all: run the studio yourself at **4** palettes, playing personas 1-4, one at a time and honestly in-method (the Naturalist names physical sources; the Constraint Poet writes its constraints before composing). Then: + +1. Run the similarity script with numbers 1-4. Where a verdict names a conflict, revise the higher-numbered palette into a different hue territory. +2. Skip peer critique: one mind reviewing itself produces agreement, not signal. +3. For each persona in turn, follow the wave-3 task yourself from its step 2 (concept, hero, sheet, look-and-retry) and record the same four facts a subagent would report (slug, both paths, final palette). + +Same done-condition as Step 4, over 4 personas. ## Step 6: Crop and compile -For each completed concept, run one command, carrying the slug and that concept's **planned palette from Step 1** (you composed it; no subagent echo needed): +For each COMPLETED report, run one command, carrying the report's slug and its `PALETTE` line: ```text node {{scripts_path}}/visual-cues.mjs crop [hero.png] [artifacts.png] \ @@ -194,7 +341,7 @@ The script copies the hero untouched to `[slug].png`, keeps the sheet under `mas } ``` -Done when: `cues.json` lists one entry per completed concept and every listed slug has its five PNGs on disk (hero plus four artifacts). +Done when: `cues.json` lists one entry per completed persona and every listed slug has its five PNGs on disk (hero plus four artifacts). ## Step 7: Pause diff --git a/skill/scripts/visual-cues.mjs b/skill/scripts/visual-cues.mjs index 0c3a2e02f..c2d9fbc18 100644 --- a/skill/scripts/visual-cues.mjs +++ b/skill/scripts/visual-cues.mjs @@ -14,6 +14,12 @@ // -2..5.png, finds each planned palette hex's closest pixel in // the hero, and updates /cues.json. // +// node visual-cues.mjs similarity ":primary=#RRGGBB;secondary=...;..." ... +// Compares 2+ numbered palettes in OKLab and prints JSON verdicts. A +// pair is flagged when the primaries share one hue family or the four +// roles read as near-duplicates. Priority-ordered: the lower number +// keeps its territory, the higher number gets the conflict. +// // Dependency-free: PNG decode/encode on node:zlib. Rejects interlaced and // indexed-color PNGs; convert those with sips/ImageMagick/PIL first. @@ -281,6 +287,117 @@ function snapPalette(img, palette) { return out; } +// -------------------------------------------------------------- similarity + +// Perceptual comparison happens in OKLab: Euclidean distance there tracks +// how different two colors *look*, which raw RGB distance does not (RGB +// overweights differences the eye barely sees and vice versa). +function hexToOklab(hex) { + const srgb = [hex.slice(1, 3), hex.slice(3, 5), hex.slice(5, 7)] + .map((h) => parseInt(h, 16) / 255) + .map((v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4)); + const [r, g, b] = srgb; + const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b); + const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b); + const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b); + return { + L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, + a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, + b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s, + }; +} + +// Hue angle (degrees) and chroma from OKLab's a/b plane. Chroma below +// ~0.03 is visually a neutral (gray/beige/near-black); its hue angle is +// noise and must not count toward "same hue family". +function oklch(lab) { + const chroma = Math.hypot(lab.a, lab.b); + const hue = ((Math.atan2(lab.b, lab.a) * 180) / Math.PI + 360) % 360; + return { L: lab.L, chroma, hue }; +} + +const NEUTRAL_CHROMA = 0.03; +const HUE_FAMILY_DEG = 30; // primaries closer than this share a hue family +const DUPLICATE_DE = 0.09; // OKLab distance under which two roles look alike + +function hueDelta(h1, h2) { + const d = Math.abs(h1 - h2) % 360; + return d > 180 ? 360 - d : d; +} + +// Compares two palettes role by role. Verdict pieces: +// sameHueFamily — both primaries are chromatic and within HUE_FAMILY_DEG +// duplicateRoles — roles whose OKLab distance is under DUPLICATE_DE +// flagged — sameHueFamily, or 3+ of the 4 roles are near-duplicates +function comparePalettes(pa, pb) { + const roles = Object.keys(pa).filter((r) => r in pb); + const distances = {}; + const duplicateRoles = []; + for (const role of roles) { + const la = hexToOklab(pa[role].hex); + const lb = hexToOklab(pb[role].hex); + const d = Math.hypot(la.L - lb.L, la.a - lb.a, la.b - lb.b); + distances[role] = Math.round(d * 1000) / 1000; + if (d < DUPLICATE_DE) duplicateRoles.push(role); + } + let sameHueFamily = false; + if (pa.primary && pb.primary) { + const ca = oklch(hexToOklab(pa.primary.hex)); + const cb = oklch(hexToOklab(pb.primary.hex)); + sameHueFamily = + ca.chroma >= NEUTRAL_CHROMA && + cb.chroma >= NEUTRAL_CHROMA && + hueDelta(ca.hue, cb.hue) < HUE_FAMILY_DEG; + } + return { distances, duplicateRoles, sameHueFamily, flagged: sameHueFamily || duplicateRoles.length >= 3 }; +} + +// ":role=#RRGGBB;role=#RRGGBB;..." — the number is the agent's priority +// rank (1 = highest). On a conflict the lower number keeps its territory. +function parseNumberedPalette(str) { + const m = str.match(/^(\d+)\s*:\s*(.+)$/s); + if (!m) throw new Error(`bad palette argument "${str}" (expected ":primary=#RRGGBB;...")`); + return { n: parseInt(m[1], 10), palette: parsePalette(m[2]) }; +} + +function cmdSimilarity(args) { + if (args._.length < 2) { + fail('usage: visual-cues.mjs similarity ":primary=#RRGGBB;secondary=...;tertiary=...;neutral=..." ... (2+ numbered palettes)'); + } + const entries = args._.map(parseNumberedPalette).sort((a, b) => a.n - b.n); + const seen = new Set(); + for (const e of entries) { + if (seen.has(e.n)) fail(`duplicate palette number ${e.n}`); + seen.add(e.n); + } + + const pairs = []; + const conflicts = []; + for (let i = 0; i < entries.length; i++) { + for (let j = i + 1; j < entries.length; j++) { + const a = entries[i]; + const b = entries[j]; + const cmp = comparePalettes(a.palette, b.palette); + pairs.push({ a: a.n, b: b.n, ...cmp }); + if (cmp.flagged) { + // Priority order: the earlier number owns the territory; the later + // number is the one told to move. + const reason = cmp.sameHueFamily + ? `primary shares a hue family with palette ${a.n}'s primary` + : `${cmp.duplicateRoles.length} of 4 roles are near-duplicates of palette ${a.n}'s (${cmp.duplicateRoles.join(', ')})`; + conflicts.push({ keep: a.n, revise: b.n, reason }); + } + } + } + + // One line per agent: "clear" or the list of things it must move away from. + const verdicts = {}; + for (const e of entries) verdicts[e.n] = []; + for (const c of conflicts) verdicts[c.revise].push(c.reason); + + console.log(JSON.stringify({ ok: true, pairs, conflicts, verdicts }, null, 2)); +} + // ---------------------------------------------------------------- cues.json // Reads the existing cues.json (if any) and merges this cue in, so cropping @@ -383,7 +500,8 @@ function main() { const args = parseArgs(rest); try { if (cmd === 'crop') cmdCrop(args); - else fail('usage: visual-cues.mjs crop --slug [options] (see reference/visual-cues.md)'); + else if (cmd === 'similarity') cmdSimilarity(args); + else fail('usage: visual-cues.mjs ... (see reference/visual-cues.md)'); } catch (err) { fail(err.message); }